diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 4753d6cfb8..fff23637cd 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -44,7 +44,7 @@ "ShowFooter": true, "CenterHeader": true, "TooltipHoverDelay": 0.5, - "BackgroundBlurEnabled": false, + "BackgroundBlurEnabled": true, "Palette": { "Background": [0.03, 0.03, 0.03, 0.39216], "Text": [1.0, 1.0, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json index 2a45e7c592..9bf77425a9 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -44,7 +44,7 @@ "UseMonochromeIcons": true, "CenterHeader": true, "TooltipHoverDelay": 0.5, - "BackgroundBlurEnabled": false, + "BackgroundBlurEnabled": true, "Palette": { "Background": [0.25, 0.05, 0.05, 0.9], "Text": [1.0, 0.85, 0.85, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json index a966e574a5..803e6a4db9 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -46,7 +46,7 @@ "ShowFooter": false, "CenterHeader": true, "TooltipHoverDelay": 0.5, - "BackgroundBlurEnabled": false, + "BackgroundBlurEnabled": true, "Palette": { "Background": [0.15, 0.12, 0.08, 0.9], "Text": [0.9, 0.75, 0.5, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json index 250d5a07e1..28ebe7d7d1 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -44,7 +44,7 @@ "UseMonochromeIcons": false, "CenterHeader": true, "TooltipHoverDelay": 0.5, - "BackgroundBlurEnabled": false, + "BackgroundBlurEnabled": true, "Palette": { "Background": [0.0, 0.0, 0.0, 0.95], "Text": [1.0, 1.0, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json index e224d215f9..f2b25f35e6 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -44,7 +44,7 @@ "ShowFooter": false, "CenterHeader": true, "TooltipHoverDelay": 0.5, - "BackgroundBlurEnabled": false, + "BackgroundBlurEnabled": true, "Palette": { "Background": [0.98, 0.98, 0.98, 0.9], "Text": [0.08, 0.08, 0.08, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json index 0437623a9e..739dcc077f 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -44,7 +44,7 @@ "UseMonochromeIcons": true, "CenterHeader": true, "TooltipHoverDelay": 0.5, - "BackgroundBlurEnabled": false, + "BackgroundBlurEnabled": true, "Palette": { "Background": [0.05, 0.15, 0.25, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], diff --git a/package/Shaders/Menu/BackgroundBlurComposite.hlsl b/package/Shaders/Menu/BackgroundBlurComposite.hlsl new file mode 100644 index 0000000000..e6e1424665 --- /dev/null +++ b/package/Shaders/Menu/BackgroundBlurComposite.hlsl @@ -0,0 +1,142 @@ +// Composite Blur Pass Shader with Rounded Rectangle Mask +// Part of the BackgroundBlur system - applies blurred texture with rounded corners + +cbuffer WindowBuffer : register(b1) +{ + float4 WindowRect; // x = minX, y = minY, z = maxX, w = maxY (in pixels) + float4 WindowParams; // x = cornerRadius, y = screenWidth, z = screenHeight, w = unused +}; + +SamplerState LinearSampler : register(s0); +Texture2D InputTexture : register(t0); + +static const float TWO_PI = 6.28318530718f; +static const int NUM_JITTER_SAMPLES = 4; +static const float DOWNSAMPLE_FACTOR = 8.0f; +static const float CLIP_EPSILON = 0.001f; + +struct VS_OUTPUT +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; +}; + +VS_OUTPUT VS_Main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + output.TexCoord = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(output.TexCoord * 2.0f - 1.0f, 0.0f, 1.0f); + output.Position.y = -output.Position.y; + return output; +} + +// High quality 2D hash - returns two independent random values in [0,1] +// Uses different prime multipliers to avoid correlation between x and y +float2 Hash22(float2 p) +{ + // Two independent hashes using different constants + float3 p3 = frac(float3(p.xyx) * float3(0.1031f, 0.1030f, 0.0973f)); + p3 += dot(p3, p3.yzx + 33.33f); + return frac((p3.xx + p3.yz) * p3.zy); +} + +// Soft sampling with blurred dithering - takes 4 samples with jittered offsets +// and averages them to smooth out the noise while still breaking up blocky pixels +float4 SampleWithSoftening(float2 uv, float2 pixelPos, float2 texelSize) +{ + // Get base random offset for this pixel + float2 noise = Hash22(pixelPos); + + // Rotated grid offsets (45 degree rotation for better coverage) + // This creates a smooth disc-like sampling pattern + static const float2 offsets[NUM_JITTER_SAMPLES] = { + float2(-0.25f, -0.25f), + float2( 0.25f, -0.25f), + float2(-0.25f, 0.25f), + float2( 0.25f, 0.25f) + }; + + // Random rotation angle based on pixel position + float angle = noise.x * TWO_PI; + float s, c; + sincos(angle, s, c); + float2x2 rotation = float2x2(c, -s, s, c); + + // Sample 4 points with rotated jittered offsets and average + float4 result = 0; + [unroll] + for (int i = 0; i < NUM_JITTER_SAMPLES; i++) + { + float2 jitter = mul(rotation, offsets[i]) * texelSize; + result += InputTexture.Sample(LinearSampler, uv + jitter); + } + + return result / (float)NUM_JITTER_SAMPLES; +} + +// Compute signed distance to a rounded rectangle +// Returns negative inside, positive outside +float RoundedRectSDF(float2 pixelPos, float2 rectMin, float2 rectMax, float radius) +{ + // Center of the rectangle + float2 rectCenter = (rectMin + rectMax) * 0.5f; + float2 rectHalfSize = (rectMax - rectMin) * 0.5f; + + // Clamp radius to not exceed half the smallest dimension + radius = min(radius, min(rectHalfSize.x, rectHalfSize.y)); + + // Distance from center + float2 p = abs(pixelPos - rectCenter) - rectHalfSize + radius; + + // SDF for rounded rectangle + return length(max(p, 0.0f)) + min(max(p.x, p.y), 0.0f) - radius; +} + +float4 PS_Main(VS_OUTPUT input) : SV_TARGET +{ + // Convert UV to pixel coordinates + float2 pixelPos = input.TexCoord * float2(WindowParams.y, WindowParams.z); + + // Get window bounds and corner radius + float2 rectMin = WindowRect.xy; + float2 rectMax = WindowRect.zw; + float cornerRadius = WindowParams.x; + + // Calculate signed distance to rounded rectangle + float sdf = RoundedRectSDF(pixelPos, rectMin, rectMax, cornerRadius); + + // Create smooth edge (anti-aliased) + // Negative = inside, positive outside + // Use 1.0 pixel transition for smooth edge + float alpha = saturate(-sdf); + + // Early out if completely outside + if (alpha <= 0.0f) + { + discard; + } + + float2 blurTexelSize = DOWNSAMPLE_FACTOR / float2(WindowParams.y, WindowParams.z); + + // Sample with soft dithering to hide blocky pixels from the downsampled blur + float4 blurColor = SampleWithSoftening(input.TexCoord, pixelPos, blurTexelSize); + + // Apply rounded corner mask to alpha + // The blur strength is applied via blend state, so just use the rounded mask here + blurColor.a = alpha; + + return blurColor; +} + +// Clear shader entry point - outputs transparent black inside rounded rect only +// Used to clear UI buffer (HUD) in the exact same shape as the blur +float4 PS_Clear(VS_OUTPUT input) : SV_TARGET +{ + float2 pixelPos = input.TexCoord * float2(WindowParams.y, WindowParams.z); + float sdf = RoundedRectSDF(pixelPos, WindowRect.xy, WindowRect.zw, WindowParams.x); + + // Discard pixels outside rounded rect to preserve HUD in corners + clip(-sdf - CLIP_EPSILON); + + return float4(0.0f, 0.0f, 0.0f, 0.0f); +} diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index b8acb6d288..9d5e9a448b 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -1541,6 +1541,14 @@ IDXGISwapChain* Upscaling::GetProxySwapChain() return dx12SwapChain.GetSwapChainProxy(); } +Upscaling::BlurResources Upscaling::GetBlurResources() const +{ + if (d3d12SwapChainActive) { + return dx12SwapChain.GetBlurResources(); + } + return {}; +} + void Upscaling::Upscale() { auto upscaleMethod = GetUpscaleMethod(); diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 38c8192dfd..3a63d87b3d 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -231,6 +231,11 @@ struct Upscaling : Feature void CreateProxyInterop(); IDXGISwapChain* GetProxySwapChain(); + using BlurResources = DX12SwapChain::BlurResources; + + // Get all D3D11 resources needed for background blur when D3D12 swap chain is active + BlurResources GetBlurResources() const; + private: struct Main_UpdateJitter { diff --git a/src/Features/Upscaling/DX12SwapChain.cpp b/src/Features/Upscaling/DX12SwapChain.cpp index a2d005809f..a9f5a8b184 100644 --- a/src/Features/Upscaling/DX12SwapChain.cpp +++ b/src/Features/Upscaling/DX12SwapChain.cpp @@ -399,6 +399,21 @@ void DX12SwapChain::SetUIBuffer() } } +DX12SwapChain::BlurResources DX12SwapChain::GetBlurResources() const +{ + BlurResources res; + if (swapChainBufferWrapped) { + res.backbufferTex = swapChainBufferWrapped->resource11; + res.backbufferRTV = swapChainBufferWrapped->rtv; + res.backbufferSRV = swapChainBufferWrapped->srv; + } + if (uiBufferWrapped) { + res.uiBufferSRV = uiBufferWrapped->srv; + res.uiBufferRTV = uiBufferWrapped->rtv; + } + return res; +} + void DX12SwapChain::CreateSharedResources() { auto renderer = globals::game::renderer; diff --git a/src/Features/Upscaling/DX12SwapChain.h b/src/Features/Upscaling/DX12SwapChain.h index 19d51e276a..69cda5c5ff 100644 --- a/src/Features/Upscaling/DX12SwapChain.h +++ b/src/Features/Upscaling/DX12SwapChain.h @@ -113,6 +113,19 @@ class DX12SwapChain void SetUIBuffer(); + // Resources needed by BackgroundBlur when D3D12 swap chain is active + struct BlurResources + { + ID3D11Texture2D* backbufferTex = nullptr; + ID3D11RenderTargetView* backbufferRTV = nullptr; + ID3D11ShaderResourceView* backbufferSRV = nullptr; + ID3D11ShaderResourceView* uiBufferSRV = nullptr; + ID3D11RenderTargetView* uiBufferRTV = nullptr; + }; + + // Get all resources needed for background blur in one call + BlurResources GetBlurResources() const; + // D3D12 interop resource management void CreateSharedResources(); }; diff --git a/src/Menu.h b/src/Menu.h index 08f6dc7fb8..83e014e66b 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -248,14 +248,14 @@ class Menu return roles; }(); - bool UseSimplePalette = false; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. - bool ShowActionIcons = true; // whether to show action buttons as icons - bool UseMonochromeIcons = false; // whether to use monochrome (white) action icons with text color tinting - bool UseMonochromeLogo = false; // whether to use monochrome CS logo - bool ShowFooter = true; // whether to show the footer with game version/GPU info - bool CenterHeader = false; // whether to center the header title and logo - float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds - bool BackgroundBlurEnabled = false; // enable background blur effect + bool UseSimplePalette = false; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. + bool ShowActionIcons = true; // whether to show action buttons as icons + bool UseMonochromeIcons = false; // whether to use monochrome (white) action icons with text color tinting + bool UseMonochromeLogo = false; // whether to use monochrome CS logo + bool ShowFooter = true; // whether to show the footer with game version/GPU info + bool CenterHeader = false; // whether to center the header title and logo + float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds + bool BackgroundBlurEnabled = true; // enable background blur effect // Scrollbar opacity settings struct ScrollbarOpacitySettings { diff --git a/src/Menu/BackgroundBlur.cpp b/src/Menu/BackgroundBlur.cpp index 6becee2aec..612faef843 100644 --- a/src/Menu/BackgroundBlur.cpp +++ b/src/Menu/BackgroundBlur.cpp @@ -3,7 +3,9 @@ // License: MIT License #include "BackgroundBlur.h" +#include "../Features/Upscaling.h" #include "../Globals.h" +#include "../ShaderCache.h" #include "../Util.h" #include @@ -21,6 +23,21 @@ constexpr float BLUR_INTENSITY = 0.03f; // Downsampling factor (8 = eighth resolution for performance) constexpr UINT DOWNSAMPLE_FACTOR = 8; +// Multiplier applied to BLUR_INTENSITY to derive the blur kernel radius +constexpr float BLUR_RADIUS_SCALE = 10.0f; + +// Number of samples per blur pass (Gaussian kernel taps) +constexpr int BLUR_SAMPLE_COUNT = 9; + +// Extra pixels added around scissor rect for anti-aliased rounded corner edges +constexpr float SCISSOR_AA_PADDING = 2.0f; + +// Scale factor applied to BLUR_INTENSITY for the final composite blend alpha +constexpr float BLEND_ALPHA_SCALE = 0.8f; + +// Vertex count for a fullscreen triangle draw call +constexpr UINT FULLSCREEN_TRIANGLE_VERTICES = 3; + namespace BackgroundBlur { // Module-local state @@ -33,12 +50,18 @@ namespace BackgroundBlur winrt::com_ptr vertexShader; winrt::com_ptr horizontalPixelShader; winrt::com_ptr verticalPixelShader; + winrt::com_ptr compositePixelShader; // For rounded corner compositing + winrt::com_ptr clearPixelShader; // For rounded corner UI buffer clearing winrt::com_ptr constantBuffer; + winrt::com_ptr windowConstantBuffer; // For window rect and corner radius winrt::com_ptr samplerState; winrt::com_ptr blendState; winrt::com_ptr scissorRasterizerState; - // Downsampled textures for blur (quarter-res for performance) + // Blend state for compositing UI over game world (alpha blending) + winrt::com_ptr compositeBlendState; + + // Downsampled textures for blur (1/8 res for performance) winrt::com_ptr downsampleTexture; winrt::com_ptr downsampleRTV; winrt::com_ptr downsampleSRV; @@ -51,6 +74,10 @@ namespace BackgroundBlur winrt::com_ptr blurSRV1; winrt::com_ptr blurSRV2; + // Cached SRV for non-upscaling path (avoids per-frame CreateShaderResourceView) + winrt::com_ptr cachedSourceSRV; + ID3D11Texture2D* cachedSourceTexture = nullptr; // raw pointer for cache invalidation check + UINT textureWidth = 0; UINT textureHeight = 0; UINT downsampledWidth = 0; @@ -66,6 +93,53 @@ namespace BackgroundBlur int blurParams[4]; // x = samples, y = unused, z = unused, w = unused }; + // Window constants for rounded corner compositing + struct WindowConstants + { + float windowRect[4]; // x = minX, y = minY, z = maxX, w = maxY (in pixels) + float windowParams[4]; // x = cornerRadius, y = screenWidth, z = screenHeight, w = unused + }; + + // Release all blur texture resources; caller must hold resourceMutex + void ReleaseBlurTextures() + { + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + blurTexture1 = nullptr; + blurTexture2 = nullptr; + blurRTV1 = nullptr; + blurRTV2 = nullptr; + blurSRV1 = nullptr; + blurSRV2 = nullptr; + textureWidth = 0; + textureHeight = 0; + downsampledWidth = 0; + downsampledHeight = 0; + } + + // Create a Texture2D with associated RTV and SRV + bool CreateTextureSet(ID3D11Device* device, const D3D11_TEXTURE2D_DESC& desc, + winrt::com_ptr& tex, + winrt::com_ptr& rtv, + winrt::com_ptr& srv, + const char* name) + { + if (FAILED(device->CreateTexture2D(&desc, nullptr, tex.put()))) { + logger::error("Failed to create {} texture", name); + return false; + } + if (FAILED(device->CreateRenderTargetView(tex.get(), nullptr, rtv.put()))) { + logger::error("Failed to create {} RTV", name); + return false; + } + if (FAILED(device->CreateShaderResourceView(tex.get(), nullptr, srv.put()))) { + logger::error("Failed to create {} SRV", name); + return false; + } + return true; + } + } // anonymous namespace bool Initialize() @@ -82,42 +156,45 @@ namespace BackgroundBlur return false; } - // Compile vertex shader from horizontal blur file (both share same vertex shader) - vertexShader.attach(static_cast(Util::CompileShader(L"Data\\Shaders\\Menu\\BackgroundBlurHorizontal.hlsl", {}, "vs_5_0", "VS_Main"))); - if (!vertexShader) { - logger::error("Failed to compile blur vertex shader"); - initializationFailed = true; - return false; - } + // Compile shaders + auto compileShader = [&](auto& shader, const wchar_t* path, const char* target, const char* entry, const char* name) -> bool { + shader.attach(static_cast(Util::CompileShader(path, {}, target, entry))); + if (!shader) { + logger::error("Failed to compile {}", name); + initializationFailed = true; + return false; + } + return true; + }; - // Compile horizontal pixel shader - horizontalPixelShader.attach(static_cast(Util::CompileShader(L"Data\\Shaders\\Menu\\BackgroundBlurHorizontal.hlsl", {}, "ps_5_0", "PS_Main"))); - if (!horizontalPixelShader) { - logger::error("Failed to compile horizontal blur pixel shader"); - initializationFailed = true; + if (!compileShader(vertexShader, L"Data\\Shaders\\Menu\\BackgroundBlurHorizontal.hlsl", "vs_5_0", "VS_Main", "blur vertex shader") || + !compileShader(horizontalPixelShader, L"Data\\Shaders\\Menu\\BackgroundBlurHorizontal.hlsl", "ps_5_0", "PS_Main", "horizontal blur pixel shader") || + !compileShader(verticalPixelShader, L"Data\\Shaders\\Menu\\BackgroundBlurVertical.hlsl", "ps_5_0", "PS_Main", "vertical blur pixel shader") || + !compileShader(compositePixelShader, L"Data\\Shaders\\Menu\\BackgroundBlurComposite.hlsl", "ps_5_0", "PS_Main", "composite blur pixel shader") || + !compileShader(clearPixelShader, L"Data\\Shaders\\Menu\\BackgroundBlurComposite.hlsl", "ps_5_0", "PS_Clear", "clear pixel shader")) return false; - } - // Compile vertical pixel shader - verticalPixelShader.attach(static_cast(Util::CompileShader(L"Data\\Shaders\\Menu\\BackgroundBlurVertical.hlsl", {}, "ps_5_0", "PS_Main"))); - if (!verticalPixelShader) { - logger::error("Failed to compile vertical blur pixel shader"); - initializationFailed = true; - return false; - } + auto checkCreate = [&](HRESULT hr, const char* name) -> bool { + if (FAILED(hr)) { + logger::error("Failed to create {}", name); + initializationFailed = true; + return false; + } + return true; + }; - // Create constant buffer - D3D11_BUFFER_DESC bufferDesc = {}; - bufferDesc.Usage = D3D11_USAGE_DEFAULT; - bufferDesc.ByteWidth = sizeof(BlurConstants); - bufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + // Create constant buffers + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.Usage = D3D11_USAGE_DEFAULT; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - HRESULT hr = device->CreateBuffer(&bufferDesc, nullptr, constantBuffer.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur constant buffer"); - initializationFailed = true; + cbDesc.ByteWidth = sizeof(BlurConstants); + if (!checkCreate(device->CreateBuffer(&cbDesc, nullptr, constantBuffer.put()), "blur constant buffer")) + return false; + + cbDesc.ByteWidth = sizeof(WindowConstants); + if (!checkCreate(device->CreateBuffer(&cbDesc, nullptr, windowConstantBuffer.put()), "window constant buffer")) return false; - } // Create sampler state D3D11_SAMPLER_DESC samplerDesc = {}; @@ -128,15 +205,10 @@ namespace BackgroundBlur samplerDesc.MaxAnisotropy = 1; samplerDesc.MinLOD = 0; samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; - - hr = device->CreateSamplerState(&samplerDesc, samplerState.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur sampler state"); - initializationFailed = true; + if (!checkCreate(device->CreateSamplerState(&samplerDesc, samplerState.put()), "blur sampler state")) return false; - } - // Create blend state + // Create blend states D3D11_BLEND_DESC blendDesc = {}; blendDesc.RenderTarget[0].BlendEnable = TRUE; blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; @@ -146,13 +218,14 @@ namespace BackgroundBlur blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + if (!checkCreate(device->CreateBlendState(&blendDesc, blendState.put()), "blur blend state")) + return false; - hr = device->CreateBlendState(&blendDesc, blendState.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur blend state"); - initializationFailed = true; + // Composite: pre-multiplied alpha (SrcBlend=ONE, DestBlendAlpha=INV_SRC_ALPHA) + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + if (!checkCreate(device->CreateBlendState(&blendDesc, compositeBlendState.put()), "composite blend state")) return false; - } // Create scissor-enabled rasterizer state D3D11_RASTERIZER_DESC rsDesc = {}; @@ -161,13 +234,8 @@ namespace BackgroundBlur rsDesc.FrontCounterClockwise = FALSE; rsDesc.DepthClipEnable = TRUE; rsDesc.ScissorEnable = TRUE; - - hr = device->CreateRasterizerState(&rsDesc, scissorRasterizerState.put()); - if (FAILED(hr)) { - logger::error("Failed to create scissor rasterizer state"); - initializationFailed = true; + if (!checkCreate(device->CreateRasterizerState(&rsDesc, scissorRasterizerState.put()), "scissor rasterizer state")) return false; - } initialized = true; return true; @@ -186,22 +254,11 @@ namespace BackgroundBlur return; } - // Calculate downsampled dimensions + ReleaseBlurTextures(); + UINT dsWidth = (std::max)(1u, width / DOWNSAMPLE_FACTOR); UINT dsHeight = (std::max)(1u, height / DOWNSAMPLE_FACTOR); - // Release old textures - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; - blurTexture1 = nullptr; - blurTexture2 = nullptr; - blurRTV1 = nullptr; - blurRTV2 = nullptr; - blurSRV1 = nullptr; - blurSRV2 = nullptr; - - // Create downsampled texture description D3D11_TEXTURE2D_DESC texDesc = {}; texDesc.Width = dsWidth; texDesc.Height = dsHeight; @@ -212,98 +269,10 @@ namespace BackgroundBlur texDesc.Usage = D3D11_USAGE_DEFAULT; texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; - // Create downsample texture - HRESULT hr = device->CreateTexture2D(&texDesc, nullptr, downsampleTexture.put()); - if (FAILED(hr)) { - logger::error("Failed to create downsample texture"); - return; - } - - hr = device->CreateRenderTargetView(downsampleTexture.get(), nullptr, downsampleRTV.put()); - if (FAILED(hr)) { - logger::error("Failed to create downsample RTV"); - downsampleTexture = nullptr; - return; - } - - hr = device->CreateShaderResourceView(downsampleTexture.get(), nullptr, downsampleSRV.put()); - if (FAILED(hr)) { - logger::error("Failed to create downsample SRV"); - downsampleTexture = nullptr; - downsampleRTV = nullptr; - return; - } - - // Create first blur texture (at downsampled resolution) - hr = device->CreateTexture2D(&texDesc, nullptr, blurTexture1.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur texture 1"); - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; - return; - } - - // Create second blur texture - hr = device->CreateTexture2D(&texDesc, nullptr, blurTexture2.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur texture 2"); - blurTexture1 = nullptr; - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; - return; - } - - // Create render target views - hr = device->CreateRenderTargetView(blurTexture1.get(), nullptr, blurRTV1.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur RTV 1"); - blurTexture1 = nullptr; - blurTexture2 = nullptr; - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; - return; - } - - hr = device->CreateRenderTargetView(blurTexture2.get(), nullptr, blurRTV2.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur RTV 2"); - blurTexture1 = nullptr; - blurTexture2 = nullptr; - blurRTV1 = nullptr; - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; - return; - } - - // Create shader resource views - hr = device->CreateShaderResourceView(blurTexture1.get(), nullptr, blurSRV1.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur SRV 1"); - blurTexture1 = nullptr; - blurTexture2 = nullptr; - blurRTV1 = nullptr; - blurRTV2 = nullptr; - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; - return; - } - - hr = device->CreateShaderResourceView(blurTexture2.get(), nullptr, blurSRV2.put()); - if (FAILED(hr)) { - logger::error("Failed to create blur SRV 2"); - blurTexture1 = nullptr; - blurTexture2 = nullptr; - blurRTV1 = nullptr; - blurRTV2 = nullptr; - blurSRV1 = nullptr; - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; + if (!CreateTextureSet(device, texDesc, downsampleTexture, downsampleRTV, downsampleSRV, "downsample") || + !CreateTextureSet(device, texDesc, blurTexture1, blurRTV1, blurSRV1, "blur 1") || + !CreateTextureSet(device, texDesc, blurTexture2, blurRTV2, blurSRV2, "blur 2")) { + ReleaseBlurTextures(); return; } @@ -313,12 +282,12 @@ namespace BackgroundBlur downsampledHeight = dsHeight; } - void PerformBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax) + void PerformBlur(ID3D11Texture2D* sourceTexture, ID3D11ShaderResourceView* sourceSRV, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax, float cornerRadius, ID3D11ShaderResourceView* uiBufferSRV = nullptr, ID3D11RenderTargetView* uiBufferRTV = nullptr) { std::lock_guard lock(resourceMutex); auto context = globals::d3d::context; - if (!context || !sourceTexture || !targetRTV) { + if (!context || !sourceTexture || !sourceSRV || !targetRTV) { return; } @@ -334,14 +303,6 @@ namespace BackgroundBlur D3D11_TEXTURE2D_DESC sourceDesc; sourceTexture->GetDesc(&sourceDesc); - // Create SRV for source - ID3D11ShaderResourceView* sourceSRV = nullptr; - HRESULT hr = globals::d3d::device->CreateShaderResourceView(sourceTexture, nullptr, &sourceSRV); - if (FAILED(hr)) { - logger::error("Failed to create source SRV for blur"); - return; - } - // Save current state ID3D11RenderTargetView* originalRTV = nullptr; ID3D11DepthStencilView* originalDSV = nullptr; @@ -354,63 +315,57 @@ namespace BackgroundBlur ID3D11RasterizerState* originalRS = nullptr; context->RSGetState(&originalRS); - // Downsample source to quarter resolution with bilinear filtering - D3D11_VIEWPORT downsampleViewport = {}; - downsampleViewport.Width = static_cast(downsampledWidth); - downsampleViewport.Height = static_cast(downsampledHeight); - downsampleViewport.MinDepth = 0.0f; - downsampleViewport.MaxDepth = 1.0f; - context->RSSetViewports(1, &downsampleViewport); + auto constantBufferPtr = constantBuffer.get(); + auto samplerStatePtr = samplerState.get(); + + ID3D11ShaderResourceView* nullSRV = nullptr; + + // Set up viewport for all blur passes (1/8 resolution for performance) + D3D11_VIEWPORT blurViewport = {}; + blurViewport.Width = static_cast(downsampledWidth); + blurViewport.Height = static_cast(downsampledHeight); + blurViewport.MinDepth = 0.0f; + blurViewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &blurViewport); auto downsampleRTVPtr = downsampleRTV.get(); context->OMSetRenderTargets(1, &downsampleRTVPtr, nullptr); - - auto constantBufferPtr = constantBuffer.get(); - auto samplerStatePtr = samplerState.get(); context->VSSetShader(vertexShader.get(), nullptr, 0); + context->PSSetShader(horizontalPixelShader.get(), nullptr, 0); context->PSSetSamplers(0, 1, &samplerStatePtr); - // Simple copy to downsample (bilinear filtering does the work) + // Step 1: Downsample game world directly (bilinear filtering does the work) BlurConstants downsampleConstants = {}; downsampleConstants.texelSize[0] = 1.0f / static_cast(sourceDesc.Width); downsampleConstants.texelSize[1] = 1.0f / static_cast(sourceDesc.Height); - downsampleConstants.texelSize[2] = 0.0f; - downsampleConstants.texelSize[3] = 0.0f; downsampleConstants.blurParams[0] = 1; // Single sample for downsample context->UpdateSubresource(constantBuffer.get(), 0, nullptr, &downsampleConstants, 0, 0); - context->PSSetConstantBuffers(0, 1, &constantBufferPtr); - context->PSSetShader(horizontalPixelShader.get(), nullptr, 0); context->PSSetShaderResources(0, 1, &sourceSRV); - context->Draw(3, 0); - - ID3D11ShaderResourceView* nullSRV = nullptr; + context->Draw(FULLSCREEN_TRIANGLE_VERTICES, 0); context->PSSetShaderResources(0, 1, &nullSRV); + // Step 2: Blend UI buffer at downsampled resolution (pre-multiplied alpha) + // Small HUD elements may be slightly softened but this is much faster + if (uiBufferSRV && compositeBlendState) { + context->OMSetBlendState(compositeBlendState.get(), nullptr, 0xFFFFFFFF); + context->PSSetShaderResources(0, 1, &uiBufferSRV); + context->Draw(FULLSCREEN_TRIANGLE_VERTICES, 0); + context->PSSetShaderResources(0, 1, &nullSRV); + context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); + } + // Calculate blur parameters at eighth resolution - float blurRadius = BLUR_INTENSITY * 10.0f; - int sampleCount = 9; + float blurRadius = BLUR_INTENSITY * BLUR_RADIUS_SCALE; + int sampleCount = BLUR_SAMPLE_COUNT; BlurConstants constants = {}; constants.texelSize[0] = blurRadius / static_cast(downsampledWidth); constants.texelSize[1] = blurRadius / static_cast(downsampledHeight); constants.texelSize[2] = BLUR_INTENSITY; - constants.texelSize[3] = 0.0f; constants.blurParams[0] = sampleCount; - constants.blurParams[1] = 0; - constants.blurParams[2] = 0; - constants.blurParams[3] = 0; context->UpdateSubresource(constantBuffer.get(), 0, nullptr, &constants, 0, 0); - - // Set up viewport for blur (quarter resolution) - D3D11_VIEWPORT blurViewport = {}; - blurViewport.Width = static_cast(downsampledWidth); - blurViewport.Height = static_cast(downsampledHeight); - blurViewport.MinDepth = 0.0f; - blurViewport.MaxDepth = 1.0f; - context->RSSetViewports(1, &blurViewport); - context->PSSetConstantBuffers(0, 1, &constantBufferPtr); // First pass: Horizontal blur (on downsampled texture) @@ -419,7 +374,7 @@ namespace BackgroundBlur context->OMSetRenderTargets(1, &rtv1Ptr, nullptr); context->PSSetShader(horizontalPixelShader.get(), nullptr, 0); context->PSSetShaderResources(0, 1, &downsampleSRVPtr); - context->Draw(3, 0); + context->Draw(FULLSCREEN_TRIANGLE_VERTICES, 0); // Second pass: Vertical blur (on downsampled texture) context->PSSetShaderResources(0, 1, &nullSRV); @@ -428,32 +383,59 @@ namespace BackgroundBlur context->OMSetRenderTargets(1, &rtv2Ptr, nullptr); context->PSSetShader(verticalPixelShader.get(), nullptr, 0); context->PSSetShaderResources(0, 1, &srv1Ptr); - context->Draw(3, 0); + context->Draw(FULLSCREEN_TRIANGLE_VERTICES, 0); context->PSSetShaderResources(0, 1, &nullSRV); - // Final composition: upscale from quarter-res with scissor test - // Bilinear sampler smooths the upscale automatically + // Final composition: upscale from quarter-res with rounded corner mask context->RSSetViewports(1, &originalViewport); + // Expand scissor rect slightly for anti-aliased rounded corner edges D3D11_RECT scissorRect; - scissorRect.left = static_cast((std::max)(0.0f, menuMin.x)); - scissorRect.top = static_cast((std::max)(0.0f, menuMin.y)); - scissorRect.right = static_cast((std::min)(static_cast(sourceDesc.Width), menuMax.x)); - scissorRect.bottom = static_cast((std::min)(static_cast(sourceDesc.Height), menuMax.y)); + scissorRect.left = static_cast((std::max)(0.0f, menuMin.x - SCISSOR_AA_PADDING)); + scissorRect.top = static_cast((std::max)(0.0f, menuMin.y - SCISSOR_AA_PADDING)); + scissorRect.right = static_cast((std::min)(static_cast(sourceDesc.Width), menuMax.x + SCISSOR_AA_PADDING)); + scissorRect.bottom = static_cast((std::min)(static_cast(sourceDesc.Height), menuMax.y + SCISSOR_AA_PADDING)); context->RSSetState(scissorRasterizerState.get()); context->RSSetScissorRects(1, &scissorRect); + // Set up window constants for rounded corner shaders (used by both composite and clear) + bool useRoundedCorners = compositePixelShader && clearPixelShader && windowConstantBuffer; + if (useRoundedCorners) { + WindowConstants windowConstants = {}; + windowConstants.windowRect[0] = menuMin.x; + windowConstants.windowRect[1] = menuMin.y; + windowConstants.windowRect[2] = menuMax.x; + windowConstants.windowRect[3] = menuMax.y; + windowConstants.windowParams[0] = cornerRadius; + windowConstants.windowParams[1] = static_cast(sourceDesc.Width); + windowConstants.windowParams[2] = static_cast(sourceDesc.Height); + windowConstants.windowParams[3] = 0.0f; + context->UpdateSubresource(windowConstantBuffer.get(), 0, nullptr, &windowConstants, 0, 0); + auto windowConstantBufferPtr = windowConstantBuffer.get(); + context->PSSetConstantBuffers(1, 1, &windowConstantBufferPtr); + } + + // Draw blur to target context->OMSetRenderTargets(1, &targetRTV, nullptr); - float blendFactor[4] = { 1.0f, 1.0f, 1.0f, BLUR_INTENSITY * 0.8f }; + float blendFactor[4] = { 1.0f, 1.0f, 1.0f, BLUR_INTENSITY * BLEND_ALPHA_SCALE }; context->OMSetBlendState(blendState.get(), blendFactor, 0xFFFFFFFF); - - // Use blurred quarter-res texture, bilinear filtering upscales smoothly + context->PSSetShader(useRoundedCorners ? compositePixelShader.get() : verticalPixelShader.get(), nullptr, 0); auto srv2Ptr = blurSRV2.get(); context->PSSetShaderResources(0, 1, &srv2Ptr); - context->Draw(3, 0); + context->Draw(FULLSCREEN_TRIANGLE_VERTICES, 0); context->PSSetShaderResources(0, 1, &nullSRV); + // Clear UI buffer where blur was drawn (prevents HUD showing through) + if (uiBufferRTV) { + context->OMSetRenderTargets(1, &uiBufferRTV, nullptr); + context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); + + // Clear with same rounded shape - window constants already bound + context->PSSetShader(clearPixelShader.get(), nullptr, 0); + context->Draw(FULLSCREEN_TRIANGLE_VERTICES, 0); + } + // Restore state context->OMSetRenderTargets(1, &originalRTV, originalDSV); context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); @@ -462,8 +444,6 @@ namespace BackgroundBlur context->RSSetScissorRects(0, nullptr); // Cleanup - if (sourceSRV) - sourceSRV->Release(); if (originalRTV) originalRTV->Release(); if (originalDSV) @@ -479,26 +459,20 @@ namespace BackgroundBlur vertexShader = nullptr; horizontalPixelShader = nullptr; verticalPixelShader = nullptr; + compositePixelShader = nullptr; + clearPixelShader = nullptr; constantBuffer = nullptr; + windowConstantBuffer = nullptr; samplerState = nullptr; blendState = nullptr; + compositeBlendState = nullptr; scissorRasterizerState = nullptr; - downsampleTexture = nullptr; - downsampleRTV = nullptr; - downsampleSRV = nullptr; - - blurTexture1 = nullptr; - blurTexture2 = nullptr; - blurRTV1 = nullptr; - blurRTV2 = nullptr; - blurSRV1 = nullptr; - blurSRV2 = nullptr; - - textureWidth = 0; - textureHeight = 0; - downsampledWidth = 0; - downsampledHeight = 0; + ReleaseBlurTextures(); + + cachedSourceSRV = nullptr; + cachedSourceTexture = nullptr; + enabled = false; initialized = false; initializationFailed = false; @@ -509,23 +483,6 @@ namespace BackgroundBlur enabled = enable; } - bool GetEnabled() - { - return enabled; - } - - bool IsEnabled() - { - return enabled && initialized; - } - - void GetTextureDimensions(UINT& outWidth, UINT& outHeight) - { - std::lock_guard lock(resourceMutex); - outWidth = textureWidth; - outHeight = textureHeight; - } - void RenderBackgroundBlur() { if (!enabled) { @@ -542,57 +499,99 @@ namespace BackgroundBlur return; } - // Get current render target - ID3D11RenderTargetView* currentRTV = nullptr; - context->OMGetRenderTargets(1, ¤tRTV, nullptr); + // Check if upscaling with D3D12 swap chain is active + auto& upscaling = globals::features::upscaling; + bool useUpscalingBackbuffer = upscaling.d3d12SwapChainActive; - if (!currentRTV) { - return; + // Back buffer is black on main/loading menu during shader compilation without upscaling + if (!useUpscalingBackbuffer && !(upscaling.loaded && upscaling.IsUpscalingActive())) { + auto ui = globals::game::ui; + bool isMainOrLoading = ui && (ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || ui->IsMenuOpen(RE::LoadingMenu::MENU_NAME)); + auto shaderCache = globals::shaderCache; + if (isMainOrLoading && shaderCache && shaderCache->IsCompiling()) { + return; + } } - // Get render target texture and its dimensions - ID3D11Resource* currentRT = nullptr; - currentRTV->GetResource(¤tRT); + winrt::com_ptr currentTexture; + winrt::com_ptr currentRTV; + ID3D11ShaderResourceView* sourceSRV = nullptr; // Non-owning; lifetime managed elsewhere + ID3D11ShaderResourceView* uiBufferSRV = nullptr; + ID3D11RenderTargetView* uiBufferRTV = nullptr; - ID3D11Texture2D* currentTexture = nullptr; - HRESULT hr = currentRT->QueryInterface(__uuidof(ID3D11Texture2D), (void**)¤tTexture); + if (useUpscalingBackbuffer) { + // When D3D12 swap chain is active, get all resources in one call + auto res = upscaling.GetBlurResources(); + if (!res.backbufferTex || !res.backbufferRTV || !res.backbufferSRV) { + return; + } + currentTexture.copy_from(res.backbufferTex); + currentRTV.copy_from(res.backbufferRTV); + sourceSRV = res.backbufferSRV; + + // During gameplay (not paused), HUD is in separate UI buffer + auto ui = globals::game::ui; + if (ui && !ui->GameIsPaused()) { + uiBufferSRV = res.uiBufferSRV; + uiBufferRTV = res.uiBufferRTV; + } + } else { + // Normal path: get current render target + ID3D11RenderTargetView* rawRTV = nullptr; + context->OMGetRenderTargets(1, &rawRTV, nullptr); + if (!rawRTV) { + return; + } + currentRTV.attach(rawRTV); // Takes ownership of the AddRef from OMGetRenderTargets - if (FAILED(hr) || !currentTexture) { - if (currentRT) - currentRT->Release(); - if (currentRTV) - currentRTV->Release(); - return; + // Get render target texture + winrt::com_ptr currentRT; + currentRTV->GetResource(currentRT.put()); + + winrt::com_ptr tex; + if (FAILED(currentRT->QueryInterface(IID_PPV_ARGS(tex.put()))) || !tex) { + return; + } + currentTexture = tex; + + // Cache SRV for non-upscaling path (avoids CreateShaderResourceView every frame) + if (cachedSourceTexture != currentTexture.get()) { + cachedSourceSRV = nullptr; + HRESULT hr = device->CreateShaderResourceView(currentTexture.get(), nullptr, cachedSourceSRV.put()); + if (FAILED(hr)) { + logger::error("Failed to create cached source SRV for blur"); + return; + } + cachedSourceTexture = currentTexture.get(); + } + sourceSRV = cachedSourceSRV.get(); } D3D11_TEXTURE2D_DESC texDesc; currentTexture->GetDesc(&texDesc); // Create blur textures if needed - UINT currentWidth, currentHeight; - GetTextureDimensions(currentWidth, currentHeight); - if (currentWidth != texDesc.Width || currentHeight != texDesc.Height) { + if (textureWidth != texDesc.Width || textureHeight != texDesc.Height) { CreateBlurTextures(texDesc.Width, texDesc.Height, texDesc.Format); } // Find ImGui windows that need blur ImGuiContext* ctx = ImGui::GetCurrentContext(); if (!ctx || ctx->Windows.Size == 0) { - currentTexture->Release(); - currentRT->Release(); - currentRTV->Release(); return; } // Apply blur behind each visible ImGui window for (int i = 0; i < ctx->Windows.Size; i++) { ImGuiWindow* window = ctx->Windows[i]; - if (!window || window->Hidden || !window->WasActive || window->SkipItems) { + // Don't check Hidden - it causes a 1-frame blur delay when windows reappear + if (!window || !window->WasActive || window->SkipItems) { continue; } // Skip child windows - only blur root windows to cover headers and footers - if (window->ParentWindow != nullptr) { + // Exception: docked windows are visually independent even though ParentWindow is set + if (window->ParentWindow != nullptr && !window->DockIsActive) { continue; } @@ -618,14 +617,13 @@ namespace BackgroundBlur ImVec2 windowMin = windowRect.Min; ImVec2 windowMax = windowRect.Max; - // Perform blur for this window area - PerformBlur(currentTexture, currentRTV, windowMin, windowMax); - } + // Get window corner rounding from the window's style + float cornerRadius = window->WindowRounding; - // Cleanup - currentTexture->Release(); - currentRT->Release(); - currentRTV->Release(); + // Perform blur for this window area with rounded corners + // Pass UI buffer SRV/RTV for compositing and clearing during upscaling gameplay + PerformBlur(currentTexture.get(), sourceSRV, currentRTV.get(), windowMin, windowMax, cornerRadius, uiBufferSRV, uiBufferRTV); + } } } // namespace BackgroundBlur diff --git a/src/Menu/BackgroundBlur.h b/src/Menu/BackgroundBlur.h index c4891a904c..6ff24bb60f 100644 --- a/src/Menu/BackgroundBlur.h +++ b/src/Menu/BackgroundBlur.h @@ -20,42 +20,11 @@ namespace BackgroundBlur */ void RenderBackgroundBlur(); - /** - * @brief Creates or recreates blur textures with specified dimensions - * @param width Texture width in pixels - * @param height Texture height in pixels - * @param format Texture format - */ - void CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT format); - - /** - * @brief Performs two-pass Gaussian blur on source texture - * @param sourceTexture Input texture to blur - * @param targetRTV Output render target - * @param menuMin Top-left corner of menu area (for scissor test) - * @param menuMax Bottom-right corner of menu area (for scissor test) - */ - void PerformBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax); - /** * @brief Cleans up all blur resources */ void Cleanup(); void SetEnabled(bool enable); - bool GetEnabled(); - - /** - * @brief Checks if blur is enabled - * @return True if blur intensity > 0 - */ - bool IsEnabled(); - - /** - * @brief Gets current blur texture dimensions - * @param outWidth Output width - * @param outHeight Output height - */ - void GetTextureDimensions(UINT& outWidth, UINT& outHeight); } // namespace BackgroundBlur