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.
This commit is contained in:
2026-06-28 15:06:50 -04:00
parent bb1a2b24c8
commit ef4c91b8da
6 changed files with 65 additions and 26 deletions
@@ -739,16 +739,24 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Span<ViewportTransform> viewportTransformSpan = _state.State.ViewportTransform.AsSpan();
Span<ViewportExtents> 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<float> region = new(x, y, width, height);
@@ -88,6 +88,23 @@ namespace Ryujinx.Graphics.Gpu.Memory
MarkDirty(SupportBuffer.ViewportSizeOffset, SupportBuffer.FieldSize);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="x">Jitter X in normalized device coordinates</param>
/// <param name="y">Jitter Y in normalized device coordinates</param>
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);
}
}
/// <summary>
/// Sets the scale of all output render targets (they should all have the same scale).
/// </summary>
+6 -1
View File
@@ -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<int> TfeOffset;
public Vector4<int> TfeVertexCount;
public Vector4<float> JitterOffset;
}
}
@@ -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));
@@ -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)
@@ -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,