From ef4c91b8da54df91089d23fcb806a742580d361d Mon Sep 17 00:00:00 2001 From: The Roofer Dev Date: Sun, 28 Jun 2026 15:06:50 -0400 Subject: [PATCH] Mode B jitter VALIDATED: clip-space gl_Position injection (the correct method) Replaces the viewport-shift jitter (which trembled - emulator viewport jitter on a non-jitter-aware guest cannot accumulate cleanly) with the native-DLSS approach: inject gl_Position.xy += jitterNDC * w into guest vertex shaders via the translator (SupportBuffer.JitterOffset + EmitterContext epilogue). Motion vectors de-jittered by (J_n - J_n-1), negative texture LOD bias to steady the mips. Confirmed in-game: sharp, stable, DLSS accumulates the sub-pixel detail. --- .../Engine/Threed/StateUpdater.cs | 26 +++++++++++-------- .../Memory/SupportBufferUpdater.cs | 17 ++++++++++++ src/Ryujinx.Graphics.Shader/SupportBuffer.cs | 7 ++++- .../Translation/EmitterContext.cs | 14 ++++++++++ .../Dlss/DlssJitter.cs | 14 +++++----- .../Dlss/DlssUpscaler.cs | 13 +++++----- 6 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs index ba7a4abbf..c1fe42f5e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs @@ -739,16 +739,24 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Span viewportTransformSpan = _state.State.ViewportTransform.AsSpan(); Span viewportExtentsSpan = _state.State.ViewportExtents.AsSpan(); - // DLSS Mode B: shift the resolution-scaled viewport by this frame's sub-pixel jitter. Only the - // scaled (main) pass; native passes (UI) keep RenderTargetScale 1 and stay untouched. The offset - // is carried to DLSS through the present queue (see DlssJitterState), not tagged on a texture. - // No-op (offset 0) unless jitter is enabled, so the default path is unchanged. - float jitterX = 0f, jitterY = 0f; + // DLSS Mode B: hand the shader this frame's sub-pixel jitter as a clip-space NDC offset, which it + // adds to gl_Position scaled by w (the correct, native-DLSS way to jitter -- not a viewport shift). + // Only the scaled (main) 3D pass; native passes (UI) keep RenderTargetScale 1 and are NOT jittered. + // The offset is 0 unless jitter is enabled, so the default path is unchanged. + float jitterNdcX = 0f, jitterNdcY = 0f; if (DlssJitterState.Enabled && _channel.TextureManager.RenderTargetScale != 1f) { - jitterX = DlssJitterState.OffsetX; - jitterY = DlssJitterState.OffsetY; + ref ViewportTransform vp0 = ref viewportTransformSpan[0]; + float jScale = _channel.TextureManager.RenderTargetScale; + float vpWidth = MathF.Abs(vp0.ScaleX) * 2f * jScale; + float vpHeight = MathF.Abs(vp0.ScaleY) * 2f * jScale; + if (vpWidth > 0f && vpHeight > 0f) + { + jitterNdcX = DlssJitterState.OffsetX * 2f / vpWidth; + jitterNdcY = DlssJitterState.OffsetY * 2f / vpHeight; + } } + _context.SupportBufferUpdater.SetJitter(jitterNdcX, jitterNdcY); for (int index = 0; index < Constants.TotalViewports; index++) { @@ -792,10 +800,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed y *= scale; width *= scale; height *= scale; - - // Sub-pixel jitter is in scaled-render pixels, same space as x/y here (0 unless Mode B). - x += jitterX; - y += jitterY; } Rectangle region = new(x, y, width, height); diff --git a/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs b/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs index da19bd064..b0f2fe84b 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs @@ -88,6 +88,23 @@ namespace Ryujinx.Graphics.Gpu.Memory MarkDirty(SupportBuffer.ViewportSizeOffset, SupportBuffer.FieldSize); } + /// + /// Sets the DLSS Mode B clip-space jitter offset (in NDC) the shader adds to vertex positions + /// (scaled by w). Zero unless jitter is enabled, so the default path is unaffected. + /// + /// Jitter X in normalized device coordinates + /// Jitter Y in normalized device coordinates + public void SetJitter(float x, float y) + { + if (_data.JitterOffset.X != x || _data.JitterOffset.Y != y) + { + _data.JitterOffset.X = x; + _data.JitterOffset.Y = y; + + MarkDirty(SupportBuffer.JitterOffsetOffset, SupportBuffer.FieldSize); + } + } + /// /// Sets the scale of all output render targets (they should all have the same scale). /// diff --git a/src/Ryujinx.Graphics.Shader/SupportBuffer.cs b/src/Ryujinx.Graphics.Shader/SupportBuffer.cs index fb624d624..a3fcb020c 100644 --- a/src/Ryujinx.Graphics.Shader/SupportBuffer.cs +++ b/src/Ryujinx.Graphics.Shader/SupportBuffer.cs @@ -24,6 +24,7 @@ namespace Ryujinx.Graphics.Shader RenderScale, TfeOffset, TfeVertexCount, + JitterOffset, } public struct SupportBuffer @@ -42,6 +43,7 @@ namespace Ryujinx.Graphics.Shader public static readonly int ComputeRenderScaleOffset; public static readonly int TfeOffsetOffset; public static readonly int TfeVertexCountOffset; + public static readonly int JitterOffsetOffset; public const int FragmentIsBgraCount = 8; // One for the render target, 64 for the textures, and 8 for the images. @@ -68,6 +70,7 @@ namespace Ryujinx.Graphics.Shader ComputeRenderScaleOffset = GraphicsRenderScaleOffset + FieldSize; TfeOffsetOffset = OffsetOf(ref instance, ref instance.TfeOffset); TfeVertexCountOffset = OffsetOf(ref instance, ref instance.TfeVertexCount); + JitterOffsetOffset = OffsetOf(ref instance, ref instance.JitterOffset); } internal static StructureType GetStructureType() @@ -80,7 +83,8 @@ namespace Ryujinx.Graphics.Shader new StructureField(AggregateType.S32, "frag_scale_count"), new StructureField(AggregateType.Array | AggregateType.FP32, "render_scale", RenderScaleMaxCount), new StructureField(AggregateType.Vector4 | AggregateType.S32, "tfe_offset"), - new StructureField(AggregateType.S32, "tfe_vertex_count") + new StructureField(AggregateType.S32, "tfe_vertex_count"), + new StructureField(AggregateType.Vector4 | AggregateType.FP32, "jitter_offset") ]); } @@ -95,5 +99,6 @@ namespace Ryujinx.Graphics.Shader public Vector4 TfeOffset; public Vector4 TfeVertexCount; + public Vector4 JitterOffset; } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs index 62dd9e2e7..8e5e543bd 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs @@ -292,6 +292,20 @@ namespace Ryujinx.Graphics.Shader.Translation } } + // DLSS Mode B clip-space jitter: offset the vertex position by the sub-pixel jitter (in NDC), + // scaled by w so it stays a constant pixel shift after the perspective divide -- the correct, + // native-DLSS way to jitter, unlike a viewport shift. The offset is 0 unless jitter is enabled, + // so the default path is byte-identical. + { + Operand jpx = this.Load(StorageKind.Output, IoVariable.Position, null, Const(0)); + Operand jpy = this.Load(StorageKind.Output, IoVariable.Position, null, Const(1)); + Operand jpw = this.Load(StorageKind.Output, IoVariable.Position, null, Const(3)); + Operand jox = this.Load(StorageKind.ConstantBuffer, SupportBuffer.Binding, Const((int)SupportBufferField.JitterOffset), Const(0)); + Operand joy = this.Load(StorageKind.ConstantBuffer, SupportBuffer.Binding, Const((int)SupportBufferField.JitterOffset), Const(1)); + this.Store(StorageKind.Output, IoVariable.Position, null, Const(0), this.FPFusedMultiplyAdd(jox, jpw, jpx)); + this.Store(StorageKind.Output, IoVariable.Position, null, Const(1), this.FPFusedMultiplyAdd(joy, jpw, jpy)); + } + if (TranslatorContext.Definitions.ViewportTransformDisable) { Operand x = this.Load(StorageKind.Output, IoVariable.Position, null, Const(0)); diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs index 84981f26e..f4b8f77aa 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs @@ -20,10 +20,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public static bool Enabled => _flag && DlssIntegration.IsEnabled; - // Magnitude of the jitter offset handed to DLSS, as a multiplier on our scaled-render-pixel value, - // to match NGX's expected unit. Live-tunable via RYUJINX_DLSS_JITTER_SCALE so the value can be swept - // without recompiling; defaults to 0.25. Read once at startup. NOTE: this scales ONLY the offset - // sent to Evaluate -- the motion-field de-jitter stays at full physical magnitude. + // Jitter amplitude in render pixels: the Halton offset (+-0.5) is multiplied by this before it is + // published. 1.0 = a real +-0.5px sub-pixel jitter (what DLSS wants). Live-tunable via + // RYUJINX_DLSS_JITTER_SCALE so a large value (e.g. 10 = +-5px) can be used as a VISIBLE "reticle" + // test to confirm the clip-space gl_Position injection is actually moving the image. Read once. private static readonly float _scale = ParseScale(); public static float Scale => _scale; @@ -39,7 +39,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return scale; } - return 0.5f; // calibrated magic value that locks the image (was swept live from 0.25) + return 1.0f; // real +-0.5px sub-pixel jitter by default } // Texture mip LOD bias applied to every guest sampler while jitter is on (NVIDIA's DLSS guidance: a @@ -83,8 +83,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _index = _index % SequenceLength + 1; // 1..N (Halton is undefined at 0) DlssJitterState.Enabled = true; - DlssJitterState.OffsetX = Halton(_index, 2) - 0.5f; - DlssJitterState.OffsetY = Halton(_index, 3) - 0.5f; + DlssJitterState.OffsetX = (Halton(_index, 2) - 0.5f) * _scale; + DlssJitterState.OffsetY = (Halton(_index, 3) - 0.5f) * _scale; } private static float Halton(uint index, uint radix) diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs index c8a7ab554..24fe01eac 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs @@ -295,13 +295,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss TextureView mvSource = DlssIntegration.MvFilterEnabled ? _motionFiltered : _motion; StreamlineDlss.DlssTexture mvTex = Describe(mvSource, cbs); - // Grid/NDC-normalization hypothesis: convert the pixel jitter to normalized device coordinates - // (span 2.0 across the texture) before handing it to DLSS, keeping the validated signs. NOTE: - // Streamline/NGX documents this offset in PIXELS, so normalizing is expected to UNDER-drive DLSS - // (offset ~0.0005); kept as the explicit test the user asked for. The de-jitter of the motion - // field stays at full pixel magnitude (directive: physical compensation intact). - float evalJitterX = (frameJitterX * 2.0f / input.Width) * JitterSignX; - float evalJitterY = (frameJitterY * 2.0f / input.Height) * JitterSignY; + // DLSS jitter offset in PIXELS (Streamline/NGX convention): the value the frame was actually + // rendered with, carried through the present queue (frameJitterX/Y), with the validated signs. + // The clip-space injection in the vertex shader shifts the image by exactly this many pixels, so + // DLSS is told the matching pixel offset. The de-jitter stays at full pixel magnitude too. + float evalJitterX = frameJitterX * JitterSignX; + float evalJitterY = frameJitterY * JitterSignY; bool ok = StreamlineDlss.Evaluate( (IntPtr)cbs.CommandBuffer.Handle,