From 4dc6b2ef8f4008750a2e6d88f7c2c53ce222e3a7 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 3 Jul 2026 21:05:16 -0500 Subject: [PATCH] perf(market): capture the portfolio backdrop once on open (+resize), not every frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the live-blur behind the modal from a per-frame capture (which flickered from per-frame re-blur non-determinism) to capture-once-on-open, re-captured only on resize. - Insert the acrylic live-capture callback into the overlay draw list ONLY for a few frames on open and whenever the viewport size changes; other frames reuse the frozen blurred snapshot (blurCacheValid_), so there's no per-frame capture/re-blur cost. State: s_pf_was_open (reset on close), s_pf_capture_frames, s_pf_capture_w/h. Two fixes from an adversarial review of the timing/pipeline interaction: - Do NOT override the backdrop blurRadius to 40. applyBlur caches one blurred buffer keyed on radius, shared with every glass panel (market cards + the modal preview card, radius ~30). A different radius thrashed that cache every frame and made the backdrop sample the wrong blur, defeating the frozen-blur guarantee. Using the theme card's radius keeps one blur serving all surfaces, frozen. - Skip the blur on the overlay's first frame (allowBlur=false) — the live content isn't captured until that frame's render, so blurring then would show stale/background pixels for one frame; draw the plain opaque scrim instead. Compiles on OpenGL (Linux) + DX11 (Windows). Needs on-device visual verification. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/material/draw_helpers.h | 12 ++++++++--- src/ui/windows/market_tab.cpp | 38 +++++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 4c94e5a..38e78dd 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -482,13 +482,19 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin, // is active and we are NOT in low-performance mode — draws a strong blur of the (static) window // backdrop on top plus a dim. In low-perf mode only the opaque base is drawn (no blur cost), // mirroring DrawGlassPanel's gating. -inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax) +// `allowBlur=false` draws only the opaque base (used on the overlay's first frame, before the live +// content has been captured, to avoid a one-frame blur of stale/background pixels). +inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, + bool allowBlur = true) { dl->AddRectFilled(pMin, pMax, IM_COL32(9, 10, 16, 255)); // opaque base (shows through if the blur is faint) - if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() + if (allowBlur && IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() && effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable()) { + // Keep the theme card's blurRadius (do NOT override it): applyBlur caches a single blurred + // buffer keyed on radius, shared with every glass panel drawn this frame (market cards + the + // modal preview card). A different radius here would thrash that cache every frame and make + // the backdrop sample the wrong blur. Same radius => one blur serves all, and it stays frozen. auto params = GetCurrentAcrylicTheme().card; - params.blurRadius = 40.0f; // strong — spans the whole window params.fallbackColor.w = 1.0f; // full-strength blur so the live content reads effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax, params, 0.0f); dl->AddRectFilled(pMin, pMax, IM_COL32(6, 8, 18, 70)); // slight dim so the card reads as foreground diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 08795ac..9211e67 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -99,6 +99,11 @@ static int s_pf_sel = -1; // selected group index in the master list; -1 = no static bool s_pf_sec_appearance = true; // right-pane accordion: which section is expanded static bool s_pf_sec_price = false; static bool s_pf_sec_addresses = false; +// Backdrop live-capture: capture once on open + on viewport resize (not every frame). The blur is a +// frozen snapshot while the modal is open — efficient and flicker-free. +static bool s_pf_was_open = false; +static int s_pf_capture_frames = 0; // frames left to (re)capture the live backdrop +static float s_pf_capture_w = 0.0f, s_pf_capture_h = 0.0f; // viewport size at the last capture trigger static char s_pf_label[64] = {0}; static std::vector s_pf_addrs; static std::string s_pf_icon; // selected wallet-icon name ("" = none) @@ -396,6 +401,12 @@ static void RenderPortfolioEditor(App* app) material::MarkOverlayDialogActive(); ImGuiViewport* vp = ImGui::GetMainViewport(); ImVec2 vpPos = vp->Pos, vpSize = vp->Size; + // Trigger a live backdrop (re)capture on OPEN and on viewport RESIZE only. A few frames are used + // so the capture lands after the theme-effect suppression (frame after open) settles. + bool justOpened = !s_pf_was_open; + s_pf_was_open = true; + if (justOpened || s_pf_capture_w != vpSize.x || s_pf_capture_h != vpSize.y) s_pf_capture_frames = 3; + s_pf_capture_w = vpSize.x; s_pf_capture_h = vpSize.y; // Don't force-focus the scrim while a popup (basis/interval combo, color picker) is open, or it // would close the popup; the same guard gates the outside-click dismiss below. const bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel); @@ -411,13 +422,20 @@ static void RenderPortfolioEditor(App* app) ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings); ImDrawList* dl = ImGui::GetWindowDrawList(); - // Capture the live UI drawn below this overlay (then reset ImGui's render state) so the backdrop - // blurs the live tab content, not the frozen wallpaper. Must precede the backdrop draw. - if (ImDrawCallback liveCap = effects::ImGuiAcrylic::GetLiveCaptureCallback()) { - dl->AddCallback(liveCap, nullptr); - dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); + // Only on capture frames (open/resize): capture the live UI drawn below this overlay (then reset + // ImGui's render state) so the backdrop blurs the live tab content. Other frames reuse the frozen + // blurred snapshot (blurCacheValid_), so there's no per-frame capture/re-blur cost or flicker. + if (s_pf_capture_frames > 0) { + if (ImDrawCallback liveCap = effects::ImGuiAcrylic::GetLiveCaptureCallback()) { + dl->AddCallback(liveCap, nullptr); + dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); + } + s_pf_capture_frames--; } - material::DrawFullWindowBlurBackdrop(dl, vpPos, ImVec2(vpPos.x + vpSize.x, vpPos.y + vpSize.y)); + // Skip the blur on the very first frame (live content isn't captured until this frame's render) — + // shows the plain opaque scrim for one frame instead of a blur of stale background pixels. + material::DrawFullWindowBlurBackdrop(dl, vpPos, ImVec2(vpPos.x + vpSize.x, vpPos.y + vpSize.y), + /*allowBlur=*/!justOpened); ImGui::SetCursorScreenPos(vpPos); ImGui::InvisibleButton("##pfInputBlocker", vpSize, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle); @@ -950,8 +968,12 @@ static void RenderPortfolioEditor(App* app) s_portfolio_editor_open = false; // Commit named edits on any close path (Esc / outside-click / Close); unnamed drafts are dropped. // Also invalidate the acrylic capture so the background (not the last live capture) is re-captured - // for the other glass panels once the overlay is gone. - if (!s_portfolio_editor_open) { commitIfNeeded(); effects::ImGuiAcrylic::InvalidateCapture(); } + // for the other glass panels once the overlay is gone, and arm a fresh capture for the next open. + if (!s_portfolio_editor_open) { + commitIfNeeded(); + effects::ImGuiAcrylic::InvalidateCapture(); + s_pf_was_open = false; + } ImGui::End(); ImGui::PopStyleColor();