Files
ObsidianDragon/src/ui/effects/acrylic.cpp
T
DanSandClaude Opus 4.8 968d6d3875 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 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00

1644 lines
58 KiB
C++

// 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 <glad/gl.h>
// 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 <cstdio>
#include <vector>
#include <cmath>
#include <algorithm>
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<int>(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<uint8_t>(
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<uint8_t>(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<const char*>(glGetString(GL_VERSION));
const char* renderer = reinterpret_cast<const char*>(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<uint8_t>(std::min(255.0f, maxAlpha * scaledNoise * 50.0f));
float tilePx = static_cast<float>(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 <d3d11.h>
#include <d3dcompiler.h>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <cstring>
// 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<ID3D11Device**>(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<uint8_t> 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