diff --git a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli index 4152bdb2d5..bd18791f4a 100644 --- a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli +++ b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli @@ -325,6 +325,7 @@ namespace ExtendedMaterials float2 GetParallaxCoords(float distance, float2 coords, float mipLevel, float3 viewDir, float3x3 tbn, float noise, Texture2D tex, SamplerState texSampler, uint channel, DisplacementParams params, out float pixelOffset) #endif { + pixelOffset = 0; float3 viewDirTS = normalize(mul(tbn, viewDir)); #if defined(LANDSCAPE) viewDirTS.xy /= viewDirTS.z * 0.7 + 0.3 + params[0].FlattenAmount; // Fix for objects at extreme viewing angles @@ -496,7 +497,7 @@ namespace ExtendedMaterials #endif nearBlendToFar *= nearBlendToFar; float offset = (1.0 - parallaxAmount) * -maxHeight + minHeight; - pixelOffset = lerp(parallaxAmount * scale, 0, nearBlendToFar); + pixelOffset = saturate(lerp(parallaxAmount, 0.5, nearBlendToFar)); return lerp(viewDirTS.xy * offset + coords.xy, coords, nearBlendToFar); } diff --git a/features/VR Stereo Optimizations/Shaders/Features/VRStereoOptimizations.ini b/features/VR Stereo Optimizations/Shaders/Features/VRStereoOptimizations.ini new file mode 100644 index 0000000000..000b60a568 --- /dev/null +++ b/features/VR Stereo Optimizations/Shaders/Features/VRStereoOptimizations.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 4ea0d4d07c..959678edff 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -20,11 +20,16 @@ namespace SharedData float Timer; uint FrameCount; uint FrameCountAlwaysActive; - bool InInterior; // If the area lacks a directional shadow light e.g. the sun or moon - bool InMapMenu; // If the world/local map is open (note that the renderer is still deferred here) - bool HideSky; // HideSky flag in WorldSpace, e.g. Blackreach - float MipBias; // Offset to mip level for TAA sharpness# - float pad0; + bool InInterior; // If the area lacks a directional shadow light e.g. the sun or moon + bool InMapMenu; // If the world/local map is open (note that the renderer is still deferred here) + bool HideSky; // HideSky flag in WorldSpace, e.g. Blackreach + float MipBias; // Offset to mip level for TAA sharpness + float VRMipBias; // Additional negative MIP bias for VR foliage sharpening (depth-scaled) + float VRMipBiasNearDist; // Game units: no VR MIP bias closer than this + float VRMipBiasFarDist; // Game units: full VR MIP bias beyond this + uint VRMipBiasMode; // 0=Off, 1=All Textures, 2=Distant Trees (TREE_ANIM) only + float VRAlphaTestThreshold; // Alpha test threshold for VR TREE_ANIM (0 = disabled) + float2 pad0; float4 AmbientSHR; float4 AmbientSHG; float4 AmbientSHB; @@ -52,7 +57,7 @@ namespace SharedData bool EnableShadows; bool ExtendShadows; bool EnableParallaxWarpingFix; - float1 pad0; + bool pad0; }; struct CubemapCreatorSettings diff --git a/package/Shaders/Common/VR.hlsli b/package/Shaders/Common/VR.hlsli index d744022781..9bf341a6db 100644 --- a/package/Shaders/Common/VR.hlsli +++ b/package/Shaders/Common/VR.hlsli @@ -21,6 +21,7 @@ cbuffer VRValues : register(b13) float2 EyeOffsetScale : packoffset(c0.z); float4 EyeClipEdge[2] : packoffset(c1); } + #endif namespace Stereo @@ -626,6 +627,20 @@ namespace Stereo vsout.VRPosition.z = clipPos.z; vsout.VRPosition.w = clipPos.w; + // Sub-pixel diagonal jitter for Eye 1 stereo edge supersampling. + // Shifts Eye 1 rasterization by ~0.75px so alpha-tested edges (tree branches, + // fences) sample at slightly different positions than Eye 0, giving StereoBlend + // reprojection better edge detail to work with. + // Hardcoded NDC values: FrameBuffer/SharedData cbuffers are not reliably + // available in all vertex shader contexts (VSHADER section only includes + // FrameBuffer.hlsli, and BufferDim lives in SharedData). These constants + // give ~0.75px offset at the 6304x3088 SBS reference resolution and scale + // proportionally at other resolutions since NDC is resolution-relative. + if (a_eyeIndex == 1) { + static const float2 kJitterNDC = float2(1.68e-4, -3.44e-4); + vsout.VRPosition.xy += kJitterNDC * vsout.VRPosition.w; + } + vsout.ClipDistance = clipEdges.y; vsout.CullDistance = clipEdges.x; # endif // VR diff --git a/package/Shaders/DeferredCompositeCS.hlsl b/package/Shaders/DeferredCompositeCS.hlsl index f149255718..61ad48bfb7 100644 --- a/package/Shaders/DeferredCompositeCS.hlsl +++ b/package/Shaders/DeferredCompositeCS.hlsl @@ -19,6 +19,10 @@ RWTexture2D NormalTAAMaskSpecularMaskRW : register(u1); RWTexture2D MotionVectorsRW : register(u2); Texture2D DepthTexture : register(t4); +#if defined(VR_STEREO_OPT) +Texture2D StereoOptModeTexture : register(t16); +#endif + #if defined(DYNAMIC_CUBEMAPS) Texture2D ReflectanceTexture : register(t5); TextureCube EnvTexture : register(t6); @@ -92,6 +96,16 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, uv *= FrameBuffer::DynamicResolutionParams2.xy; // adjust for dynamic res uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + +#if defined(VR_STEREO_OPT) + if (eyeIndex == 1) { + uint mode = StereoOptModeTexture[uint2(dispatchID.xy)]; + if (mode == 2) { // MODE_MAIN — stencil-culled, no valid G-buffer + return; + } + } +#endif + uv = Stereo::ConvertFromStereoUV(uv, eyeIndex); float3 normalGlossiness = NormalRoughnessTexture[dispatchID.xy]; diff --git a/package/Shaders/DistantTree.hlsl b/package/Shaders/DistantTree.hlsl index cbd4608676..0fa3314f0f 100644 --- a/package/Shaders/DistantTree.hlsl +++ b/package/Shaders/DistantTree.hlsl @@ -203,8 +203,14 @@ PS_OUTPUT main(PS_INPUT input) float alpha = TexDiffuse.SampleBias(SampDiffuse, input.TexCoord.xy, SharedData::MipBias).w; - if ((alpha - AlphaTestRefRS) < 0) { - discard; + { + float alphaRef = AlphaTestRefRS; +# if defined(VR) + alphaRef -= eyeIndex * 0.1; +# endif + if ((alpha - alphaRef) < 0) { + discard; + } } psout.Diffuse.xyz = input.Depth.xxx / input.Depth.yyy; @@ -213,8 +219,14 @@ PS_OUTPUT main(PS_INPUT input) float4 baseColor = TexDiffuse.SampleBias(SampDiffuse, input.TexCoord.xy, SharedData::MipBias); baseColor.xyz = Color::Diffuse(baseColor.xyz); - if ((baseColor.w - AlphaTestRefRS) < 0) { - discard; + { + float alphaRef = AlphaTestRefRS; +# if defined(VR) + alphaRef -= eyeIndex * 0.1; +# endif + if ((baseColor.w - alphaRef) < 0) { + discard; + } } # if defined(DEFERRED) diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 777d0bd0c0..a28f6ed7ab 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -1780,7 +1780,19 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) rawRMAOS = blendedRMAOS; # endif # else // Non-landscape code - float4 rawBaseColor = TexColorSampler.SampleBias(SampColorSampler, diffuseUv, SharedData::MipBias); + // VR MIP bias: depth-gated sharpening for distant textures + // Mode 1 = All Textures, Mode 2 = Distant Trees (TREE_ANIM) only + float vrFoliageBias = 0; +# if defined(TREE_ANIM) + if (SharedData::VRMipBias < 0) { +# else + if (SharedData::VRMipBias < 0 && SharedData::VRMipBiasMode == 1) { +# endif + float linDepth = SharedData::GetScreenDepth(input.Position.z); + float t = saturate((linDepth - SharedData::VRMipBiasNearDist) / max(SharedData::VRMipBiasFarDist - SharedData::VRMipBiasNearDist, 1.0)); + vrFoliageBias = SharedData::VRMipBias * t; + } + float4 rawBaseColor = TexColorSampler.SampleBias(SampColorSampler, diffuseUv, SharedData::MipBias + vrFoliageBias); baseColor = float4(Color::Diffuse(rawBaseColor.rgb), rawBaseColor.a); float4 normalColor = TexNormalSampler.SampleBias(SampNormalSampler, uv, SharedData::MipBias); normal = normalColor; @@ -3021,11 +3033,11 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float alpha = baseColor.w; # if defined(EMAT) && !defined(LANDSCAPE) # if defined(PARALLAX) - alpha = TexColorSampler.SampleBias(SampColorSampler, uvOriginal, SharedData::MipBias).w; + alpha = TexColorSampler.SampleBias(SampColorSampler, uvOriginal, SharedData::MipBias + vrFoliageBias).w; # elif defined(TRUE_PBR) [branch] if (PBRParallax) { - alpha = TexColorSampler.SampleBias(SampColorSampler, uvOriginal, SharedData::MipBias).w; + alpha = TexColorSampler.SampleBias(SampColorSampler, uvOriginal, SharedData::MipBias + vrFoliageBias).w; } # endif # endif @@ -3074,9 +3086,28 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } alpha = saturate(1.05 * alpha); # endif // DEPTH_WRITE_DECALS +# if defined(TREE_ANIM) + // Fixed alpha floor — catch zombie texels with near-zero alpha + if (alpha < 0.1) { + discard; + } + if (alpha - AlphaTestRefRS < 0) { + discard; + } + // Suppress RGB fringe contamination from negative MIP bias. + // Low-alpha texels near the foliage boundary have bright padding bleeding into RGB. + // Alpha is a direct proxy for contamination — low alpha = more padding contribution. + // Scale correction by bias strength so close-range (no bias) textures are untouched. + if (vrFoliageBias < 0) { + float biasStrength = saturate(vrFoliageBias / min(SharedData::VRMipBias, -0.001)); + float fringeScale = 5.0; // higher = more aggressive fringe suppression + baseColor.rgb *= saturate(alpha * lerp(1.0, fringeScale, biasStrength)); + } +# else if (alpha - AlphaTestRefRS < 0) { discard; } +# endif // TREE_ANIM # endif // DO_ALPHA_TEST # if defined(ANISOTROPIC_ALPHA) @@ -3166,7 +3197,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # endif - psout.Reflectance = float4(indirectLobeWeights.specular, psout.Diffuse.w); +# if (defined(EMAT) || defined(TRUE_PBR)) && (defined(PARALLAX) || defined(LANDSCAPE)) + psout.Reflectance = float4(indirectLobeWeights.specular, + (pixelOffset > 0.0) ? saturate(pixelOffset) : 0.0); +# else + psout.Reflectance = float4(indirectLobeWeights.specular, 0.0); +# endif psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), saturate(1.0 - material.Roughness), psout.Diffuse.w); # if defined(SNOW) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index f05c3d0edd..664fe97a6a 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -480,22 +480,37 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) bool complex = abs(complexLength - 1.0) < SharedData::grassLightingSettings.ComplexGrassThreshold; # endif // !TRUE_PBR + // VR MIP bias: depth-gated sharpening for distant textures + float vrGrassBias = 0; + if (SharedData::VRMipBias < 0 && SharedData::VRMipBiasMode == 1) { + float linDepth = SharedData::GetScreenDepth(input.HPosition.z); + float t = saturate((linDepth - SharedData::VRMipBiasNearDist) / max(SharedData::VRMipBiasFarDist - SharedData::VRMipBiasNearDist, 1.0)); + vrGrassBias = SharedData::VRMipBias * t; + } + float4 baseColor; # if !defined(TRUE_PBR) if (complex) { - baseColor = TexBaseSampler.SampleBias(SampBaseSampler, float2(input.TexCoord.x, input.TexCoord.y * 0.5), SharedData::MipBias); + baseColor = TexBaseSampler.SampleBias(SampBaseSampler, float2(input.TexCoord.x, input.TexCoord.y * 0.5), SharedData::MipBias + vrGrassBias); } else # endif // !TRUE_PBR { - baseColor = TexBaseSampler.SampleBias(SampBaseSampler, input.TexCoord.xy, SharedData::MipBias); + baseColor = TexBaseSampler.SampleBias(SampBaseSampler, input.TexCoord.xy, SharedData::MipBias + vrGrassBias); } baseColor.xyz = Color::Diffuse(baseColor.xyz); # if defined(RENDER_DEPTH) float diffuseAlpha = input.VertexColor.w * baseColor.w; - if ((diffuseAlpha - AlphaTestRefRS) < 0) { - discard; + { + float alphaRef = AlphaTestRefRS; +# if defined(VR) + uint convergenceEyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); + alphaRef -= convergenceEyeIndex * 0.1; +# endif + if ((diffuseAlpha - alphaRef) < 0) { + discard; + } } # endif // RENDER_DEPTH || DO_ALPHA_TEST @@ -505,9 +520,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.PS.w = diffuseAlpha; # else # if !defined(TRUE_PBR) - float4 specColor = complex ? TexBaseSampler.SampleBias(SampBaseSampler, float2(input.TexCoord.x, 0.5 + input.TexCoord.y * 0.5), SharedData::MipBias) : 1; + float4 specColor = complex ? TexBaseSampler.SampleBias(SampBaseSampler, float2(input.TexCoord.x, 0.5 + input.TexCoord.y * 0.5), SharedData::MipBias + vrGrassBias) : 1; # else - float4 specColor = TexNormalSampler.SampleBias(SampNormalSampler, input.TexCoord.xy, SharedData::MipBias); + float4 specColor = TexNormalSampler.SampleBias(SampNormalSampler, input.TexCoord.xy, SharedData::MipBias + vrGrassBias); # endif uint eyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); @@ -548,7 +563,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // !TRUE_PBR # if defined(TRUE_PBR) - float4 rawRMAOS = TexRMAOSSampler.SampleBias(SampRMAOSSampler, input.TexCoord.xy, SharedData::MipBias) * float4(PBRParams1.x, 1, 1, PBRParams1.y); + float4 rawRMAOS = TexRMAOSSampler.SampleBias(SampRMAOSSampler, input.TexCoord.xy, SharedData::MipBias + vrGrassBias) * float4(PBRParams1.x, 1, 1, PBRParams1.y); PBR::SurfaceProperties pbrSurfaceProperties = PBR::InitSurfaceProperties(); @@ -846,13 +861,27 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; - float4 baseColor = TexBaseSampler.SampleBias(SampBaseSampler, input.TexCoord.xy, SharedData::MipBias); + // VR MIP bias: depth-gated sharpening for distant textures + float vrGrassBias = 0; + if (SharedData::VRMipBias < 0 && SharedData::VRMipBiasMode == 1) { + float linDepth = SharedData::GetScreenDepth(input.HPosition.z); + float t = saturate((linDepth - SharedData::VRMipBiasNearDist) / max(SharedData::VRMipBiasFarDist - SharedData::VRMipBiasNearDist, 1.0)); + vrGrassBias = SharedData::VRMipBias * t; + } + + float4 baseColor = TexBaseSampler.SampleBias(SampBaseSampler, input.TexCoord.xy, SharedData::MipBias + vrGrassBias); # if defined(RENDER_DEPTH) float diffuseAlpha = input.VertexColor.w * baseColor.w; - - if ((diffuseAlpha - AlphaTestRefRS) < 0) { - discard; + { + float alphaRef = AlphaTestRefRS; +# if defined(VR) + uint convergenceEyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); + alphaRef -= convergenceEyeIndex * 0.1; +# endif + if ((diffuseAlpha - alphaRef) < 0) { + discard; + } } # endif // RENDER_DEPTH || DO_ALPHA_TEST diff --git a/package/Shaders/VR/CASCS.hlsl b/package/Shaders/VR/CASCS.hlsl new file mode 100644 index 0000000000..6ed2768e43 --- /dev/null +++ b/package/Shaders/VR/CASCS.hlsl @@ -0,0 +1,74 @@ +// AMD Contrast Adaptive Sharpening (CAS) - Sharpen-only for VR +// Based on AMD FidelityFX CAS (sharpen-only path) +// Reference: https://gpuopen.com/fidelityfx-cas/ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// CASParams[0] = sharpness (0.0 = no sharpening, 1.0 = maximum sharpening) +StructuredBuffer CASParams : register(t1); + +Texture2D Source : register(t0); +RWTexture2D Dest : register(u0); + +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + uint2 texDim; + Dest.GetDimensions(texDim.x, texDim.y); + + if (DTid.x >= texDim.x || DTid.y >= texDim.y) + return; + + float sharpness = CASParams[0]; + + // Fetch 3x3 neighborhood + int2 sp = int2(DTid.xy); + float3 a = Source.Load(int3(sp + int2(-1, -1), 0)).rgb; + float3 b = Source.Load(int3(sp + int2(0, -1), 0)).rgb; + float3 c = Source.Load(int3(sp + int2(1, -1), 0)).rgb; + float3 d = Source.Load(int3(sp + int2(-1, 0), 0)).rgb; + float3 e = Source.Load(int3(sp, 0)).rgb; + float3 f = Source.Load(int3(sp + int2(1, 0), 0)).rgb; + float3 g = Source.Load(int3(sp + int2(-1, 1), 0)).rgb; + float3 h = Source.Load(int3(sp + int2(0, 1), 0)).rgb; + float3 i = Source.Load(int3(sp + int2(1, 1), 0)).rgb; + + // Soft min/max of cross neighborhood + float3 mnRGB = min(min(min(d, e), min(f, b)), h); + float3 mxRGB = max(max(max(d, e), max(f, b)), h); + + // Expand with diagonal neighbors for soft min/max + float3 mnRGB2 = min(min(a, c), min(g, i)); + float3 mxRGB2 = max(max(a, c), max(g, i)); + mnRGB += mnRGB2; + mxRGB += mxRGB2; + + // Adaptive sharpening amount + float3 ampRGB = saturate(min(mnRGB, 2.0 - mxRGB) * rcp(max(mxRGB, 1e-4))); + ampRGB = rsqrt(ampRGB); + + // Peak controls sharpening strength: + // sharpness 0.0 -> peak 8.0 (no sharpening) + // sharpness 1.0 -> peak 5.0 (maximum sharpening) + float peak = -3.0 * sharpness + 8.0; + float3 wRGB = -rcp(ampRGB * peak); + float3 rcpWeightRGB = rcp(4.0 * wRGB + 1.0); + + // Apply sharpening filter + float3 outColor = saturate(((b + d) + (f + h)) * wRGB + e) * rcpWeightRGB; + + Dest[DTid.xy] = float4(outColor, 1.0); +} diff --git a/package/Shaders/VR/StereoBlendCS.hlsl b/package/Shaders/VR/StereoBlendCS.hlsl index 7322e9e513..c443d04d1b 100644 --- a/package/Shaders/VR/StereoBlendCS.hlsl +++ b/package/Shaders/VR/StereoBlendCS.hlsl @@ -11,6 +11,7 @@ #include "Common/Color.hlsli" #include "Common/FrameBuffer.hlsli" +#include "Common/SharedData.hlsli" #include "Common/VR.hlsli" Texture2D ColorTexture : register(t0); @@ -18,6 +19,38 @@ Texture2D DepthTexture : register(t1); RWTexture2D OutputRW : register(u0); +#ifdef STEREO_OVERWRITE +RWTexture2D MotionRW : register(u1); +Texture2D ModeTexture : register(t2); +Texture2D ReflectanceTexture : register(t3); // .w = POM pixelOffset from Lighting pass +SamplerState LinearSampler : register(s0); + +// Mode constants matching VRStereoOptimizations/cbuffers.hlsli +// (can't include directly — its cbuffer on b1 conflicts with StereoBlendCB) +# define MODE_DISOCCLUDED 0 +# define MODE_EDGE 1 +# define MODE_MAIN 2 +# define MODE_EDGE_NEIGHBOUR 3 +# define MODE_FULL_BLEND 4 + +// Hardware bilinear color sample from reprojected pixel coordinates. +// Converts integer pixel coords to proper full-texture UV for SampleLevel, +// clamped to the active DRS viewport to prevent sampling stale data. +// Motion vectors stay as integer Load() — filtering them breaks DLSS. +float4 SampleReprojectedColor(int2 reprojPx, float2 frameDim) +{ + uint texW, texH; + ColorTexture.GetDimensions(texW, texH); + float2 texSize = float2(texW, texH); + float2 sampleUV = (float2(reprojPx) + 0.5) / texSize; + // Clamp to active DRS viewport bounds (half-texel inset to keep bilinear inside valid region) + float2 minUV = 0.5 / texSize; + float2 maxUV = (frameDim - 0.5) / texSize; + sampleUV = clamp(sampleUV, minUV, maxUV); + return ColorTexture.SampleLevel(LinearSampler, sampleUV, 0); +} +#endif + cbuffer StereoBlendCB : register(b1) { float2 FrameDim; @@ -25,11 +58,16 @@ cbuffer StereoBlendCB : register(b1) float DepthSigma; float MaxBlendFactor; float ColorDiffThreshold; - float pad; + float DebugEdgeTint; + uint DebugMode; // 0 = normal, 1 = depth map diagnostic, 2 = full blend depth visualizer, 3 = POM depth heatmap + float FullBlendDistance; + float POMDepthScale; + float _pad; }; -static const float kEdgeDepthThreshold = 0.05; // NDC depth difference above which a pixel is considered a depth discontinuity and excluded from stereo blend -static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check +static const float kEdgeDepthThreshold = 0.05; // NDC depth difference above which a pixel is considered a depth discontinuity and excluded from stereo blend +static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check +static const float kDepthAgreementThreshold = 0.015; // Relative depth difference threshold for overwrite mode disocclusion rejection // Samples four depth neighbors in a cross pattern (±offset pixels) around center, // clamped to eyeIndex's half of the packed stereo buffer to avoid seam contamination. @@ -46,6 +84,175 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) if (any(dtid >= uint2(FrameDim))) return; +#ifdef STEREO_OVERWRITE + // ========================================================================= + // Mode-driven stereo merge: reads per-pixel classification from StencilCS + // and applies appropriate action per mode and eye. + // Mode texture is full SBS resolution — ModeTexture[dtid] maps directly. + // ========================================================================= + + float2 uv = (dtid + 0.5) * RcpFrameDim; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + float centerDepth = DepthTexture[dtid]; + + // HMD mask pixels (depth >= 1.0 in reversed-Z) — always skip + if (centerDepth >= 1.0) + return; + + uint pixelMode = ModeTexture[dtid]; + + // Debug mode 1: depth map diagnostic — show mode texture as solid colors (all pixels) + if (DebugMode == 1) { + float4 c = ColorTexture[dtid]; + if (pixelMode == MODE_EDGE) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), 0.5), c.a); + else if (pixelMode == MODE_EDGE_NEIGHBOUR) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0, 1), 0.5), c.a); + else if (pixelMode == MODE_DISOCCLUDED) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 0.5, 1), 0.3), c.a); + else if (pixelMode == MODE_FULL_BLEND) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0.5, 0), 0.5), c.a); + return; + } + + // Debug mode 2: full blend depth visualizer — cyan tint based on proximity to FullBlendDistance + if (DebugMode == 2) { + if (centerDepth < 1e-5 || centerDepth >= 1.0) + return; + float linDepth = SharedData::GetScreenDepth(centerDepth); + if (linDepth < FullBlendDistance) { + float4 c = ColorTexture[dtid]; + float proximity = saturate(1.0 - linDepth / max(FullBlendDistance, 1.0)); + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 1), proximity * 0.4), c.a); + } + return; + } + + // Debug mode 3: POM depth data visualizer — show Reflectance.w as color + if (DebugMode == 3) { + float pomVal = ReflectanceTexture[dtid].w; + float4 c = ColorTexture[dtid]; + if (pomVal > 1e-2) { + // POM pixel: red-to-green gradient based on parallaxAmount + // Red = peak (high pomVal, closer to camera), Green = valley (low pomVal, farther), Yellow = geometry plane + float3 pomColor = float3(pomVal, 1.0 - pomVal, 0); + OutputRW[dtid] = float4(lerp(c.rgb, pomColor, 0.7), c.a); + } + // Non-POM pixels (pomVal ~ 0) left untouched + return; + } + + // MODE_DISOCCLUDED: fully shaded, leave untouched + if (pixelMode == MODE_DISOCCLUDED) + return; + + // MODE_FULL_BLEND: bilateral blend for 2x supersampling + if (pixelMode == MODE_FULL_BLEND) { + float4 center = ColorTexture[dtid]; + + // Check for POM depth offset at this pixel + // pixelOffset = parallaxAmount (0-1) from ExtendedMaterials, 0.5 = geometry plane. + // Values > 0.5 are peaks (closer to camera), < 0.5 are valleys (farther from camera). + // Correction: high pomVal should push depth closer (smaller linear depth), + // so we use (0.5 - pomOffset) to get a negative correction for peaks. + // Non-POM pixels store 0.0, so threshold > 1e-2 distinguishes them. + float reprojDepthFB = centerDepth; + float pomOffsetFB = ReflectanceTexture[dtid].w; + if (pomOffsetFB > 1e-2 && POMDepthScale > 0) { + float linDepthFB = SharedData::GetScreenDepth(centerDepth); + float depthCorrectionFB = (0.5 - pomOffsetFB) * POMDepthScale; + float newLinDepthFB = max(linDepthFB + depthCorrectionFB, 1e-4); + reprojDepthFB = (SharedData::CameraData.x - SharedData::CameraData.w / newLinDepthFB) / SharedData::CameraData.z; + } + + // Reproject to the other eye + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, reprojDepthFB, eyeIndex, FrameDim); + if (!r.valid) { + // Debug tint for failed reprojection + if (DebugEdgeTint > 0) + OutputRW[dtid] = float4(lerp(center.rgb, float3(1, 0.5, 0), DebugEdgeTint), center.a); + return; + } + + // Only blend with pixels that have valid composited data in both eyes + uint otherMode = ModeTexture[r.otherPx]; + if (otherMode != MODE_FULL_BLEND && otherMode != MODE_DISOCCLUDED) + return; + + float4 otherColor = SampleReprojectedColor(r.otherPx, FrameDim); + float otherDepth = DepthTexture[r.otherPx]; + + // Depth-weighted bilateral blend + float maxDepth = max(max(centerDepth, otherDepth), 1e-5); + float depthAgreement = 1.0 - saturate(abs(centerDepth - otherDepth) / maxDepth / 0.02); + float blendWeight = 0.5 * depthAgreement; + + float4 result = lerp(center, otherColor, blendWeight); + + if (DebugEdgeTint > 0) + result.rgb = lerp(result.rgb, float3(0, 1, 1), DebugEdgeTint); + + OutputRW[dtid] = result; + return; + } + + if (eyeIndex == 0) { + // Eye 0: fully shaded for all modes — only apply debug tint to edge pixels + if (DebugEdgeTint > 0 && pixelMode == MODE_EDGE) { + float4 c = ColorTexture[dtid]; + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), DebugEdgeTint), c.a); + } + return; + } + + // Eye 1: reproject all non-disoccluded, non-full-blend pixels (MAIN, EDGE) from Eye 0. + // StencilCS already performed the authoritative disocclusion check with the correct + // depth buffer state — no redundant depth agreement check here. + float reprojDepth = centerDepth; + + // First-pass reprojection to find Eye 0 source pixel + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, reprojDepth, eyeIndex, FrameDim); + if (!r.valid) + return; + + // Read POM offset from Eye 0 source's reflectance.w + // pixelOffset = parallaxAmount (0-1) from ExtendedMaterials, 0.5 = geometry plane. + // Values > 0.5 are peaks (closer to camera), < 0.5 are valleys (farther from camera). + // Correction: high pomVal should push depth closer (smaller linear depth), + // so we use (0.5 - pomOffset) to get a negative correction for peaks. + // Non-POM pixels store 0.0, so threshold > 1e-2 distinguishes them. + float pomOffset = ReflectanceTexture[r.otherPx].w; + if (pomOffset > 1e-2) { + // Re-reproject with POM-adjusted depth centered at geometry plane + float linearDepth = SharedData::GetScreenDepth(centerDepth); + float depthCorrection = (0.5 - pomOffset) * POMDepthScale; + float newLinearDepth = max(linearDepth + depthCorrection, 1e-4); + reprojDepth = (SharedData::CameraData.x - SharedData::CameraData.w / newLinearDepth) / SharedData::CameraData.z; + r = Stereo::ReprojectToOtherEye(uv, reprojDepth, eyeIndex, FrameDim); + if (!r.valid) + return; + } + + // Skip if the Eye 0 source pixel is sky/unrendered (depth at clear value). + // At DeferredPasses time, sky hasn't rendered yet — source would have clear color. + // Let the sky/water pass fill these pixels later instead. + float sourceDepth = DepthTexture[r.otherPx]; + if (sourceDepth >= 1.0 || sourceDepth < 1e-5) + return; + + OutputRW[dtid] = SampleReprojectedColor(r.otherPx, FrameDim); + MotionRW[dtid] = MotionRW[r.otherPx]; + +#else // Normal bilateral blend path + +# ifdef EYE0_ONLY + // Only process Eye 0 (left half) - Eye 1 left untouched + float2 uvCheck = (dtid + 0.5) * RcpFrameDim; + if (Stereo::GetEyeIndexFromTexCoord(uvCheck) == 1) + return; +# endif + float2 uv = (dtid + 0.5) * RcpFrameDim; uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); @@ -68,8 +275,7 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) // depth == 1.0: sky/far plane (no real geometry, bilateral reprojection not meaningful) bool isSkipPixel = centerDepth < 1e-5 || centerDepth >= 1.0; if (!isSkipPixel) { - // Source edge detection: skip at depth discontinuities (arm/world silhouettes, - // object edges). Saves VP reprojection work and prevents halo artifacts. + // Normal bilateral blend path float4 srcEdgeDepths = SampleCrossDepths(dtid, 1, eyeIndex); if (Stereo::MaxDepthDiff(centerDepth, srcEdgeDepths) > kEdgeDepthThreshold) { debugState = 1; @@ -78,10 +284,6 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) if (r.valid) { float otherDepth = DepthTexture[r.otherPx]; - // Destination edge detection: skip if the reprojected pixel is near the HMD - // mask boundary or at a depth discontinuity in the other eye. Due to VR - // parallax the arm silhouette appears at a different screen position per eye, - // so the reprojection can cross a boundary invisible from this eye. float4 dstEdgeDepths = SampleCrossDepths(r.otherPx, kEdgeMargin, 1 - eyeIndex); if (any(dstEdgeDepths < 1e-5) || Stereo::MaxDepthDiff(otherDepth, dstEdgeDepths) > kEdgeDepthThreshold) { debugState = 2; @@ -89,9 +291,6 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) float4 otherColor = ColorTexture[r.otherPx]; Stereo::FinalizeStereoBlend(r, uv, centerDepth, otherDepth, eyeIndex, FrameDim, DepthSigma, MaxBlendFactor); - // Only blend where the two eyes actually disagree (screen-space effect - // inconsistency). Luminance difference below the threshold means both - // eyes computed the same result and blending would only destroy parallax. float colorDiff = abs(dot(centerColor.rgb, float3(0.2126, 0.7152, 0.0722)) - dot(otherColor.rgb, float3(0.2126, 0.7152, 0.0722))); float colorGate = smoothstep(ColorDiffThreshold * 0.5, ColorDiffThreshold * 2.0, colorDiff); @@ -106,7 +305,7 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) } } -#ifdef DEBUG_BACKCHECK +# ifdef DEBUG_BACKCHECK // Debug visualization (6 states): // Blue = mask/sky: skipped // Yellow = source edge: depth discontinuity at this pixel @@ -123,7 +322,7 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) float3(0.5, 0.0, 0.0) // 5: back-check failed - red }; OutputRW[dtid] = float4(lerp(centerColor.rgb, debugColors[debugState], 0.7), centerColor.a); -#elif defined(DEBUG_BLEND_WEIGHT) +# elif defined(DEBUG_BLEND_WEIGHT) // Blend weight heatmap: only pixels with actual blend activity are colorized. // Untouched pixels pass through unmodified. float w = saturate(r.blendWeight / max(MaxBlendFactor, 1e-5)); @@ -133,7 +332,7 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) } else { OutputRW[dtid] = centerColor; } -#elif defined(DEBUG_EDGE_DETECTION) +# elif defined(DEBUG_EDGE_DETECTION) // Edge detection visualizer: highlights pixels excluded by depth discontinuity checks. // Non-edge pixels show the normal blended output for scene context. // Bright yellow = source edge: discontinuity at this pixel @@ -145,7 +344,9 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) } else { OutputRW[dtid] = blendedColor; } -#else +# else OutputRW[dtid] = blendedColor; -#endif +# endif + +#endif // STEREO_OVERWRITE } diff --git a/package/Shaders/VR/VRPostProcessCS.hlsl b/package/Shaders/VR/VRPostProcessCS.hlsl new file mode 100644 index 0000000000..29df310420 --- /dev/null +++ b/package/Shaders/VR/VRPostProcessCS.hlsl @@ -0,0 +1,113 @@ +// VR Post-Process - Bilateral blend for near-camera 2x supersampling +// +// Runs after all compositing and stereo blending is complete. +// Reads per-pixel classification from StencilCS and applies: +// - MODE_FULL_BLEND: bilateral depth-weighted blend for 2x supersampling +// +// Only MODE_FULL_BLEND pixels are processed. All others pass through untouched. + +#include "Common/FrameBuffer.hlsli" +#include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" + +Texture2D ColorTexture : register(t0); // Copy of final composited image +Texture2D ModeTexture : register(t1); +Texture2D DepthTexture : register(t2); + +RWTexture2D OutputRW : register(u0); + +cbuffer VRPostProcessCB : register(b1) +{ + float2 FrameDim; + float2 RcpFrameDim; + float DebugEdgeTint; // 0 = off, >0 = debug visualization strength + uint DebugMode; // 0 = normal, 1 = depth map diagnostic, 2 = full blend depth visualizer + float FullBlendDistance; // Linearized depth threshold for full blend zone visualization + float _pad; // Pad to 16-byte alignment +}; + +#define MODE_DISOCCLUDED 0 +#define MODE_EDGE 1 +#define MODE_MAIN 2 +#define MODE_EDGE_NEIGHBOUR 3 +#define MODE_FULL_BLEND 4 + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + if (any(dtid >= uint2(FrameDim))) + return; + + uint pixelMode = ModeTexture[dtid]; + + // Depth map diagnostic: show mode texture contents as solid colors + if (DebugMode == 1) { + float4 c = ColorTexture[dtid]; + if (pixelMode == MODE_EDGE) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), 0.5), c.a); + else if (pixelMode == MODE_EDGE_NEIGHBOUR) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0, 1), 0.5), c.a); + else if (pixelMode == MODE_DISOCCLUDED) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 0.5, 1), 0.3), c.a); + else if (pixelMode == MODE_FULL_BLEND) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0.5, 0), 0.5), c.a); // Orange = full blend zone + return; + } + + // Full blend depth visualizer: shows the depth boundary as a cyan tint + if (DebugMode == 2) { + float2 uvDb = (dtid + 0.5) * RcpFrameDim; + float depthDb = DepthTexture[dtid]; + if (depthDb < 1e-5 || depthDb >= 1.0) + return; + float linDepth = SharedData::GetScreenDepth(depthDb); + if (linDepth < FullBlendDistance) { + float4 c = ColorTexture[dtid]; + float proximity = saturate(1.0 - linDepth / max(FullBlendDistance, 1.0)); + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 1), proximity * 0.4), c.a); + } + return; + } + + // Only process full blend pixels + if (pixelMode != MODE_FULL_BLEND) + return; + + float2 uv = (dtid + 0.5) * RcpFrameDim; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + float4 result = ColorTexture[dtid]; + + // === MODE_FULL_BLEND: bilateral blend for 2x supersampling === + { + float4 center = result; + float centerDepth = DepthTexture[dtid]; + + // Reproject to the other eye + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, centerDepth, eyeIndex, FrameDim); + if (!r.valid) { + // Debug tint for failed reprojection + if (DebugEdgeTint > 0) + OutputRW[dtid] = float4(lerp(center.rgb, float3(1, 0.5, 0), DebugEdgeTint), center.a); + return; + } + + // Only blend with pixels that have valid composited data in both eyes. + uint otherMode = ModeTexture[r.otherPx]; + if (otherMode != MODE_FULL_BLEND && otherMode != MODE_DISOCCLUDED) + return; + + float4 otherColor = ColorTexture[r.otherPx]; + float otherDepth = DepthTexture[r.otherPx]; + + // Depth-weighted bilateral blend + float maxDepth = max(max(centerDepth, otherDepth), 1e-5); + float depthAgreement = 1.0 - saturate(abs(centerDepth - otherDepth) / maxDepth / 0.02); + float blendWeight = 0.5 * depthAgreement; + + result = lerp(center, otherColor, blendWeight); + + if (DebugEdgeTint > 0) + result.rgb = lerp(result.rgb, float3(0, 1, 1), DebugEdgeTint); + } + + OutputRW[dtid] = result; +} diff --git a/package/Shaders/VRStereoOptimizations/ReprojectionCS.hlsl b/package/Shaders/VRStereoOptimizations/ReprojectionCS.hlsl new file mode 100644 index 0000000000..bd34d26d58 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/ReprojectionCS.hlsl @@ -0,0 +1,55 @@ +// VR Stereo Optimizations - Reprojection Compute Shader +// +// Fills Eye 1 pixels that were stencil-culled during rendering by reprojecting +// color data from Eye 0. Only operates on pixels classified as MODE_MAIN. +// +// Reads Eye 0 color directly from the OutputRW UAV (left half) and writes to +// Eye 1 (right half). No read-write conflict because reads and writes target +// strictly different halves of the texture. +// +// Input: +// t0 = Depth buffer +// t1 = Per-pixel mode classification texture +// Output: +// u0 = Main render target UAV (reads Eye 0, writes Eye 1) + +#include "Common/VR.hlsli" +#include "VRStereoOptimizations/cbuffers.hlsli" + +Texture2D DepthTexture : register(t0); +Texture2D ModeTexture : register(t1); + +RWTexture2D OutputRW : register(u0); + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + uint eyeWidth = (uint)FrameDim.x / 2; + uint eyeHeight = (uint)FrameDim.y; + + if (any(dtid >= uint2(eyeWidth, eyeHeight))) + return; + + // dtid is in Eye 1 local coords; convert to stereo buffer coords + uint2 stereoCoord = uint2(dtid.x + eyeWidth, dtid.y); + + // Only fill pixels that were marked for reprojection + // Mode texture is full SBS resolution, so use stereoCoord for Eye 1 + uint mode = ModeTexture[stereoCoord]; + if (mode != MODE_MAIN) + return; + + float depth = DepthTexture[stereoCoord]; + + // Compute mono UV for this Eye 1 pixel + float2 stereoUV = (float2(stereoCoord) + 0.5) * RcpFrameDim; + float2 monoUV = Stereo::ConvertFromStereoUV(stereoUV, 1); + + // Reproject to Eye 0 and sample color + float3 otherEyeUV = Stereo::ConvertMonoUVToOtherEye(float3(monoUV, depth), 1); + float2 eye0StereoUV = Stereo::ConvertToStereoUV(otherEyeUV.xy, 0); + int2 eye0Px = clamp(int2(eye0StereoUV * FrameDim), int2(0, 0), int2(FrameDim) - 1); + + float4 reprojectedColor = OutputRW[eye0Px]; + + // Write to Eye 1 in the main render target + OutputRW[stereoCoord] = reprojectedColor; +} diff --git a/package/Shaders/VRStereoOptimizations/StencilCS.hlsl b/package/Shaders/VRStereoOptimizations/StencilCS.hlsl new file mode 100644 index 0000000000..6ff7b13376 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/StencilCS.hlsl @@ -0,0 +1,149 @@ +// VR Stereo Optimizations - Stencil Classification Compute Shader +// +// Classifies BOTH eyes over the full SBS buffer. Each pixel is tagged as: +// MODE_DISOCCLUDED - Must be fully shaded (sky, HMD mask, parallax-occluded) +// MODE_EDGE - Depth edge boundary (dist 1) or inner/foreground band; fully shaded + bilateral blend +// MODE_MAIN - Standard pixel eligible for reprojection / bilateral blend +// MODE_FULL_BLEND - Near-camera geometry: both eyes fully shaded for 2x supersampling +// +// Dispatched over full SBS resolution (FrameDim.x x FrameDim.y). + +#include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" +#include "VRStereoOptimizations/cbuffers.hlsli" + +Texture2D DepthTexture : register(t0); + +RWTexture2D ModeTextureRW : register(u0); + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + if (any(dtid >= uint2(FrameDim))) + return; + + // Determine which eye this pixel belongs to + float2 uv = (float2(dtid) + 0.5) / FrameDim; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + // Read depth directly in SBS coords + float centerDepth = DepthTexture[dtid]; + +#ifdef DEBUG_DEPTH_MAP + // DIAGNOSTIC: Visualize what depth values StencilCS sees. + // Green (MODE_EDGE) = depth >= 1.0 (HMD mask threshold) + // Magenta (MODE_EDGE_NEIGHBOUR) = depth < 1e-5 (sky threshold) + // No tint (MODE_MAIN) = normal geometry with valid depth + if (centerDepth >= 1.0) { + ModeTextureRW[dtid] = MODE_EDGE; + return; + } + if (centerDepth < 1e-5) { + ModeTextureRW[dtid] = MODE_EDGE_NEIGHBOUR; + return; + } + ModeTextureRW[dtid] = MODE_MAIN; + return; +#endif + + // Sky/unrendered pixels (depth >= 1.0 at z-prepass time = depth buffer clear value) + // and HMD mask pixels both have depth >= 1.0 here. Treat them the same as sky: + // let edge detection run so geometry-vs-sky boundaries get classified. + // HMD mask pixels are in lens corners with no nearby geometry, so they'll + // fall through to MODE_DISOCCLUDED at the end. + bool isSky = (centerDepth < 1e-5) || (centerDepth >= 1.0); + float linCenter = isSky ? 999999.0 : SharedData::GetScreenDepth(centerDepth); + + // Near-camera supersampling: geometry closer than FullBlendDistance gets full + // shading in both eyes for bilateral blend (2x supersampling in VRPostProcess). + if (!isSky && linCenter < FullBlendDistance) { + ModeTextureRW[dtid] = MODE_FULL_BLEND; + return; + } + + // --- Disocclusion detection via reprojection (runs for all non-sky pixels) --- + // Early return: disoccluded pixels are always MODE_DISOCCLUDED regardless of edge proximity. + // This ensures MinEdgeDistance never affects disocclusion classification. + if (!isSky) { + Stereo::StereoBilateralResult reproj = Stereo::ReprojectToOtherEye( + uv, + centerDepth, + eyeIndex, + FrameDim); + + bool isDisoccluded = false; + if (!reproj.valid) { + isDisoccluded = true; + } else { + float otherDepth = DepthTexture[reproj.otherPx]; + float maxDepth = max(max(centerDepth, otherDepth), 1e-5); + float relativeDepthDiff = abs(centerDepth - otherDepth) / maxDepth; + isDisoccluded = (relativeDepthDiff > DisocclusionThreshold); + } + + if (isDisoccluded) { + ModeTextureRW[dtid] = MODE_DISOCCLUDED; + return; + } + } + + // Depth gate: skip edge detection for nearby geometry (saves perf, distant AA matters more) + // Sky pixels always run edge detection — they need to expand the edge band outward. + // Disocclusion detection (above) is independent of this gate and always runs. + bool skipEdgeDetection = !isSky && (linCenter < MinEdgeDistance); + + // --- Edge detection with two-tier classification --- + // MODE_EDGE: immediate neighbor (distance 1) has depth discontinuity, OR + // inner/foreground band (distance <= kInnerWidth). + static const uint kInnerWidth = 2; + int2 offsets[4] = { int2(-1, 0), int2(1, 0), int2(0, -1), int2(0, 1) }; + + uint nearestEdgeDist = 0xFFFFFFFF; // nearest distance at which a discontinuity was found + bool nearestWeAreOuter = false; // whether we are on the background side at that nearest hit + + // Use the larger of inner/outer widths for the search + uint maxWidth = kInnerWidth; + + if (!skipEdgeDetection) { + [loop] for (uint d = 1; d <= maxWidth; d++) + { + [unroll] for (int i = 0; i < 4; i++) + { + int2 rawNeighbor = int2(dtid) + offsets[i] * (int)d; + uint2 neighborCoord = Stereo::ClampToEyeBounds(rawNeighbor, eyeIndex, FrameDim); + + float neighborDepth = DepthTexture[neighborCoord]; + bool neighborIsSky = (neighborDepth < 1e-5) || (neighborDepth >= 1.0); + float linNeighbor = neighborIsSky ? 999999.0 : SharedData::GetScreenDepth(neighborDepth); + float maxLin = max(max(linCenter, linNeighbor), 1e-5); + float relDepthDiff = abs(linCenter - linNeighbor) / maxLin; + + if (relDepthDiff > EdgeDepthThreshold && d < nearestEdgeDist) { + nearestEdgeDist = d; + nearestWeAreOuter = (linNeighbor < linCenter); // neighbor closer to camera = we are background + } + } + } + + } // !skipEdgeDetection + + if (nearestEdgeDist != 0xFFFFFFFF) { + // Classify based on distance and side + if (nearestEdgeDist == 1) { + // Immediate neighbor discontinuity: always MODE_EDGE regardless of side + ModeTextureRW[dtid] = MODE_EDGE; + return; + } else if (!nearestWeAreOuter && nearestEdgeDist <= kInnerWidth) { + // Inner/foreground band beyond distance 1 + ModeTextureRW[dtid] = MODE_EDGE; + return; + } + } + + // Sky pixels that aren't near edges -> disoccluded (reprojection is meaningless for sky) + if (isSky) { + ModeTextureRW[dtid] = MODE_DISOCCLUDED; + return; + } + + // Standard pixel + ModeTextureRW[dtid] = MODE_MAIN; +} diff --git a/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl b/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl new file mode 100644 index 0000000000..c45c2a2409 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl @@ -0,0 +1,54 @@ +// VR Stereo Optimizations - Stencil Write Pixel Shader +// +// Reads from the per-pixel mode classification texture and depth texture. +// Discards pixels that should NOT be stencil-culled: +// - MODE_DISOCCLUDED (0) = fully shaded in Eye 1, no reprojection needed +// - MODE_FULL_BLEND (4) = near-camera pixels fully shaded in both eyes for supersampling +// - Sky/HMD-mask pixels (depth >= 1.0 or depth < 1e-5) = need normal rendering +// in the sky pass; they keep their MODE_EDGE tag in +// the mode texture for VRPostProcess but must not be stencil-culled. +// +// Only geometry MODE_MAIN/MODE_EDGE pixels survive and get stencil ref=1 written. +// +// Mode texture is full SBS resolution (same as render target). +// The DSS is configured with StencilFunc=ALWAYS, StencilPassOp=REPLACE, ref=1. +// Pixels that survive (not discarded) get stencil=1 written. + +#include "VRStereoOptimizations/cbuffers.hlsli" + +Texture2D ModeTexture : register(t0); +Texture2D DepthTexture : register(t1); + +struct PS_INPUT +{ + float4 Position: SV_Position; + float2 TexCoord: TEXCOORD0; +}; + +void main(PS_INPUT input) +{ + // Mode texture is full SBS resolution — SV_Position maps directly + // (viewport is Eye 1 half, so SV_Position.x starts at eyeWidth) + int2 modeCoord = int2(input.Position.xy); + + uint mode = ModeTexture[modeCoord]; + + // MODE_MAIN and MODE_EDGE in Eye 1 write stencil ref=1 (reprojectable). + // These are reprojected from Eye 0; MODE_DISOCCLUDED and MODE_FULL_BLEND are fully shaded in Eye 1. + if (mode == MODE_DISOCCLUDED) + discard; + + // Sky/HMD-mask pixels must not be stencil-culled regardless of edge classification. + // They keep their MODE_EDGE tag in the mode texture for VRPostProcess, + // but must render normally in the sky pass (which runs after stencil culling). + float depth = DepthTexture[modeCoord]; + if (depth >= 1.0 || depth < 1e-5) + discard; + + // MODE_FULL_BLEND: near-camera pixels fully shaded in both eyes for supersampling + if (mode == MODE_FULL_BLEND) + discard; + + // Pixel survives: DSS writes stencil ref=1 + // No color output (no RTV bound) +} diff --git a/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl b/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl new file mode 100644 index 0000000000..353aa53379 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl @@ -0,0 +1,24 @@ +// VR Stereo Optimizations - Stencil Write Vertex Shader +// +// Procedural fullscreen triangle covering Eye 1 (right half of SBS buffer). +// No vertex buffer needed — vertex positions are generated from SV_VertexID. +// The viewport is set to Eye 1 by the C++ code, so we just emit a standard +// fullscreen triangle in clip space. + +struct VS_OUTPUT +{ + float4 Position: SV_Position; + float2 TexCoord: TEXCOORD0; +}; + +VS_OUTPUT main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + + // Fullscreen triangle: 3 vertices covering [-1,1] clip space + float2 uv = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); + output.TexCoord = uv; + + return output; +} diff --git a/package/Shaders/VRStereoOptimizations/cbuffers.hlsli b/package/Shaders/VRStereoOptimizations/cbuffers.hlsli new file mode 100644 index 0000000000..5f8c79caf7 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/cbuffers.hlsli @@ -0,0 +1,35 @@ +// VR Stereo Optimizations - Shared constant buffer layout +// Must match VRStereoOptParams in VRStereoOptimizations.h exactly + +#ifndef __VR_STEREO_OPT_CBUFFERS_HLSLI__ +#define __VR_STEREO_OPT_CBUFFERS_HLSLI__ + +cbuffer VRStereoOptParams : register(b1) +{ + float2 FrameDim; // Full stereo buffer dimensions (both eyes) + float2 RcpFrameDim; // 1.0 / FrameDim + + uint StereoModeValue; // 0=Off, 1=Enable + float DisocclusionThreshold; // Depth difference threshold for disocclusion detection + float EdgeDepthThreshold; // Relative depth difference threshold for edge detection + uint EdgeWidth; // Half-width of edge detection band in pixels + + float2 QualityJitter; // Sub-pixel jitter offset (Quality mode) + float FoveatedRadius; // Radius of foveal region in UV space + float pad2; + + float2 FoveatedCenter; // Center of foveal region in UV space + float MinEdgeDistance; + float FullBlendDistance; // Linearized depth below which pixels get MODE_FULL_BLEND (game units) +}; + +#define STEREO_MODE_OFF 0 +#define STEREO_MODE_ENABLE 1 + +#define MODE_DISOCCLUDED 0 // Fully shaded, no reprojection, no blend (sky, HMD mask, parallax-occluded) +#define MODE_EDGE 1 // Depth edge boundary (distance 1) or inner/foreground band; fully shaded + bilateral blend +#define MODE_MAIN 2 // Eye 0: no reproject (Perf) / bilateral (Quality). Eye 1: overwrite reproject (Perf) / bilateral (Quality) +#define MODE_EDGE_NEIGHBOUR 3 // (Legacy, unused) Outer/background band — now classified as MODE_MAIN +#define MODE_FULL_BLEND 4 // Near-camera geometry: both eyes fully shaded + bilateral blend for 2x supersampling + +#endif diff --git a/src/Deferred.cpp b/src/Deferred.cpp index a0a322500b..ba8c1f3830 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -14,6 +14,7 @@ #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" #include "Features/VR.h" +#include "Features/VRStereoOptimizations.h" #include "Features/WeatherEditor.h" #include "Hooks.h" @@ -275,6 +276,11 @@ void Deferred::StartDeferred() PrepassPasses(); OverrideBlendStates(); + + // VR: Classify Eye 1 pixels and write hardware stencil marks before geometry rendering + if (globals::game::isVR) { + globals::features::vrStereoOptimizations.DispatchStencil(); + } } void Deferred::DeferredPasses() @@ -363,6 +369,14 @@ void Deferred::DeferredPasses() context->CSSetShaderResources(0, ARRAYSIZE(srvs), srvs); + // Bind VRStereoOptimizations mode texture for Eye 1 skip + auto& vrStereoOpt = globals::features::vrStereoOptimizations; + if (REL::Module::IsVR() && vrStereoOpt.loaded && vrStereoOpt.settings.stereoMode != VRStereoOptimizations::StereoMode::Off) { + ID3D11ShaderResourceView* modeSRV = vrStereoOpt.GetModeTextureSRV(); + if (modeSRV) + context->CSSetShaderResources(16, 1, &modeSRV); + } + ID3D11UnorderedAccessView* uavs[3]{ main.UAV, normals.UAV, motionVectors.UAV }; context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); @@ -370,13 +384,28 @@ void Deferred::DeferredPasses() context->CSSetShader(shader, nullptr, 0); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + + // Unbind mode texture SRV + if (REL::Module::IsVR() && vrStereoOpt.loaded && vrStereoOpt.settings.stereoMode != VRStereoOptimizations::StereoMode::Off) { + ID3D11ShaderResourceView* nullSRV = nullptr; + context->CSSetShaderResources(16, 1, &nullSRV); + } + } + + // VR: Deactivate stencil culling now that geometry rendering is complete. + // Must happen before StereoBlend so the blend pass itself isn't stencil-blocked. + if (globals::game::isVR) { + auto& stereoOpt = globals::features::vrStereoOptimizations; + if (stereoOpt.IsStencilActive()) { + stereoOpt.DeactivateStencil(); + } } - // VR stereo consistency blend - depth-aware bilateral blend at the eye seam - // Runs after composite as a general safety net for all screen-space effects. - // Must run before clearing b12/b13 -- needs FrameBuffer matrices for reprojection. - if (globals::game::isVR) + // VR: Stereo reprojection fills Eye 1 holes here (after DeferredComposite, before SSR/water/sky) + // so that ISReflectionsRayTracing sees valid pixels in both eyes. + if (globals::game::isVR) { globals::features::vr.DrawStereoBlend(); + } // Clear { @@ -475,6 +504,10 @@ void Deferred::OverrideBlendStates() blendDesc.RenderTarget[i].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; } + // RT[5] = REFLECTANCE: enable alpha writes for POM depth data + // stored in Reflectance.w, used by StereoBlendCS for depth-aware reprojection + blendDesc.RenderTarget[5].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + DX::ThrowIfFailed(device->CreateBlendState(&blendDesc, &deferredBlendStates[a][b][c][d])); } else { deferredBlendStates[a][b][c][d] = nullptr; @@ -551,6 +584,9 @@ ID3D11ComputeShader* Deferred::GetComputeMainComposite() if (REL::Module::IsVR()) defines.push_back({ "FRAMEBUFFER", nullptr }); + if (REL::Module::IsVR() && globals::features::vrStereoOptimizations.loaded) + defines.push_back({ "VR_STEREO_OPT", nullptr }); + mainCompositeCS = static_cast(Util::CompileShader(L"Data\\Shaders\\DeferredCompositeCS.hlsl", defines, "cs_5_0")); } return mainCompositeCS; @@ -576,6 +612,9 @@ ID3D11ComputeShader* Deferred::GetComputeMainCompositeInterior() if (REL::Module::IsVR()) defines.push_back({ "FRAMEBUFFER", nullptr }); + if (REL::Module::IsVR() && globals::features::vrStereoOptimizations.loaded) + defines.push_back({ "VR_STEREO_OPT", nullptr }); + mainCompositeInteriorCS = static_cast(Util::CompileShader(L"Data\\Shaders\\DeferredCompositeCS.hlsl", defines, "cs_5_0")); } return mainCompositeInteriorCS; @@ -593,6 +632,7 @@ void Deferred::Hooks::Main_RenderWorld::thunk(bool a1) state->permutationData.ExtraShaderDescriptor |= static_cast(State::ExtraShaderDescriptors::InWorld); state->inWorld = true; func(a1); + state->inWorld = false; state->permutationData.ExtraShaderDescriptor &= ~static_cast(State::ExtraShaderDescriptors::InWorld); }; diff --git a/src/Feature.cpp b/src/Feature.cpp index 24b634979f..f3625e7d48 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -30,6 +30,7 @@ #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" #include "Features/VR.h" +#include "Features/VRStereoOptimizations.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" @@ -247,6 +248,7 @@ const std::vector& Feature::GetFeatureList() static auto BuildVRList = []() -> std::vector { auto v = features; v.push_back(&globals::features::vr); + v.push_back(&globals::features::vrStereoOptimizations); // In developer mode, keep all features for testing // In production mode, filter to VR-compatible only diff --git a/src/Features/ExtendedMaterials.h b/src/Features/ExtendedMaterials.h index 2a05a940aa..83da3abed3 100644 --- a/src/Features/ExtendedMaterials.h +++ b/src/Features/ExtendedMaterials.h @@ -36,7 +36,7 @@ struct ExtendedMaterials : Feature uint ExtendShadows = 1; uint EnableParallaxWarpingFix = 1; - float pad[1]; + uint pad0 = 0; }; STATIC_ASSERT_ALIGNAS_16(Settings); diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index e6ed6af7bb..a35c20d377 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -88,6 +88,12 @@ void VR::SetupResources() if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\VR\\StereoBlendCS.hlsl", edgeDetectionDefines, "cs_5_0"))) stereoBlendDebugEdgeDetectionCS.attach(rawPtr); + // Overwrite mode: direct replacement instead of blend (for stencil culling) + auto overwriteDefines = defines; + overwriteDefines.push_back({ "STEREO_OVERWRITE", "" }); + if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\VR\\StereoBlendCS.hlsl", overwriteDefines, "cs_5_0"))) + stereoBlendOverwriteCS.attach(rawPtr); + auto renderer = globals::game::renderer; auto mainTex = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; D3D11_TEXTURE2D_DESC mainDesc; diff --git a/src/Features/VR.h b/src/Features/VR.h index e8372cbb6f..07e3a1609e 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -260,7 +260,7 @@ struct VR : OverlayFeature StereoBlendDepthSigma = std::clamp(StereoBlendDepthSigma, 0.001f, 0.1f); StereoBlendMaxFactor = std::clamp(StereoBlendMaxFactor, 0.0f, 0.5f); StereoBlendColorThreshold = std::clamp(StereoBlendColorThreshold, 0.0f, 0.2f); - StereoBlendDebugMode = std::clamp(StereoBlendDebugMode, 0, 3); + StereoBlendDebugMode = std::clamp(StereoBlendDebugMode, 0, 5); } }; @@ -358,8 +358,10 @@ struct VR : OverlayFeature winrt::com_ptr stereoBlendDebugBackCheckCS; winrt::com_ptr stereoBlendDebugBlendWeightCS; winrt::com_ptr stereoBlendDebugEdgeDetectionCS; + winrt::com_ptr stereoBlendOverwriteCS; eastl::unique_ptr stereoBlendCopyTex; eastl::unique_ptr stereoBlendCB; + winrt::com_ptr stereoBlendLinearSampler; struct alignas(16) StereoBlendCB { @@ -368,7 +370,11 @@ struct VR : OverlayFeature float DepthSigma; float MaxBlendFactor; float ColorDiffThreshold; - float pad; + float DebugEdgeTint; + uint32_t DebugMode; + float FullBlendDistance; + float POMDepthScale; + float _pad; }; // Engine hook integration points diff --git a/src/Features/VR/SettingsUI.cpp b/src/Features/VR/SettingsUI.cpp index c4ea6a562f..3e15342846 100644 --- a/src/Features/VR/SettingsUI.cpp +++ b/src/Features/VR/SettingsUI.cpp @@ -323,7 +323,7 @@ namespace ImGui::Separator(); - const char* debugModes[] = { "Off", "Back-Check", "Blend Weight", "Edge Detection" }; + const char* debugModes[] = { "Off", "Back-Check", "Blend Weight", "Edge Detection", "Overwrite", "Overwrite Eye1" }; ImGui::Combo("Debug View", &settings.StereoBlendDebugMode, debugModes, IM_ARRAYSIZE(debugModes)); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( diff --git a/src/Features/VR/StereoBlend.cpp b/src/Features/VR/StereoBlend.cpp index 1fa5d22240..61ce5763f7 100644 --- a/src/Features/VR/StereoBlend.cpp +++ b/src/Features/VR/StereoBlend.cpp @@ -1,9 +1,12 @@ #include "Features/VR.h" +#include "Deferred.h" #include "Features/DynamicCubemaps.h" #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" +#include "Features/VRStereoOptimizations.h" #include "State.h" +#include "Utils/D3D.h" void VR::ClearShaderCache() { @@ -11,6 +14,7 @@ void VR::ClearShaderCache() stereoBlendDebugBackCheckCS = nullptr; stereoBlendDebugBlendWeightCS = nullptr; stereoBlendDebugEdgeDetectionCS = nullptr; + stereoBlendOverwriteCS = nullptr; } bool VR::AnyScreenSpaceEffectLoaded() @@ -22,10 +26,20 @@ bool VR::AnyScreenSpaceEffectLoaded() void VR::DrawStereoBlend() { - if (!REL::Module::IsVR() || !settings.EnableStereoBlend || !stereoBlendCS || !stereoBlendCopyTex || !stereoBlendCB) + bool vrStereoOptActive = globals::features::vrStereoOptimizations.loaded && + globals::features::vrStereoOptimizations.settings.stereoMode != VRStereoOptimizations::StereoMode::Off && + stereoBlendOverwriteCS; + + if (!REL::Module::IsVR() || !stereoBlendCopyTex || !stereoBlendCB) + return; + + if (vrStereoOptActive && globals::features::vrStereoOptimizations.settings.debugSkipMerge) + return; + + if (!vrStereoOptActive && (!settings.EnableStereoBlend || !stereoBlendCS)) return; - if (!AnyScreenSpaceEffectLoaded() && !globals::state->IsDeveloperMode()) + if (!vrStereoOptActive && !AnyScreenSpaceEffectLoaded() && !globals::state->IsDeveloperMode()) return; ZoneScoped; @@ -40,7 +54,6 @@ void VR::DrawStereoBlend() auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; auto* depthSRV = Util::GetCurrentSceneDepthSRV(); - // Copy main color to read-only texture to avoid read/write race between eyes context->CopyResource(stereoBlendCopyTex->resource.get(), main.texture); auto dispatchCount = Util::GetScreenDispatchCount(true); @@ -55,37 +68,118 @@ void VR::DrawStereoBlend() cbData.MaxBlendFactor = settings.StereoBlendMaxFactor; cbData.ColorDiffThreshold = settings.StereoBlendColorThreshold; + // Pass debug edge tint from VRStereoOptimizations settings + if (vrStereoOptActive && globals::features::vrStereoOptimizations.settings.debugVisualization) + cbData.DebugEdgeTint = 0.3f; + else + cbData.DebugEdgeTint = 0.0f; + + // Debug mode: 0=normal, 1=depth map diagnostic, 2=full blend depth visualizer + if (vrStereoOptActive && globals::features::vrStereoOptimizations.settings.debugDepthMap) + cbData.DebugMode = 1u; + else if (vrStereoOptActive && globals::features::vrStereoOptimizations.settings.debugFullBlendDepth) + cbData.DebugMode = 2u; + else if (vrStereoOptActive && globals::features::vrStereoOptimizations.settings.debugPOMDepth) + cbData.DebugMode = 3u; + else + cbData.DebugMode = 0u; + + cbData.FullBlendDistance = vrStereoOptActive ? globals::features::vrStereoOptimizations.settings.fullBlendDistance : 0.0f; + cbData.POMDepthScale = vrStereoOptActive ? globals::features::vrStereoOptimizations.settings.pomDepthScale : 1.0f; + stereoBlendCB->Update(cbData); auto cbPtr = stereoBlendCB->CB(); - ID3D11ShaderResourceView* srvs[2]{ stereoBlendCopyTex->srv.get(), depthSRV }; - ID3D11UnorderedAccessView* uavs[1]{ main.UAV }; + auto& motionVectors = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + + bool isOverwriteMode = vrStereoOptActive; ID3D11ComputeShader* activeCS = stereoBlendCS.get(); - if (settings.StereoBlendDebugMode == 1 && stereoBlendDebugBackCheckCS) - activeCS = stereoBlendDebugBackCheckCS.get(); - else if (settings.StereoBlendDebugMode == 2 && stereoBlendDebugBlendWeightCS) - activeCS = stereoBlendDebugBlendWeightCS.get(); - else if (settings.StereoBlendDebugMode == 3 && stereoBlendDebugEdgeDetectionCS) - activeCS = stereoBlendDebugEdgeDetectionCS.get(); + if (vrStereoOptActive) { + activeCS = stereoBlendOverwriteCS.get(); + } else { + int effectiveMode = settings.StereoBlendDebugMode; + if (effectiveMode == 1 && stereoBlendDebugBackCheckCS) + activeCS = stereoBlendDebugBackCheckCS.get(); + else if (effectiveMode == 2 && stereoBlendDebugBlendWeightCS) + activeCS = stereoBlendDebugBlendWeightCS.get(); + else if (effectiveMode == 3 && stereoBlendDebugEdgeDetectionCS) + activeCS = stereoBlendDebugEdgeDetectionCS.get(); + } + + // Save and unbind DSV to avoid SRV/DSV conflict on depth buffer in overwrite mode + ID3D11RenderTargetView* savedRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; + ID3D11DepthStencilView* savedDSV = nullptr; + if (isOverwriteMode) { + context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, &savedDSV); + context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, nullptr); + for (auto& rtv : savedRTVs) { + if (rtv) + rtv->Release(); + } + } + ID3D11ShaderResourceView* srvs[2]{ stereoBlendCopyTex->srv.get(), depthSRV }; context->CSSetConstantBuffers(1, 1, &cbPtr); context->CSSetShaderResources(0, 2, srvs); - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - context->CSSetShader(activeCS, nullptr, 0); + if (isOverwriteMode) { + ID3D11ShaderResourceView* modeSRV = globals::features::vrStereoOptimizations.GetModeTextureSRV(); + if (modeSRV) + context->CSSetShaderResources(2, 1, &modeSRV); + + // Bind REFLECTANCE SRV for POM depth offset (stored in .w by Lighting pass) + auto& reflectanceRT = renderer->GetRuntimeData().renderTargets[REFLECTANCE]; + context->CSSetShaderResources(3, 1, &reflectanceRT.SRV); + + ID3D11UnorderedAccessView* uavs[2]{ main.UAV, motionVectors.UAV }; + context->CSSetUnorderedAccessViews(0, 2, uavs, nullptr); + } else { + ID3D11UnorderedAccessView* uavs[1]{ main.UAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + } + + // Bind linear sampler for hardware bilinear color sampling in overwrite mode + if (isOverwriteMode) { + if (!stereoBlendLinearSampler) { + D3D11_SAMPLER_DESC sampDesc = {}; + sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + globals::d3d::device->CreateSamplerState(&sampDesc, stereoBlendLinearSampler.put()); + } + ID3D11SamplerState* samplers[] = { stereoBlendLinearSampler.get() }; + context->CSSetSamplers(0, 1, samplers); + } + + context->CSSetShader(activeCS, nullptr, 0); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); // Cleanup - srvs[0] = nullptr; - srvs[1] = nullptr; - uavs[0] = nullptr; - cbPtr = nullptr; - context->CSSetShaderResources(0, 2, srvs); - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - context->CSSetConstantBuffers(1, 1, &cbPtr); + ID3D11ShaderResourceView* nullSRVs[4] = {}; + context->CSSetShaderResources(0, isOverwriteMode ? 4 : 2, nullSRVs); + ID3D11UnorderedAccessView* nullUAVs[2] = {}; + context->CSSetUnorderedAccessViews(0, isOverwriteMode ? 2 : 1, nullUAVs, nullptr); + ID3D11Buffer* nullCB = nullptr; + context->CSSetConstantBuffers(1, 1, &nullCB); + if (isOverwriteMode) { + ID3D11SamplerState* nullSampler[] = { nullptr }; + context->CSSetSamplers(0, 1, nullSampler); + } context->CSSetShader(nullptr, nullptr, 0); + // Restore DSV after CS dispatch in overwrite mode + if (isOverwriteMode && savedDSV) { + context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, nullptr); + context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, savedDSV); + for (auto& rtv : savedRTVs) { + if (rtv) + rtv->Release(); + } + savedDSV->Release(); + } + if (globals::state->frameAnnotations) globals::state->EndPerfEvent(); } diff --git a/src/Features/VRStereoOptimizations.cpp b/src/Features/VRStereoOptimizations.cpp new file mode 100644 index 0000000000..4b9fb4d481 --- /dev/null +++ b/src/Features/VRStereoOptimizations.cpp @@ -0,0 +1,828 @@ +#include "VRStereoOptimizations.h" + +#include "ExtendedMaterials.h" +#include "Globals.h" +#include "State.h" +#include "Utils/D3D.h" +#include "Utils/Game.h" + +#include + +// JSON enum serialization for StereoMode +NLOHMANN_JSON_SERIALIZE_ENUM(VRStereoOptimizations::StereoMode, { + { VRStereoOptimizations::StereoMode::Off, "Off" }, + { VRStereoOptimizations::StereoMode::Enable, "Enable" }, + }) + +//============================================================================= +// SETTINGS MANAGEMENT +//============================================================================= + +void VRStereoOptimizations::SaveSettings(json& o_json) +{ + o_json["StereoMode"] = settings.stereoMode; + o_json["DisocclusionDepthThreshold"] = settings.disocclusionDepthThreshold; + o_json["FullBlendDistance"] = settings.fullBlendDistance; + o_json["QualityJitterOffset"] = settings.qualityJitterOffset; + o_json["FoveatedRegionRadius"] = settings.foveatedRegionRadius; + o_json["FoveatedRegionCenterX"] = settings.foveatedRegionCenterX; + o_json["FoveatedRegionCenterY"] = settings.foveatedRegionCenterY; + o_json["UseEyeTracking"] = settings.useEyeTracking; + o_json["DebugVisualization"] = settings.debugVisualization; + o_json["DebugSkipMerge"] = settings.debugSkipMerge; + o_json["DebugForceAllStencil"] = settings.debugForceAllStencil; + o_json["DebugForceAllReprojectCS"] = settings.debugForceAllReprojectCS; + o_json["DebugDepthMap"] = settings.debugDepthMap; + o_json["MipBiasMode"] = settings.mipBiasMode; + o_json["MipLodBias"] = settings.mipLodBias; + o_json["MipBiasNearDist"] = settings.mipBiasNearDist; + o_json["MipBiasFarDist"] = settings.mipBiasFarDist; + o_json["CASStrength"] = settings.casStrength; + o_json["AlphaTestThreshold"] = settings.alphaTestThreshold; +} + +void VRStereoOptimizations::LoadSettings(json& o_json) +{ + if (o_json.contains("StereoMode")) + settings.stereoMode = o_json["StereoMode"].get(); + if (o_json.contains("DisocclusionDepthThreshold")) + settings.disocclusionDepthThreshold = o_json["DisocclusionDepthThreshold"].get(); + if (o_json.contains("QualityJitterOffset")) + settings.qualityJitterOffset = o_json["QualityJitterOffset"].get(); + if (o_json.contains("FoveatedRegionRadius")) + settings.foveatedRegionRadius = o_json["FoveatedRegionRadius"].get(); + if (o_json.contains("FoveatedRegionCenterX")) + settings.foveatedRegionCenterX = o_json["FoveatedRegionCenterX"].get(); + if (o_json.contains("FoveatedRegionCenterY")) + settings.foveatedRegionCenterY = o_json["FoveatedRegionCenterY"].get(); + if (o_json.contains("UseEyeTracking")) + settings.useEyeTracking = o_json["UseEyeTracking"].get(); + if (o_json.contains("DebugVisualization")) + settings.debugVisualization = o_json["DebugVisualization"].get(); + if (o_json.contains("DebugSkipMerge")) + settings.debugSkipMerge = o_json["DebugSkipMerge"].get(); + if (o_json.contains("DebugForceAllStencil")) + settings.debugForceAllStencil = o_json["DebugForceAllStencil"].get(); + if (o_json.contains("DebugForceAllReprojectCS")) + settings.debugForceAllReprojectCS = o_json["DebugForceAllReprojectCS"].get(); + if (o_json.contains("DebugDepthMap")) + settings.debugDepthMap = o_json["DebugDepthMap"].get(); + if (o_json.contains("FullBlendDistance")) + settings.fullBlendDistance = o_json["FullBlendDistance"].get(); + if (o_json.contains("MipBiasMode")) + settings.mipBiasMode = o_json["MipBiasMode"].get(); + // Backwards compat: old bool EnableMipBias -> mode 2 (Distant Trees) + else if (o_json.contains("EnableMipBias") && o_json["EnableMipBias"].get()) + settings.mipBiasMode = 2; + if (o_json.contains("MipLodBias")) + settings.mipLodBias = o_json["MipLodBias"].get(); + if (o_json.contains("MipBiasNearDist")) + settings.mipBiasNearDist = o_json["MipBiasNearDist"].get(); + if (o_json.contains("MipBiasFarDist")) + settings.mipBiasFarDist = o_json["MipBiasFarDist"].get(); + // CAS disabled for now — ignore saved value + // if (o_json.contains("CASStrength")) + // settings.casStrength = o_json["CASStrength"].get(); + settings.casStrength = 0.0f; + if (o_json.contains("AlphaTestThreshold")) + settings.alphaTestThreshold = o_json["AlphaTestThreshold"].get(); +} + +void VRStereoOptimizations::RestoreDefaultSettings() +{ + settings = {}; +} + +//============================================================================= +// RESOURCE SETUP +//============================================================================= + +void VRStereoOptimizations::SetupResources() +{ + if (!REL::Module::IsVR()) + return; + + auto device = globals::d3d::device; + auto renderer = globals::game::renderer; + + // Constant buffers + paramsCB = eastl::make_unique(ConstantBufferDesc()); + + // Get main RT dimensions for per-eye calculations + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + D3D11_TEXTURE2D_DESC mainDesc; + main.texture->GetDesc(&mainDesc); + + // Per-pixel mode texture (R8_UINT, full SBS resolution = both eyes) + { + D3D11_TEXTURE2D_DESC modeDesc{}; + modeDesc.Width = mainDesc.Width; + modeDesc.Height = mainDesc.Height; + modeDesc.MipLevels = 1; + modeDesc.ArraySize = 1; + modeDesc.Format = DXGI_FORMAT_R8_UINT; + modeDesc.SampleDesc.Count = 1; + modeDesc.SampleDesc.Quality = 0; + modeDesc.Usage = D3D11_USAGE_DEFAULT; + modeDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + modeDesc.CPUAccessFlags = 0; + modeDesc.MiscFlags = 0; + + texPerPixelMode = eastl::make_unique(modeDesc); + texPerPixelMode->CreateSRV(D3D11_SHADER_RESOURCE_VIEW_DESC{ + .Format = DXGI_FORMAT_R8_UINT, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } }); + texPerPixelMode->CreateUAV(D3D11_UNORDERED_ACCESS_VIEW_DESC{ + .Format = DXGI_FORMAT_R8_UINT, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } }); + } + + // Depth-stencil state for stencil write pass: + // Depth test OFF (not rendering geometry), stencil ALWAYS + REPLACE with ref=1 + { + D3D11_DEPTH_STENCIL_DESC dssDesc{}; + dssDesc.DepthEnable = FALSE; + dssDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; + dssDesc.StencilEnable = TRUE; + dssDesc.StencilReadMask = 0xFF; + dssDesc.StencilWriteMask = 0xFF; + dssDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; + dssDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; + dssDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; + dssDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; + dssDesc.BackFace = dssDesc.FrontFace; + + DX::ThrowIfFailed(device->CreateDepthStencilState(&dssDesc, stencilWriteDSS.put())); + } + + // Rasterizer state for stencil write: no culling, no depth clip + { + D3D11_RASTERIZER_DESC rsDesc{}; + rsDesc.FillMode = D3D11_FILL_SOLID; + rsDesc.CullMode = D3D11_CULL_NONE; + rsDesc.DepthClipEnable = FALSE; + + DX::ThrowIfFailed(device->CreateRasterizerState(&rsDesc, stencilWriteRS.put())); + } + + // Read-only depth DSV for stencil write pass: allows simultaneous depth SRV binding. + // We write stencil but never write depth, so D3D11_DSV_READ_ONLY_DEPTH is safe. + { + auto& depthData = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + if (depthData.views[0] && depthData.texture) { + D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc{}; + depthData.views[0]->GetDesc(&dsvDesc); + dsvDesc.Flags = D3D11_DSV_READ_ONLY_DEPTH; + + DX::ThrowIfFailed(device->CreateDepthStencilView(depthData.texture, &dsvDesc, stencilWriteReadOnlyDSV.put())); + } else { + logger::warn("[VRStereoOptimizations] Could not create read-only DSV: depth stencil data not available"); + } + } + + // CAS sharpness parameter buffer (structured buffer SRV to avoid cbuffer conflicts) + { + D3D11_BUFFER_DESC bufDesc{}; + bufDesc.ByteWidth = sizeof(float); + bufDesc.Usage = D3D11_USAGE_DYNAMIC; + bufDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + bufDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + bufDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; + bufDesc.StructureByteStride = sizeof(float); + + float initSharpness = settings.casStrength; + D3D11_SUBRESOURCE_DATA initData{}; + initData.pSysMem = &initSharpness; + + DX::ThrowIfFailed(device->CreateBuffer(&bufDesc, &initData, casParamsBuf.put())); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = DXGI_FORMAT_UNKNOWN; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER; + srvDesc.Buffer.FirstElement = 0; + srvDesc.Buffer.NumElements = 1; + DX::ThrowIfFailed(device->CreateShaderResourceView(casParamsBuf.get(), &srvDesc, casParamsSRV.put())); + } + + // CAS output texture (same format as main RT, with UAV capability) + { + D3D11_TEXTURE2D_DESC casDesc{}; + casDesc.Width = mainDesc.Width; + casDesc.Height = mainDesc.Height; + casDesc.MipLevels = 1; + casDesc.ArraySize = 1; + casDesc.Format = mainDesc.Format; + casDesc.SampleDesc.Count = 1; + casDesc.SampleDesc.Quality = 0; + casDesc.Usage = D3D11_USAGE_DEFAULT; + casDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + casDesc.CPUAccessFlags = 0; + casDesc.MiscFlags = 0; + + casTex = eastl::make_unique(casDesc); + casTex->CreateSRV(D3D11_SHADER_RESOURCE_VIEW_DESC{ + .Format = mainDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } }); + casTex->CreateUAV(D3D11_UNORDERED_ACCESS_VIEW_DESC{ + .Format = mainDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } }); + } + + CompileShaders(); + + logger::info("[VRStereoOptimizations] Resources created: mode tex {}x{} (full SBS)", mainDesc.Width, mainDesc.Height); +} + +void VRStereoOptimizations::CompileShaders() +{ + std::vector> csDefines = { + { "VR", nullptr }, + { "FRAMEBUFFER", nullptr } + }; + + std::vector> vspsDefines = { + { "VR", nullptr } + }; + + if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilCS.hlsl", csDefines, "cs_5_0")) + stencilCS.attach(reinterpret_cast(ptr)); + else + logger::error("[VRStereoOptimizations] Failed to compile StencilCS"); + + { + auto debugDefines = csDefines; + debugDefines.push_back({ "DEBUG_DEPTH_MAP", nullptr }); + if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilCS.hlsl", debugDefines, "cs_5_0")) + stencilDebugDepthMapCS.attach(reinterpret_cast(ptr)); + else + logger::error("[VRStereoOptimizations] Failed to compile StencilCS (DEBUG_DEPTH_MAP)"); + } + + if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilWriteVS.hlsl", vspsDefines, "vs_5_0")) + stencilWriteVS.attach(reinterpret_cast(ptr)); + else + logger::error("[VRStereoOptimizations] Failed to compile StencilWriteVS"); + + if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilWritePS.hlsl", vspsDefines, "ps_5_0")) + stencilWritePS.attach(reinterpret_cast(ptr)); + else + logger::error("[VRStereoOptimizations] Failed to compile StencilWritePS"); + + if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\ReprojectionCS.hlsl", csDefines, "cs_5_0")) + reprojectionCS.attach(reinterpret_cast(ptr)); + else + logger::error("[VRStereoOptimizations] Failed to compile ReprojectionCS"); + + { + std::vector> casDefines = {}; + if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VR\\CASCS.hlsl", casDefines, "cs_5_0")) + casCS.attach(reinterpret_cast(ptr)); + else + logger::error("[VRStereoOptimizations] Failed to compile CASCS"); + } +} + +void VRStereoOptimizations::ClearShaderCache() +{ + stencilCS = nullptr; + stencilDebugDepthMapCS = nullptr; + stencilWriteVS = nullptr; + stencilWritePS = nullptr; + reprojectionCS = nullptr; + casCS = nullptr; + dssCache.clear(); +} + +void VRStereoOptimizations::Reset() +{ + stencilActive = false; + stencilSwapCount = 0; +} + +//============================================================================= +// IMGUI SETTINGS +//============================================================================= + +void VRStereoOptimizations::DrawSettings() +{ + const char* modeNames[] = { "Off", "Enable" }; + int currentMode = static_cast(settings.stereoMode); + if (ImGui::Combo("Feature Enable", ¤tMode, modeNames, IM_ARRAYSIZE(modeNames))) + settings.stereoMode = static_cast(currentMode); + + // MIP LOD Bias section (always shown, independent of stereo mode) + ImGui::Separator(); + const char* mipBiasModes[] = { "Off", "All Textures", "Distant Trees" }; + ImGui::Combo("MIP LOD Bias", &settings.mipBiasMode, mipBiasModes, 3); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Off: No MIP bias\nAll Textures: Depth-gated sharpening for all textures\nDistant Trees: Depth-gated sharpening for foliage only"); + + if (settings.mipBiasMode > 0) { + ImGui::SliderFloat("MIP Bias Strength", &settings.mipLodBias, -3.0f, 0.0f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Negative = sharper. -0.5 subtle, -1.0 moderate, -2.0 aggressive."); + ImGui::SliderFloat("MIP Near Distance", &settings.mipBiasNearDist, 0.0f, 10000.0f, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Game units. No MIP bias closer than this distance."); + ImGui::SliderFloat("MIP Far Distance", &settings.mipBiasFarDist, 0.0f, 20000.0f, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Game units. Full MIP bias beyond this distance.\nSmooth ramp between near and far."); + } + ImGui::Separator(); + + // CAS slider hidden for now — forced to 0 + // ImGui::SliderFloat("CAS Sharpening", &settings.casStrength, 0.0f, 1.0f, "%.2f"); + // if (ImGui::IsItemHovered()) + // ImGui::SetTooltip("Contrast Adaptive Sharpening (intended for use with TAA).\n0 = disabled, higher = sharper."); + // ImGui::Separator(); + + if (settings.stereoMode == StereoMode::Off) + return; + + ImGui::SliderFloat("Disocclusion Depth Threshold", &settings.disocclusionDepthThreshold, 0.001f, 0.1f, "%.4f"); + + if (globals::state->IsDeveloperMode()) { + if (ImGui::TreeNode("Debug")) { + ImGui::SliderFloat("Full Blend Distance", &settings.fullBlendDistance, 0.0f, 10000.0f, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled."); + + ImGui::SliderFloat("POM Depth Scale", &settings.pomDepthScale, 0.0f, 500.0f, "%.1f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth."); + ImGui::Checkbox("Skip Pixel Reprojection", &settings.debugSkipMerge); + ImGui::Checkbox("Full Blend Depth View", &settings.debugFullBlendDepth); + ImGui::Checkbox("Debug POM Depth", &settings.debugPOMDepth); + if (settings.debugFullBlendDepth) + ImGui::TextColored(ImVec4(0, 1, 1, 1), " Cyan = full blend zone (closer = stronger tint)"); + ImGui::Text("Stencil swaps this frame: %u", stencilSwapCount); + ImGui::TreePop(); + } + } +} + +//============================================================================= +// CONSTANT BUFFER UPDATE +//============================================================================= + +void VRStereoOptimizations::UpdateConstantBuffer() +{ + float2 resolution = Util::ConvertToDynamic(globals::state->screenSize); + + VRStereoOptParams params{}; + params.FrameDim[0] = resolution.x; + params.FrameDim[1] = resolution.y; + params.RcpFrameDim[0] = 1.0f / resolution.x; + params.RcpFrameDim[1] = 1.0f / resolution.y; + params.StereoModeValue = static_cast(settings.stereoMode); + params.DisocclusionThreshold = settings.disocclusionDepthThreshold; + params.EdgeDepthThreshold = settings.edgeDepthThreshold; + params.EdgeWidth = static_cast(settings.edgeWidth); + params.QualityJitter[0] = settings.qualityJitterOffset; + params.QualityJitter[1] = settings.qualityJitterOffset; + params.FoveatedRadius = settings.foveatedRegionRadius; + params.FoveatedCenter[0] = settings.foveatedRegionCenterX; + params.FoveatedCenter[1] = settings.foveatedRegionCenterY; + params.MinEdgeDistance = settings.minEdgeDistance; + params.FullBlendDistance = settings.fullBlendDistance; + + paramsCB->Update(params); +} + +//============================================================================= +// PHASE 1: STENCIL CLASSIFICATION + WRITE +//============================================================================= + +void VRStereoOptimizations::DispatchStencil() +{ + if (!REL::Module::IsVR()) + return; + if (settings.stereoMode == StereoMode::Off) + return; + if (!stencilCS || !stencilWriteVS || !stencilWritePS || !texPerPixelMode || !paramsCB) + return; + + ZoneScoped; + TracyD3D11Zone(globals::state->tracyCtx, "VR Stereo Opt - Stencil"); + + if (globals::state->frameAnnotations) + globals::state->BeginPerfEvent("VR Stereo Opt - Stencil"); + + auto context = globals::d3d::context; + + UpdateConstantBuffer(); + auto cbPtr = paramsCB->CB(); + // Use live depth buffer (kMAIN) instead of kPOST_ZPREPASS_COPY — at StartDeferred time, + // kPOST_ZPREPASS_COPY is stale (previous frame). kMAIN has fresh z-prepass depth so + // StencilCS can correctly detect sky-vs-geometry edges in the current frame. + auto renderer = globals::game::renderer; + auto* depthSRV = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV; + if (!depthSRV) { + logger::warn("[VRStereoOptimizations] DispatchStencil: depthSRV is null, skipping"); + return; + } + + // Dispatch classification CS over Eye 1 region + // Input: t0 = depth, b1 = params CB + // Output: u0 = per-pixel mode texture + { + ID3D11ShaderResourceView* srvs[1]{ depthSRV }; + ID3D11UnorderedAccessView* uavs[1]{ texPerPixelMode->uav.get() }; + + context->CSSetConstantBuffers(1, 1, &cbPtr); + context->CSSetShaderResources(0, 1, srvs); + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + auto* activeStencilCS = (settings.debugDepthMap && stencilDebugDepthMapCS) ? stencilDebugDepthMapCS.get() : stencilCS.get(); + context->CSSetShader(activeStencilCS, nullptr, 0); + + uint32_t fullWidth = texPerPixelMode->desc.Width; + uint32_t fullHeight = texPerPixelMode->desc.Height; + context->Dispatch((fullWidth + 7) / 8, (fullHeight + 7) / 8, 1); + + // Cleanup CS bindings + ID3D11ShaderResourceView* nullSRV = nullptr; + ID3D11UnorderedAccessView* nullUAV = nullptr; + ID3D11Buffer* nullCB = nullptr; + context->CSSetShaderResources(0, 1, &nullSRV); + context->CSSetUnorderedAccessViews(0, 1, &nullUAV, nullptr); + context->CSSetConstantBuffers(1, 1, &nullCB); + context->CSSetShader(nullptr, nullptr, 0); + } + + // Transfer classification to hardware stencil buffer + ExecuteStencilWritePass(); + + stencilActive = true; + stencilSwapCount = 0; + + if (globals::state->frameAnnotations) + globals::state->EndPerfEvent(); +} + +void VRStereoOptimizations::ExecuteStencilWritePass() +{ + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + + // ===== SAVE FULL D3D11 PIPELINE STATE ===== + + ID3D11RenderTargetView* savedRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; + ID3D11DepthStencilView* savedDSV = nullptr; + context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, &savedDSV); + + ID3D11DepthStencilState* savedDSS = nullptr; + UINT savedStencilRef = 0; + context->OMGetDepthStencilState(&savedDSS, &savedStencilRef); + + ID3D11BlendState* savedBlendState = nullptr; + FLOAT savedBlendFactor[4] = {}; + UINT savedSampleMask = 0; + context->OMGetBlendState(&savedBlendState, savedBlendFactor, &savedSampleMask); + + ID3D11RasterizerState* savedRS = nullptr; + context->RSGetState(&savedRS); + + D3D11_VIEWPORT savedViewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE] = {}; + UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + context->RSGetViewports(&numViewports, savedViewports); + + ID3D11VertexShader* savedVS = nullptr; + context->VSGetShader(&savedVS, nullptr, nullptr); + + ID3D11PixelShader* savedPS = nullptr; + context->PSGetShader(&savedPS, nullptr, nullptr); + + ID3D11GeometryShader* savedGS = nullptr; + context->GSGetShader(&savedGS, nullptr, nullptr); + + ID3D11InputLayout* savedInputLayout = nullptr; + context->IAGetInputLayout(&savedInputLayout); + + D3D11_PRIMITIVE_TOPOLOGY savedTopology = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED; + context->IAGetPrimitiveTopology(&savedTopology); + + ID3D11ShaderResourceView* savedPSSRVs[2] = {}; + context->PSGetShaderResources(0, 2, savedPSSRVs); + + ID3D11Buffer* savedPSCB = nullptr; + context->PSGetConstantBuffers(1, 1, &savedPSCB); + + // ===== SET UP STENCIL WRITE PASS ===== + + // Use our custom read-only-depth DSV to allow simultaneous depth SRV binding (t1). + // D3D11_DSV_READ_ONLY_DEPTH permits depth SRV + stencil write simultaneously. + // Using views[0] would cause D3D11 to silently NULL the depth SRV. + // depthData.readOnlyViews[0] has BOTH read-only flags and doesn't allow stencil writes. + context->OMSetRenderTargets(0, nullptr, stencilWriteReadOnlyDSV.get()); + context->OMSetDepthStencilState(stencilWriteDSS.get(), 1); + context->RSSetState(stencilWriteRS.get()); + + // Eye 1 viewport (right half of SBS buffer) + { + D3D11_TEXTURE2D_DESC mainDesc; + renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].texture->GetDesc(&mainDesc); + + D3D11_VIEWPORT vp{}; + vp.TopLeftX = static_cast(mainDesc.Width / 2); + vp.TopLeftY = 0.0f; + vp.Width = static_cast(mainDesc.Width / 2); + vp.Height = static_cast(mainDesc.Height); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + context->RSSetViewports(1, &vp); + } + + // Bind shaders and mode texture + context->VSSetShader(stencilWriteVS.get(), nullptr, 0); + context->PSSetShader(stencilWritePS.get(), nullptr, 0); + context->GSSetShader(nullptr, nullptr, 0); + + ID3D11ShaderResourceView* modeSRV = texPerPixelMode->srv.get(); + context->PSSetShaderResources(0, 1, &modeSRV); + + auto* depthSRV = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV; + context->PSSetShaderResources(1, 1, &depthSRV); + + // Bind params CB to pixel shader (CS and PS have separate CB bindings) + auto cbPtr = paramsCB->CB(); + context->PSSetConstantBuffers(1, 1, &cbPtr); + + // Fullscreen triangle: no VB/IB, procedurally generated in VS + context->IASetInputLayout(nullptr); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + context->Draw(3, 0); + + // ===== RESTORE FULL D3D11 PIPELINE STATE ===== + + ID3D11ShaderResourceView* nullSRVs[2] = {}; + context->PSSetShaderResources(0, 2, nullSRVs); + + context->PSSetConstantBuffers(1, 1, &savedPSCB); + + context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, savedDSV); + context->OMSetDepthStencilState(savedDSS, savedStencilRef); + context->OMSetBlendState(savedBlendState, savedBlendFactor, savedSampleMask); + context->RSSetState(savedRS); + context->RSSetViewports(numViewports, savedViewports); + context->VSSetShader(savedVS, nullptr, 0); + context->PSSetShader(savedPS, nullptr, 0); + context->GSSetShader(savedGS, nullptr, 0); + context->IASetInputLayout(savedInputLayout); + context->IASetPrimitiveTopology(savedTopology); + context->PSSetShaderResources(0, 2, savedPSSRVs); + + // Release COM references acquired by Get* calls + for (auto& rtv : savedRTVs) { + if (rtv) + rtv->Release(); + } + if (savedDSV) + savedDSV->Release(); + if (savedDSS) + savedDSS->Release(); + if (savedBlendState) + savedBlendState->Release(); + if (savedRS) + savedRS->Release(); + if (savedVS) + savedVS->Release(); + if (savedPS) + savedPS->Release(); + if (savedGS) + savedGS->Release(); + if (savedInputLayout) + savedInputLayout->Release(); + if (savedPSSRVs[0]) + savedPSSRVs[0]->Release(); + if (savedPSSRVs[1]) + savedPSSRVs[1]->Release(); + if (savedPSCB) + savedPSCB->Release(); +} + +void VRStereoOptimizations::PerformLateStencilWrite() +{ + // Placeholder for future multi-pass stencil strategies +} + +//============================================================================= +// DSS CACHE: CLONE + STENCIL NOT_EQUAL ENFORCEMENT +//============================================================================= + +ID3D11DepthStencilState* VRStereoOptimizations::GetOrCreateModifiedDSS(ID3D11DepthStencilState* originalDSS) +{ + if (!originalDSS || !stencilActive) + return originalDSS; + + stencilSwapCount++; + + auto it = dssCache.find(originalDSS); + if (it != dssCache.end()) + return it->second.get(); + + // Clone original desc and add read-only stencil NOT_EQUAL test + D3D11_DEPTH_STENCIL_DESC desc{}; + originalDSS->GetDesc(&desc); + + desc.StencilEnable = TRUE; + desc.StencilReadMask = 0xFF; + desc.StencilWriteMask = 0x00; // Read-only: game rendering must not modify our marks + + // NOT_EQUAL with ref=1: skip pixels where stencil == 1 (MODE_MAIN) + desc.FrontFace.StencilFunc = D3D11_COMPARISON_NOT_EQUAL; + desc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; + desc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; + desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; + desc.BackFace = desc.FrontFace; + + winrt::com_ptr modifiedDSS; + HRESULT hr = globals::d3d::device->CreateDepthStencilState(&desc, modifiedDSS.put()); + if (FAILED(hr)) { + logger::warn("[VRStereoOptimizations] Failed to create modified DSS (HRESULT: {:#x})", static_cast(hr)); + return originalDSS; + } + + auto* result = modifiedDSS.get(); + dssCache[originalDSS] = std::move(modifiedDSS); + + return result; +} + +//============================================================================= +// PHASE 3: REPROJECTION COMPUTE SHADER +//============================================================================= + +void VRStereoOptimizations::DispatchReprojection() +{ + if (!REL::Module::IsVR()) + return; + if (settings.stereoMode == StereoMode::Off) + return; + if (!reprojectionCS || !texPerPixelMode || !paramsCB) { + DeactivateStencil(); + return; + } + if (settings.debugSkipMerge) { + DeactivateStencil(); + return; + } + + ZoneScoped; + TracyD3D11Zone(globals::state->tracyCtx, "VR Stereo Opt - Reprojection"); + + if (globals::state->frameAnnotations) + globals::state->BeginPerfEvent("VR Stereo Opt - Reprojection"); + + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + UpdateConstantBuffer(); + auto cbPtr = paramsCB->CB(); + auto* depthSRV = Util::GetCurrentSceneDepthSRV(); + + // Bind: t0 = depth, t1 = mode texture, u0 = main UAV, b1 = params + ID3D11ShaderResourceView* srvs[2]{ + depthSRV, + texPerPixelMode->srv.get() + }; + ID3D11UnorderedAccessView* uavs[1]{ main.UAV }; + + context->CSSetConstantBuffers(1, 1, &cbPtr); + context->CSSetShaderResources(0, 2, srvs); + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetShader(reprojectionCS.get(), nullptr, 0); + + // Dispatch over full SBS texture + uint32_t fullWidth = texPerPixelMode->desc.Width; + uint32_t fullHeight = texPerPixelMode->desc.Height; + context->Dispatch((fullWidth + 7) / 8, (fullHeight + 7) / 8, 1); + + // Cleanup + ID3D11ShaderResourceView* nullSRVs[2] = {}; + ID3D11UnorderedAccessView* nullUAV = nullptr; + ID3D11Buffer* nullCB = nullptr; + context->CSSetShaderResources(0, 2, nullSRVs); + context->CSSetUnorderedAccessViews(0, 1, &nullUAV, nullptr); + context->CSSetConstantBuffers(1, 1, &nullCB); + context->CSSetShader(nullptr, nullptr, 0); + + // Stencil culling is done for this frame + logger::trace("[VRStereoOptimizations] Frame: stencilSwapCount={}", stencilSwapCount); + stencilActive = false; + + if (globals::state->frameAnnotations) + globals::state->EndPerfEvent(); +} + +void VRStereoOptimizations::DeactivateStencil() +{ + if (!stencilActive) + return; + logger::trace("[VRStereoOptimizations] Frame: stencilSwapCount={}", stencilSwapCount); + stencilActive = false; +} + +//============================================================================= +// CAS (CONTRAST ADAPTIVE SHARPENING) - POST-TAA +//============================================================================= + +void VRStereoOptimizations::ApplyCAS(RE::RENDER_TARGET a_target) +{ + logger::trace("[VRStereoOptimizations] CAS: entered (strength={}, casCS={}, casTex={}, casParamsBuf={})", + settings.casStrength, (void*)casCS.get(), (void*)casTex.get(), (void*)casParamsBuf.get()); + + if (settings.casStrength <= 0.0f || !casCS || !casTex || !casParamsBuf) + return; + + if (!REL::Module::IsVR()) + return; + + auto renderer = globals::game::renderer; + auto context = globals::d3d::context; + + // Get the render target that post-processing just wrote to + auto& target = renderer->GetRuntimeData().renderTargets[a_target]; + if (!target.texture || !target.SRV) { + logger::trace("[VRStereoOptimizations] CAS: target RT has no texture/SRV, skipping"); + return; + } + + D3D11_TEXTURE2D_DESC targetDesc; + target.texture->GetDesc(&targetDesc); + logger::trace("[VRStereoOptimizations] CAS: dispatching on RT {} ({}x{}, strength={})", (int)a_target, targetDesc.Width, targetDesc.Height, settings.casStrength); + + // Check for dimension/format mismatch with intermediate texture + D3D11_TEXTURE2D_DESC casTexDesc; + static_cast(casTex->resource.get())->GetDesc(&casTexDesc); + if (casTexDesc.Width != targetDesc.Width || casTexDesc.Height != targetDesc.Height || casTexDesc.Format != targetDesc.Format) { + logger::info("[VRStereoOptimizations] CAS: recreating casTex to match target ({}x{} fmt={} -> {}x{} fmt={})", + casTexDesc.Width, casTexDesc.Height, (int)casTexDesc.Format, + targetDesc.Width, targetDesc.Height, (int)targetDesc.Format); + + D3D11_TEXTURE2D_DESC newDesc{}; + newDesc.Width = targetDesc.Width; + newDesc.Height = targetDesc.Height; + newDesc.MipLevels = 1; + newDesc.ArraySize = 1; + newDesc.Format = targetDesc.Format; + newDesc.SampleDesc.Count = 1; + newDesc.SampleDesc.Quality = 0; + newDesc.Usage = D3D11_USAGE_DEFAULT; + newDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + newDesc.CPUAccessFlags = 0; + newDesc.MiscFlags = 0; + + casTex = eastl::make_unique(newDesc); + casTex->CreateSRV(D3D11_SHADER_RESOURCE_VIEW_DESC{ + .Format = targetDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } }); + casTex->CreateUAV(D3D11_UNORDERED_ACCESS_VIEW_DESC{ + .Format = targetDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } }); + } + + // Update sharpness parameter via Map/Unmap + { + D3D11_MAPPED_SUBRESOURCE mapped; + if (SUCCEEDED(context->Map(casParamsBuf.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + *static_cast(mapped.pData) = settings.casStrength; + context->Unmap(casParamsBuf.get(), 0); + } + } + + // Unbind the RT so we can read from it + context->OMSetRenderTargets(0, nullptr, nullptr); + + // Dispatch CAS: read from target SRV, write to casTex UAV + { + ID3D11ShaderResourceView* views[2] = { target.SRV, casParamsSRV.get() }; + context->CSSetShaderResources(0, 2, views); + + ID3D11UnorderedAccessView* uavs[1] = { casTex->uav.get() }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + + context->CSSetShader(casCS.get(), nullptr, 0); + + context->Dispatch((targetDesc.Width + 7) / 8, (targetDesc.Height + 7) / 8, 1); + } + + // Cleanup CS state + ID3D11ShaderResourceView* nullSRV[2] = { nullptr, nullptr }; + context->CSSetShaderResources(0, 2, nullSRV); + ID3D11UnorderedAccessView* nullUAV[1] = { nullptr }; + context->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); + context->CSSetShader(nullptr, nullptr, 0); + + // Copy sharpened result back to the render target + context->CopyResource(target.texture, casTex->resource.get()); + + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); +} diff --git a/src/Features/VRStereoOptimizations.h b/src/Features/VRStereoOptimizations.h new file mode 100644 index 0000000000..5f14963854 --- /dev/null +++ b/src/Features/VRStereoOptimizations.h @@ -0,0 +1,232 @@ +#pragma once + +#include "Feature.h" + +#include +#include +#include + +/** + * @brief VR Stereo Rendering Optimizations feature. + * + * Uses hardware stencil culling to skip Eye 1 pixel shading for pixels that can be + * reprojected from Eye 0 via lateral stereo reprojection, then runs a compute shader + * to fill those pixels. This avoids redundant pixel shading in overlapping stereo regions. + * + * Pipeline: + * 1. DispatchStencil() - CS classifies per-pixel reprojection viability into a mode texture, + * then a fullscreen VS/PS pass writes that classification into the stencil buffer. + * 2. (Game renders Eye 1) - Hardware stencil test skips shading for marked pixels. + * 3. DispatchReprojection() - CS reprojects Eye 0 color into the skipped Eye 1 pixels. + */ +struct VRStereoOptimizations : public Feature +{ + //============================================================================= + // ENUMS + //============================================================================= + + /// Operating mode for stereo reprojection + enum class StereoMode : uint32_t + { + Off = 0, ///< Feature disabled + Enable = 1 ///< Stereo reprojection enabled + }; + + /// Per-pixel classification written by StencilCS + enum PixelMode : uint8_t + { + MODE_DISOCCLUDED = 0, ///< Fully shaded, no reprojection, no blend + MODE_EDGE = 1, ///< Fully shaded + bilateral blend with other eye + MODE_MAIN = 2, ///< Eye 0: no reproject (Perf) / bilateral (Quality). Eye 1: overwrite (Perf) / bilateral (Quality) + MODE_EDGE_NEIGHBOUR = 3, ///< Outer band: background pixels near edge, blended in post-process + }; + + //============================================================================= + // FEATURE BASE CLASS OVERRIDES + //============================================================================= + + virtual inline std::string GetName() override { return "VR Stereo Optimizations"; } + virtual inline std::string GetShortName() override { return "VRStereoOptimizations"; } + virtual inline std::string_view GetShaderDefineName() override { return "VR_STEREO_OPT"; } + virtual inline std::string_view GetCategory() const override { return "Display"; } + virtual inline bool HasShaderDefine(RE::BSShader::Type t) override { return t == RE::BSShader::Type::Utility; } + virtual inline bool SupportsVR() override { return true; } + + virtual void SetupResources() override; + virtual void Reset() override; + virtual void DrawSettings() override; + virtual void SaveSettings(json& o_json) override; + virtual void LoadSettings(json& o_json) override; + virtual void RestoreDefaultSettings() override; + virtual void ClearShaderCache() override; + + virtual std::pair> GetFeatureSummary() override + { + return { + "Stereo rendering optimizations for VR that skip redundant pixel shading via stencil culling and lateral reprojection.", + { "Hardware stencil culling of Eye 1 pixels reprojectable from Eye 0", + "Compute shader lateral reprojection to fill culled pixels", + "Performance, Quality, and Foveated modes", + "Debug visualization overlays" } + }; + } + + //============================================================================= + // SETTINGS + //============================================================================= + + struct Settings + { + StereoMode stereoMode = StereoMode::Enable; + float disocclusionDepthThreshold = 0.01f; + float edgeDepthThreshold = 0.05f; + int edgeWidth = 3; ///< Half-width of edge band in pixels (total band = 2 * edgeWidth) + float minEdgeDistance = 5000.0f; ///< Minimum linearized depth for edge AA (game units) + float fullBlendDistance = 0.0f; ///< Linearized depth below which both eyes are fully shaded + blended (game units) + float pomDepthScale = 22.5f; ///< Scale factor for POM depth correction in stereo reprojection + bool debugFullBlendDepth = false; ///< Show full blend depth zone as cyan overlay + float qualityJitterOffset = 0.125f; + float foveatedRegionRadius = 0.3f; + float foveatedRegionCenterX = 0.5f; + float foveatedRegionCenterY = 0.5f; + bool useEyeTracking = false; + + int reprojectionMode = 5; // 0=Blend, 4=Overwrite, 5=Overwrite Eye1 Only + + // Debug controls + bool debugVisualization = false; + bool debugSkipMerge = false; + bool debugForceAllStencil = false; + bool debugForceAllReprojectCS = false; + bool debugDepthMap = false; + bool debugPOMDepth = false; ///< Show POM depth data (Reflectance.w) as heatmap overlay + + // MIP LOD Bias (negative = sharper textures) + // 0 = Off, 1 = All textures (global), 2 = Distant trees only (depth-gated TREE_ANIM) + int mipBiasMode = 0; + float mipLodBias = -2.0f; + float mipBiasNearDist = 2000.0f; ///< Game units: no bias closer than this + float mipBiasFarDist = 6000.0f; ///< Game units: full bias beyond this + + // CAS (Contrast Adaptive Sharpening) - post-TAA + float casStrength = 0.0f; ///< 0.0 = disabled, 0.0-1.0 = subtle to strong (hidden for now) + float alphaTestThreshold = 0.001f; ///< Alpha floor for TREE_ANIM zombie texel removal + } settings; + + //============================================================================= + // GPU CONSTANT BUFFER (must match HLSL cbuffer layout exactly) + //============================================================================= + + struct alignas(16) VRStereoOptParams + { + float FrameDim[2]; // Full stereo buffer dimensions + float RcpFrameDim[2]; // 1.0 / FrameDim + + uint32_t StereoModeValue; // Cast of StereoMode enum (0-3) + float DisocclusionThreshold; + float EdgeDepthThreshold; + uint32_t EdgeWidth; + + float QualityJitter[2]; // Sub-pixel jitter offset (Quality mode) + float FoveatedRadius; + float pad2; + + float FoveatedCenter[2]; // Foveal region center UV + float MinEdgeDistance; + float FullBlendDistance; // Linearized depth for full blend zone + }; + static_assert(sizeof(VRStereoOptParams) % 16 == 0, "VRStereoOptParams must be 16-byte aligned for HLSL cbuffer."); + + //============================================================================= + // PUBLIC API + //============================================================================= + + /** + * @brief Classify Eye 1 pixels and write stencil marks. + * + * Dispatches the stencil classification CS, then performs a fullscreen triangle pass + * to write the classification into the hardware stencil buffer. + * Called from Deferred::StartDeferred() after OverrideBlendStates(). + */ + void DispatchStencil(); + + /** + * @brief Reproject Eye 0 color into stencil-culled Eye 1 pixels. + * + * Copies the main render target, then dispatches a CS to fill skipped pixels + * using lateral reprojection from Eye 0. + * Called from Deferred::DeferredPasses() after DeferredCompositeCS. + */ + void DispatchReprojection(); + + /** + * @brief Creates or retrieves a modified DSS with stencil NOT_EQUAL test. + * + * Clones the given DSS with read-only stencil (WriteMask=0x00, Func=NOT_EQUAL, ref=1) + * so that pixels marked by our stencil write pass are skipped during normal rendering. + * Cached per unique input DSS pointer. + * + * @param originalDSS The original depth-stencil state to modify. + * @return Modified DSS with stencil test, or original if creation fails. + */ + ID3D11DepthStencilState* GetOrCreateModifiedDSS(ID3D11DepthStencilState* originalDSS); + + /// Whether the stencil pass is currently active this frame + bool IsStencilActive() const { return stencilActive; } + + /// Deactivate stencil culling (called from Deferred after geometry rendering completes) + void DeactivateStencil(); + + /// Apply CAS sharpening to the main render target (called after TAA) + void ApplyCAS(RE::RENDER_TARGET a_target); + + /// Get mode texture SRV for external consumers (e.g., DeferredCompositeCS Eye 1 skip) + ID3D11ShaderResourceView* GetModeTextureSRV() const { return texPerPixelMode ? texPerPixelMode->srv.get() : nullptr; } + +private: + //============================================================================= + // INTERNAL METHODS + //============================================================================= + + /// Fullscreen triangle pass: reads mode texture, writes stencil ref=1 for MODE_MAIN pixels + void ExecuteStencilWritePass(); + + /// Late stencil write callback (placeholder for future multi-pass strategies) + void PerformLateStencilWrite(); + + /// Compiles all shaders used by this feature + void CompileShaders(); + + /// Updates the constant buffer with current settings and frame dimensions + void UpdateConstantBuffer(); + + //============================================================================= + // GPU RESOURCES + //============================================================================= + + eastl::unique_ptr paramsCB; + eastl::unique_ptr texPerPixelMode; ///< R8_UINT classification texture (full SBS resolution) + eastl::unique_ptr reprojectionCopyTex; ///< Copy of main RT for reprojection read + + winrt::com_ptr stencilWriteDSS; + winrt::com_ptr stencilWriteRS; + winrt::com_ptr stencilWriteReadOnlyDSV; ///< Read-only-depth DSV for stencil write pass (allows simultaneous depth SRV) + + winrt::com_ptr stencilCS; + winrt::com_ptr stencilDebugDepthMapCS; + winrt::com_ptr stencilWriteVS; + winrt::com_ptr stencilWritePS; + winrt::com_ptr reprojectionCS; + + // CAS sharpening resources + winrt::com_ptr casCS; + eastl::unique_ptr casTex; ///< UAV-capable texture for CAS output + winrt::com_ptr casParamsBuf; ///< Structured buffer for CAS sharpness param + winrt::com_ptr casParamsSRV; ///< SRV for CAS sharpness param + + /// Cache of original DSS -> modified DSS with stencil NOT_EQUAL enforcement + std::unordered_map> dssCache; + + bool stencilActive = false; + uint32_t stencilSwapCount = 0; +}; diff --git a/src/Globals.cpp b/src/Globals.cpp index e90c3bf4ce..f4c245e3a4 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -29,6 +29,7 @@ #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" #include "Features/VR.h" +#include "Features/VRStereoOptimizations.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" @@ -84,6 +85,7 @@ namespace globals RenderDoc renderDoc{}; WeatherEditor weatherEditor{}; ExponentialHeightFog exponentialHeightFog{}; + VRStereoOptimizations vrStereoOptimizations{}; namespace llf { @@ -266,13 +268,79 @@ namespace globals { static void thunk(ID3D11DeviceContext* This, ID3D11Resource* pResource, UINT Subresource) { - if (*globals::game::perFrame.get() == pResource && globals::game::mappedFrameBuffer) + if (*globals::game::perFrame.get() == pResource && globals::game::mappedFrameBuffer) { CacheFramebuffer(); + } func(This, pResource, Subresource); } static inline REL::Relocation func; }; + /** + * @brief Hooked OMSetDepthStencilState — replaces DSS with stencil-enforcing version when VR stereo opt is active. + * + * vtable index 36 for ID3D11DeviceContext::OMSetDepthStencilState. + * When VRStereoOptimizations has written stencil marks, this hook transparently swaps + * the game's DSS for a modified version that adds a stencil NOT_EQUAL test, causing + * marked Eye 1 pixels to be skipped during normal rendering. + */ + struct ID3D11DeviceContext_OMSetDepthStencilState + { + static void thunk(ID3D11DeviceContext* This, ID3D11DepthStencilState* pDepthStencilState, UINT StencilRef) + { + if (globals::game::isVR && pDepthStencilState) { + auto& stereoOpt = globals::features::vrStereoOptimizations; + if (stereoOpt.loaded && stereoOpt.IsStencilActive()) { + pDepthStencilState = stereoOpt.GetOrCreateModifiedDSS(pDepthStencilState); + StencilRef = 1; // Must match the ref written by our stencil pass + } + } + func(This, pDepthStencilState, StencilRef); + } + static inline REL::Relocation func; + }; + + /** + * @brief Hooked ClearDepthStencilView — blocks stencil clears when VR stereo opt stencil is active. + * + * vtable index 53 for ID3D11DeviceContext::ClearDepthStencilView. + * Prevents the game from clearing our stencil marks between the stencil write and + * the reprojection pass by stripping the D3D11_CLEAR_STENCIL flag. + */ + struct ID3D11DeviceContext_ClearDepthStencilView + { + static void thunk(ID3D11DeviceContext* This, ID3D11DepthStencilView* pDepthStencilView, UINT ClearFlags, FLOAT Depth, UINT8 Stencil) + { + if (globals::game::isVR) { + auto& stereoOpt = globals::features::vrStereoOptimizations; + if (stereoOpt.loaded && stereoOpt.IsStencilActive()) { + // Only protect the main scene DSV — allow other DSVs to clear normally + auto renderer = globals::game::renderer; + auto& mainDepth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + if (mainDepth.views[0]) { + // Compare the DSV being cleared against the main scene DSV + ID3D11Resource* clearRes = nullptr; + ID3D11Resource* mainRes = nullptr; + pDepthStencilView->GetResource(&clearRes); + mainDepth.views[0]->GetResource(&mainRes); + bool isMainDSV = (clearRes == mainRes); + if (clearRes) + clearRes->Release(); + if (mainRes) + mainRes->Release(); + if (isMainDSV) { + ClearFlags &= ~D3D11_CLEAR_STENCIL; + if (ClearFlags == 0) + return; + } + } + } + } + func(This, pDepthStencilView, ClearFlags, Depth, Stencil); + } + static inline REL::Relocation func; + }; + /** * @brief Installs hooks on the Map and Unmap methods of the provided D3D11 device context. * @@ -282,5 +350,11 @@ namespace globals { stl::detour_vfunc<14, ID3D11DeviceContext_Map>(a_context); stl::detour_vfunc<15, ID3D11DeviceContext_Unmap>(a_context); + + // VR stereo optimization hooks: intercept DSS and stencil clear + if (globals::game::isVR) { + stl::detour_vfunc<36, ID3D11DeviceContext_OMSetDepthStencilState>(a_context); + stl::detour_vfunc<53, ID3D11DeviceContext_ClearDepthStencilView>(a_context); + } } } diff --git a/src/Globals.h b/src/Globals.h index fa96446891..9318c6b8d3 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -34,6 +34,7 @@ struct ExtendedTranslucency; struct Upscaling; struct WeatherEditor; struct ExponentialHeightFog; +struct VRStereoOptimizations; class State; class Deferred; @@ -91,6 +92,7 @@ namespace globals extern RenderDoc renderDoc; extern WeatherEditor weatherEditor; extern ExponentialHeightFog exponentialHeightFog; + extern VRStereoOptimizations vrStereoOptimizations; namespace llf { diff --git a/src/State.cpp b/src/State.cpp index 9794ce9441..ae66ae1e43 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -11,6 +11,7 @@ #include "Features/TerrainBlending.h" #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" +#include "Features/VRStereoOptimizations.h" #include "Features/VolumetricShadows.h" #include "Features/WeatherEditor.h" #include "Menu.h" @@ -850,6 +851,22 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b data.MipBias = 0; } + // VR MIP bias modes: 1 = All (global), 2 = Distant Trees (per-pixel in TREE_ANIM) + data.VRMipBias = 0; + data.VRMipBiasNearDist = 2000.0f; + data.VRMipBiasFarDist = 6000.0f; + data.VRMipBiasMode = 0; + if (globals::game::isVR) { + auto& s = globals::features::vrStereoOptimizations.settings; + if (s.mipBiasMode == 1 || s.mipBiasMode == 2) { + data.VRMipBias = s.mipLodBias; + data.VRMipBiasNearDist = s.mipBiasNearDist; + data.VRMipBiasFarDist = s.mipBiasFarDist; + data.VRMipBiasMode = static_cast(s.mipBiasMode); + } + data.VRAlphaTestThreshold = s.alphaTestThreshold; + } + // DALC to SH const auto& m = dalcTransform.rotate; const auto& t = dalcTransform.translate; diff --git a/src/State.h b/src/State.h index 5682562681..0542a7e1a6 100644 --- a/src/State.h +++ b/src/State.h @@ -210,7 +210,12 @@ class State uint InMapMenu; uint HideSky; float MipBias; - float pad0; + float VRMipBias; + float VRMipBiasNearDist; + float VRMipBiasFarDist; + uint VRMipBiasMode; // 0=Off, 1=All Textures, 2=Distant Trees only + float VRAlphaTestThreshold; // Alpha test threshold for VR TREE_ANIM (0 = use vanilla) + float4 pad0; // HLSL: float2 + implicit 8-byte gap before float4 AmbientSHR float4 AmbientSHR; float4 AmbientSHG; float4 AmbientSHB;