From 968d6d3875a6f4337a367f37cda64c0273dd8fad Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 13 Oct 2026 13:30:20 -0500 Subject: [PATCH] fix(effects): capture the acrylic backdrop at framebuffer size on HiDPI The acrylic effect blits its backdrop from the default framebuffer using the logical viewport size as the source rectangle. On a HiDPI display the framebuffer is larger than the logical size, so the blur sampled only the lower-left corner of the screen and the glass showed the wrong region stretched across each panel. - The capture blit source now uses the real framebuffer pixel size (logical size times ImGui's DisplayFramebufferScale, truncated the same way the GL backend does), while the capture and blur targets stay at logical size. The framebuffer size is tracked separately from the viewport size so quality or buffer-lifetime resets cannot lose it, and moving the window between a Retina and a non-Retina display redoes the capture. DX11 is unaffected; its scale is one. - The debug screenshot sweep captures at framebuffer size too, so HiDPI captures are no longer cropped. Tests: the framebuffer-extent helper matches the GL backend's truncation and falls back to the logical size when the scale is unknown. Co-Authored-By: Claude Opus 4.8 --- src/main.cpp | 13 ++++++++++--- src/ui/effects/acrylic.cpp | 6 +++--- src/ui/effects/acrylic.h | 11 +++++++++++ src/ui/effects/capture_extent.h | 12 ++++++++++++ src/ui/effects/framebuffer.cpp | 4 ++-- src/ui/effects/framebuffer.h | 2 +- src/ui/effects/imgui_acrylic.cpp | 16 ++++++++++++++++ src/ui/effects/imgui_acrylic.h | 5 ++++- tests/test_phase4.cpp | 13 +++++++++++++ 9 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 src/ui/effects/capture_extent.h diff --git a/src/main.cpp b/src/main.cpp index ff66bc2..9e3418a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "imgui_internal.h" #include "imgui_impl_sdl3.h" #include "ui/effects/imgui_acrylic.h" +#include "ui/effects/capture_extent.h" #include "ui/effects/theme_effects.h" #include "util/texture_loader.h" #include "util/platform.h" @@ -1557,7 +1558,9 @@ int main(int argc, char* argv[]) ImGuiIO& io = ImGui::GetIO(); dragonx::ui::effects::ImGuiAcrylic::BeginFrame( - (int)io.DisplaySize.x, (int)io.DisplaySize.y); + (int)io.DisplaySize.x, (int)io.DisplaySize.y, + dragonx::ui::effects::framebufferExtent(io.DisplaySize.x, io.DisplayFramebufferScale.x), + dragonx::ui::effects::framebufferExtent(io.DisplaySize.y, io.DisplayFramebufferScale.y)); ctx->app->update(); ctx->app->render(); @@ -2049,7 +2052,9 @@ int main(int argc, char* argv[]) ImGuiIO& io = ImGui::GetIO(); if (!dragonx::ui::effects::isLowSpecMode()) { dragonx::ui::effects::ImGuiAcrylic::BeginFrame( - (int)io.DisplaySize.x, (int)io.DisplaySize.y); + (int)io.DisplaySize.x, (int)io.DisplaySize.y, + dragonx::ui::effects::framebufferExtent(io.DisplaySize.x, io.DisplayFramebufferScale.x), + dragonx::ui::effects::framebufferExtent(io.DisplaySize.y, io.DisplayFramebufferScale.y)); } // Update theme visual effects timing (held still under Reduce Motion) @@ -2213,7 +2218,9 @@ int main(int argc, char* argv[]) // Debug screenshot sweep — grab the finished frame before swap. if (app.wantsScreenshotThisFrame()) { - captureFrameToPng(app.screenshotSweepPath(), (int)io.DisplaySize.x, (int)io.DisplaySize.y); + captureFrameToPng(app.screenshotSweepPath(), + dragonx::ui::effects::framebufferExtent(io.DisplaySize.x, io.DisplayFramebufferScale.x), + dragonx::ui::effects::framebufferExtent(io.DisplaySize.y, io.DisplayFramebufferScale.y)); app.onScreenshotCaptured(); } diff --git a/src/ui/effects/acrylic.cpp b/src/ui/effects/acrylic.cpp index 93434b8..dcb5b7f 100644 --- a/src/ui/effects/acrylic.cpp +++ b/src/ui/effects/acrylic.cpp @@ -198,7 +198,7 @@ void AcrylicMaterial::captureBackground() --dirtyFrames_; // Capture current screen content - captureBuffer_.captureScreen(viewportWidth_, viewportHeight_); + captureBuffer_.captureScreen(captureSrcWidth(), captureSrcHeight()); hasValidCapture_ = true; blurCacheValid_ = false; // New capture invalidates blur cache } @@ -226,7 +226,7 @@ void AcrylicMaterial::captureBackgroundDirect() // draw framebuffer, and ImGui has scissor enabled during rendering. // The subsequent ImDrawCallback_ResetRenderState re-enables it. glDisable(GL_SCISSOR_TEST); - captureBuffer_.captureScreen(viewportWidth_, viewportHeight_); + captureBuffer_.captureScreen(captureSrcWidth(), captureSrcHeight()); hasValidCapture_ = true; blurCacheValid_ = false; // force re-blur with fresh capture @@ -241,7 +241,7 @@ void AcrylicMaterial::captureLiveFramebuffer() if (!initialized_ || shouldSkipEffect()) return; if (viewportWidth_ <= 0 || viewportHeight_ <= 0 || !captureBuffer_.isValid()) return; glDisable(GL_SCISSOR_TEST); - captureBuffer_.captureScreen(viewportWidth_, viewportHeight_); + captureBuffer_.captureScreen(captureSrcWidth(), captureSrcHeight()); hasValidCapture_ = true; blurCacheValid_ = false; // force re-blur with the fresh live capture } diff --git a/src/ui/effects/acrylic.h b/src/ui/effects/acrylic.h index 5ef06ec..a21d629 100644 --- a/src/ui/effects/acrylic.h +++ b/src/ui/effects/acrylic.h @@ -147,6 +147,13 @@ public: * @param height Viewport height */ void resize(int width, int height); + + /// Size of FB 0 in pixels (differs from the logical viewport on HiDPI GL). Used only as the capture + /// blit source. Kept apart from viewportWidth_/Height_ so setQuality()/syncBufferLifetime() resets + /// of those never lose it. DX11 ignores it (CopyResource needs identical dimensions, scale is 1). + void setFramebufferSize(int w, int h) { + if (w != fbWidth_ || h != fbHeight_) { fbWidth_ = w; fbHeight_ = h; markBackgroundDirty(); } + } /** * @brief Capture current screen content for blur source @@ -375,6 +382,10 @@ private: // Screen dimensions int viewportWidth_ = 0; int viewportHeight_ = 0; + int fbWidth_ = 0; // FB 0 pixel size (0 = unknown -> use the viewport size) + int fbHeight_ = 0; + int captureSrcWidth() const { return fbWidth_ > 0 ? fbWidth_ : viewportWidth_; } + int captureSrcHeight() const { return fbHeight_ > 0 ? fbHeight_ : viewportHeight_; } // Noise texture config int noiseTextureSize_ = 256; // Texture resolution (larger = less repetition) diff --git a/src/ui/effects/capture_extent.h b/src/ui/effects/capture_extent.h new file mode 100644 index 0000000..abf4c44 --- /dev/null +++ b/src/ui/effects/capture_extent.h @@ -0,0 +1,12 @@ +#pragma once +#include +namespace dragonx { namespace ui { namespace effects { +/// Framebuffer pixel extent for a window-unit extent and ImGui's DisplayFramebufferScale. Same truncation as +/// imgui_impl_opengl3.cpp (`(int)(DisplaySize.x * DisplayFramebufferScale.x)`), so the blit source matches FB 0. +inline int framebufferExtent(float logical, float scale) { + if (!(scale > 0.0f) || !(logical > 0.0f)) return (int)logical; + return (int)(logical * scale); +} +/// Source extent for the capture blit: the framebuffer size when known, else the logical size. +inline int captureSourceExtent(int fbExtent, int logicalExtent) { return fbExtent > 0 ? fbExtent : logicalExtent; } +}}} diff --git a/src/ui/effects/framebuffer.cpp b/src/ui/effects/framebuffer.cpp index 7eb5201..d2920bc 100644 --- a/src/ui/effects/framebuffer.cpp +++ b/src/ui/effects/framebuffer.cpp @@ -198,14 +198,14 @@ void Framebuffer::blitFrom(GLuint srcFbo, int srcX, int srcY, int srcWidth, int glBindFramebuffer(GL_FRAMEBUFFER, 0); } -void Framebuffer::captureScreen(int viewportWidth, int viewportHeight) +void Framebuffer::captureScreen(int srcWidth, int srcHeight) { // Blit from default framebuffer (0) to this framebuffer glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo_); glBlitFramebuffer( - 0, 0, viewportWidth, viewportHeight, // Source (screen) + 0, 0, srcWidth, srcHeight, // Source (screen) 0, 0, width_, height_, // Dest (this FBO) GL_COLOR_BUFFER_BIT, GL_LINEAR diff --git a/src/ui/effects/framebuffer.h b/src/ui/effects/framebuffer.h index c8610d9..5755dfa 100644 --- a/src/ui/effects/framebuffer.h +++ b/src/ui/effects/framebuffer.h @@ -117,7 +117,7 @@ public: * @param viewportWidth Current viewport width * @param viewportHeight Current viewport height */ - void captureScreen(int viewportWidth, int viewportHeight); + void captureScreen(int srcWidth, int srcHeight); private: void cleanup(); diff --git a/src/ui/effects/imgui_acrylic.cpp b/src/ui/effects/imgui_acrylic.cpp index 65a2712..5c4b2e2 100644 --- a/src/ui/effects/imgui_acrylic.cpp +++ b/src/ui/effects/imgui_acrylic.cpp @@ -125,6 +125,8 @@ namespace ImGuiAcrylic { static bool s_initialized = false; static int s_viewportWidth = 0; static int s_viewportHeight = 0; +static int s_fbWidth = 0; +static int s_fbHeight = 0; static bool s_backgroundCaptured = false; // Track window state for acrylic rendering @@ -179,11 +181,24 @@ bool IsAvailable() // ============================================================================ void BeginFrame(int width, int height) +{ + BeginFrame(width, height, width, height); +} + +void BeginFrame(int width, int height, int fbWidth, int fbHeight) { if (!s_initialized) { return; } + // Moving the window between a Retina and a non-Retina display changes the framebuffer size while the + // logical size stays put: the capture must be redone against the new pixel grid. + if (fbWidth != s_fbWidth || fbHeight != s_fbHeight) { + s_fbWidth = fbWidth; + s_fbHeight = fbHeight; + getAcrylicMaterial().setFramebufferSize(fbWidth, fbHeight); + } + // Update viewport dimensions if changed if (width != s_viewportWidth || height != s_viewportHeight) { s_viewportWidth = width; @@ -670,6 +685,7 @@ void Shutdown() {} bool IsAvailable() { return false; } void BeginFrame(int, int) {} +void BeginFrame(int, int, int, int) {} void CaptureBackground() {} void InvalidateCapture() {} void ReleaseIdleBuffers() {} diff --git a/src/ui/effects/imgui_acrylic.h b/src/ui/effects/imgui_acrylic.h index cac9be2..caff102 100644 --- a/src/ui/effects/imgui_acrylic.h +++ b/src/ui/effects/imgui_acrylic.h @@ -61,7 +61,10 @@ bool IsAvailable(); * @param width Viewport width * @param height Viewport height */ -void BeginFrame(int width, int height); +void BeginFrame(int width, int height); ///< framebuffer == window size +/// HiDPI: fbWidth/fbHeight are FB 0's pixel size (DisplaySize * DisplayFramebufferScale). The capture +/// and blur targets stay logical-size; only the capture blit source rectangle uses the pixel size. +void BeginFrame(int width, int height, int fbWidth, int fbHeight); /** * @brief End the acrylic frame diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 822a943..5b053c4 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -71,6 +71,7 @@ #include "ui/staleness_badge.h" #include "ui/effects/effect_clock.h" #include "ui/effects/border_sampling.h" +#include "ui/effects/capture_extent.h" #include "ui/material/rect_cull.h" #include "ui/notifications.h" #include "data/seed_migration_resume.h" @@ -17931,6 +17932,17 @@ void testLoadLanguageFromLooseFiles() EXPECT_EQ(std::string(i18n.translate("overview")), std::string("Overview")); } +void testCaptureExtent() +{ + using namespace dragonx::ui::effects; + EXPECT_EQ(framebufferExtent(1280.0f, 1.0f), 1280); + EXPECT_EQ(framebufferExtent(1280.0f, 2.0f), 2560); + EXPECT_EQ(framebufferExtent(1001.0f, 1.5f), 1501); // truncation, same as imgui_impl_opengl3.cpp + EXPECT_EQ(framebufferExtent(800.0f, 0.0f), 800); // unknown/zero scale -> logical + EXPECT_EQ(captureSourceExtent(0, 1280), 1280); + EXPECT_EQ(captureSourceExtent(2560, 1280), 2560); +} + // resources::writeFileIfDifferent (bundled-theme extraction): rewrite a file only when its bytes differ. void testWriteFileIfDifferent() { @@ -18312,6 +18324,7 @@ int main() testTextFitBanner(); testLoadLanguageFromLooseFiles(); testWriteFileIfDifferent(); + testCaptureExtent(); if (g_failures != 0) { std::cerr << g_failures << " test assertion(s) failed\n";