diff --git a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli index b8a4e3ccea..f66859c9da 100644 --- a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli +++ b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli @@ -320,12 +320,24 @@ namespace ExtendedMaterials # if defined(TERRAIN_VARIATION) StochasticOffsets sharedOffset, float2 dx, float2 dy, # endif - out float pixelOffset, out float weights[6]) + out float pixelOffset, +# if defined(VR_STEREO_OPT) + out bool hasPOM, +# endif + out float weights[6]) #else - 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) + 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 +# if defined(VR_STEREO_OPT) + , + out bool hasPOM +# endif + ) #endif { - pixelOffset = 0.5; + pixelOffset = 0.0; +#if defined(VR_STEREO_OPT) + hasPOM = false; +#endif 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 @@ -498,6 +510,9 @@ namespace ExtendedMaterials nearBlendToFar *= nearBlendToFar; float offset = (1.0 - parallaxAmount) * -maxHeight + minHeight; pixelOffset = saturate(lerp(parallaxAmount, 0.5, nearBlendToFar)); +#if defined(VR_STEREO_OPT) + hasPOM = true; +#endif return lerp(viewDirTS.xy * offset + coords.xy, coords, nearBlendToFar); } @@ -510,7 +525,7 @@ namespace ExtendedMaterials weights[5] = input.LandBlendWeights2.y; #endif - pixelOffset = 0.5; + pixelOffset = 0.0; return coords; } diff --git a/package/Shaders/Common/VR.hlsli b/package/Shaders/Common/VR.hlsli index 686a503043..0b8ea117ba 100644 --- a/package/Shaders/Common/VR.hlsli +++ b/package/Shaders/Common/VR.hlsli @@ -25,6 +25,12 @@ cbuffer VRValues : register(b13) namespace Stereo { +#ifdef VR_STEREO_OPT + /// Sentinel written to PomOffsetTex when a pixel's Lighting PS did not run POM. + /// Convention: -1.0 = no POM; >= 0.0 = POM ran (StereoBlendCS detects by sign). + /// Must match kPomOffsetNoData in VRStereoOptimizations.h. + static const float POM_NO_DATA = -1.0; +#endif /** Converts to the eye specific uv [0,1]. In VR, texture buffers include the left and right eye in the same buffer. Flat diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 203e3876ff..0b2ef652aa 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -11,6 +11,7 @@ #include "Common/SharedData.hlsli" #include "Common/Skinned.hlsli" #include "Common/Triplanar.hlsli" +#include "Common/VR.hlsli" #if defined(FACEGEN) || defined(FACEGEN_RGB_TINT) # define SKIN @@ -357,6 +358,13 @@ struct PS_OUTPUT #ifdef PSHADER +# if defined(VR_STEREO_OPT) && !defined(SNOW) +// POM depth offset UAV — written per-pixel for StereoBlendCS depth-aware reprojection. +// Bound at u7 (after the 7 deferred MRT slots 0-6) via OMSetRenderTargetsAndUnorderedAccessViews. +// -1.0 = no POM (sentinel, matches ClearPomOffsetTexture); >= 0 = POM ran (StereoBlendCS checks >= 0). +RWTexture2D PomOffsetTex : register(u7); +# endif + SamplerState SampTerrainParallaxSampler : register(s1); # if defined(LANDSCAPE) @@ -1020,6 +1028,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // LANDSCAPE float sh0 = 0; float pixelOffset = 0; +# if defined(VR_STEREO_OPT) && !defined(SNOW) + bool hasPOM = false; +# endif # if defined(EMAT) # if defined(LANDSCAPE) @@ -1076,7 +1087,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(PARALLAX) if (SharedData::extendedMaterialSettings.EnableParallax) { mipLevel = ExtendedMaterials::GetMipLevel(uv, TexParallaxSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset +# if defined(VR_STEREO_OPT) && !defined(SNOW) + , + hasPOM +# endif + ); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexParallaxSampler.SampleLevel(SampParallaxSampler, uv, mipLevel).x; } @@ -1104,7 +1120,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (envMaskSample.w > kMaskEpsilon) { complexMaterialParallax = true; mipLevel = ExtendedMaterials::GetMipLevel(uv, TexEnvMaskSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexEnvMaskSampler, SampTerrainParallaxSampler, 3, displacementParams, pixelOffset); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexEnvMaskSampler, SampTerrainParallaxSampler, 3, displacementParams, pixelOffset +# if defined(VR_STEREO_OPT) && !defined(SNOW) + , + hasPOM +# endif + ); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexEnvMaskSampler.SampleLevel(SampEnvMaskSampler, uv, mipLevel).w; complexMaterialColor = TexEnvMaskSampler.Sample(SampEnvMaskSampler, uv); @@ -1151,7 +1172,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) displacementParams.HeightScale *= PBRParams1.y; } mipLevel = ExtendedMaterials::GetMipLevel(uv, TexParallaxSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, refractedViewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, refractedViewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset +# if defined(VR_STEREO_OPT) && !defined(SNOW) + , + hasPOM +# endif + ); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexParallaxSampler.SampleLevel(SampParallaxSampler, uv, mipLevel).x; } @@ -1246,9 +1272,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) // Initialize weights array weights[0] = weights[1] = weights[2] = weights[3] = weights[4] = weights[5] = 0.0; # if defined(TERRAIN_VARIATION) - uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, sharedOffset, dx, dy, pixelOffset, weights); + uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, sharedOffset, dx, dy, pixelOffset, +# if defined(VR_STEREO_OPT) && !defined(SNOW) + hasPOM, +# endif + weights); # else - uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, pixelOffset, weights); + uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, pixelOffset, +# if defined(VR_STEREO_OPT) && !defined(SNOW) + hasPOM, +# endif + weights); # endif if (SharedData::extendedMaterialSettings.EnableHeightBlending) { input.LandBlendWeights1.x = weights[0]; @@ -3202,17 +3236,16 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # endif -# if defined(VR) && (defined(EMAT) || defined(TRUE_PBR)) && (defined(PARALLAX) || defined(LANDSCAPE) || defined(EMAT_ENVMAP) || defined(TRUE_PBR)) - // VR: store POM parallax amount for stereo reprojection depth correction. - // Read by StereoBlendCS to adjust Eye 1 (right eye) reprojection depth - // at POM-displaced surfaces. Not consumed on flat (SE/AE). - psout.Reflectance = float4(indirectLobeWeights.specular, - (pixelOffset > 0.0) ? saturate(pixelOffset) : psout.Diffuse.w); -# else psout.Reflectance = float4(indirectLobeWeights.specular, psout.Diffuse.w); -# endif psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), saturate(1.0 - material.Roughness), psout.Diffuse.w); +# if defined(VR_STEREO_OPT) && !defined(SNOW) + // VR stereo reprojection: write POM depth offset to dedicated texture (u7) for StereoBlendCS. + // hasPOM disambiguates "POM ran at geometry plane (pixelOffset=0.5)" from "POM did not run". + // -1.0 is the explicit no-POM sentinel (R16_FLOAT supports negatives); StereoBlendCS checks >= 0. + PomOffsetTex[uint2(input.Position.xy)] = hasPOM ? pixelOffset : Stereo::POM_NO_DATA; +# endif + # if defined(SNOW) # if defined(TRUE_PBR) psout.Parameters.x = Color::RGBToLuminanceAlternative(specularColor); diff --git a/package/Shaders/VR/StereoBlendCS.hlsl b/package/Shaders/VR/StereoBlendCS.hlsl index bf5a082685..a6e66d1ea0 100644 --- a/package/Shaders/VR/StereoBlendCS.hlsl +++ b/package/Shaders/VR/StereoBlendCS.hlsl @@ -22,7 +22,7 @@ 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 +Texture2D PomOffsetTexture : register(t3); // R16_FLOAT: Stereo::POM_NO_DATA (-1.0) = no POM; >= 0.0 = POM ran SamplerState LinearSampler : register(s0); # include "VRStereoOptimizations/modes.hlsli" @@ -121,17 +121,17 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) return; } - // Debug mode 3: POM depth data visualizer — show Reflectance.w as color + // Debug mode 3: POM depth data visualizer — show PomOffsetTexture as color if (DebugMode == 3) { - float pomVal = ReflectanceTexture[dtid].w; + float pomVal = PomOffsetTexture[dtid]; float4 c = ColorTexture[dtid]; - if (pomVal > 1e-2) { + if (pomVal >= 0.0) { // 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 + // Non-POM pixels store -1.0 sentinel, left untouched return; } @@ -148,10 +148,10 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) // 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. + // Non-POM pixels store -1.0 (sentinel); hasPOM is encoded by sign: >= 0 means POM ran. float reprojDepthFB = centerDepth; - float pomOffsetFB = ReflectanceTexture[dtid].w; - if (pomOffsetFB > 1e-2 && POMDepthScale > 0) { + float pomOffsetFB = PomOffsetTexture[dtid]; + if (pomOffsetFB >= 0.0 && POMDepthScale > 0) { float linDepthFB = SharedData::GetScreenDepth(centerDepth); float depthCorrectionFB = (0.5 - pomOffsetFB) * POMDepthScale; float newLinDepthFB = max(linDepthFB + depthCorrectionFB, 1e-4); @@ -214,14 +214,14 @@ float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) // Save first-pass result as fallback before POM adjustment Stereo::StereoBilateralResult firstPassR = r; - // Read POM offset from Eye 0 source's reflectance.w + // Read POM offset from dedicated POM texture (R16_FLOAT, written by Lighting PS at u7). // 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), + // Correction: high pomOffset 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) { + // Non-POM pixels store -1.0 (sentinel); hasPOM is encoded by sign: >= 0 means POM ran. + float pomOffset = PomOffsetTexture[r.otherPx]; + if (pomOffset >= 0.0) { // Re-reproject with POM-adjusted depth centered at geometry plane float linearDepth = SharedData::GetScreenDepth(centerDepth); float depthCorrection = (0.5 - pomOffset) * POMDepthScale; diff --git a/src/Deferred.cpp b/src/Deferred.cpp index c93a30078f..5c6f91e3d9 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -260,6 +260,10 @@ void Deferred::StartDeferred() { auto context = globals::d3d::context; + // Clear POM offset texture to -1.0 sentinel so pixels the Lighting PS never touches read "no POM" + if (globals::features::vr.stereoOpt.loaded) + globals::features::vr.stereoOpt.ClearPomOffsetTexture(); + ID3D11Buffer* buffers[1] = { *globals::game::perFrame.get() }; ID3D11Buffer* vrBuffer = nullptr; @@ -506,10 +510,6 @@ 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; diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index 4e7a706f80..8d3bf8f35c 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -122,7 +122,10 @@ void VR::SetupResources() if (globals::game::isVR && stereoOpt.settings.stereoMode != VRStereoOptimizations::StereoMode::Off) { stereoOpt.SetupResources(); - stereoOpt.loaded = stereoOpt.GetModeTextureSRV() != nullptr; + stereoOpt.loaded = + stereoOpt.GetModeTextureSRV() != nullptr && + stereoOpt.GetPomOffsetSRV() != nullptr && + stereoOpt.GetPomOffsetUAV() != nullptr; } else { stereoOpt.loaded = false; } diff --git a/src/Features/VR.h b/src/Features/VR.h index f23af5fc78..52b1d4c168 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -111,7 +111,7 @@ struct VR : OverlayFeature } virtual inline std::string_view GetShaderDefineName() override { return "VR_STEREO_OPT"; } - virtual inline bool HasShaderDefine(RE::BSShader::Type t) override { return stereoOpt.CanDispatchStencil() && t == RE::BSShader::Type::Utility; } + virtual inline bool HasShaderDefine(RE::BSShader::Type t) override { return stereoOpt.CanDispatchStencil() && (t == RE::BSShader::Type::Utility || t == RE::BSShader::Type::Lighting); } virtual void Reset() override; virtual void SetupResources() override; virtual void ClearShaderCache() override; diff --git a/src/Features/VR/StereoBlend.cpp b/src/Features/VR/StereoBlend.cpp index 7684d1e5f8..5f44e933e2 100644 --- a/src/Features/VR/StereoBlend.cpp +++ b/src/Features/VR/StereoBlend.cpp @@ -123,9 +123,9 @@ void VR::DrawStereoBlend() ID3D11ShaderResourceView* modeSRV = globals::features::vr.stereoOpt.GetModeTextureSRV(); 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); + // Bind dedicated POM offset SRV (R16_FLOAT, written by Lighting PS at u7) + auto* pomSRV = globals::features::vr.stereoOpt.GetPomOffsetSRV(); + context->CSSetShaderResources(3, 1, &pomSRV); ID3D11UnorderedAccessView* uavs[2]{ main.UAV, motionVectors.UAV }; context->CSSetUnorderedAccessViews(0, 2, uavs, nullptr); diff --git a/src/Features/VRStereoOptimizations.cpp b/src/Features/VRStereoOptimizations.cpp index 929ef672f1..ad81d5a14c 100644 --- a/src/Features/VRStereoOptimizations.cpp +++ b/src/Features/VRStereoOptimizations.cpp @@ -2,6 +2,7 @@ #include "ExtendedMaterials.h" #include "Globals.h" +#include "Menu.h" #include "State.h" #include "Utils/D3D.h" #include "Utils/Game.h" @@ -128,6 +129,34 @@ void VRStereoOptimizations::SetupResources() .Texture2D = { .MipSlice = 0 } }); } + // POM offset texture (R16_FLOAT, full SBS resolution) + // Written by Lighting PS (u7) for POM-active pixels, read by StereoBlendCS for depth-aware reprojection. + // Replaces the former overloading of Reflectance.w, so Reflectance stays R11G11B10 with no alpha. + { + D3D11_TEXTURE2D_DESC pomDesc{}; + pomDesc.Width = mainDesc.Width; + pomDesc.Height = mainDesc.Height; + pomDesc.MipLevels = 1; + pomDesc.ArraySize = 1; + pomDesc.Format = DXGI_FORMAT_R16_FLOAT; + pomDesc.SampleDesc.Count = 1; + pomDesc.SampleDesc.Quality = 0; + pomDesc.Usage = D3D11_USAGE_DEFAULT; + pomDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + pomDesc.CPUAccessFlags = 0; + pomDesc.MiscFlags = 0; + + texPomOffset = eastl::make_unique(pomDesc); + texPomOffset->CreateSRV(D3D11_SHADER_RESOURCE_VIEW_DESC{ + .Format = DXGI_FORMAT_R16_FLOAT, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } }); + texPomOffset->CreateUAV(D3D11_UNORDERED_ACCESS_VIEW_DESC{ + .Format = DXGI_FORMAT_R16_FLOAT, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } }); + } + // Depth-stencil state for stencil write pass: // Depth test OFF (not rendering geometry), depth writes OFF, stencil ALWAYS + REPLACE with ref=1. // We use the normal (writable) kMAIN DSV — no simultaneous SRV binding needed. @@ -213,6 +242,14 @@ void VRStereoOptimizations::Reset() stencilSwapCount = 0; } +void VRStereoOptimizations::ClearPomOffsetTexture() +{ + if (!texPomOffset) + return; + const float clearValue[4] = { kPomOffsetNoData, kPomOffsetNoData, kPomOffsetNoData, kPomOffsetNoData }; + globals::d3d::context->ClearUnorderedAccessViewFloat(texPomOffset->uav.get(), clearValue); +} + //============================================================================= // IMGUI SETTINGS //============================================================================= @@ -226,6 +263,11 @@ void VRStereoOptimizations::DrawSettings() if (ImGui::IsItemHovered()) ImGui::SetTooltip("Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame."); + if (globals::game::isVR && settings.stereoMode == StereoMode::Enable && !loaded) { + const auto& themeSettings = Menu::GetSingleton()->GetTheme(); + ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, + "Restart is required to enable VR stereo reprojection."); + } if (settings.stereoMode == StereoMode::Off) return; diff --git a/src/Features/VRStereoOptimizations.h b/src/Features/VRStereoOptimizations.h index 3e4adb38ae..4f324395ce 100644 --- a/src/Features/VRStereoOptimizations.h +++ b/src/Features/VRStereoOptimizations.h @@ -45,6 +45,14 @@ struct VRStereoOptimizations MODE_FULL_BLEND = 4, ///< Near-camera pixels: fully shaded in both eyes + bilateral blended }; + //============================================================================= + // CONSTANTS + //============================================================================= + + /// Sentinel written to texPomOffset when POM did not run for a pixel. + /// -1.0 = no POM; >= 0.0 = POM ran. Matches Stereo::POM_NO_DATA in Common/VR.hlsli. + static constexpr float kPomOffsetNoData = -1.0f; + //============================================================================= // PUBLIC METHODS //============================================================================= @@ -83,7 +91,7 @@ struct VRStereoOptimizations bool debugForceAllStencil = false; bool debugForceAllReprojectCS = false; bool debugDepthMap = false; - bool debugPOMDepth = false; ///< Show POM depth data (Reflectance.w) as heatmap overlay + bool debugPOMDepth = false; ///< Show POM depth data (texPomOffset) as heatmap overlay } settings; @@ -166,6 +174,15 @@ struct VRStereoOptimizations /// Get mode texture SRV for external consumers (e.g., DeferredCompositeCS Eye 1 skip) ID3D11ShaderResourceView* GetModeTextureSRV() const { return texPerPixelMode ? texPerPixelMode->srv.get() : nullptr; } + /// Get POM offset texture SRV for StereoBlendCS (reads per-pixel parallax depth offset) + ID3D11ShaderResourceView* GetPomOffsetSRV() const { return texPomOffset ? texPomOffset->srv.get() : nullptr; } + + /// Get POM offset texture UAV for PS writes during deferred lighting (injected at u7) + ID3D11UnorderedAccessView* GetPomOffsetUAV() const { return texPomOffset ? texPomOffset->uav.get() : nullptr; } + + /// Clear the POM offset texture to -1.0 (no-POM sentinel) at the start of each deferred frame + void ClearPomOffsetTexture(); + private: //============================================================================= // INTERNAL METHODS @@ -186,6 +203,7 @@ struct VRStereoOptimizations eastl::unique_ptr paramsCB; eastl::unique_ptr texPerPixelMode; ///< R8_UINT classification texture (full SBS resolution) + eastl::unique_ptr texPomOffset; ///< R16_FLOAT POM depth offset written by Lighting PS, read by StereoBlendCS winrt::com_ptr stencilWriteDSS; winrt::com_ptr stencilWriteRS; diff --git a/src/Globals.cpp b/src/Globals.cpp index 572baaf086..221bb8c3ad 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -281,6 +281,36 @@ namespace globals static inline REL::Relocation func; }; + /** + * @brief Hooked OMSetRenderTargets — injects POM offset UAV at slot 7 when in the deferred pass. + * + * vtable index 33 for ID3D11DeviceContext::OMSetRenderTargets. + * After Skyrim binds the deferred MRT (clearing all UAVs), this hook re-adds the POM offset + * UAV at slot u7 so the Lighting PS (VR_STEREO_OPT permutation) can write per-pixel parallax + * depth offsets without overloading Reflectance.w. + */ + struct ID3D11DeviceContext_OMSetRenderTargets + { + static void STDMETHODCALLTYPE thunk(ID3D11DeviceContext* This, UINT NumViews, ID3D11RenderTargetView* const* ppRenderTargetViews, ID3D11DepthStencilView* pDepthStencilView) + { + func(This, NumViews, ppRenderTargetViews, pDepthStencilView); + + // D3D11 handles any SRV/UAV conflict automatically (silently unbinds the UAV when + // the same resource is later bound as an SRV), so no NumViews guard is needed. + if (globals::deferred->deferredPass) { + auto& stereoOpt = globals::features::vr.stereoOpt; + if (stereoOpt.loaded) { + if (auto* uav = stereoOpt.GetPomOffsetUAV()) { + This->OMSetRenderTargetsAndUnorderedAccessViews( + D3D11_KEEP_RENDER_TARGETS_AND_DEPTH_STENCIL, nullptr, nullptr, + 7, 1, &uav, nullptr); + } + } + } + } + static inline REL::Relocation func; + }; + /** * @brief Hooked OMSetDepthStencilState — replaces DSS with stencil-enforcing version when VR stereo opt is active. * @@ -357,8 +387,10 @@ 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 + // VR stereo optimization hooks: installed only when stereo reprojection is enabled at startup. + // Changing stereoMode at runtime requires a restart; the UI communicates this to the user. if (globals::game::isVR && globals::features::vr.stereoOpt.settings.stereoMode != VRStereoOptimizations::StereoMode::Off) { + stl::detour_vfunc<33, ID3D11DeviceContext_OMSetRenderTargets>(a_context); stl::detour_vfunc<36, ID3D11DeviceContext_OMSetDepthStencilState>(a_context); stl::detour_vfunc<53, ID3D11DeviceContext_ClearDepthStencilView>(a_context); }