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";