Files
ObsidianDragon/src/ui/effects/acrylic.h
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

473 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "framebuffer.h"
#include "blur_shader.h"
#include "noise_texture.h"
#include "imgui.h"
#include <memory>
#include <string>
#ifdef DRAGONX_USE_DX11
#include <d3d11.h>
#endif
namespace dragonx {
namespace ui {
namespace effects {
/**
* @brief Acrylic material parameters
*
* These parameters control the appearance of the acrylic effect,
* matching Microsoft's Fluent Design system.
*/
struct AcrylicParams {
// Tint color (RGBA, pre-multiplied alpha)
ImVec4 tintColor = ImVec4(0.1f, 0.1f, 0.12f, 1.0f);
// Tint opacity (0.0 = fully transparent, 1.0 = fully opaque)
float tintOpacity = 0.75f;
// Luminosity opacity (controls saturation pass-through)
// Lower values = more desaturated/milky appearance
float luminosityOpacity = 0.5f;
// Blur radius in pixels (typical: 20-60)
float blurRadius = 30.0f;
// When true, blurRadius is used AS-IS — NOT scaled by the user's blur-strength slider
// (blurRadiusMultiplier). The modal backdrop sets this so its blur is a fixed value, independent
// of the global acrylic slider.
bool absoluteBlurRadius = false;
// Noise texture opacity (typical: 0.02-0.04)
float noiseOpacity = 0.02f;
// Fallback color when acrylic is disabled.
// The alpha channel also controls the glass opacity of the blurred
// background when acrylic IS active (lower alpha = more see-through).
ImVec4 fallbackColor = ImVec4(0.15f, 0.15f, 0.18f, 0.5f);
// Whether acrylic is enabled (false = use fallback)
bool enabled = true;
};
/**
* @brief Acrylic quality levels
*/
enum class AcrylicQuality {
Off, // Solid fallback color only
Low, // Single blur pass, downsampled
Medium, // Two blur passes, slight downsample
High // Full quality blur
};
/**
* @brief Acrylic fallback modes for graceful degradation
*/
enum class AcrylicFallback {
None, // Full acrylic (blur + tint + noise)
TintedOnly, // No blur, just semi-transparent tint overlay
Solid // Opaque fallback color only
};
/**
* @brief Acrylic settings for accessibility and performance
*/
struct AcrylicSettings {
bool enabled = true; // Master toggle
bool reducedTransparency = false; // Accessibility: use solid colors
float uiOpacity = 1.0f; // Card/sidebar opacity multiplier (0.3–1.0)
AcrylicQuality quality = AcrylicQuality::Medium; // Medium default for balance of quality/performance
float blurRadiusMultiplier = 1.0f; // Scales all blur radii (0.5 - 2.0)
float noiseOpacityMultiplier = 1.0f; // Scales noise opacity (0.0 - 2.0)
// Auto-disable conditions (checked each frame)
bool disableOnBattery = false; // Disable when on battery power
bool disableOnLowFPS = false; // Disable if FPS drops below threshold
float lowFPSThreshold = 30.0f; // FPS threshold for auto-disable
};
/**
* @brief System capabilities for acrylic effects
*/
struct AcrylicCapabilities {
bool hasFramebufferSupport = false;
bool hasShaderSupport = false;
bool hasTextureSupport = false;
int maxTextureSize = 0;
std::string glVersion;
std::string glRenderer;
bool isLowEndGPU = false;
bool isOnBattery = false; // Linux: check /sys/class/power_supply
};
/**
* @brief Main acrylic material rendering system
*
* Implements Microsoft Fluent Design's acrylic material effect:
* - Gaussian blur of background content
* - Tint color overlay
* - Luminosity blend for depth
* - Subtle noise texture for grain
*/
class AcrylicMaterial {
public:
AcrylicMaterial();
~AcrylicMaterial();
// Non-copyable
AcrylicMaterial(const AcrylicMaterial&) = delete;
AcrylicMaterial& operator=(const AcrylicMaterial&) = delete;
/**
* @brief Initialize the acrylic system
* @return true if successful
*/
bool init();
/**
* @brief Release all resources
*/
void shutdown();
/**
* @brief Check if system is initialized
*/
bool isInitialized() const { return initialized_; }
/**
* @brief Update internal buffers for new viewport size
* @param width Viewport width
* @param height Viewport height
*/
void resize(int width, int height);
/// Size of FB 0 in pixels (differs from the logical viewport on HiDPI GL). Used only as the capture
/// blit source. Kept apart from viewportWidth_/Height_ so setQuality()/syncBufferLifetime() resets
/// of those never lose it. DX11 ignores it (CopyResource needs identical dimensions, scale is 1).
void setFramebufferSize(int w, int h) {
if (w != fbWidth_ || h != fbHeight_) { fbWidth_ = w; fbHeight_ = h; markBackgroundDirty(); }
}
/**
* @brief Capture current screen content for blur source
*
* Call this BEFORE rendering any acrylic surfaces.
* Only recaptures when the background is marked dirty.
*/
void captureBackground();
/**
* @brief Capture background directly from the current framebuffer.
*
* Intended to be called from an ImGui draw callback inserted at
* the end of the BackgroundDrawList. At that point during
* RenderDrawData(), only the background (gradient / image / noise)
* has been rasterized — no UI elements are in the framebuffer yet.
* This gives a clean background capture for acrylic blur.
*
* Respects the dirtyFrames_ counter so the capture and blur
* are only recomputed when the background actually changes
* (resize, theme change, etc.).
*/
void captureBackgroundDirect();
/**
* @brief Capture the LIVE framebuffer (whatever has been drawn so far this
* frame), bypassing the dirtyFrames_ gate. Used by a full-window modal
* overlay to blur the live UI behind it: insert GetLiveCaptureCallback()
* at the start of the overlay window's draw list so it fires mid-pass,
* after the app UI is rasterized and before the overlay's own backdrop.
* Always forces a re-blur of the fresh capture.
*/
void captureLiveFramebuffer();
/**
* @brief Returns true once a valid background capture has been made.
*
* drawRect() checks this and falls back to a tinted fill on the
* very first frame before the callback has fired.
*/
bool hasValidCapture() const { return hasValidCapture_; }
/// Mark background as needing recapture (call on resize, theme change, etc.)
/// Uses a 2-frame counter so the capture re-runs after the new background
/// has actually been rendered (theme switch happens mid-frame, before the
/// BackgroundDrawList is rebuilt with new colors/images).
void markBackgroundDirty() { dirtyFrames_ = 2; }
/// Frame-boundary maintenance. Frees the capture/blur targets while the effect is skipped
/// (Off / disabled / reduced transparency) and rebuilds them when wanted again.
void syncBufferLifetime();
/**
* @brief Render an acrylic-filled rectangle
*
* @param drawList ImGui draw list to add commands to
* @param pMin Top-left corner in screen coordinates
* @param pMax Bottom-right corner in screen coordinates
* @param params Acrylic parameters
* @param rounding Corner rounding
*/
void drawRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax,
const AcrylicParams& params, float rounding = 0.0f);
/**
* @brief Get the blurred background texture for manual rendering
*/
ImTextureID getBlurredTexture() const;
/**
* @brief Get the noise texture
*/
ImTextureID getNoiseTexture() const;
// ========================================================================
// Settings
// ========================================================================
/**
* @brief Set global acrylic quality
*/
void setQuality(AcrylicQuality quality);
AcrylicQuality getQuality() const { return settings_.quality; }
/**
* @brief Enable/disable acrylic globally
*/
void setEnabled(bool enabled) { settings_.enabled = enabled; }
bool isEnabled() const { return settings_.enabled; }
/**
* @brief Set blur radius multiplier (scales all blur radii)
* @param multiplier Value from 0.0 to 5.0
*/
void setBlurMultiplier(float multiplier) {
float clamped = std::max(0.0f, std::min(5.0f, multiplier));
settings_.blurRadiusMultiplier = clamped;
}
float getBlurMultiplier() const { return settings_.blurRadiusMultiplier; }
/**
* @brief Set reduced transparency mode (accessibility)
*/
void setReducedTransparency(bool reduced) { settings_.reducedTransparency = reduced; }
bool getReducedTransparency() const { return settings_.reducedTransparency; }
/**
* @brief Set UI opacity multiplier for cards/sidebar (0.3–1.0, 1=opaque)
*/
void setUIOpacity(float opacity) {
settings_.uiOpacity = std::max(0.3f, std::min(1.0f, opacity));
}
float getUIOpacity() const { return settings_.uiOpacity; }
/**
* @brief Set noise opacity multiplier (0.0 = no noise, 1.0 = default, 2.0 = double)
*/
void setNoiseOpacityMultiplier(float m) {
settings_.noiseOpacityMultiplier = std::max(0.0f, std::min(5.0f, m));
}
float getNoiseOpacityMultiplier() const { return settings_.noiseOpacityMultiplier; }
/**
* @brief Get full settings struct
*/
const AcrylicSettings& getSettings() const { return settings_; }
void setSettings(const AcrylicSettings& settings) { settings_ = settings; }
// ========================================================================
// Fallback System
// ========================================================================
/**
* @brief Detect system capabilities and determine fallback mode
*
* Checks OpenGL capabilities, GPU info, and user preferences to
* determine the appropriate rendering mode.
*
* @return The recommended fallback mode
*/
AcrylicFallback detectFallback() const;
/**
* @brief Get current fallback mode (cached)
*/
AcrylicFallback getCurrentFallback() const { return currentFallback_; }
/**
* @brief Force a specific fallback mode
*/
void setForcedFallback(AcrylicFallback fallback) {
forcedFallback_ = fallback;
hasForcedFallback_ = true;
}
/**
* @brief Clear forced fallback and use auto-detection
*/
void clearForcedFallback() { hasForcedFallback_ = false; }
/**
* @brief Get detected system capabilities
*/
const AcrylicCapabilities& getCapabilities() const { return capabilities_; }
/**
* @brief Refresh capability detection (call after context changes)
*/
void refreshCapabilities();
// ========================================================================
// Preset Parameters
// ========================================================================
/**
* @brief Get dark theme acrylic preset
*/
static AcrylicParams getDarkPreset();
/**
* @brief Get light theme acrylic preset
*/
static AcrylicParams getLightPreset();
/**
* @brief Get DragonX branded acrylic preset
*/
static AcrylicParams getDragonXPreset();
/**
* @brief Get popup/dialog acrylic preset
*/
static AcrylicParams getPopupPreset();
/**
* @brief Get sidebar acrylic preset
*/
static AcrylicParams getSidebarPreset();
private:
/**
* @brief Apply blur passes to captured content
*/
void applyBlur(float radius, bool ignoreMultiplier = false);
/**
* @brief Check if effect should be skipped (settings/quality)
*/
bool shouldSkipEffect() const;
/**
* @brief Draw tinted-only fallback (no blur)
*/
void drawTintedRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax,
const AcrylicParams& params, float rounding);
bool initialized_ = false;
AcrylicSettings settings_; // All settings in one struct
// Fallback system
AcrylicCapabilities capabilities_;
AcrylicFallback currentFallback_ = AcrylicFallback::None;
AcrylicFallback forcedFallback_ = AcrylicFallback::None;
bool hasForcedFallback_ = false;
// Screen dimensions
int viewportWidth_ = 0;
int viewportHeight_ = 0;
int fbWidth_ = 0; // FB 0 pixel size (0 = unknown -> use the viewport size)
int fbHeight_ = 0;
int captureSrcWidth() const { return fbWidth_ > 0 ? fbWidth_ : viewportWidth_; }
int captureSrcHeight() const { return fbHeight_ > 0 ? fbHeight_ : viewportHeight_; }
// Noise texture config
int noiseTextureSize_ = 256; // Texture resolution (larger = less repetition)
// Caching for performance
float lastBlurRadius_ = 0.0f;
bool blurCacheValid_ = false;
int dirtyFrames_ = 2; // >0 means recapture needed; counts down each frame
bool hasValidCapture_ = false; // true once a clean BG capture exists
void releaseBuffers();
bool needsRealloc_ = false; // targets were freed or left stale while the effect was skipped
// Capture and blur framebuffers
Framebuffer captureBuffer_;
FramebufferPingPong blurBuffers_;
// Shaders
BlurShader blurShader_;
// Fullscreen quad for post-processing
FullscreenQuad quad_;
// Noise texture
NoiseTexture noiseTexture_;
#ifdef DRAGONX_USE_DX11
// ---- DX11 acrylic resources (used instead of GL objects above) ----
ID3D11Device* dx_device_ = nullptr;
ID3D11DeviceContext* dx_context_ = nullptr;
// Capture buffer (full viewport resolution)
ID3D11Texture2D* dx_captureTex_ = nullptr;
ID3D11ShaderResourceView* dx_captureSRV_ = nullptr;
ID3D11RenderTargetView* dx_captureRTV_ = nullptr;
// Blur ping-pong buffers (possibly downscaled)
ID3D11Texture2D* dx_blurTex_[2] = {};
ID3D11ShaderResourceView* dx_blurSRV_[2] = {};
ID3D11RenderTargetView* dx_blurRTV_[2] = {};
int dx_blurWidth_ = 0;
int dx_blurHeight_ = 0;
int dx_blurCurrent_ = 0; // which buffer is "source"
// Shaders & pipeline objects
ID3D11VertexShader* dx_blurVS_ = nullptr;
ID3D11PixelShader* dx_blurPS_ = nullptr;
ID3D11InputLayout* dx_inputLayout_ = nullptr;
ID3D11Buffer* dx_blurCB_ = nullptr;
ID3D11Buffer* dx_vertexBuf_ = nullptr;
ID3D11SamplerState* dx_sampler_ = nullptr;
ID3D11RasterizerState* dx_blurRS_ = nullptr;
ID3D11BlendState* dx_blurBS_ = nullptr;
ID3D11DepthStencilState* dx_blurDSS_ = nullptr;
// Noise texture (DX11)
ID3D11Texture2D* dx_noiseTex_ = nullptr;
ID3D11ShaderResourceView* dx_noiseSRV_ = nullptr;
// Internal helpers
bool dx_initPipeline();
void dx_releasePipeline();
void dx_createRenderTarget(ID3D11Texture2D*& tex,
ID3D11ShaderResourceView*& srv,
ID3D11RenderTargetView*& rtv,
int w, int h,
DXGI_FORMAT format);
void dx_releaseRenderTarget(ID3D11Texture2D*& tex,
ID3D11ShaderResourceView*& srv,
ID3D11RenderTargetView*& rtv);
#endif // DRAGONX_USE_DX11
};
// ============================================================================
// Global Acrylic Instance
// ============================================================================
/**
* @brief Get the global acrylic material instance
*/
AcrylicMaterial& getAcrylicMaterial();
} // namespace effects
} // namespace ui
} // namespace dragonx