From 2e32ec25fcd619732ebee4145f4939fad7034902 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 2 Jun 2026 22:08:21 -0700 Subject: [PATCH 1/2] fix(TAA): restore VR per-eye reprojection and opaque alpha PR #2399 (upstream) replaced the high-level TAA implementation with a faithful SE decompile transcription, but dropped two VR-specific behaviors that Open Shaders' combined-eye render target depends on: 1. Per-eye reprojection. Vanilla VR renders eyes to separate targets, so plain texCoord+velocity is safe there; Open Shaders packs both eyes in one RT, so a pixel near the x=0.5 seam sampled the other eye's history (cross-eye ghosting). Restored Stereo::ApplyVelocityToUV plus the per-eye history clamp via GetPreviousDynamicResolutionAdjustedScreen- Position, and fed its mono-space out-of-bounds flag into the history reject (the stereo-space [0,1] test missed the seam). 2. Output alpha. The VR branch wrote colorOut.w = allTransparent ? 1 : 0, so opaque pixels (nearly the whole frame) got alpha 0. Restored the vanilla decompile behavior (o0.w = 1) on both editions. Both changes are guarded by #ifdef VR; the SE path is byte-identical. Validated with fxc ps_5_0 across flat / VR / HDR / VR+HDR permutations. Co-Authored-By: Claude Opus 4.8 (1M context) --- package/Shaders/ISTemporalAA.hlsl | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/package/Shaders/ISTemporalAA.hlsl b/package/Shaders/ISTemporalAA.hlsl index 659d4ccc4a..ef76a51f28 100644 --- a/package/Shaders/ISTemporalAA.hlsl +++ b/package/Shaders/ISTemporalAA.hlsl @@ -2,6 +2,7 @@ #include "Common/DisplayMapping.hlsli" #include "Common/DummyVSTexCoord.hlsl" #include "Common/FrameBuffer.hlsli" +#include "Common/VR.hlsli" typedef VS_OUTPUT PS_INPUT; @@ -264,9 +265,20 @@ PS_OUTPUT main(PS_INPUT input) // --- motion vector and history sample --- history.xy = drMax; motionReject.xy = velocityTex.Sample(velocitySampler, ClampScreenUV(motionReject.xy, history.xy)).xy; +# ifdef VR + // Both eyes share one render target in VR, so reprojection must stay within the current + // eye: Stereo::ApplyVelocityToUV converts to mono, applies velocity, clamps per-eye, and + // maps back. Plain texCoord+velocity (the SE path) lets a pixel near the x=0.5 seam sample + // the other eye's history (cross-eye ghosting). GetPreviousDynamicResolutionAdjustedScreen- + // Position keeps the DR history clamp per-eye too; prevUVOutOfBounds drives rejection below. + bool prevUVOutOfBounds; + float2 prevUV = Stereo::ApplyVelocityToUV(texCoord.xy, motionReject.xy, prevUVOutOfBounds); + tapMin.xy = FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(prevUV); +# else motionReject.zw = texCoord.xy + motionReject.xy; - motionReject.x = sqrt(dot(motionReject.xy, motionReject.xy)); tapMin.xy = ClampHistoryUV(motionReject.zw); +# endif + motionReject.x = sqrt(dot(motionReject.xy, motionReject.xy)); history.xyw = historyTex.Sample(historySampler, tapMin.xy).xyz; # ifdef HDR_OUTPUT // history.x is stored as game-gamma luma (see EncodeFeedbackLuma on write). @@ -418,12 +430,18 @@ PS_OUTPUT main(PS_INPUT input) corner.xyz = history.yyy * corner.xyz + tapMin.xyz; // --- disocclusion / mask rejection --- - // motionReject.zw still holds reprojected UV from motion pass; motionReject.x = motion length + // motionReject.x = motion length (motionReject.zw held the reprojected UV on the SE path). +# ifdef VR + // Out-of-bounds was already resolved per-eye in mono space (Stereo::ApplyVelocityToUV); the + // stereo-space [0,1] test used on the SE path would miss the x=0.5 eye seam. + motionReject.z = prevUVOutOfBounds ? 1 : 0; +# else sampleUV.w = min(motionReject.z, motionReject.w); motionReject.zw = cmp(motionReject.zw >= float2(1, 1)); sampleUV.w = cmp(0 >= sampleUV.w); motionReject.z = (int)motionReject.z | (int)sampleUV.w; motionReject.z = (int)motionReject.w | (int)motionReject.z; +# endif history.yz = maskTex.Sample(maskSampler, sampleUV.xy).xy; motionReject.w = AlphaCoverageMask(sampleUV.xy); sampleUV.x = cmp(ThresholdParams.w < history.z); @@ -513,11 +531,8 @@ PS_OUTPUT main(PS_INPUT input) # else feedbackOut.x = saturate(sampleUV.x * motionReject.z); # endif -# if defined(VR) - colorOut.w = motionReject.x ? 1 : 0; -# else + // Vanilla writes opaque alpha unconditionally on both SE and VR (decompile o0.w = 1). colorOut.w = 1; -# endif feedbackOut.w = 1; # ifdef HDR_OUTPUT From 882b9d847428b2816ade49826a4b3ad1cfa85047 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 2 Jun 2026 22:15:51 -0700 Subject: [PATCH 2/2] docs(TAA): tighten VR fix comments to repo standard Condense the reprojection/reject why-comments per CLAUDE.md concise-comment guidance. Comment-only; compiled bytecode unchanged across all permutations. --- package/Shaders/ISTemporalAA.hlsl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/package/Shaders/ISTemporalAA.hlsl b/package/Shaders/ISTemporalAA.hlsl index ef76a51f28..ab52a7722f 100644 --- a/package/Shaders/ISTemporalAA.hlsl +++ b/package/Shaders/ISTemporalAA.hlsl @@ -266,11 +266,8 @@ PS_OUTPUT main(PS_INPUT input) history.xy = drMax; motionReject.xy = velocityTex.Sample(velocitySampler, ClampScreenUV(motionReject.xy, history.xy)).xy; # ifdef VR - // Both eyes share one render target in VR, so reprojection must stay within the current - // eye: Stereo::ApplyVelocityToUV converts to mono, applies velocity, clamps per-eye, and - // maps back. Plain texCoord+velocity (the SE path) lets a pixel near the x=0.5 seam sample - // the other eye's history (cross-eye ghosting). GetPreviousDynamicResolutionAdjustedScreen- - // Position keeps the DR history clamp per-eye too; prevUVOutOfBounds drives rejection below. + // Eyes share one RT in VR; reproject within the current eye (mono-space velocity, per-eye + // clamp) so a pixel near the x=0.5 seam never samples the other eye's history. OOB feeds reject. bool prevUVOutOfBounds; float2 prevUV = Stereo::ApplyVelocityToUV(texCoord.xy, motionReject.xy, prevUVOutOfBounds); tapMin.xy = FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(prevUV); @@ -432,8 +429,7 @@ PS_OUTPUT main(PS_INPUT input) // --- disocclusion / mask rejection --- // motionReject.x = motion length (motionReject.zw held the reprojected UV on the SE path). # ifdef VR - // Out-of-bounds was already resolved per-eye in mono space (Stereo::ApplyVelocityToUV); the - // stereo-space [0,1] test used on the SE path would miss the x=0.5 eye seam. + // VR resolves out-of-bounds per-eye in mono space; the SE stereo-space test misses the seam. motionReject.z = prevUVOutOfBounds ? 1 : 0; # else sampleUV.w = min(motionReject.z, motionReject.w);