diff --git a/src/main.cpp b/src/main.cpp index 437e849..8514a85 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1538,6 +1538,8 @@ int main(int argc, char* argv[]) SDL_GL_MakeCurrent(ctx->window, ctx->gl_context); ImGui_ImplOpenGL3_NewFrame(); #endif + // Free/rebuild acrylic targets at the frame boundary (after MakeCurrent on GL: FBOs are per-context). + dragonx::ui::effects::ImGuiAcrylic::ReleaseIdleBuffers(); ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); @@ -1951,6 +1953,8 @@ int main(int argc, char* argv[]) // Pre-frame: font atlas rebuilds and schema hot-reload must // happen BEFORE NewFrame() because NewFrame() caches font ptrs. app.preFrame(); + // Acrylic targets are freed/rebuilt here, between frames: nothing recorded can reference them. + dragonx::ui::effects::ImGuiAcrylic::ReleaseIdleBuffers(); // Smooth font-scale: compensate visual font size via FontScaleMain // when the atlas hasn't been rebuilt yet (during slider drag). diff --git a/src/ui/effects/acrylic.cpp b/src/ui/effects/acrylic.cpp index 7ef20c2..bcbc0c5 100644 --- a/src/ui/effects/acrylic.cpp +++ b/src/ui/effects/acrylic.cpp @@ -260,6 +260,7 @@ void AcrylicMaterial::resize(int width, int height) viewportWidth_ = width; viewportHeight_ = height; blurCacheValid_ = false; // Invalidate cache on resize + if (shouldSkipEffect()) { needsRealloc_ = true; return; } // targets freed/rebuilt by syncBufferLifetime() // Determine blur buffer size based on quality int blurWidth = width; @@ -301,16 +302,46 @@ void AcrylicMaterial::resize(int width, int height) DEBUG_LOGF("AcrylicMaterial: Resized to %dx%d (blur: %dx%d)\n", width, height, blurWidth, blurHeight); + needsRealloc_ = false; dirtyFrames_ = 2; // recapture after resize } +void AcrylicMaterial::releaseBuffers() +{ + captureBuffer_.destroy(); + blurBuffers_.destroy(); + hasValidCapture_ = false; // drawRect -> drawTintedRect until a fresh capture + blurCacheValid_ = false; + needsRealloc_ = true; + DEBUG_LOGF("AcrylicMaterial: released targets (effect skipped)\n"); +} + +void AcrylicMaterial::syncBufferLifetime() +{ + if (!initialized_) return; + if (shouldSkipEffect()) { + // Test the names, not isValid(): a half-allocated ping-pong pair must be freed too. + if (captureBuffer_.getFbo() || blurBuffers_.getSourceTexture() || blurBuffers_.getDestTexture()) + releaseBuffers(); + return; + } + if (needsRealloc_ && viewportWidth_ > 0 && viewportHeight_ > 0) { + needsRealloc_ = false; // one attempt; no per-frame retry on alloc failure + int w = viewportWidth_, h = viewportHeight_; + viewportWidth_ = viewportHeight_ = 0; // force resize() past its early-out + resize(w, h); + hasValidCapture_ = false; + markBackgroundDirty(); + } +} + void AcrylicMaterial::captureBackground() { if (!initialized_ || shouldSkipEffect()) { return; } - if (viewportWidth_ <= 0 || viewportHeight_ <= 0) { + if (viewportWidth_ <= 0 || viewportHeight_ <= 0 || !captureBuffer_.isValid()) { return; } @@ -332,7 +363,7 @@ void AcrylicMaterial::captureBackgroundDirect() return; } - if (viewportWidth_ <= 0 || viewportHeight_ <= 0) { + if (viewportWidth_ <= 0 || viewportHeight_ <= 0 || !captureBuffer_.isValid()) { return; } @@ -362,7 +393,7 @@ void AcrylicMaterial::captureLiveFramebuffer() // inserted at the overlay window's draw-list start), so FB 0 already holds the app UI drawn // below the overlay. A paired ImDrawCallback_ResetRenderState restores ImGui's GL state. if (!initialized_ || shouldSkipEffect()) return; - if (viewportWidth_ <= 0 || viewportHeight_ <= 0) return; + if (viewportWidth_ <= 0 || viewportHeight_ <= 0 || !captureBuffer_.isValid()) return; glDisable(GL_SCISSOR_TEST); captureBuffer_.captureScreen(viewportWidth_, viewportHeight_); hasValidCapture_ = true; @@ -390,7 +421,7 @@ void AcrylicMaterial::setQuality(AcrylicQuality quality) void AcrylicMaterial::applyBlur(float radius, bool ignoreMultiplier) { - if (!blurBuffers_.isValid() || !blurShader_.isValid()) { + if (!blurBuffers_.isValid() || !blurShader_.isValid() || !captureBuffer_.isValid()) { return; } @@ -1188,7 +1219,7 @@ void AcrylicMaterial::resize(int width, int height) viewportHeight_ = height; blurCacheValid_ = false; - if (settings_.quality == AcrylicQuality::Off) return; + if (shouldSkipEffect()) { needsRealloc_ = true; return; } // targets freed/rebuilt by syncBufferLifetime() // (Re)create capture buffer at full resolution — must be B8G8R8A8 to match swap chain for CopyResource dx_releaseRenderTarget(dx_captureTex_, dx_captureSRV_, dx_captureRTV_); @@ -1215,10 +1246,43 @@ void AcrylicMaterial::resize(int width, int height) dx_blurWidth_ = bw; dx_blurHeight_ = bh; dx_blurCurrent_ = 0; + needsRealloc_ = false; dirtyFrames_ = 2; VERBOSE_LOGF("[Acrylic DX11] Resized %dx%d (blur %dx%d)\n", width, height, bw, bh); } +void AcrylicMaterial::releaseBuffers() +{ + dx_releaseRenderTarget(dx_captureTex_, dx_captureSRV_, dx_captureRTV_); + for (int i = 0; i < 2; ++i) + dx_releaseRenderTarget(dx_blurTex_[i], dx_blurSRV_[i], dx_blurRTV_[i]); + dx_blurWidth_ = dx_blurHeight_ = 0; + dx_blurCurrent_ = 0; + hasValidCapture_ = false; // drawRect -> drawTintedRect until a fresh capture + blurCacheValid_ = false; + needsRealloc_ = true; + DEBUG_LOGF("[Acrylic DX11] released targets (effect skipped)\n"); +} + +void AcrylicMaterial::syncBufferLifetime() +{ + if (!initialized_) return; + if (shouldSkipEffect()) { + // Per texture: each dx_createRenderTarget can fail on its own. + if (dx_captureTex_ || dx_blurTex_[0] || dx_blurTex_[1]) + releaseBuffers(); + return; + } + if (needsRealloc_ && viewportWidth_ > 0 && viewportHeight_ > 0) { + needsRealloc_ = false; // one attempt; no per-frame retry on alloc failure + int w = viewportWidth_, h = viewportHeight_; + viewportWidth_ = viewportHeight_ = 0; // force resize() past its early-out + resize(w, h); + hasValidCapture_ = false; + markBackgroundDirty(); + } +} + void AcrylicMaterial::captureBackground() { // Unused on DX11 — capture is done via captureBackgroundDirect() diff --git a/src/ui/effects/acrylic.h b/src/ui/effects/acrylic.h index 0efcc0d..a3486d1 100644 --- a/src/ui/effects/acrylic.h +++ b/src/ui/effects/acrylic.h @@ -194,6 +194,10 @@ public: /// has actually been rendered (theme switch happens mid-frame, before the /// BackgroundDrawList is rebuilt with new colors/images). void markBackgroundDirty() { dirtyFrames_ = 2; } + + /// Frame-boundary maintenance. Frees the capture/blur targets while the effect is skipped + /// (Off / disabled / reduced transparency) and rebuilds them when wanted again. + void syncBufferLifetime(); /** * @brief Render an acrylic-filled rectangle @@ -386,6 +390,8 @@ private: bool blurCacheValid_ = false; int dirtyFrames_ = 2; // >0 means recapture needed; counts down each frame bool hasValidCapture_ = false; // true once a clean BG capture exists + void releaseBuffers(); + bool needsRealloc_ = false; // targets were freed or left stale while the effect was skipped // Capture and blur framebuffers Framebuffer captureBuffer_; diff --git a/src/ui/effects/imgui_acrylic.cpp b/src/ui/effects/imgui_acrylic.cpp index 94715ba..65a2712 100644 --- a/src/ui/effects/imgui_acrylic.cpp +++ b/src/ui/effects/imgui_acrylic.cpp @@ -221,6 +221,11 @@ void InvalidateCapture() getAcrylicMaterial().markBackgroundDirty(); } +void ReleaseIdleBuffers() +{ + if (s_initialized) getAcrylicMaterial().syncBufferLifetime(); +} + // Draw callback: captures framebuffer 0 (which at this point during // RenderDrawData() contains only the rasterized BackgroundDrawList — // no UI windows yet) into the acrylic capture buffer. @@ -667,6 +672,7 @@ bool IsAvailable() { return false; } void BeginFrame(int, int) {} void CaptureBackground() {} void InvalidateCapture() {} +void ReleaseIdleBuffers() {} ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; } ImDrawCallback GetLiveCaptureCallback() { return nullptr; } diff --git a/src/ui/effects/imgui_acrylic.h b/src/ui/effects/imgui_acrylic.h index d56a817..cac9be2 100644 --- a/src/ui/effects/imgui_acrylic.h +++ b/src/ui/effects/imgui_acrylic.h @@ -78,6 +78,16 @@ void CaptureBackground(); */ void InvalidateCapture(); +/** + * @brief Frame-boundary maintenance: free the acrylic capture/blur targets + * while the effect is skipped (blur off, low-spec, reduced + * transparency) and rebuild them when it is wanted again. + * + * Call once per frame right after App::preFrame(), before NewFrame(), so + * nothing recorded in a draw list can still reference a freed target. + */ +void ReleaseIdleBuffers(); + /** * @brief Get a draw callback for background capture. *