From b60a8dc491f542b2d6025548ec05f1419f2ebc39 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Mon, 20 Apr 2026 15:12:44 -0700 Subject: [PATCH] Optimize ImGui renderer: viewport culling and width caching Large files made idle CPU spike because every frame re-measured every line, recompiled the search regex per line, and ran full per-line rendering work even for off-screen rows. - Render only visible rows (with a small margin for smooth scrolling) and advance the layout cursor to preserve total content height. - Hoist the search std::regex compilation out of the per-line loop. - Cache the max line width on the renderer; reset only when the buffer or font changes, not on every edit. Co-Authored-By: Claude Opus 4.7 (1M context) --- ImGuiRenderer.cc | 123 +++++++++++++++++++++++++++++++++++++---------- ImGuiRenderer.h | 14 ++++++ 2 files changed, 111 insertions(+), 26 deletions(-) diff --git a/ImGuiRenderer.cc b/ImGuiRenderer.cc index 8291a05..d29d7b6 100644 --- a/ImGuiRenderer.cc +++ b/ImGuiRenderer.cc @@ -106,7 +106,6 @@ ImGuiRenderer::Draw(Editor &ed) ImVec2 child_window_pos = ImGui::GetWindowPos(); float scroll_y = ImGui::GetScrollY(); float scroll_x = ImGui::GetScrollX(); - std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui // Synchronize buffer offsets from ImGui scroll if user scrolled manually bool forced_scroll = false; @@ -134,8 +133,65 @@ ImGuiRenderer::Draw(Editor &ed) } prev_buf_rowoffs_ = buf_rowoffs; prev_buf_coloffs_ = buf_coloffs; - // Track the widest line in pixels for ImGui content width - float max_line_width_px = 0.0f; + + // Max-line-width cache: reset when the buffer or font changes. We only + // measure visible lines per frame and maintain a running max, so the + // scrollbar may be slightly conservative on first view of a file until + // the user scrolls, but idle CPU drops dramatically on large files. + // Edits bump the buffer version but we intentionally do NOT reset the + // max on version changes — keeping a conservative (possibly too-wide) + // scrollbar is preferable to per-keystroke jitter. + { + ImFont *cur_font = ImGui::GetFont(); + float cur_fsize = ImGui::GetFontSize(); + if (buf != max_width_buf_ + || cur_font != max_width_font_ + || cur_fsize != max_width_font_size_) { + max_width_buf_ = buf; + max_width_font_ = cur_font; + max_width_font_size_ = cur_fsize; + max_width_px_ = 0.0f; + } + max_width_version_ = buf->Version(); + } + + // Hoist the search-regex compilation out of the per-line loop. Compiling + // std::regex per line per frame was a large source of idle CPU on macOS. + const bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); + const bool regex_mode = search_mode && ed.PromptActive() && ( + ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || + ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind); + std::regex search_rx; + bool search_rx_valid = false; + if (regex_mode) { + try { + search_rx = std::regex(ed.SearchQuery()); + search_rx_valid = true; + } catch (const std::regex_error &) { + search_rx_valid = false; + } + } + + // Compute the visible row range and skip rendering work for off-screen + // lines. ImGui clips drawing, but string allocation, tab expansion, + // syntax-highlight lookups, and width measurement are not free. + const std::size_t total_rows = lines.size(); + std::size_t first_vis = 0; + std::size_t last_vis = total_rows; // exclusive + if (row_h > 0.0f && total_rows > 0) { + const long margin = 4; // render a few extra rows above/below for smoother scrolling + long fv = static_cast(std::floor(scroll_y / row_h)) - margin; + long lv = static_cast( + std::ceil((scroll_y + child_h_plan) / row_h)) + margin; + if (fv < 0) fv = 0; + if (lv < 0) lv = 0; + if (static_cast(fv) > total_rows) + fv = static_cast(total_rows); + if (static_cast(lv) > total_rows) + lv = static_cast(total_rows); + first_vis = static_cast(fv); + last_vis = static_cast(lv); + } // Mark selection state (mark -> cursor), in source coordinates bool sel_active = false; @@ -257,7 +313,13 @@ ImGuiRenderer::Draw(Editor &ed) if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { mouse_selecting_ = false; } - for (std::size_t i = rowoffs; i < lines.size(); ++i) { + // Advance the cursor Y so the first visible line draws at its correct + // scroll position. Skipped rows simply leave the layout cursor untouched. + if (first_vis > 0) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + + static_cast(first_vis) * row_h); + } + for (std::size_t i = first_vis; i < last_vis; ++i) { // Capture the screen position before drawing the line ImVec2 line_pos = ImGui::GetCursorScreenPos(); std::string line = static_cast(lines[i]); @@ -291,24 +353,22 @@ ImGuiRenderer::Draw(Editor &ed) }; // Compute search highlight ranges for this line in source indices - bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); std::vector > hl_src_ranges; if (search_mode) { - // If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring - if (ed.PromptActive() && ( - ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed. - CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) { - try { - std::regex rx(ed.SearchQuery()); - for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); - it != std::sregex_iterator(); ++it) { - const auto &m = *it; - std::size_t sx = static_cast(m.position()); - std::size_t ex = sx + static_cast(m.length()); - hl_src_ranges.emplace_back(sx, ex); + // In regex mode, reuse the compiled regex hoisted above the loop. + if (regex_mode) { + if (search_rx_valid) { + try { + for (auto it = std::sregex_iterator(line.begin(), line.end(), search_rx); + it != std::sregex_iterator(); ++it) { + const auto &m = *it; + std::size_t sx = static_cast(m.position()); + std::size_t ex = sx + static_cast(m.length()); + hl_src_ranges.emplace_back(sx, ex); + } + } catch (const std::regex_error &) { + // ignore invalid patterns here; status line already shows the error } - } catch (const std::regex_error &) { - // ignore invalid patterns here; status line already shows the error } } else { const std::string &q = ed.SearchQuery(); @@ -483,18 +543,29 @@ ImGuiRenderer::Draw(Editor &ed) ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); } - // Track widest line for content width reporting + // Track widest line for content width reporting. We only measure + // visible lines and fold the result into a monotonic max cached on + // the renderer (see max_width_* members). Off-screen lines are not + // measured per frame; if the user scrolls or edits, the cache is + // refreshed accordingly. if (!expanded.empty()) { float line_w = ImGui::CalcTextSize(expanded.c_str()).x; - if (line_w > max_line_width_px) - max_line_width_px = line_w; + if (line_w > max_width_px_) + max_width_px_ = line_w; } } - // Report content width to ImGui so horizontal scrollbar works correctly. - if (max_line_width_px > 0.0f) { - ImGui::SetCursorPosX(max_line_width_px); - ImGui::Dummy(ImVec2(0, 0)); + // After the visible-range loop, advance the layout cursor to the end of + // the (virtual) content so ImGui sees the correct total size. Vertical + // height comes from total_rows; horizontal width comes from the cached + // max line width. A Dummy at the final position records both. + if (total_rows > last_vis) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + + static_cast(total_rows - last_vis) * row_h); } + if (max_width_px_ > 0.0f) { + ImGui::SetCursorPosX(max_width_px_); + } + ImGui::Dummy(ImVec2(0, 0)); // Synchronize cursor and scrolling after rendering all lines so content size is known. { float child_h_actual = ImGui::GetWindowHeight(); diff --git a/ImGuiRenderer.h b/ImGuiRenderer.h index 000914e..079711a 100644 --- a/ImGuiRenderer.h +++ b/ImGuiRenderer.h @@ -2,8 +2,13 @@ * ImGuiRenderer - ImGui-based renderer for GUI mode */ #pragma once +#include +#include #include "Renderer.h" +struct ImFont; +class Buffer; + class ImGuiRenderer final : public Renderer { public: ImGuiRenderer() = default; @@ -20,4 +25,13 @@ private: float prev_scroll_y_ = -1.0f; float prev_scroll_x_ = -1.0f; bool mouse_selecting_ = false; + + // Max-line-width cache for the horizontal scrollbar. Measuring every line + // every frame is prohibitively expensive on large files; we only update the + // running max from visible lines and reset when buffer/version/font changes. + const Buffer *max_width_buf_ = nullptr; + std::uint64_t max_width_version_ = 0; + ImFont *max_width_font_ = nullptr; + float max_width_font_size_ = 0.0f; + float max_width_px_ = 0.0f; };