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>
473 lines
16 KiB
C++
473 lines
16 KiB
C++
// 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
|