// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "acrylic.h" #include "../schema/ui_schema.h" #include "../../util/noise_texture.h" #include "../../util/logger.h" #ifdef DRAGONX_HAS_GLAD #include // GL constants that may be missing from minimal GLAD builds #ifndef GL_VERSION #define GL_VERSION 0x1F02 #endif #ifndef GL_RENDERER #define GL_RENDERER 0x1F01 #endif #ifndef GL_MAX_TEXTURE_SIZE #define GL_MAX_TEXTURE_SIZE 0x0D33 #endif #ifndef GL_SCISSOR_TEST #define GL_SCISSOR_TEST 0x0C11 #endif #include #include #include #include namespace dragonx { namespace ui { namespace effects { // ============================================================================ // AcrylicMaterial Implementation (GLEW Available) // ============================================================================ AcrylicMaterial::AcrylicMaterial() = default; AcrylicMaterial::~AcrylicMaterial() { shutdown(); } bool AcrylicMaterial::init() { if (initialized_) return true; DEBUG_LOGF("AcrylicMaterial: Initializing...\n"); // Initialize blur shader if (!blurShader_.init()) { DEBUG_LOGF("AcrylicMaterial: Failed to init blur shader\n"); return false; } // Initialize fullscreen quad if (!quad_.init()) { DEBUG_LOGF("AcrylicMaterial: Failed to init fullscreen quad\n"); return false; } // Generate noise texture (texture size is a fixed default; // noise-tile-scale is read per-frame once schema is loaded) if (!noiseTexture_.generate(noiseTextureSize_, 0.5f)) { DEBUG_LOGF("AcrylicMaterial: Failed to generate noise texture\n"); return false; } // Detect system capabilities refreshCapabilities(); currentFallback_ = detectFallback(); initialized_ = true; DEBUG_LOGF("AcrylicMaterial: Initialized successfully (fallback mode: %d)\n", static_cast(currentFallback_)); return true; } void AcrylicMaterial::shutdown() { if (!initialized_) return; captureBuffer_.destroy(); blurBuffers_.destroy(); blurShader_.destroy(); quad_.destroy(); noiseTexture_.destroy(); initialized_ = false; viewportWidth_ = 0; viewportHeight_ = 0; DEBUG_LOGF("AcrylicMaterial: Shutdown complete\n"); } void AcrylicMaterial::resize(int width, int height) { if (width == viewportWidth_ && height == viewportHeight_) { return; // No change } 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; int blurHeight = height; switch (settings_.quality) { case AcrylicQuality::Low: blurWidth /= 4; blurHeight /= 4; break; case AcrylicQuality::Medium: blurWidth /= 2; blurHeight /= 2; break; case AcrylicQuality::High: // Full resolution break; case AcrylicQuality::Off: // No blur buffers needed return; } // Ensure minimum size blurWidth = std::max(64, blurWidth); blurHeight = std::max(64, blurHeight); // Initialize/resize buffers if (!captureBuffer_.isValid()) { captureBuffer_.init(width, height); } else { captureBuffer_.resize(width, height); } if (!blurBuffers_.isValid()) { blurBuffers_.init(blurWidth, blurHeight, true); // float16 to avoid banding } else { blurBuffers_.resize(blurWidth, blurHeight); } 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 || !captureBuffer_.isValid()) { return; } // P7: Only recapture when the background has been marked dirty if (dirtyFrames_ <= 0) { return; } --dirtyFrames_; // Capture current screen content captureBuffer_.captureScreen(captureSrcWidth(), captureSrcHeight()); hasValidCapture_ = true; blurCacheValid_ = false; // New capture invalidates blur cache } void AcrylicMaterial::captureBackgroundDirect() { if (!initialized_ || shouldSkipEffect()) { return; } if (viewportWidth_ <= 0 || viewportHeight_ <= 0 || !captureBuffer_.isValid()) { return; } // Only recapture when dirty (resize, theme change, first frame). if (dirtyFrames_ <= 0) { return; } --dirtyFrames_; // At this point during RenderDrawData(), only the background draw // list has been rasterized to framebuffer 0 — the UI windows have // not been drawn yet. blit from FB 0 into our capture buffer. // Disable scissor test: glBlitFramebuffer applies scissor to the // draw framebuffer, and ImGui has scissor enabled during rendering. // The subsequent ImDrawCallback_ResetRenderState re-enables it. glDisable(GL_SCISSOR_TEST); captureBuffer_.captureScreen(captureSrcWidth(), captureSrcHeight()); hasValidCapture_ = true; blurCacheValid_ = false; // force re-blur with fresh capture } void AcrylicMaterial::captureLiveFramebuffer() { // Like captureBackgroundDirect but with NO dirtyFrames_ gate — refresh every frame the // overlay is open so the blur tracks the live UI. Fires mid-RenderDrawData (from a callback // 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 || !captureBuffer_.isValid()) return; glDisable(GL_SCISSOR_TEST); captureBuffer_.captureScreen(captureSrcWidth(), captureSrcHeight()); hasValidCapture_ = true; blurCacheValid_ = false; // force re-blur with the fresh live capture } bool AcrylicMaterial::shouldSkipEffect() const { return !settings_.enabled || settings_.reducedTransparency || settings_.quality == AcrylicQuality::Off; } void AcrylicMaterial::setQuality(AcrylicQuality quality) { if (settings_.quality != quality) { settings_.quality = quality; blurCacheValid_ = false; // Quality change invalidates cache // Trigger resize to adjust buffer sizes int w = viewportWidth_, h = viewportHeight_; viewportWidth_ = viewportHeight_ = 0; resize(w, h); } } void AcrylicMaterial::applyBlur(float radius, bool ignoreMultiplier) { if (!blurBuffers_.isValid() || !blurShader_.isValid() || !captureBuffer_.isValid()) { return; } // Apply the user's blur-strength multiplier — unless the caller wants an absolute radius. float scaledRadius = ignoreMultiplier ? radius : radius * settings_.blurRadiusMultiplier; // Skip blur entirely when multiplier is at or near zero — show sharp background if (scaledRadius < 0.5f) { // Still blit capture to blur source so getBlurredTexture() returns // the un-blurred captured background int blurW = blurBuffers_.getSource().getWidth(); int blurH = blurBuffers_.getSource().getHeight(); glDisable(GL_SCISSOR_TEST); glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND); GLuint srcFbo = captureBuffer_.getFbo(); GLuint dstFbo = blurBuffers_.getSource().getFbo(); glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dstFbo); glBlitFramebuffer(0, 0, viewportWidth_, viewportHeight_, 0, 0, blurW, blurH, GL_COLOR_BUFFER_BIT, GL_LINEAR); glBindFramebuffer(GL_FRAMEBUFFER, 0); lastBlurRadius_ = scaledRadius; blurCacheValid_ = true; return; } // Check cache - skip blur if same radius and cache is valid if (blurCacheValid_ && std::abs(scaledRadius - lastBlurRadius_) < 0.1f) { return; // Use cached blur result } int blurWidth = blurBuffers_.getSource().getWidth(); int blurHeight = blurBuffers_.getSource().getHeight(); // Determine number of passes based on quality int passes = 1; float effectiveRadius = scaledRadius; switch (settings_.quality) { case AcrylicQuality::Low: passes = 1; effectiveRadius = scaledRadius * 0.5f; break; case AcrylicQuality::Medium: passes = 2; break; case AcrylicQuality::High: passes = 3; break; default: return; } // Copy captured content into the first blur buffer, scaling down // from full viewport resolution to the blur buffer resolution. // Disable scissor/depth/blend BEFORE the blit — glBlitFramebuffer // applies scissor to the draw FBO, and a stale rect would clip it. glDisable(GL_SCISSOR_TEST); glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND); { GLuint srcFbo = captureBuffer_.getFbo(); GLuint dstFbo = blurBuffers_.getSource().getFbo(); glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dstFbo); glBlitFramebuffer( 0, 0, viewportWidth_, viewportHeight_, // src rect (full res) 0, 0, blurWidth, blurHeight, // dst rect (blur res) GL_COLOR_BUFFER_BIT, GL_LINEAR); glBindFramebuffer(GL_FRAMEBUFFER, 0); } blurShader_.bind(); blurShader_.setResolution(blurWidth, blurHeight); blurShader_.setTexture(0); for (int pass = 0; pass < passes; pass++) { float passRadius = effectiveRadius / passes; blurShader_.setRadius(passRadius); // Horizontal pass blurBuffers_.getDest().bind(); blurShader_.setDirection(true); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, blurBuffers_.getSourceTexture()); quad_.draw(); blurBuffers_.swap(); // Vertical pass blurBuffers_.getDest().bind(); blurShader_.setDirection(false); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, blurBuffers_.getSourceTexture()); quad_.draw(); blurBuffers_.swap(); } blurShader_.unbind(); blurBuffers_.getDest().unbind(); // Restore viewport glViewport(0, 0, viewportWidth_, viewportHeight_); static bool s_blurRunTraced = false; if (!s_blurRunTraced) { s_blurRunTraced = true; DEBUG_LOGF("[Acrylic] applyBlur: %d passes, radius=%.1f, blurSize=%dx%d " "srcFBO=%u dstTex=%u\n", passes, effectiveRadius, blurWidth, blurHeight, captureBuffer_.getFbo(), blurBuffers_.getSourceTexture()); } // Update cache state lastBlurRadius_ = scaledRadius; blurCacheValid_ = true; } ImTextureID AcrylicMaterial::getBlurredTexture() const { return (ImTextureID)(intptr_t)blurBuffers_.getSourceTexture(); } ImTextureID AcrylicMaterial::getNoiseTexture() const { return (ImTextureID)(intptr_t)noiseTexture_.getTexture(); } void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax, const AcrylicParams& params, float rounding) { if (!initialized_) return; // Update fallback mode (could change based on settings) currentFallback_ = detectFallback(); // Fallback solid fill — scaled by UI opacity for card transparency auto scaledFallback = [&]() { ImVec4 c = params.fallbackColor; c.w *= settings_.uiOpacity; return ImGui::ColorConvertFloat4ToU32(c); }; // If acrylic is disabled in params, always use solid fallback if (!params.enabled) { drawList->AddRectFilled(pMin, pMax, scaledFallback(), rounding); return; } // Handle fallback modes switch (currentFallback_) { case AcrylicFallback::Solid: drawList->AddRectFilled(pMin, pMax, scaledFallback(), rounding); return; case AcrylicFallback::TintedOnly: drawTintedRect(drawList, pMin, pMax, params, rounding); return; case AcrylicFallback::None: default: // Continue with full acrylic rendering break; } // No valid background capture yet (first frame before the // BackgroundDrawList callback has fired). Fall back to tinted. if (!hasValidCapture_) { drawTintedRect(drawList, pMin, pMax, params, rounding); return; } // Apply blur to captured background applyBlur(params.blurRadius, params.absoluteBlurRadius); // Calculate UV coordinates for sampling the blur FBO texture. // With multi-viewport enabled, ImGui draw coordinates are in // absolute OS screen space. The FBO capture covers only the main // window, so we must subtract the main viewport's screen position // to get window-local coordinates before dividing by viewport size. // OpenGL FBO textures have V=0 at the bottom and V=1 at the top. // ImGui screen coordinates have Y=0 at the top and Y=H at bottom. // So texture V for screen Y is: V = 1.0 - localY/H ImGuiViewport* mainVp = ImGui::GetMainViewport(); float localX0 = pMin.x - mainVp->Pos.x; float localY0 = pMin.y - mainVp->Pos.y; float localX1 = pMax.x - mainVp->Pos.x; float localY1 = pMax.y - mainVp->Pos.y; float u0 = localX0 / viewportWidth_; float v0 = 1.0f - localY0 / viewportHeight_; // V at top-left (high) float u1 = localX1 / viewportWidth_; float v1 = 1.0f - localY1 / viewportHeight_; // V at bottom-right (low) // Draw the blurred background at (near) full strength so the panel is actually FROSTED — the // blurred capture replaces the sharp background in the card. UI opacity is applied only to the // tint overlay (in DrawGlassPanel), NOT here: scaling the blur by uiOpacity used to let the // SHARP background bleed straight through at low opacity, which made the acrylic slider look // like a no-op. A small floor keeps it frosted even for low-alpha presets. ImTextureID blurTex = getBlurredTexture(); uint8_t glassAlpha = static_cast( std::min(255.0f, std::max(0.0f, std::max(params.fallbackColor.w, 0.90f) * 255.0f))); if (blurTex) { drawList->AddImageRounded( blurTex, pMin, pMax, ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32(255, 255, 255, glassAlpha), rounding ); } // Noise grain overlay — single draw call via pre-tiled texture if (params.noiseOpacity > 0.0f && settings_.noiseOpacityMultiplier > 0.0f) { float maxAlpha = schema::UI().drawElement("backdrop", "noise-max-alpha").sizeOr(6.0f); float scaledNoise = params.noiseOpacity * settings_.noiseOpacityMultiplier; uint8_t alpha = static_cast(std::min(255.0f, maxAlpha * scaledNoise * 50.0f)); ImU32 col = IM_COL32(255, 255, 255, alpha); drawList->PushClipRect(pMin, pMax, true); dragonx::util::DrawTiledNoiseRect(drawList, pMin, pMax, col); drawList->PopClipRect(); } } // ============================================================================ // Preset Parameters // ============================================================================ AcrylicParams AcrylicMaterial::getDarkPreset() { AcrylicParams params; params.tintColor = ImVec4(0.1f, 0.1f, 0.12f, 0.95f); params.tintOpacity = 0.75f; params.luminosityOpacity = 0.6f; params.blurRadius = 30.0f; params.noiseOpacity = 0.02f; params.fallbackColor = ImVec4(0.12f, 0.12f, 0.14f, 0.82f); return params; } AcrylicParams AcrylicMaterial::getLightPreset() { AcrylicParams params; params.tintColor = ImVec4(0.95f, 0.95f, 0.97f, 0.95f); params.tintOpacity = 0.80f; params.luminosityOpacity = 0.7f; params.blurRadius = 25.0f; params.noiseOpacity = 0.02f; params.fallbackColor = ImVec4(0.96f, 0.96f, 0.98f, 0.82f); return params; } AcrylicParams AcrylicMaterial::getDragonXPreset() { AcrylicParams params; // Deep dragon red tint params.tintColor = ImVec4(0.6f, 0.15f, 0.1f, 0.95f); // #993319 params.tintOpacity = 0.85f; params.luminosityOpacity = 0.3f; // More desaturated for dramatic effect params.blurRadius = 40.0f; params.noiseOpacity = 0.025f; params.fallbackColor = ImVec4(0.5f, 0.12f, 0.08f, 0.82f); return params; } AcrylicParams AcrylicMaterial::getPopupPreset() { AcrylicParams params; params.tintColor = ImVec4(0.15f, 0.15f, 0.18f, 0.95f); params.tintOpacity = 0.70f; params.luminosityOpacity = 0.5f; params.blurRadius = 30.0f; params.noiseOpacity = 0.02f; params.fallbackColor = ImVec4(0.18f, 0.18f, 0.2f, 0.88f); return params; } AcrylicParams AcrylicMaterial::getSidebarPreset() { AcrylicParams params; params.tintColor = ImVec4(0.08f, 0.08f, 0.1f, 0.95f); params.tintOpacity = 0.80f; params.luminosityOpacity = 0.4f; params.blurRadius = 35.0f; params.noiseOpacity = 0.02f; params.fallbackColor = ImVec4(0.1f, 0.1f, 0.12f, 0.85f); return params; } // ============================================================================ // Fallback System // ============================================================================ void AcrylicMaterial::refreshCapabilities() { // Get OpenGL info const char* version = reinterpret_cast(glGetString(GL_VERSION)); const char* renderer = reinterpret_cast(glGetString(GL_RENDERER)); capabilities_.glVersion = version ? version : "Unknown"; capabilities_.glRenderer = renderer ? renderer : "Unknown"; // Get max texture size glGetIntegerv(GL_MAX_TEXTURE_SIZE, &capabilities_.maxTextureSize); // Check for required features capabilities_.hasFramebufferSupport = (glGenFramebuffers != nullptr && glBindFramebuffer != nullptr); capabilities_.hasShaderSupport = (glCreateShader != nullptr && glCreateProgram != nullptr); capabilities_.hasTextureSupport = (capabilities_.maxTextureSize >= 1024); // Detect low-end GPU based on renderer string std::string rendererLower = capabilities_.glRenderer; std::transform(rendererLower.begin(), rendererLower.end(), rendererLower.begin(), ::tolower); capabilities_.isLowEndGPU = (rendererLower.find("intel") != std::string::npos && rendererLower.find("hd") != std::string::npos) || (rendererLower.find("mesa") != std::string::npos && rendererLower.find("llvmpipe") != std::string::npos) || (rendererLower.find("software") != std::string::npos) || (rendererLower.find("swrast") != std::string::npos); // Check battery status on Linux capabilities_.isOnBattery = false; #ifdef __linux__ // Check /sys/class/power_supply/*/status FILE* f = fopen("/sys/class/power_supply/BAT0/status", "r"); if (f) { char status[32] = {0}; if (fgets(status, sizeof(status), f)) { capabilities_.isOnBattery = (strncmp(status, "Discharging", 11) == 0); } fclose(f); } #endif DEBUG_LOGF("AcrylicMaterial: Capabilities detected:\n"); DEBUG_LOGF(" GL Version: %s\n", capabilities_.glVersion.c_str()); DEBUG_LOGF(" GL Renderer: %s\n", capabilities_.glRenderer.c_str()); DEBUG_LOGF(" Max Texture Size: %d\n", capabilities_.maxTextureSize); DEBUG_LOGF(" Framebuffer Support: %s\n", capabilities_.hasFramebufferSupport ? "Yes" : "No"); DEBUG_LOGF(" Shader Support: %s\n", capabilities_.hasShaderSupport ? "Yes" : "No"); DEBUG_LOGF(" Low-End GPU: %s\n", capabilities_.isLowEndGPU ? "Yes" : "No"); DEBUG_LOGF(" On Battery: %s\n", capabilities_.isOnBattery ? "Yes" : "No"); } AcrylicFallback AcrylicMaterial::detectFallback() const { // Check if forced fallback is set if (hasForcedFallback_) { return forcedFallback_; } // User preference: reduced transparency if (settings_.reducedTransparency) { return AcrylicFallback::Solid; } // Check OpenGL capabilities if (!capabilities_.hasFramebufferSupport || !capabilities_.hasShaderSupport) { return AcrylicFallback::Solid; } // Texture size too small if (capabilities_.maxTextureSize < 1024) { return AcrylicFallback::Solid; } // Quality set to Off if (settings_.quality == AcrylicQuality::Off) { return AcrylicFallback::TintedOnly; } // Low-end GPU detection if (capabilities_.isLowEndGPU && settings_.quality == AcrylicQuality::High) { return AcrylicFallback::TintedOnly; } // Battery mode (if user enabled) if (settings_.disableOnBattery && capabilities_.isOnBattery) { return AcrylicFallback::TintedOnly; } // Global disable if (!settings_.enabled) { return AcrylicFallback::TintedOnly; } return AcrylicFallback::None; } void AcrylicMaterial::drawTintedRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax, const AcrylicParams& params, float rounding) { // Draw semi-transparent tint without blur — scale by UI opacity // so the tinted fallback also respects the card transparency slider. ImU32 tintCol = ImGui::ColorConvertFloat4ToU32( ImVec4(params.tintColor.x, params.tintColor.y, params.tintColor.z, params.tintColor.w * params.tintOpacity * 0.9f * settings_.uiOpacity) ); drawList->AddRectFilled(pMin, pMax, tintCol, rounding); // Still add noise overlay for visual interest (W11-style) ImTextureID noiseTex = getNoiseTexture(); if (noiseTex && params.noiseOpacity > 0.0f && settings_.noiseOpacityMultiplier > 0.0f) { float maxAlpha = schema::UI().drawElement("backdrop", "noise-max-alpha").sizeOr(6.0f); float scaledNoise = params.noiseOpacity * settings_.noiseOpacityMultiplier; uint8_t alpha = static_cast(std::min(255.0f, maxAlpha * scaledNoise * 50.0f)); float tilePx = static_cast(noiseTextureSize_); float uvScale = 1.0f / tilePx; ImVec2 uv0(pMin.x * uvScale, pMin.y * uvScale); ImVec2 uv1(pMax.x * uvScale, pMax.y * uvScale); drawList->AddImageRounded( noiseTex, pMin, pMax, uv0, uv1, IM_COL32(255, 255, 255, alpha), rounding ); } } // ============================================================================ // Global Instance // ============================================================================ AcrylicMaterial& getAcrylicMaterial() { static AcrylicMaterial instance; return instance; } } // namespace effects } // namespace ui } // namespace dragonx #else // !DRAGONX_HAS_GLAD // ============================================================================ // DX11 Implementation (Windows) // ============================================================================ #ifdef DRAGONX_USE_DX11 #include #include #include #include #include #include #include // Get D3D11 device from ImGui backend (same pattern as texture_loader.cpp) static ID3D11Device* GetImGuiD3D11Device() { ImGuiIO& io = ImGui::GetIO(); if (!io.BackendRendererUserData) return nullptr; return *reinterpret_cast(io.BackendRendererUserData); } static ID3D11DeviceContext* GetImGuiD3D11DeviceContext() { ID3D11Device* dev = GetImGuiD3D11Device(); if (!dev) return nullptr; ID3D11DeviceContext* ctx = nullptr; dev->GetImmediateContext(&ctx); // GetImmediateContext adds a ref, release the extra ref. if (ctx) ctx->Release(); return ctx; } namespace dragonx { namespace ui { namespace effects { // ---- HLSL shader source (compiled at runtime via D3DCompile) ---- static const char* DX_BLUR_VS = R"hlsl( struct VS_IN { float2 pos : POSITION; float2 uv : TEXCOORD0; }; struct VS_OUT { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; VS_OUT main(VS_IN i) { VS_OUT o; o.pos = float4(i.pos, 0.0, 1.0); o.uv = i.uv; return o; } )hlsl"; static const char* DX_BLUR_PS = R"hlsl( cbuffer BlurCB : register(b0) { float2 direction; float2 resolution; float radius; // Gaussian sigma in texels float3 _pad; }; Texture2D tex : register(t0); SamplerState smp : register(s0); // Screen-space hash for dithering (returns 0..1) float hash12(float2 p) { float3 p3 = frac(float3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return frac((p3.x + p3.y) * p3.z); } float4 main(float4 svpos : SV_POSITION, float2 uv : TEXCOORD0) : SV_TARGET { float2 texelSize = 1.0 / resolution; float2 step = direction * texelSize; // Dynamic Gaussian kernel: radius = sigma, extend to ~2.5 sigma float sigma = max(radius, 0.5); int iRadius = clamp(int(sigma * 2.5 + 0.5), 1, 40); float inv2sigma2 = 1.0 / (2.0 * sigma * sigma); float3 sum = float3(0, 0, 0); float wSum = 0; [loop] for (int i = -iRadius; i <= iRadius; i++) { float w = exp(-float(i * i) * inv2sigma2); sum += tex.Sample(smp, uv + step * float(i)).rgb * w; wSum += w; } float3 color = sum / wSum; // Triangular-PDF dithering to break up 8-bit quantization banding. // Two uniform hashes combined give a triangular distribution (-1..+1). float d = hash12(svpos.xy) + hash12(svpos.xy + 71.37) - 1.0; color += d / 255.0; return float4(color, 1.0); } )hlsl"; // ---- Constant buffer layout (must be 16-byte aligned) ---- struct BlurCB { float dirX, dirY; float resX, resY; float radius; float _pad[3]; }; // ---------------------------------------------------------------- // Helper: create a 2-D render-target + SRV pair // ---------------------------------------------------------------- void AcrylicMaterial::dx_createRenderTarget( ID3D11Texture2D*& tex, ID3D11ShaderResourceView*& srv, ID3D11RenderTargetView*& rtv, int w, int h, DXGI_FORMAT format) { D3D11_TEXTURE2D_DESC td = {}; td.Width = w; td.Height = h; td.MipLevels = 1; td.ArraySize = 1; td.Format = format; td.SampleDesc.Count = 1; td.Usage = D3D11_USAGE_DEFAULT; td.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; HRESULT hr = dx_device_->CreateTexture2D(&td, nullptr, &tex); if (FAILED(hr)) { DEBUG_LOGF("[Acrylic DX11] CreateTexture2D failed 0x%08lx\n", hr); return; } dx_device_->CreateShaderResourceView(tex, nullptr, &srv); dx_device_->CreateRenderTargetView(tex, nullptr, &rtv); } void AcrylicMaterial::dx_releaseRenderTarget( ID3D11Texture2D*& tex, ID3D11ShaderResourceView*& srv, ID3D11RenderTargetView*& rtv) { if (srv) { srv->Release(); srv = nullptr; } if (rtv) { rtv->Release(); rtv = nullptr; } if (tex) { tex->Release(); tex = nullptr; } } // ---------------------------------------------------------------- // Compile shaders, create pipeline objects // ---------------------------------------------------------------- bool AcrylicMaterial::dx_initPipeline() { HRESULT hr; // ---- Compile vertex shader ---- ID3DBlob* vsBlob = nullptr; ID3DBlob* errBlob = nullptr; hr = D3DCompile(DX_BLUR_VS, strlen(DX_BLUR_VS), "blur_vs", nullptr, nullptr, "main", "vs_4_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vsBlob, &errBlob); if (FAILED(hr)) { if (errBlob) { DEBUG_LOGF("[Acrylic DX11] VS compile: %s\n", (char*)errBlob->GetBufferPointer()); errBlob->Release(); } return false; } hr = dx_device_->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &dx_blurVS_); if (FAILED(hr)) { vsBlob->Release(); return false; } // ---- Input layout ---- D3D11_INPUT_ELEMENT_DESC layout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 8, D3D11_INPUT_PER_VERTEX_DATA, 0 }, }; hr = dx_device_->CreateInputLayout(layout, 2, vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), &dx_inputLayout_); vsBlob->Release(); if (FAILED(hr)) return false; // ---- Compile pixel shader ---- ID3DBlob* psBlob = nullptr; hr = D3DCompile(DX_BLUR_PS, strlen(DX_BLUR_PS), "blur_ps", nullptr, nullptr, "main", "ps_4_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &psBlob, &errBlob); if (FAILED(hr)) { if (errBlob) { DEBUG_LOGF("[Acrylic DX11] PS compile: %s\n", (char*)errBlob->GetBufferPointer()); errBlob->Release(); } return false; } hr = dx_device_->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &dx_blurPS_); psBlob->Release(); if (FAILED(hr)) return false; // ---- Constant buffer ---- D3D11_BUFFER_DESC cbd = {}; cbd.ByteWidth = sizeof(BlurCB); cbd.Usage = D3D11_USAGE_DYNAMIC; cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; hr = dx_device_->CreateBuffer(&cbd, nullptr, &dx_blurCB_); if (FAILED(hr)) return false; // ---- Fullscreen quad vertex buffer ---- struct Vert { float x, y, u, v; }; Vert verts[] = { {-1, 1, 0, 0}, {-1,-1, 0, 1}, { 1,-1, 1, 1}, {-1, 1, 0, 0}, { 1,-1, 1, 1}, { 1, 1, 1, 0}, }; D3D11_BUFFER_DESC vbd = {}; vbd.ByteWidth = sizeof(verts); vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; D3D11_SUBRESOURCE_DATA vi = {}; vi.pSysMem = verts; hr = dx_device_->CreateBuffer(&vbd, &vi, &dx_vertexBuf_); if (FAILED(hr)) return false; // ---- Linear clamp sampler ---- D3D11_SAMPLER_DESC sd = {}; sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; hr = dx_device_->CreateSamplerState(&sd, &dx_sampler_); if (FAILED(hr)) return false; // ---- Rasterizer state: scissor OFF, no culling, solid fill ---- D3D11_RASTERIZER_DESC rd = {}; rd.FillMode = D3D11_FILL_SOLID; rd.CullMode = D3D11_CULL_NONE; rd.ScissorEnable = FALSE; rd.DepthClipEnable = TRUE; hr = dx_device_->CreateRasterizerState(&rd, &dx_blurRS_); if (FAILED(hr)) return false; // ---- Blend state: opaque overwrite (no alpha blending) ---- D3D11_BLEND_DESC bd = {}; bd.RenderTarget[0].BlendEnable = FALSE; bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; hr = dx_device_->CreateBlendState(&bd, &dx_blurBS_); if (FAILED(hr)) return false; // ---- Depth-stencil state: depth test OFF ---- D3D11_DEPTH_STENCIL_DESC dsd = {}; dsd.DepthEnable = FALSE; dsd.StencilEnable = FALSE; hr = dx_device_->CreateDepthStencilState(&dsd, &dx_blurDSS_); if (FAILED(hr)) return false; DEBUG_LOGF("[Acrylic DX11] Pipeline created successfully\n"); return true; } void AcrylicMaterial::dx_releasePipeline() { if (dx_blurVS_) { dx_blurVS_->Release(); dx_blurVS_ = nullptr; } if (dx_blurPS_) { dx_blurPS_->Release(); dx_blurPS_ = nullptr; } if (dx_inputLayout_) { dx_inputLayout_->Release(); dx_inputLayout_ = nullptr; } if (dx_blurCB_) { dx_blurCB_->Release(); dx_blurCB_ = nullptr; } if (dx_vertexBuf_) { dx_vertexBuf_->Release(); dx_vertexBuf_ = nullptr; } if (dx_sampler_) { dx_sampler_->Release(); dx_sampler_ = nullptr; } if (dx_blurRS_) { dx_blurRS_->Release(); dx_blurRS_ = nullptr; } if (dx_blurBS_) { dx_blurBS_->Release(); dx_blurBS_ = nullptr; } if (dx_blurDSS_) { dx_blurDSS_->Release(); dx_blurDSS_ = nullptr; } 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]); if (dx_noiseSRV_) { dx_noiseSRV_->Release(); dx_noiseSRV_ = nullptr; } if (dx_noiseTex_) { dx_noiseTex_->Release(); dx_noiseTex_ = nullptr; } } // ============================================================================ // AcrylicMaterial — DX11 implementations of the public API // ============================================================================ AcrylicMaterial::AcrylicMaterial() = default; AcrylicMaterial::~AcrylicMaterial() { shutdown(); } bool AcrylicMaterial::init() { if (initialized_) return true; dx_device_ = GetImGuiD3D11Device(); dx_context_ = GetImGuiD3D11DeviceContext(); if (!dx_device_ || !dx_context_) { DEBUG_LOGF("[Acrylic DX11] No D3D11 device available\n"); return false; } if (!dx_initPipeline()) { DEBUG_LOGF("[Acrylic DX11] Pipeline init failed\n"); dx_releasePipeline(); return false; } // Generate noise texture (CPU-side, then upload) // Pure white noise — no coherent patterns, perfectly seamless tiling { const int SZ = noiseTextureSize_; std::vector data(SZ * SZ * 4); uint32_t seed = 12345; auto rng = [&]() -> float { seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5; return float(seed) / float(0xFFFFFFFF); }; for (int y = 0; y < SZ; ++y) for (int x = 0; x < SZ; ++x) { float v = 0.5f + (rng() - 0.5f) * 0.5f; v = std::max(0.f, std::min(1.f, v)); uint8_t g = uint8_t(v * 255.f); int i = (y * SZ + x) * 4; data[i] = g; data[i+1] = g; data[i+2] = g; data[i+3] = 255; } D3D11_TEXTURE2D_DESC td = {}; td.Width = SZ; td.Height = SZ; td.MipLevels = 1; td.ArraySize = 1; td.Format = DXGI_FORMAT_R8G8B8A8_UNORM; td.SampleDesc.Count = 1; td.Usage = D3D11_USAGE_DEFAULT; td.BindFlags = D3D11_BIND_SHADER_RESOURCE; D3D11_SUBRESOURCE_DATA init = {}; init.pSysMem = data.data(); init.SysMemPitch = SZ * 4; dx_device_->CreateTexture2D(&td, &init, &dx_noiseTex_); if (dx_noiseTex_) dx_device_->CreateShaderResourceView(dx_noiseTex_, nullptr, &dx_noiseSRV_); } // Detect capabilities (simplified for DX11 — always capable) refreshCapabilities(); currentFallback_ = detectFallback(); initialized_ = true; DEBUG_LOGF("[Acrylic DX11] Initialized (fallback=%d)\n", (int)currentFallback_); return true; } void AcrylicMaterial::shutdown() { if (!initialized_) return; dx_releasePipeline(); dx_device_ = nullptr; dx_context_ = nullptr; initialized_ = false; viewportWidth_ = viewportHeight_ = 0; DEBUG_LOGF("[Acrylic DX11] Shutdown\n"); } void AcrylicMaterial::resize(int width, int height) { if (width == viewportWidth_ && height == viewportHeight_) return; viewportWidth_ = width; viewportHeight_ = height; blurCacheValid_ = false; 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_); dx_createRenderTarget(dx_captureTex_, dx_captureSRV_, dx_captureRTV_, width, height, DXGI_FORMAT_B8G8R8A8_UNORM); // Blur buffers — size depends on quality int bw = width, bh = height; switch (settings_.quality) { case AcrylicQuality::Low: bw /= 4; bh /= 4; break; case AcrylicQuality::Medium: bw /= 2; bh /= 2; break; default: break; } bw = std::max(64, bw); bh = std::max(64, bh); for (int i = 0; i < 2; ++i) dx_releaseRenderTarget(dx_blurTex_[i], dx_blurSRV_[i], dx_blurRTV_[i]); // Use R16G16B16A16_FLOAT for blur intermediates to avoid banding from precision loss for (int i = 0; i < 2; ++i) dx_createRenderTarget(dx_blurTex_[i], dx_blurSRV_[i], dx_blurRTV_[i], bw, bh, DXGI_FORMAT_R16G16B16A16_FLOAT); 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() } void AcrylicMaterial::captureBackgroundDirect() { if (!initialized_ || shouldSkipEffect()) return; if (viewportWidth_ <= 0 || viewportHeight_ <= 0) return; if (dirtyFrames_ <= 0) return; --dirtyFrames_; if (!dx_captureTex_ || !dx_context_) return; // Get the current render target (swap-chain back buffer) and // copy it into our capture texture. ID3D11RenderTargetView* curRTV = nullptr; dx_context_->OMGetRenderTargets(1, &curRTV, nullptr); if (!curRTV) return; ID3D11Resource* bbRes = nullptr; curRTV->GetResource(&bbRes); curRTV->Release(); if (!bbRes) return; dx_context_->CopyResource(dx_captureTex_, bbRes); bbRes->Release(); static bool s_traced = false; if (!s_traced) { s_traced = true; DEBUG_LOGF("[Acrylic DX11] captureBackgroundDirect OK %dx%d\n", viewportWidth_, viewportHeight_); } hasValidCapture_ = true; blurCacheValid_ = false; } void AcrylicMaterial::captureLiveFramebuffer() { // DX11 twin of the GL live capture: copy the currently-bound backbuffer (which holds the app // UI drawn below the overlay) into the capture texture, with NO dirtyFrames_ gate. if (!initialized_ || shouldSkipEffect()) return; if (viewportWidth_ <= 0 || viewportHeight_ <= 0) return; if (!dx_captureTex_ || !dx_context_) return; ID3D11RenderTargetView* curRTV = nullptr; dx_context_->OMGetRenderTargets(1, &curRTV, nullptr); if (!curRTV) return; ID3D11Resource* bbRes = nullptr; curRTV->GetResource(&bbRes); curRTV->Release(); if (!bbRes) return; dx_context_->CopyResource(dx_captureTex_, bbRes); bbRes->Release(); hasValidCapture_ = true; blurCacheValid_ = false; } void AcrylicMaterial::applyBlur(float radius, bool ignoreMultiplier) { if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return; float scaledRadius = ignoreMultiplier ? radius : radius * settings_.blurRadiusMultiplier; if (blurCacheValid_ && std::abs(scaledRadius - lastBlurRadius_) < 0.1f) return; int passes = 1; float effectiveRadius = scaledRadius; switch (settings_.quality) { case AcrylicQuality::Low: passes = 1; effectiveRadius *= 0.5f; break; case AcrylicQuality::Medium: passes = 2; break; case AcrylicQuality::High: passes = 3; break; default: return; } // ---- Save D3D11 state that ImGui might need later ---- ID3D11RenderTargetView* savedRTV = nullptr; ID3D11DepthStencilView* savedDSV = nullptr; ID3D11RasterizerState* savedRS = nullptr; ID3D11BlendState* savedBS = nullptr; FLOAT savedBlendFactor[4] = {}; UINT savedSampleMask = 0; ID3D11DepthStencilState* savedDSS = nullptr; UINT savedStencilRef = 0; D3D11_VIEWPORT savedVP = {}; UINT numVP = 1; dx_context_->OMGetRenderTargets(1, &savedRTV, &savedDSV); dx_context_->RSGetState(&savedRS); dx_context_->OMGetBlendState(&savedBS, savedBlendFactor, &savedSampleMask); dx_context_->OMGetDepthStencilState(&savedDSS, &savedStencilRef); dx_context_->RSGetViewports(&numVP, &savedVP); // ---- Setup blur pipeline ---- dx_context_->IASetInputLayout(dx_inputLayout_); dx_context_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); UINT stride = 16, offset = 0; dx_context_->IASetVertexBuffers(0, 1, &dx_vertexBuf_, &stride, &offset); dx_context_->VSSetShader(dx_blurVS_, nullptr, 0); dx_context_->PSSetShader(dx_blurPS_, nullptr, 0); dx_context_->PSSetSamplers(0, 1, &dx_sampler_); dx_context_->PSSetConstantBuffers(0, 1, &dx_blurCB_); // Set clean rasterizer/blend/depth states for blur passes. // ImGui leaves ScissorEnable=TRUE with a small scissor rect which // would clip our fullscreen quads; alpha-blending would blend with // stale buffer contents instead of overwriting. dx_context_->RSSetState(dx_blurRS_); dx_context_->OMSetBlendState(dx_blurBS_, nullptr, 0xFFFFFFFF); dx_context_->OMSetDepthStencilState(dx_blurDSS_, 0); // Viewport for blur buffers D3D11_VIEWPORT vp = {}; vp.Width = (float)dx_blurWidth_; vp.Height = (float)dx_blurHeight_; vp.MaxDepth = 1.0f; dx_context_->RSSetViewports(1, &vp); // First pass: downsample capture → blurBuf[0] using a pass-through // (horizontal blur with radius → serves as downscale + first H-pass) dx_blurCurrent_ = 0; // Initial downscale: capture → blur[0] (horizontal pass) // Use blur-buffer resolution for step size so the blur kernel covers the // same fraction of the image as subsequent passes. The linear sampler // handles the implicit downscale from capture resolution. Using the // capture resolution here would make each step only 1 capture texel, // producing a horizontal blur ~Nx weaker than the vertical passes at // blur resolution (N = downscale factor). { // Unbind blur[0] as input if it's bound ID3D11ShaderResourceView* nullSRV = nullptr; dx_context_->PSSetShaderResources(0, 1, &nullSRV); dx_context_->OMSetRenderTargets(1, &dx_blurRTV_[0], nullptr); dx_context_->PSSetShaderResources(0, 1, &dx_captureSRV_); D3D11_MAPPED_SUBRESOURCE mapped; dx_context_->Map(dx_blurCB_, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); BlurCB* cb = (BlurCB*)mapped.pData; cb->dirX = 1.f; cb->dirY = 0.f; cb->resX = (float)dx_blurWidth_; cb->resY = (float)dx_blurHeight_; cb->radius = effectiveRadius / passes; dx_context_->Unmap(dx_blurCB_, 0); dx_context_->Draw(6, 0); } // Remaining blur passes on the blur-sized buffers for (int pass = 0; pass < passes; ++pass) { float passRadius = effectiveRadius / passes; // Vertical pass: blur[0] → blur[1] { ID3D11ShaderResourceView* nullSRV = nullptr; dx_context_->PSSetShaderResources(0, 1, &nullSRV); dx_context_->OMSetRenderTargets(1, &dx_blurRTV_[1], nullptr); dx_context_->PSSetShaderResources(0, 1, &dx_blurSRV_[0]); D3D11_MAPPED_SUBRESOURCE mapped; dx_context_->Map(dx_blurCB_, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); BlurCB* cb = (BlurCB*)mapped.pData; cb->dirX = 0.f; cb->dirY = 1.f; cb->resX = (float)dx_blurWidth_; cb->resY = (float)dx_blurHeight_; cb->radius = passRadius; dx_context_->Unmap(dx_blurCB_, 0); dx_context_->Draw(6, 0); } // Horizontal pass (if not last in this pass): blur[1] → blur[0] if (pass < passes - 1) { ID3D11ShaderResourceView* nullSRV = nullptr; dx_context_->PSSetShaderResources(0, 1, &nullSRV); dx_context_->OMSetRenderTargets(1, &dx_blurRTV_[0], nullptr); dx_context_->PSSetShaderResources(0, 1, &dx_blurSRV_[1]); D3D11_MAPPED_SUBRESOURCE mapped; dx_context_->Map(dx_blurCB_, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); BlurCB* cb = (BlurCB*)mapped.pData; cb->dirX = 1.f; cb->dirY = 0.f; cb->resX = (float)dx_blurWidth_; cb->resY = (float)dx_blurHeight_; cb->radius = passRadius; dx_context_->Unmap(dx_blurCB_, 0); dx_context_->Draw(6, 0); } } // The final result is in blur[1] (last pass was vertical blur[0]→blur[1]) dx_blurCurrent_ = 1; // ---- Restore saved D3D11 state ---- { ID3D11ShaderResourceView* nullSRV = nullptr; dx_context_->PSSetShaderResources(0, 1, &nullSRV); } dx_context_->OMSetRenderTargets(1, &savedRTV, savedDSV); dx_context_->RSSetState(savedRS); dx_context_->OMSetBlendState(savedBS, savedBlendFactor, savedSampleMask); dx_context_->OMSetDepthStencilState(savedDSS, savedStencilRef); dx_context_->RSSetViewports(1, &savedVP); if (savedRTV) savedRTV->Release(); if (savedDSV) savedDSV->Release(); if (savedRS) savedRS->Release(); if (savedBS) savedBS->Release(); if (savedDSS) savedDSS->Release(); static bool s_traced = false; if (!s_traced) { s_traced = true; VERBOSE_LOGF("[Acrylic DX11] applyBlur: %d passes, radius=%.1f, blurSize=%dx%d\n", passes, effectiveRadius, dx_blurWidth_, dx_blurHeight_); } lastBlurRadius_ = scaledRadius; blurCacheValid_ = true; } ImTextureID AcrylicMaterial::getBlurredTexture() const { return (ImTextureID)dx_blurSRV_[dx_blurCurrent_]; } ImTextureID AcrylicMaterial::getNoiseTexture() const { return (ImTextureID)dx_noiseSRV_; } void AcrylicMaterial::refreshCapabilities() { capabilities_.glVersion = "DX11"; capabilities_.glRenderer = "Direct3D 11"; capabilities_.hasFramebufferSupport = true; capabilities_.hasShaderSupport = true; capabilities_.hasTextureSupport = true; capabilities_.maxTextureSize = 16384; capabilities_.isLowEndGPU = false; capabilities_.isOnBattery = false; #ifdef _WIN32 // Check battery status (Windows) SYSTEM_POWER_STATUS sps; if (GetSystemPowerStatus(&sps)) { capabilities_.isOnBattery = (sps.ACLineStatus == 0); } #endif DEBUG_LOGF("[Acrylic DX11] Capabilities: DX11 renderer\n"); } AcrylicFallback AcrylicMaterial::detectFallback() const { if (hasForcedFallback_) return forcedFallback_; if (settings_.reducedTransparency) return AcrylicFallback::Solid; if (settings_.quality == AcrylicQuality::Off) return AcrylicFallback::TintedOnly; if (!settings_.enabled) return AcrylicFallback::TintedOnly; if (settings_.disableOnBattery && capabilities_.isOnBattery) return AcrylicFallback::TintedOnly; return AcrylicFallback::None; } void AcrylicMaterial::setQuality(AcrylicQuality quality) { if (settings_.quality != quality) { settings_.quality = quality; blurCacheValid_ = false; int w = viewportWidth_, h = viewportHeight_; viewportWidth_ = viewportHeight_ = 0; resize(w, h); } } bool AcrylicMaterial::shouldSkipEffect() const { return !settings_.enabled || settings_.reducedTransparency || settings_.quality == AcrylicQuality::Off; } void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax, const AcrylicParams& params, float rounding) { if (!initialized_) return; currentFallback_ = detectFallback(); static bool s_traced = false; if (!s_traced) { s_traced = true; VERBOSE_LOGF("[Acrylic DX11] drawRect first call: fallback=%d enabled=%d quality=%d " "hasCapture=%d blurValid=%d viewport=%dx%d\n", (int)currentFallback_, (int)settings_.enabled, (int)settings_.quality, (int)hasValidCapture_, (int)blurCacheValid_, viewportWidth_, viewportHeight_); } // Fallback solid fill for DX11 — scaled by UI opacity for card transparency auto dx_scaledFallback = [&]() { ImVec4 c = params.fallbackColor; c.w *= settings_.uiOpacity; return ImGui::ColorConvertFloat4ToU32(c); }; if (!params.enabled) { drawList->AddRectFilled(pMin, pMax, dx_scaledFallback(), rounding); return; } switch (currentFallback_) { case AcrylicFallback::Solid: drawList->AddRectFilled(pMin, pMax, dx_scaledFallback(), rounding); return; case AcrylicFallback::TintedOnly: drawTintedRect(drawList, pMin, pMax, params, rounding); return; default: break; } if (!hasValidCapture_) { drawTintedRect(drawList, pMin, pMax, params, rounding); return; } // Run DX11 blur pipeline applyBlur(params.blurRadius, params.absoluteBlurRadius); // DX11 UV: top-left = (0,0), bottom-right = (1,1) — same as ImGui // With multi-viewport, pMin/pMax are in OS screen space; subtract // the main viewport position to get window-local coords for UVs. ImGuiViewport* mainVp = ImGui::GetMainViewport(); float localX0 = pMin.x - mainVp->Pos.x; float localY0 = pMin.y - mainVp->Pos.y; float localX1 = pMax.x - mainVp->Pos.x; float localY1 = pMax.y - mainVp->Pos.y; float u0 = localX0 / viewportWidth_; float v0 = localY0 / viewportHeight_; float u1 = localX1 / viewportWidth_; float v1 = localY1 / viewportHeight_; ImTextureID blurTex = getBlurredTexture(); static bool s_blurTraced = false; if (!s_blurTraced && blurTex) { s_blurTraced = true; VERBOSE_LOGF("[Acrylic DX11] blur tex=%p UV: (%.3f,%.3f)-(%.3f,%.3f)\n", (void*)blurTex, u0, v0, u1, v1); } // Draw the blurred background at (near) full strength so the panel is actually FROSTED — UI // opacity is applied only to the tint overlay, NOT here (scaling the blur by uiOpacity let the // SHARP background bleed through at low opacity, making the acrylic slider a no-op). Mirrors the // GL drawRect fix above. uint8_t glassAlpha = (uint8_t)std::min(255.f, std::max(0.f, std::max(params.fallbackColor.w, 0.90f) * 255.f)); if (blurTex) { drawList->AddImageRounded( blurTex, pMin, pMax, ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32(255, 255, 255, glassAlpha), rounding); } // Noise grain overlay — single draw call via pre-tiled texture if (params.noiseOpacity > 0.0f && settings_.noiseOpacityMultiplier > 0.0f) { float maxAlpha = schema::UI().drawElement("backdrop", "noise-max-alpha").sizeOr(6.0f); float scaledNoise = params.noiseOpacity * settings_.noiseOpacityMultiplier; uint8_t alpha = (uint8_t)std::min(255.f, maxAlpha * scaledNoise * 50.f); ImU32 col = IM_COL32(255, 255, 255, alpha); drawList->PushClipRect(pMin, pMax, true); dragonx::util::DrawTiledNoiseRect(drawList, pMin, pMax, col); drawList->PopClipRect(); } } void AcrylicMaterial::drawTintedRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax, const AcrylicParams& params, float rounding) { ImU32 tintCol = ImGui::ColorConvertFloat4ToU32( ImVec4(params.tintColor.x, params.tintColor.y, params.tintColor.z, params.tintColor.w * params.tintOpacity * 0.9f * settings_.uiOpacity)); drawList->AddRectFilled(pMin, pMax, tintCol, rounding); // Noise grain overlay — single draw call via pre-tiled texture if (params.noiseOpacity > 0.0f && settings_.noiseOpacityMultiplier > 0.0f) { float maxAlpha = schema::UI().drawElement("backdrop", "noise-max-alpha").sizeOr(6.0f); float scaledNoise = params.noiseOpacity * settings_.noiseOpacityMultiplier; uint8_t alpha = (uint8_t)std::min(255.f, maxAlpha * scaledNoise * 50.f); ImGuiViewport* vp = ImGui::GetMainViewport(); int vpW = (int)vp->Size.x, vpH = (int)vp->Size.y; int texW = 0, texH = 0; ImTextureID noiseTex = dragonx::util::GetTiledNoiseTexture(vpW, vpH, &texW, &texH); if (noiseTex && texW > 0 && texH > 0) { float u0 = (pMin.x - vp->Pos.x) / (float)texW; float v0 = (pMin.y - vp->Pos.y) / (float)texH; float u1 = (pMax.x - vp->Pos.x) / (float)texW; float v1 = (pMax.y - vp->Pos.y) / (float)texH; drawList->AddImageRounded( noiseTex, pMin, pMax, ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32(255, 255, 255, alpha), rounding); } } } // ============================================================================ // Preset Parameters (same values as GL build) // ============================================================================ AcrylicParams AcrylicMaterial::getDarkPreset() { AcrylicParams p; p.tintColor = ImVec4(0.1f, 0.1f, 0.12f, 0.95f); p.tintOpacity = 0.75f; p.luminosityOpacity = 0.6f; p.blurRadius = 30.f; p.noiseOpacity = 0.02f; p.fallbackColor = ImVec4(0.12f, 0.12f, 0.14f, 1.f); return p; } AcrylicParams AcrylicMaterial::getLightPreset() { AcrylicParams p; p.tintColor = ImVec4(0.95f, 0.95f, 0.97f, 0.95f); p.tintOpacity = 0.80f; p.luminosityOpacity = 0.7f; p.blurRadius = 25.f; p.noiseOpacity = 0.02f; p.fallbackColor = ImVec4(0.96f, 0.96f, 0.98f, 1.f); return p; } AcrylicParams AcrylicMaterial::getDragonXPreset() { AcrylicParams p; p.tintColor = ImVec4(0.6f, 0.15f, 0.1f, 0.95f); p.tintOpacity = 0.85f; p.luminosityOpacity = 0.3f; p.blurRadius = 40.f; p.noiseOpacity = 0.025f; p.fallbackColor = ImVec4(0.5f, 0.12f, 0.08f, 1.f); return p; } AcrylicParams AcrylicMaterial::getPopupPreset() { AcrylicParams p; p.tintColor = ImVec4(0.15f, 0.15f, 0.18f, 0.95f); p.tintOpacity = 0.70f; p.luminosityOpacity = 0.5f; p.blurRadius = 30.f; p.noiseOpacity = 0.02f; p.fallbackColor = ImVec4(0.18f, 0.18f, 0.2f, 1.f); return p; } AcrylicParams AcrylicMaterial::getSidebarPreset() { AcrylicParams p; p.tintColor = ImVec4(0.08f, 0.08f, 0.1f, 0.95f); p.tintOpacity = 0.80f; p.luminosityOpacity = 0.4f; p.blurRadius = 35.f; p.noiseOpacity = 0.02f; p.fallbackColor = ImVec4(0.1f, 0.1f, 0.12f, 1.f); return p; } AcrylicMaterial& getAcrylicMaterial() { static AcrylicMaterial instance; return instance; } } // namespace effects } // namespace ui } // namespace dragonx #else // neither GLAD nor DX11 // ============================================================================ // Stub Implementation (No graphics backend) // ============================================================================ namespace dragonx { namespace ui { namespace effects { AcrylicMaterial::AcrylicMaterial() = default; AcrylicMaterial::~AcrylicMaterial() = default; bool AcrylicMaterial::init() { return false; } void AcrylicMaterial::shutdown() {} void AcrylicMaterial::resize(int, int) {} void AcrylicMaterial::captureBackground() {} void AcrylicMaterial::captureBackgroundDirect() {} void AcrylicMaterial::captureLiveFramebuffer() {} void AcrylicMaterial::applyBlur(float, bool) {} ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; } ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; } void AcrylicMaterial::refreshCapabilities() {} AcrylicFallback AcrylicMaterial::detectFallback() const { return AcrylicFallback::Solid; } void AcrylicMaterial::drawTintedRect(ImDrawList* dl, const ImVec2& a, const ImVec2& b, const AcrylicParams& p, float r) { dl->AddRectFilled(a, b, ImGui::ColorConvertFloat4ToU32(p.fallbackColor), r); } void AcrylicMaterial::drawRect(ImDrawList* dl, const ImVec2& a, const ImVec2& b, const AcrylicParams& p, float r) { dl->AddRectFilled(a, b, ImGui::ColorConvertFloat4ToU32(p.fallbackColor), r); } AcrylicParams AcrylicMaterial::getDarkPreset() { AcrylicParams p; p.enabled=false; return p; } AcrylicParams AcrylicMaterial::getLightPreset() { AcrylicParams p; p.enabled=false; return p; } AcrylicParams AcrylicMaterial::getDragonXPreset() { AcrylicParams p; p.enabled=false; return p; } AcrylicParams AcrylicMaterial::getPopupPreset() { AcrylicParams p; p.enabled=false; return p; } AcrylicParams AcrylicMaterial::getSidebarPreset() { AcrylicParams p; p.enabled=false; return p; } AcrylicMaterial& getAcrylicMaterial() { static AcrylicMaterial i; return i; } } // namespace effects } // namespace ui } // namespace dragonx #endif // DRAGONX_USE_DX11 #endif // DRAGONX_HAS_GLAD