diff --git a/src/Ryujinx.Graphics.GAL/DlssCameraState.cs b/src/Ryujinx.Graphics.GAL/DlssCameraState.cs
index 5259bd871..d9e21c03f 100644
--- a/src/Ryujinx.Graphics.GAL/DlssCameraState.cs
+++ b/src/Ryujinx.Graphics.GAL/DlssCameraState.cs
@@ -2,6 +2,7 @@
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
@@ -31,11 +32,53 @@ namespace Ryujinx.Graphics.GAL
///
/// View-projection of the frame currently being presented, republished at dequeue.
- /// Written and read on the render thread; no locking needed on this side.
+ /// Written by and read on the render thread only; the
+ /// GPU thread hands values across through the present ring below, never directly.
///
public static Matrix4x4 PresentVp;
public static bool PresentVpValid;
+ // Present hand-off (GPU thread -> render thread). TryConsumeOrdered below pairs the game
+ // camera to the presented frame ON the GPU thread; this ring carries that paired result
+ // across to the render thread. A single static slot raced here: the GPU thread overwrote
+ // it while the render thread read it mid-evaluate (torn 64-byte matrix, or frame N
+ // evaluated with frame N+1's camera whenever the render thread lagged a present behind --
+ // exactly the heavy scenes where DLSS matters most). Order does the pairing instead: one
+ // entry enqueued per GAL present (GPU thread), one dequeued per executed present (render
+ // thread), so a lagging consumer still reads the camera of the frame it is presenting.
+ private static readonly ConcurrentQueue<(Matrix4x4 Vp, bool Valid)> _presentRing = new();
+
+ ///
+ /// Enqueues the camera paired with the frame being enqueued for presentation (GPU thread).
+ ///
+ public static void PublishPresent(in Matrix4x4 vp, bool valid)
+ {
+ _presentRing.Enqueue((vp, valid));
+
+ // A consumer-less backend (OpenGL, or presents executed before the consumer exists)
+ // must not grow the ring unbounded: keep the same short resync window as the ordered
+ // fifo below.
+ while (_presentRing.Count > 4)
+ {
+ _presentRing.TryDequeue(out _);
+ }
+ }
+
+ ///
+ /// Dequeues the camera paired with the present being executed and republishes it on
+ /// /. Render thread, exactly once per
+ /// executed present. A re-present without a new frame (swapchain recreation) finds the
+ /// ring empty and keeps the previous values: same camera for the same content.
+ ///
+ public static void ConsumePresent()
+ {
+ if (_presentRing.TryDequeue(out (Matrix4x4 Vp, bool Valid) entry))
+ {
+ PresentVp = entry.Vp;
+ PresentVpValid = entry.Valid;
+ }
+ }
+
///
/// Publishes the view-projection captured for the frame being rendered (GPU thread).
///
diff --git a/src/Ryujinx.Graphics.GAL/MvppDev.cs b/src/Ryujinx.Graphics.GAL/MvppDev.cs
new file mode 100644
index 000000000..56bcf9cb3
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/MvppDev.cs
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
+// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
+
+namespace Ryujinx.Graphics.GAL
+{
+ ///
+ /// Single switch for the MV++ development diagnostics: heartbeats, per-second stats
+ /// readbacks and counter log lines. Release default = OFF, where the pipelines run
+ /// identically and only the instruments go quiet (anything feeding the UI, like the FG
+ /// counter poll, keeps running silently). RYUJINX_MVPP_DEV=1 brings every gauge back.
+ ///
+ public static class MvppDev
+ {
+ public static readonly bool Enabled =
+ System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEV") == "1";
+ }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
index aac7f7c25..2189b0cc7 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
@@ -482,6 +482,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
MvppFinalDrawProbe.OnDrawTexture(_channel, texture); // read-only: DrawTexture bypasses DrawEnd (gated)
+ float gSrcX0 = srcX0, gSrcY0 = srcY0, gSrcX1 = srcX1, gSrcY1 = srcY1;
+ float gDstX0 = dstX0, gDstY0 = dstY0, gDstX1 = dstX1, gDstY1 = dstY1;
+
srcX0 *= texture.EffectiveScaleX;
srcY0 *= texture.EffectiveScaleY;
srcX1 *= texture.EffectiveScaleX;
@@ -495,6 +498,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
dstX1 *= dstScaleX;
dstY1 *= dstScaleY;
+ bool anyScaled = (texture != null && (texture.EffectiveScaleX != 1f || texture.EffectiveScaleY != 1f)) ||
+ dstScaleX != 1f || dstScaleY != 1f;
+
+ if (anyScaled)
+ {
+ Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated); the bound RT is not resolvable here
+ "DRAWTEX",
+ texture, null,
+ gSrcX0, gSrcY0, gSrcX1, gSrcY1,
+ gDstX0, gDstY0, gDstX1, gDstY1,
+ srcX0, srcY0, srcX1, srcY1,
+ dstX0, dstY0, dstX1, dstY1,
+ "sampler");
+ }
+
_context.Renderer.Pipeline.DrawTexture(
texture?.HostTexture,
sampler?.GetHostSampler(texture),
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs
index c7c65f93d..64f75e46b 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs
@@ -118,7 +118,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly bool _pairProbe =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_PAIRPROBE") == "1";
- private static ulong _probeLastFp;
+ // Change detection on the 0xC0 camera block: raw byte compare against the last copy.
+ // Vectorized SequenceEqual beats hashing all 192 bytes on every scaled draw, and an
+ // exact compare cannot collide.
+ private static readonly byte[] _probeLastBlock = new byte[0xC0];
private static bool _probeHasFp;
private static int _probeChanges;
private static readonly int[] _probeHist = new int[4];
@@ -604,16 +607,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ReadOnlySpan block = channel.MemoryManager.Physical.GetSpan(address + (ulong)_cachedOffset, 0xC0);
- ulong fp = 14695981039346656037UL; // FNV-1a
- foreach (byte b in block)
- {
- fp = (fp ^ b) * 1099511628211UL;
- }
-
- if (!_probeHasFp || fp != _probeLastFp)
+ if (!_probeHasFp || !block.SequenceEqual(_probeLastBlock))
{
_probeHasFp = true;
- _probeLastFp = fp;
+ block.CopyTo(_probeLastBlock);
_statBlockChanges++;
if (_pairProbe)
@@ -978,7 +975,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_lastPosZ = camPosZ;
long now = Environment.TickCount64;
- if (now - _lastHeartbeatMs >= 5000)
+ if (GAL.MvppDev.Enabled && now - _lastHeartbeatMs >= 5000)
{
_lastHeartbeatMs = now;
Logger.Info?.Print(LogClass.Gpu,
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
index ff928c8fa..481a814ee 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
@@ -907,10 +907,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
float scaleY = _channel.TextureManager.RenderTargetScaleY;
if (scaleX != 1f || scaleY != 1f)
{
+ int gx = x, gy = y, gw = width, gh = height;
+
x = (int)(x * scaleX);
y = (int)(y * scaleY);
width = (int)MathF.Ceiling(width * scaleX);
height = (int)MathF.Ceiling(height * scaleY);
+
+ Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated); truncated pos + ceiled size = the +-1px seam suspect
+ "SCISSOR",
+ null, null,
+ gx, gy, gx + gw, gy + gh,
+ gx, gy, gx + gw, gy + gh,
+ x, y, x + width, y + height,
+ x, y, x + width, y + height,
+ "n/a");
}
regions[index] = new Rectangle(x, y, width, height);
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs
index 22962690b..c77aee330 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs
@@ -385,6 +385,17 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
bool linearFilter = _state.State.SetPixelsFromMemorySampleModeFilter == SetPixelsFromMemorySampleModeFilter.Bilinear;
+ Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated)
+ "2D-BLIT",
+ srcTexture, dstTexture,
+ srcX1 / srcTexture.Info.SamplesInX, srcY1 / srcTexture.Info.SamplesInY,
+ srcX2 / srcTexture.Info.SamplesInX, srcY2 / srcTexture.Info.SamplesInY,
+ dstX1 / dstTexture.Info.SamplesInX, dstY1 / dstTexture.Info.SamplesInY,
+ dstX2 / dstTexture.Info.SamplesInX, dstY2 / dstTexture.Info.SamplesInY,
+ srcRegion.X1, srcRegion.Y1, srcRegion.X2, srcRegion.Y2,
+ dstRegion.X1, dstRegion.Y1, dstRegion.X2, dstRegion.Y2,
+ linearFilter ? "linear" : "point");
+
srcTexture.HostTexture.CopyTo(dstTexture.HostTexture, srcRegion, dstRegion, linearFilter);
dstTexture.SignalModified();
diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppSeamProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppSeamProbe.cs
new file mode 100644
index 000000000..f5e3a6c79
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Image/MvppSeamProbe.cs
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
+// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
+
+using Ryujinx.Common.Logging;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.Gpu.Image
+{
+ ///
+ /// [SEAMPROBE, READ-ONLY, default OFF] Residual x3 seam hunt: logs every SCALED copy/draw
+ /// whose source or destination rectangle has an EDGE at a watched guest-space coordinate.
+ /// A full-surface copy cannot create an interior line, so only edge hits matter; the watched
+ /// coordinates come from the dump analysis (seamscan), never hard-coded per game.
+ ///
+ /// Enable with RYUJINX_MVPP_SEAMPROBE="x60,y727,y810,y844" (guest coords, axis-prefixed);
+ /// RYUJINX_MVPP_SEAMPROBE_TOL widens the edge match (default 2 guest pixels).
+ /// Call sites: the 2D blit engine (TwodClass) and DrawTexture (DrawManager) -- the only two
+ /// paths that copy scaled SUB-regions (texture-cache family copies are full-slice).
+ /// Never modifies textures, regions or draws.
+ ///
+ static class MvppSeamProbe
+ {
+ private const int MaxShapes = 400; // stop logging new shapes past this (bounds a pathological run)
+
+ private static readonly int[] _watchX;
+ private static readonly int[] _watchY;
+ private static readonly int _tol;
+ private static bool _enabled;
+
+ private static readonly HashSet _seenShapes = new();
+ private static int _matches;
+ private static long _summaryMs;
+
+ static MvppSeamProbe()
+ {
+ string spec = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SEAMPROBE");
+
+ if (string.IsNullOrEmpty(spec))
+ {
+ _watchX = _watchY = Array.Empty();
+ return;
+ }
+
+ List xs = new(), ys = new();
+
+ foreach (string token in spec.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (token.Length > 1 && int.TryParse(token.AsSpan(1), out int v))
+ {
+ if (char.ToLowerInvariant(token[0]) == 'x')
+ {
+ xs.Add(v);
+ continue;
+ }
+
+ if (char.ToLowerInvariant(token[0]) == 'y')
+ {
+ ys.Add(v);
+ continue;
+ }
+ }
+
+ Logger.Warning?.Print(LogClass.Gpu, $"SEAMPROBE: ignored malformed token '{token}' (want x or y)");
+ }
+
+ _watchX = xs.ToArray();
+ _watchY = ys.ToArray();
+ _tol = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_SEAMPROBE_TOL"), out int t)
+ ? Math.Clamp(t, 0, 32)
+ : 2;
+ _enabled = _watchX.Length + _watchY.Length > 0;
+
+ if (_enabled)
+ {
+ Logger.Info?.Print(LogClass.Gpu,
+ $"SEAMPROBE armed: x=[{string.Join(",", _watchX)}] y=[{string.Join(",", _watchY)}] tol={_tol}");
+ }
+ }
+
+ private static bool _announced;
+
+ ///
+ /// Prints the probe's status once, from a site that always runs (TextureManager creation).
+ /// Without this, a run where no call site ever fires is indistinguishable from a run
+ /// where the environment variable was never set (13/07: two silent logs in a row).
+ ///
+ public static void Announce()
+ {
+ if (_announced)
+ {
+ return;
+ }
+
+ _announced = true;
+ Logger.Info?.Print(LogClass.Gpu,
+ $"SEAMPROBE status: enabled={_enabled} x=[{string.Join(",", _watchX)}] y=[{string.Join(",", _watchY)}] tol={_tol}");
+ }
+
+ ///
+ /// Inspects one scaled copy/draw. Guest rectangles are PRE-scale (the space the watched
+ /// coordinates live in); host rectangles are what actually reaches the backend.
+ ///
+ public static void OnScaledCopy(
+ string op,
+ Texture src, Texture dst,
+ float srcGx1, float srcGy1, float srcGx2, float srcGy2,
+ float dstGx1, float dstGy1, float dstGx2, float dstGy2,
+ float srcHx1, float srcHy1, float srcHx2, float srcHy2,
+ float dstHx1, float dstHy1, float dstHx2, float dstHy2,
+ string filter)
+ {
+ if (!_enabled)
+ {
+ return;
+ }
+
+ // A diagnostic on the GPU thread must never take the process down.
+ try
+ {
+ // Unscaled ops cannot produce a scaling seam. When one side is unknown (null,
+ // e.g. the bound render target of a DrawTexture), the call site gates instead.
+ if (src != null && dst != null &&
+ src.EffectiveScaleX == 1f && src.EffectiveScaleY == 1f &&
+ dst.EffectiveScaleX == 1f && dst.EffectiveScaleY == 1f)
+ {
+ return;
+ }
+
+ string hit = EdgeHit(srcGx1, srcGx2, dstGx1, dstGx2, _watchX, "x")
+ ?? EdgeHit(srcGy1, srcGy2, dstGy1, dstGy2, _watchY, "y");
+
+ if (hit == null)
+ {
+ return;
+ }
+
+ _matches++;
+
+ long shape = Fnv(
+ (ulong)op.GetHashCode(),
+ src?.Range.GetSubRange(0).Address ?? 0,
+ dst?.Range.GetSubRange(0).Address ?? 0,
+ ((long)(int)srcGx1 << 32) ^ (int)srcGy1,
+ ((long)(int)srcGx2 << 32) ^ (int)srcGy2,
+ ((long)(int)dstGx1 << 32) ^ (int)dstGy1,
+ ((long)(int)dstGx2 << 32) ^ (int)dstGy2);
+
+ if (_seenShapes.Count < MaxShapes && _seenShapes.Add(shape))
+ {
+ Logger.Info?.Print(LogClass.Gpu,
+ $"SEAMPROBE {op} hit={hit} filter={filter} " +
+ $"src={Describe(src)} g[{srcGx1:0.#},{srcGy1:0.#},{srcGx2:0.#},{srcGy2:0.#}] h[{srcHx1:0.#},{srcHy1:0.#},{srcHx2:0.#},{srcHy2:0.#}] -> " +
+ $"dst={Describe(dst)} g[{dstGx1:0.#},{dstGy1:0.#},{dstGx2:0.#},{dstGy2:0.#}] h[{dstHx1:0.#},{dstHy1:0.#},{dstHx2:0.#},{dstHy2:0.#}]");
+ }
+
+ long now = Environment.TickCount64;
+ if (now - _summaryMs >= 5000)
+ {
+ _summaryMs = now;
+ Logger.Info?.Print(LogClass.Gpu,
+ $"SEAMPROBE summary: matches={_matches} shapes={_seenShapes.Count}");
+ }
+ }
+ catch (Exception e)
+ {
+ _enabled = false;
+ Logger.Warning?.Print(LogClass.Gpu, $"SEAMPROBE: disabled after unexpected error: {e}");
+ }
+ }
+
+ /// Names the first watched coordinate an edge lands on, or null.
+ private static string EdgeHit(float sA, float sB, float dA, float dB, int[] watch, string axis)
+ {
+ foreach (int w in watch)
+ {
+ if (Math.Abs(sA - w) <= _tol || Math.Abs(sB - w) <= _tol)
+ {
+ return $"{axis}{w}(src)";
+ }
+
+ if (Math.Abs(dA - w) <= _tol || Math.Abs(dB - w) <= _tol)
+ {
+ return $"{axis}{w}(dst)";
+ }
+ }
+
+ return null;
+ }
+
+ private static string Describe(Texture t)
+ {
+ if (t == null)
+ {
+ return "null";
+ }
+
+ return $"@0x{t.Range.GetSubRange(0).Address:X}/{t.Info.FormatInfo.Format}/{t.Info.Width}x{t.Info.Height}" +
+ $"/lvl{t.FirstLevel}+{t.Info.Levels}/eff({t.EffectiveScaleX:0.####},{t.EffectiveScaleY:0.####})";
+ }
+
+ private static long Fnv(params ulong[] vals)
+ {
+ unchecked
+ {
+ ulong x = 14695981039346656037UL;
+ foreach (ulong v in vals)
+ {
+ x = (x ^ v) * 1099511628211UL;
+ }
+ return (long)x;
+ }
+ }
+
+ private static long Fnv(ulong a, ulong b, ulong c, long d, long e, long f, long g)
+ {
+ return Fnv(a, b, c, (ulong)d, (ulong)e, (ulong)f, (ulong)g);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
index 4ce5f07b1..464d4791e 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -557,7 +557,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
- if (nowMs - _forceMipsSummaryMs > 5000)
+ if (GAL.MvppDev.Enabled && nowMs - _forceMipsSummaryMs > 5000)
{
_forceMipsSummaryMs = nowMs;
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
index 553e41060..ece097e7f 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
@@ -374,6 +374,11 @@ namespace Ryujinx.Graphics.Gpu.Image
case ShaderStage.Fragment:
// The exact (quantized) host/guest ratio, not the nominal scale:
// shader coordinate compensation must land on the real content size.
+ // KNOWN LIMIT: this support-buffer slot is a single scalar, so Y
+ // compensates with X's quantized ratio (they can differ by the
+ // size-dependent mip pad). Sub-texel error; separating them means a
+ // support-buffer layout + shader codegen change (invalidates every
+ // shader cache) -> deliberately parked as its own dossier.
float scale = texture.EffectiveScaleX;
if (scale != 1)
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
index 87a30eb86..522cce3d0 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
@@ -46,6 +46,9 @@ namespace Ryujinx.Graphics.Gpu.Image
///
public float RenderTargetScaleY { get; private set; } = 1f;
+ // [COUTURE PROBE -- TEMPORARY, DISPOSABLE] throttle for the edge-seam hunt log.
+ private long _coutureLogMs;
+
///
/// Creates a new instance of the texture manager.
///
@@ -68,6 +71,8 @@ namespace Ryujinx.Graphics.Gpu.Image
_rtColors = new Texture[Constants.TotalRenderTargets];
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
_rtColorsBound = new bool[Constants.TotalRenderTargets];
+
+ MvppSeamProbe.Announce(); // one status line per boot, even if no call site ever fires
}
///
@@ -200,6 +205,22 @@ namespace Ryujinx.Graphics.Gpu.Image
return texture != null && !(texture.ScaleMode == TextureScaleMode.Blacklisted || texture.ScaleMode == TextureScaleMode.Undesired) && texture.ScaleFactor != GraphicsConfig.ResScale;
}
+ ///
+ /// [DLSS-SR fractional] Nominal equality is not enough to skip the scale update: two
+ /// targets can share the nominal scale yet carry different quantized host/guest ratios
+ /// (the mip-align pad is size-dependent, e.g. a 1600x900 and a 1280x720 pass). Switching
+ /// between them without re-deriving the group ratio leaves viewport/scissor covering the
+ /// OTHER pass's allocation: the last column/row is never rasterized (edge flicker).
+ ///
+ /// The texture being bound as a render target
+ /// True if the texture's exact ratio disagrees with the current group ratio
+ private bool EffectiveScaleMismatch(Texture texture)
+ {
+ return TextureCache.DlssSrSpike &&
+ texture.ScaleMode == TextureScaleMode.Scaled &&
+ (texture.EffectiveScaleX != RenderTargetScaleX || texture.EffectiveScaleY != RenderTargetScaleY);
+ }
+
///
/// Sets the render target color buffer.
///
@@ -209,7 +230,10 @@ namespace Ryujinx.Graphics.Gpu.Image
public bool SetRenderTargetColor(int index, Texture color)
{
bool hasValue = color != null;
- bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && RenderTargetScale != color.ScaleFactor);
+ // [P1.5 RE-APPLIED 12/07 soir, A/B user] First in-game read blamed this for the seam
+ // flicker, but the pre-v1.2.1 binaries flicker too (user A/B) -- verdict reopened.
+ // User hypothesis: WITH this fix, Quality mode no longer showed the seam. Testing.
+ bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && (RenderTargetScale != color.ScaleFactor || EffectiveScaleMismatch(color)));
if (_rtColors[index] != color)
{
@@ -242,7 +266,7 @@ namespace Ryujinx.Graphics.Gpu.Image
public bool SetRenderTargetDepthStencil(Texture depthStencil)
{
bool hasValue = depthStencil != null;
- bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && RenderTargetScale != depthStencil.ScaleFactor);
+ bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && (RenderTargetScale != depthStencil.ScaleFactor || EffectiveScaleMismatch(depthStencil))); // [P1.5 RE-APPLIED, A/B user] see SetRenderTargetColor
if (_rtDepthStencil != depthStencil)
{
@@ -449,6 +473,40 @@ namespace Ryujinx.Graphics.Gpu.Image
RenderTargetScaleX = scaleX;
RenderTargetScaleY = scaleY;
+
+ // [COUTURE PROBE -- TEMPORARY, DISPOSABLE] Edge-seam hunt (12/07): whenever the chosen
+ // group ratio disagrees with a bound target's exact ratio, the scissor covers
+ // ceil(guestW x group) while that target really spans guestW x effective -- the
+ // uncovered last columns ARE the black line. Print the exact numbers, throttled ~1/s.
+ // covX/covY = covered/needed host pixels; the difference is the line's width.
+ if (TextureCache.DlssSrSpike && targetScale != 1f &&
+ Environment.TickCount64 - _coutureLogMs >= 1000)
+ {
+ void Report(Texture target, string slot)
+ {
+ if (target == null || target.ScaleMode != TextureScaleMode.Scaled)
+ {
+ return;
+ }
+
+ if (target.EffectiveScaleX != scaleX || target.EffectiveScaleY != scaleY)
+ {
+ _coutureLogMs = Environment.TickCount64;
+ Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
+ $"COUTURE: {slot} {target.Info.Width}x{target.Info.Height} eff=({target.EffectiveScaleX:0.00000},{target.EffectiveScaleY:0.00000}) " +
+ $"vs group=({scaleX:0.00000},{scaleY:0.00000}) nominal={targetScale:0.00000} singleUse={singleUse} " +
+ $"covX={(int)MathF.Ceiling(target.Info.Width * scaleX)}/{(int)MathF.Ceiling(target.Info.Width * target.EffectiveScaleX)} " +
+ $"covY={(int)MathF.Ceiling(target.Info.Height * scaleY)}/{(int)MathF.Ceiling(target.Info.Height * target.EffectiveScaleY)}");
+ }
+ }
+
+ for (int i = 0; i < _rtColors.Length; i++)
+ {
+ Report(_rtColors[i], $"color{i}");
+ }
+
+ Report(_rtDepthStencil, "depth");
+ }
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index e59f8571a..317407881 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -322,12 +322,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
throw new DiskCacheLoadException(DiskCacheLoadResult.IncompatibleVersion);
}
- // MV++ Phase 2.1 run A: the position-slice analysis only runs at translation
- // time, so the probe flag forces the guest-recompile path (the same one every
- // codegen version bump takes) -- host cache is skipped, every shader gets
- // re-translated and analyzed once, the cache is refreshed with identical code.
- bool loadHostCache = header.CodeGenVersion == CodeGenVersion &&
- !Ryujinx.Graphics.Shader.Translation.MvppPositionProbe.Enabled;
+ // MV++ shader-codegen probes: any flag that changes or instruments the generated
+ // host code must take the guest-recompile path (the same one every codegen version
+ // bump takes). Loading would serve shaders built with the OTHER setting when the
+ // flag was toggled between launches -- the stale-instrumentation trap. The write
+ // side is guarded in AddShader for the same reason.
+ bool loadHostCache = header.CodeGenVersion == CodeGenVersion && !_mvppCodegenProbeActive;
int programIndex = 0;
@@ -562,8 +562,27 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// Cached program
/// Optional host binary code
/// Output streams to use
+ // MV++ shader-codegen probes that alter or instrument the generated host code. While any
+ // of them is active the shared host cache must be neither loaded (stale code from the
+ // other setting) nor written (instrumented code would poison the next probe-less launch).
+ private static readonly bool _mvppCodegenProbeActive =
+ Ryujinx.Graphics.Shader.Translation.MvppPositionProbe.Enabled ||
+ Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.Enabled ||
+ Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.MipMarkEnabled ||
+ Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled ||
+ Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.ProbeEnabled ||
+ Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.RewriteEnabled ||
+ Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.CaptureEnabled ||
+ Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.LodBiasEnabled;
+
public void AddShader(GpuContext context, CachedShaderProgram program, ReadOnlySpan hostCode, DiskCacheOutputStreams streams = null)
{
+ // Probe builds never persist: their generated code only matches while the flag is on.
+ if (_mvppCodegenProbeActive)
+ {
+ return;
+ }
+
uint stagesBitMask = 0;
for (int index = 0; index < program.Shaders.Length; index++)
diff --git a/src/Ryujinx.Graphics.Gpu/Window.cs b/src/Ryujinx.Graphics.Gpu/Window.cs
index fd7048123..b723662cc 100644
--- a/src/Ryujinx.Graphics.Gpu/Window.cs
+++ b/src/Ryujinx.Graphics.Gpu/Window.cs
@@ -122,6 +122,7 @@ namespace Ryujinx.Graphics.Gpu
private int _mvppTakes;
private int _mvppTakeHits;
private Image.Texture _mvppHeldSceneDepth;
+ private int _mvppHeldSceneDepthSeq;
private long _colorPubDiagMs; // [COLORCAP snapshot] throttled publish diagnostic
public bool IsFrameAvailable => _framesAvailable != 0;
@@ -320,10 +321,10 @@ namespace Ryujinx.Graphics.Gpu
// the queue with the frame.
if (DlssCameraState.FifoEnabled)
{
- DlssCameraState.PresentVpValid = DlssCameraState.TryConsumeOrdered(out System.Numerics.Matrix4x4 orderedVp);
- DlssCameraState.PresentVp = orderedVp;
+ bool orderedValid = DlssCameraState.TryConsumeOrdered(out System.Numerics.Matrix4x4 orderedVp);
+ DlssCameraState.PublishPresent(in orderedVp, orderedValid);
- if (Environment.TickCount64 - _mvppFifoLogMs >= 5000)
+ if (MvppDev.Enabled && Environment.TickCount64 - _mvppFifoLogMs >= 5000)
{
_mvppFifoLogMs = Environment.TickCount64;
Logger.Info?.Print(LogClass.Gpu,
@@ -338,8 +339,7 @@ namespace Ryujinx.Graphics.Gpu
}
else
{
- DlssCameraState.PresentVp = pt.CameraVp;
- DlssCameraState.PresentVpValid = pt.CameraVpValid;
+ DlssCameraState.PublishPresent(pt.CameraVp, pt.CameraVpValid);
}
// MV++: prefer the depth pinned at the scaled main pass (deterministic) over the
@@ -358,10 +358,14 @@ namespace Ryujinx.Graphics.Gpu
if (scene != null)
{
_mvppHeldSceneDepth = scene;
+ _mvppHeldSceneDepthSeq = scene.InvalidatedSequence;
}
- else if (_mvppHeldSceneDepth?.HostTexture == null)
+ else if (_mvppHeldSceneDepth != null && _mvppHeldSceneDepth.InvalidatedSequence != _mvppHeldSceneDepthSeq)
{
- _mvppHeldSceneDepth = null; // disposed under us: drop the stale hold
+ // Drop the stale hold. HostTexture is never nulled on dispose (stock keeps the
+ // dead reference), so testing it can NOT detect destruction; InvalidatedSequence
+ // is the stock validity idiom and bumps on dispose, unmap AND host replacement.
+ _mvppHeldSceneDepth = null;
}
presentDepth = _mvppHeldSceneDepth;
@@ -376,7 +380,7 @@ namespace Ryujinx.Graphics.Gpu
}
// MV++ diag: what depth is published at present time? (heartbeat, capture runs only)
- if (DlssCameraState.Enabled && Environment.TickCount64 - _mvppDepthLogMs >= 5000)
+ if (MvppDev.Enabled && DlssCameraState.Enabled && Environment.TickCount64 - _mvppDepthLogMs >= 5000)
{
_mvppDepthLogMs = Environment.TickCount64;
Image.Texture d = presentDepth;
diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs
index e2037e65f..cad89b031 100644
--- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs
@@ -36,6 +36,64 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
public const string StreamlineReflex = "sl.reflex.dll";
public const string StreamlinePcl = "sl.pcl.dll";
+ // Memoized auto-scan result. The library scan walks every game folder of every launcher
+ // on every drive - hundreds of directories on every single launch when the DLL is not in
+ // the BYO folder - and the winning path almost never moves. Remember it, validate it
+ // still exists, and only rescan when it vanished. The BYO folder is checked first every
+ // time, so dropping a DLL there always overrides the cache.
+ private static string PathCacheFile
+ {
+ get
+ {
+ try
+ {
+ return Path.Combine(Common.Configuration.AppDataManager.BaseDirPath, "dlss_path.cfg");
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+
+ private static string ReadCachedNgxPath()
+ {
+ try
+ {
+ string cacheFile = PathCacheFile;
+ if (cacheFile != null && File.Exists(cacheFile))
+ {
+ string cached = File.ReadAllText(cacheFile).Trim();
+ if (cached.Length > 0 && File.Exists(cached))
+ {
+ return cached;
+ }
+ }
+ }
+ catch
+ {
+ // Unreadable cache = no cache; the scan below rebuilds it.
+ }
+
+ return null;
+ }
+
+ private static void WriteCachedNgxPath(string path)
+ {
+ try
+ {
+ string cacheFile = PathCacheFile;
+ if (cacheFile != null)
+ {
+ File.WriteAllText(cacheFile, path);
+ }
+ }
+ catch
+ {
+ // Not fatal: next launch just scans again.
+ }
+ }
+
///
/// Returns the path to a usable nvngx_dlss.dll, or null if none could be found.
///
@@ -58,7 +116,14 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
return null;
}
- // 2. Auto-scan known locations and pick the newest version found.
+ // 2. A still-valid memoized result skips the library walk entirely.
+ string cached = ReadCachedNgxPath();
+ if (cached != null)
+ {
+ return cached;
+ }
+
+ // 3. Auto-scan known locations and pick the newest version found.
string best = null;
Version bestVersion = null;
@@ -76,6 +141,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
}
}
+ if (best != null)
+ {
+ WriteCachedNgxPath(best);
+ }
+
return best;
}
diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs
index 6580a56d6..7fa42eacb 100644
--- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs
@@ -37,6 +37,26 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
/// True when the user opted into DLSS via RYUJINX_DLSS=1.
public static bool IsEnabled => _enabled;
+ ///
+ /// True when the DLSS-SR fractional path (RYUJINX_DLSS_SR=1) is active. Read by
+ /// TextureStorage to zero fresh image allocations at birth: the memory suballocator
+ /// recycles freed device memory within the process, and on this path a render target
+ /// can reach DLSS (and the screen) before the game's own writes cover the quantized
+ /// allocation -- the recycled bytes are then a recognizable earlier frame (the
+ /// startup ghost image in Quality). Off this path the flag stays false and texture
+ /// creation is byte-identical to upstream.
+ ///
+ public static bool SrFractional => _srFractional;
+
+ ///
+ /// RYUJINX_DLSS_NOBIRTHCLEAR=1: diagnostic kill-switch for the TEXTURE birth-clear only
+ /// (the swapchain birth-clear stays on -- it owns the validated FG startup-ghost fix).
+ /// A/B for the 13/07 question: does zeroing fresh allocations HARDEN the fractional-path
+ /// black line (the uncovered rim used to show soft recycled memory, now pure black)?
+ ///
+ public static readonly bool BirthClearDisabled =
+ IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_DLSS_NOBIRTHCLEAR"));
+
// Porte A - DLSS Frame Generation probe (RYUJINX_DLSS_FG=1, requires RYUJINX_DLSS=1):
// adds DLSS_G to slInit's featuresToLoad and probes slIsFeatureSupported(DLSS_G) after
// device registration. Diagnostic only: no frame is ever generated (present-injection is
diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs
index 663d8d5d1..796291b85 100644
--- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs
@@ -597,6 +597,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
// resets once instead of every frame (which would leave the image permanently aliased).
private BufferHandle _sceneChangeBuffer;
private bool _wasChanging;
+
+ // [SKYFLASH] True once the motion meter has measured real motion at least once this
+ // session; a "low motion" scene-cut verdict means nothing before that (the MVs simply
+ // have not started flowing), and acting on it resets history on the FIRST camera move.
+ private bool _motionMeterWarm;
private int _framesSinceReset;
private long _lastResetMs = Environment.TickCount64; // cooldown counts from DLSS start (huge value = never reset this session)
private string _lastSizeSig; // zoom dossier: last logged (input, mode, output) triple
@@ -797,6 +802,18 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
return false;
}
+ // [FG lifetime] one tick per present: dispose parked views once dlfg's queue has drained.
+ _presentSerial++;
+ DrainFgRetired(all: false);
+
+ // MV++: pick up the camera paired with THIS frame (enqueued per present by the GPU
+ // thread; order does the cross-thread pairing). Consumed HERE and not in the Vulkan
+ // window's Present: once the DLSS-G swapchain proxy engages, that entry stops running
+ // every frame and the ring starves -- the frozen-camera regression of 12/07 evening
+ // (consumer saw 0 changes while the capture published dozens). This method provably
+ // runs once per evaluated frame: its consumer stats count every present.
+ DlssCameraState.ConsumePresent();
+
// MV++ Phase 1 plumbing proof: the presented frame's camera view-projection arrives
// here through the frame queue; keep the previous one (the reprojection pair the MV
// compute pass will consume) and heartbeat so the end-to-end chain is verifiable in
@@ -1252,7 +1269,19 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
// every 6s, measured: changed~85% but motion 3-4.5%), while still catching genuine cuts (motion
// near 0). This is the dominant flicker fix.
_framesSinceReset++;
+
+ // [SKYFLASH 13/07] Trust a low-motion verdict only after the meter has measured real
+ // motion once. Measured on the user's run: the session's ONLY reset fired at the FIRST
+ // camera move (changed=91% motion=0.2%) -- the change metric saturates while the MVs
+ // have not started flowing yet -> false scene cut -> one-frame history reset = the
+ // startup sky flash. The real session start is already covered by the first-frame reset.
+ if (motionFraction >= _sceneCutMaxMotion)
+ {
+ _motionMeterWarm = true;
+ }
+
bool sceneCut = _hasPrev && changing && !_wasChanging
+ && _motionMeterWarm
&& motionFraction < _sceneCutMaxMotion
&& _framesSinceReset >= ResetCooldownFrames
&& Environment.TickCount64 - _lastResetMs >= (long)_resetCooldownMs;
@@ -1670,9 +1699,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
System.Numerics.Matrix4x4 prevT = System.Numerics.Matrix4x4.Transpose(vpPrev);
// Log the PREVIOUS frame's agreement stats ~1/s (host-mapped buffer, no GPU stall),
- // then clear the counters for this frame's dispatch.
+ // then clear the counters for this frame's dispatch. Dev gauge: with MvppDev off the
+ // stats are never read, so the whole read/zero cycle is skipped (release default).
long nowMs = Environment.TickCount64;
- if (nowMs - _mvppStatsLogMs >= 1000)
+ if (MvppDev.Enabled && nowMs - _mvppStatsLogMs >= 1000)
{
_mvppStatsLogMs = nowMs;
@@ -1712,8 +1742,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
}
}
- Span statsZero = stackalloc uint[MvppStatsCount];
- _gd.BufferManager.SetData(_mvppStatsBuffer, 0, statsZero);
+ if (MvppDev.Enabled)
+ {
+ Span statsZero = stackalloc uint[MvppStatsCount];
+ _gd.BufferManager.SetData(_mvppStatsBuffer, 0, statsZero);
+ }
// Window-depth -> NDC-z convention: from the guest's viewport transform unless the
// env override forces it. Rotation MVs are immune to a wrong mapping, translation
@@ -3147,6 +3180,44 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
return crc;
}
+ // [FG lifetime] DLSS-G's present workers read the tagged depth/mvec views on their own
+ // command streams, invisible to our fence tracking. Destroying a view the instant we
+ // recreate it (dyn-res ramp) can pull it from under an in-flight interpolation:
+ // intermittent DEVICE_LOST. With FG loaded, park old views and dispose them a few
+ // presents later, once dlfg's queue depth has certainly drained. FG off = immediate
+ // dispose, exactly the previous behavior.
+ private readonly System.Collections.Generic.List<(TextureView View, long RetireAfter)> _fgRetired = new();
+ private long _presentSerial;
+
+ private void RetireFgVisible(TextureView view)
+ {
+ if (view == null)
+ {
+ return;
+ }
+
+ if (_gd.FgHooks != null)
+ {
+ _fgRetired.Add((view, _presentSerial + 4));
+ }
+ else
+ {
+ view.Dispose();
+ }
+ }
+
+ private void DrainFgRetired(bool all)
+ {
+ for (int i = _fgRetired.Count - 1; i >= 0; i--)
+ {
+ if (all || _fgRetired[i].RetireAfter <= _presentSerial)
+ {
+ _fgRetired[i].View.Dispose();
+ _fgRetired.RemoveAt(i);
+ }
+ }
+ }
+
private void EnsureResources(TextureView input, int outW, int outH, CommandBufferScoped cbs)
{
if (_output != null && _inW == input.Width && _inH == input.Height && _outW == outW && _outH == outH)
@@ -3160,10 +3231,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
if (_dlssDynRes && _output != null && _outW == outW && _outH == outH &&
(_inW != input.Width || _inH != input.Height))
{
- _depth?.Dispose();
- _motion?.Dispose();
- _motionFiltered?.Dispose();
- _prevColor?.Dispose();
+ RetireFgVisible(_depth);
+ RetireFgVisible(_motion);
+ RetireFgVisible(_motionFiltered);
+ RetireFgVisible(_prevColor);
_heldDepth = null;
_hasHeldDepth = false;
@@ -3181,11 +3252,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
return;
}
- _output?.Dispose();
- _depth?.Dispose();
- _motion?.Dispose();
- _motionFiltered?.Dispose();
- _prevColor?.Dispose(); // was leaked on every reallocation (dynamic-resolution games churn this)
+ RetireFgVisible(_output);
+ RetireFgVisible(_depth);
+ RetireFgVisible(_motion);
+ RetireFgVisible(_motionFiltered);
+ RetireFgVisible(_prevColor); // was leaked on every reallocation (dynamic-resolution games churn this)
// E9b: _heldDepth points INTO the per-size cache -- never dispose entries during
// gameplay (in-flight CB race); just drop the reference, the cache reuses/keeps them.
_heldDepth = null;
@@ -3260,11 +3331,13 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
public void Dispose()
{
+ DrainFgRetired(all: true);
_output?.Dispose();
_depth?.Dispose();
_motion?.Dispose();
_motionFiltered?.Dispose();
_prevColor?.Dispose();
+ _injectTintTex?.Dispose(); // [TINTPROBE] was the one view Dispose() forgot
_injectDepthZero?.Dispose();
_injectMotionZero?.Dispose();
_injectMotion?.Dispose(); // [C4] scene-res reproj MV + its shader-required aux images
diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs
index 03361d1ca..a0c96a243 100644
--- a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs
@@ -294,11 +294,33 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
// 13=M (newer transformer -- preset-M chantier). When the env is set EXPLICITLY it now also
// overrides the DLAA slot (preset selectable end-to-end, per external user request); without
// it, defaults are untouched: DLAA pinned to K, upscaling modes K.
- private static readonly bool _presetEnvSet =
- uint.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_PRESET"), out _);
+ private static readonly uint? _presetEnv = ParsePresetEnv();
- private static readonly uint _upscalePreset =
- uint.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_PRESET"), out uint _p) ? _p : 11u;
+ private static readonly bool _presetEnvSet = _presetEnv.HasValue;
+
+ private static readonly uint _upscalePreset = _presetEnv ?? 11u;
+
+ // Validated: the shipped Streamline's DLSSPreset enum stops at 13 (M). An undefined value
+ // (99, garbage) would go straight to slDLSSSetOptions with driver-undefined behavior.
+ private static uint? ParsePresetEnv()
+ {
+ string raw = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_PRESET");
+
+ if (string.IsNullOrEmpty(raw))
+ {
+ return null;
+ }
+
+ if (uint.TryParse(raw, out uint value) && value <= 13u)
+ {
+ return value;
+ }
+
+ Ryujinx.Common.Logging.Logger.Warning?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
+ $"RYUJINX_DLSS_PRESET=\"{raw}\" is not a valid DLSS preset (0-13): using engine defaults.");
+
+ return null;
+ }
// Optional DLSS sharpening (RYUJINX_DLSS_SHARPNESS, 0..1; default 0 = off, unchanged).
private static readonly float _sharpness =
@@ -626,7 +648,17 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
///
/// True while DLSS super resolution is actually evaluating frames -- drives the green
/// status-bar DLSS chip (user request 05/07: same treatment as the VRR/FG indicators).
+ /// Freshness-based: refreshed by every successful evaluate, considered off when none
+ /// happened for 2 seconds. A latched bool only went false on evaluate FAILURE, so the
+ /// chip kept lying whenever DLSS simply stopped being called (path switched away, game
+ /// stopped, emulation paused).
///
- public static volatile bool UiActive;
+ public static bool UiActive
+ {
+ get => Environment.TickCount64 - System.Threading.Interlocked.Read(ref _uiActiveAtMs) < 2000;
+ set => System.Threading.Interlocked.Exchange(ref _uiActiveAtMs, value ? Environment.TickCount64 : 0);
+ }
+
+ private static long _uiActiveAtMs;
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs
index aefb2249e..571657a71 100644
--- a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs
@@ -274,8 +274,17 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
/// the current multiplier. Drives the status-bar FG chip (user request 05/07: green like
/// the VRR indicator when interpolating, visible-but-neutral when armed at x1 -- the old
/// suffix silently vanished at x1, which read as "it disappeared").
+ /// Freshness-based like the DLSS chip: refreshed by the once-per-second counter poll,
+ /// considered off when no poll happened for 3 seconds -- a latched bool survived the
+ /// game's shutdown and showed a stale FG chip on the next game until FG re-armed.
///
- public static volatile bool UiArmed;
+ public static bool UiArmed
+ {
+ get => Environment.TickCount64 - System.Threading.Interlocked.Read(ref _uiArmedAtMs) < 3000;
+ set => System.Threading.Interlocked.Exchange(ref _uiArmedAtMs, value ? Environment.TickCount64 : 0);
+ }
+
+ private static long _uiArmedAtMs;
// FG counter (NVIDIA-overlay style): count OUR app presents over a wall-clock 1s window, and read
// DLSSGState.numFramesActuallyPresented (= frames SL actually pushed to the display since the last
@@ -739,10 +748,15 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
UiMultiplier = mult >= 2 ? (int)mult : 1;
UiArmed = true;
- Logger.Info?.Print(LogClass.Gpu,
- $"DLSS-FG COUNTER: app {appFps:0.0} fps x{mult} (numFramesActuallyPresented) -> display ~{displayFps:0.0} fps " +
- $"[app {_appPresentsInWindow} presents in {elapsedMs} ms] status=0x{state.Status:X}. " +
- (mult >= 2 ? "FG IS INTERPOLATING." : "FG enabled but NOT generating extra frames."));
+ // The poll itself feeds the status-bar chips and must keep running; only the log
+ // line is a dev gauge (MvppDev, release default quiet).
+ if (GAL.MvppDev.Enabled)
+ {
+ Logger.Info?.Print(LogClass.Gpu,
+ $"DLSS-FG COUNTER: app {appFps:0.0} fps x{mult} (numFramesActuallyPresented) -> display ~{displayFps:0.0} fps " +
+ $"[app {_appPresentsInWindow} presents in {elapsedMs} ms] status=0x{state.Status:X}. " +
+ (mult >= 2 ? "FG IS INTERPOLATING." : "FG enabled but NOT generating extra frames."));
+ }
}
private static void LogState(uint viewportId, bool force)
diff --git a/src/Ryujinx.Graphics.Vulkan/Shader.cs b/src/Ryujinx.Graphics.Vulkan/Shader.cs
index 7cefc0dfc..ec6d14841 100644
--- a/src/Ryujinx.Graphics.Vulkan/Shader.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Shader.cs
@@ -27,6 +27,10 @@ namespace Ryujinx.Graphics.Vulkan
// MV++ (b1) 5B3 modprobe: FNV64 of the SPIR-V bytes ACTUALLY handed to vkCreateShaderModule
// (identity check against the dumped .spv files -- the disk-cache trap hit twice already).
+ // Only computed under the probe: hashing every compiled shader byte-by-byte costs real time
+ // on a big game's first boot, and the probe log is the sole consumer.
+ private static readonly bool _mvppModProbe =
+ Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_MODHASH") == "1";
public ulong SpirvHash { get; private set; }
public readonly Task CompileTask;
@@ -56,12 +60,15 @@ namespace Ryujinx.Graphics.Vulkan
}
}
- ulong fnv = 14695981039346656037UL;
- foreach (byte b in spirv)
+ if (_mvppModProbe)
{
- fnv = (fnv ^ b) * 1099511628211UL;
+ ulong fnv = 14695981039346656037UL;
+ foreach (byte b in spirv)
+ {
+ fnv = (fnv ^ b) * 1099511628211UL;
+ }
+ SpirvHash = fnv;
}
- SpirvHash = fnv;
fixed (byte* pCode = spirv)
{
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
index b102efaf2..cb101c53f 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
@@ -150,7 +150,13 @@ namespace Ryujinx.Graphics.Vulkan
_allocationAuto = new Auto(allocation);
_imageAuto = new Auto(new DisposableImage(_gd.Api, device, _image), null, _allocationAuto);
- InitialTransition(ImageLayout.Undefined, ImageLayout.General);
+ // [BIRTHCLEAR] Fresh allocations only: the suballocator recycles freed device memory
+ // within the process, and on the DLSS-SR fractional path a render target can reach
+ // DLSS before the game's writes cover the quantized allocation -- the recycled bytes
+ // are then an earlier frame (the startup ghost image in Quality). Never on the
+ // foreign branch below: that memory aliases live content.
+ InitialTransition(ImageLayout.Undefined, ImageLayout.General,
+ zeroFill: Dlss.DlssIntegration.SrFractional && !Dlss.DlssIntegration.BirthClearDisabled);
}
else
{
@@ -253,7 +259,7 @@ namespace Ryujinx.Graphics.Vulkan
return false;
}
- private unsafe void InitialTransition(ImageLayout srcLayout, ImageLayout dstLayout)
+ private unsafe void InitialTransition(ImageLayout srcLayout, ImageLayout dstLayout, bool zeroFill = false)
{
CommandBufferScoped cbs;
bool useTempCbs = !_gd.CommandBufferPool.OwnedByCurrentThread;
@@ -304,6 +310,48 @@ namespace Ryujinx.Graphics.Vulkan
1,
in barrier);
+ // [BIRTHCLEAR] Zero the whole image so the first read sees defined data instead of the
+ // recycled allocation's previous contents. Transfer clears are forbidden on compressed
+ // formats (and those textures are always fully written from guest data anyway).
+ if (zeroFill && !_info.IsCompressed)
+ {
+ if ((aspectFlags & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit)) != 0)
+ {
+ ClearDepthStencilValue zeroDepth = new() { Depth = 0f, Stencil = 0 };
+ _gd.Api.CmdClearDepthStencilImage(cbs.CommandBuffer, _imageAuto.Get(cbs).Value, dstLayout, in zeroDepth, 1, in subresourceRange);
+ }
+ else
+ {
+ ClearColorValue zeroColor = default;
+ _gd.Api.CmdClearColorImage(cbs.CommandBuffer, _imageAuto.Get(cbs).Value, dstLayout, in zeroColor, 1, in subresourceRange);
+ }
+
+ ImageMemoryBarrier clearBarrier = new()
+ {
+ SType = StructureType.ImageMemoryBarrier,
+ SrcAccessMask = AccessFlags.TransferWriteBit,
+ DstAccessMask = DefaultAccessMask,
+ OldLayout = dstLayout,
+ NewLayout = dstLayout,
+ SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ Image = _imageAuto.Get(cbs).Value,
+ SubresourceRange = subresourceRange,
+ };
+
+ _gd.Api.CmdPipelineBarrier(
+ cbs.CommandBuffer,
+ PipelineStageFlags.TransferBit,
+ PipelineStageFlags.AllCommandsBit,
+ 0,
+ 0,
+ null,
+ 0,
+ null,
+ 1,
+ in clearBarrier);
+ }
+
if (useTempCbs)
{
cbs.Dispose();
diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs
index c26d2fed6..439d7dbdf 100644
--- a/src/Ryujinx.Graphics.Vulkan/Window.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Window.cs
@@ -84,7 +84,7 @@ namespace Ryujinx.Graphics.Vulkan
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP_FORCE") == "1";
private bool _isLinear;
private float _scalingFilterLevel;
- private string _dlssSrPresentSig;
+ private (int, int, int, int, int, int, int, int, int, int, Type, Type)? _dlssSrPresentSig;
private bool _updateScalingFilter;
private ScalingFilter _currentScalingFilter;
private bool _colorSpacePassthroughEnabled;
@@ -318,6 +318,15 @@ namespace Ryujinx.Graphics.Vulkan
swapchainApi.GetSwapchainImages(_device, _swapchain, &imageCount, pSwapchainImages);
}
+ // [BIRTHCLEAR-SWAPCHAIN] Presentable images start as recycled device memory, and on
+ // the DLSS-FG path the Streamline proxy can present before our first blit covers
+ // them -- the seconds-long startup ghost image (user repro 13/07: FG on = ghost,
+ // FG off = gone). Zero them once at creation; the daily (no DLSS) is untouched.
+ if (Dlss.DlssIntegration.IsEnabled)
+ {
+ BirthClearSwapchainImages();
+ }
+
if (viaFgProxy)
{
// DLSS-FG Porte B.2: DLSSGOptions wants numBackBuffers + the swapchain format
@@ -355,6 +364,87 @@ namespace Ryujinx.Graphics.Vulkan
}
}
+ ///
+ /// [BIRTHCLEAR-SWAPCHAIN] Zeroes every swapchain image once, right after creation, so
+ /// anything presented before the first real blit shows black instead of recycled VRAM.
+ /// Same command-buffer acquisition pattern as TextureStorage.InitialTransition.
+ ///
+ private unsafe void BirthClearSwapchainImages()
+ {
+ CommandBufferScoped cbs;
+ bool useTempCbs = !_gd.CommandBufferPool.OwnedByCurrentThread;
+
+ if (useTempCbs)
+ {
+ cbs = _gd.BackgroundResources.Get().GetPool().Rent();
+ }
+ else
+ {
+ if (_gd.PipelineInternal != null)
+ {
+ cbs = _gd.PipelineInternal.GetPreloadCommandBuffer();
+ }
+ else
+ {
+ cbs = _gd.CommandBufferPool.Rent();
+ useTempCbs = true;
+ }
+ }
+
+ ImageSubresourceRange range = new(ImageAspectFlags.ColorBit, 0, 1, 0, 1);
+ ClearColorValue zeroColor = default;
+
+ foreach (Image image in _swapchainImages)
+ {
+ ImageMemoryBarrier toClear = new()
+ {
+ SType = StructureType.ImageMemoryBarrier,
+ SrcAccessMask = 0,
+ DstAccessMask = AccessFlags.TransferWriteBit,
+ OldLayout = ImageLayout.Undefined,
+ NewLayout = ImageLayout.TransferDstOptimal,
+ SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ Image = image,
+ SubresourceRange = range,
+ };
+
+ _gd.Api.CmdPipelineBarrier(
+ cbs.CommandBuffer,
+ PipelineStageFlags.TopOfPipeBit,
+ PipelineStageFlags.TransferBit,
+ 0, 0, null, 0, null, 1, in toClear);
+
+ _gd.Api.CmdClearColorImage(cbs.CommandBuffer, image, ImageLayout.TransferDstOptimal, in zeroColor, 1, in range);
+
+ // PresentSrc so an early present (the FG proxy path) finds a legal layout; our own
+ // present path transitions from Undefined anyway, so this never blocks the blit.
+ ImageMemoryBarrier toPresent = new()
+ {
+ SType = StructureType.ImageMemoryBarrier,
+ SrcAccessMask = AccessFlags.TransferWriteBit,
+ DstAccessMask = 0,
+ OldLayout = ImageLayout.TransferDstOptimal,
+ NewLayout = ImageLayout.PresentSrcKhr,
+ SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ Image = image,
+ SubresourceRange = range,
+ };
+
+ _gd.Api.CmdPipelineBarrier(
+ cbs.CommandBuffer,
+ PipelineStageFlags.TransferBit,
+ PipelineStageFlags.BottomOfPipeBit,
+ 0, 0, null, 0, null, 1, in toPresent);
+ }
+
+ if (useTempCbs)
+ {
+ cbs.Dispose();
+ }
+ }
+
private unsafe TextureView CreateSwapchainImageView(Image swapchainImage, VkFormat format, TextureCreateInfo info)
{
ComponentMapping componentMapping = new(
@@ -687,17 +777,21 @@ namespace Ryujinx.Graphics.Vulkan
// Logged on every CHANGE, not once: the "screen shrinks/grows by itself at startup"
// report (quality mode, 04/07) needs the present-geometry timeline -- a once-only
// probe hid whatever bounces during the boot/dyn-res ramp. Steady state = one line.
- string presentSig =
- $"view {view.Width}x{view.Height} active {view.ActiveWidth}x{view.ActiveHeight} " +
- $"src[{srcX0},{srcY0},{srcX1},{srcY1}] dst {_width}x{_height} " +
- $"filter={_scalingFilter?.GetType().Name ?? "none"} effect={_effect?.GetType().Name ?? "none"}";
+ // Compared as values: building the description string on every present cost 60-120
+ // allocations/second for a probe that almost never fires.
+ (int, int, int, int, int, int, int, int, int, int, Type, Type) presentSig =
+ (view.Width, view.Height, view.ActiveWidth, view.ActiveHeight,
+ srcX0, srcY0, srcX1, srcY1, _width, _height,
+ _scalingFilter?.GetType(), _effect?.GetType());
- if (presentSig != _dlssSrPresentSig)
+ if (_dlssSrPresentSig == null || !presentSig.Equals(_dlssSrPresentSig.Value))
{
bool first = _dlssSrPresentSig == null;
_dlssSrPresentSig = presentSig;
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
- $"DLSS-SR probe{(first ? "" : " (CHANGED)")}: {presentSig}");
+ $"DLSS-SR probe{(first ? "" : " (CHANGED)")}: view {view.Width}x{view.Height} active {view.ActiveWidth}x{view.ActiveHeight} " +
+ $"src[{srcX0},{srcY0},{srcX1},{srcY1}] dst {_width}x{_height} " +
+ $"filter={_scalingFilter?.GetType().Name ?? "none"} effect={_effect?.GetType().Name ?? "none"}");
}
if (ScreenCaptureRequested)
@@ -1050,6 +1144,18 @@ namespace Ryujinx.Graphics.Vulkan
// RYUJINX_TAA=1 forces the native temporal filter regardless of the UI selection (dev override).
AntiAliasing antiAliasing = Effects.TemporalFilter.IsEnabled ? AntiAliasing.Taa : _currentAntiAliasing;
+ // Guard (user report 12/07, crash at launch): TAA and DLSS together stack two
+ // temporal accumulators over the same present path. DLSS already does the
+ // temporal work, so it wins; the TAA choice stays untouched in the config and
+ // comes back the moment DLSS is off.
+ if (antiAliasing == AntiAliasing.Taa && Dlss.DlssIntegration.IsEnabled)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
+ "TAA is selected but DLSS is active: skipping the native TAA pass (DLSS already provides temporal AA).");
+
+ antiAliasing = AntiAliasing.None;
+ }
+
switch (antiAliasing)
{
case AntiAliasing.Fxaa:
@@ -1213,6 +1319,16 @@ namespace Ryujinx.Graphics.Vulkan
{
unsafe
{
+ // DLSS-FG: same teardown protocol as RecreateSwapchain -- DLSS-G must be eOff and
+ // its in-flight present work retired BEFORE this swapchain dies, or dlfg's present
+ // workers hang the device on exit (close-with-FG freeze).
+ if (_swapchainViaFgProxy && _gd.FgHooks != null)
+ {
+ Dlss.StreamlineFrameGen.OnSwapchainTeardown(Dlss.DlssUpscaler.ViewportId);
+
+ _gd.FgHooks.DeviceWaitIdle();
+ }
+
for (int i = 0; i < _swapchainImageViews.Length; i++)
{
_swapchainImageViews[i].Dispose();
@@ -1235,6 +1351,15 @@ namespace Ryujinx.Graphics.Vulkan
_effect?.Dispose();
_scalingFilter?.Dispose();
_dlss?.Dispose();
+
+ // [COLORCAP snapshot] the held per-size color copies -- same cleanup loop as the
+ // twin _heldDepthCache in DlssUpscaler.Dispose.
+ foreach (TextureView held in _heldColorCache.Values)
+ {
+ held?.Dispose();
+ }
+
+ _heldColorCache.Clear();
}
}
diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs
index 91b4b7d9f..0ee240479 100644
--- a/src/Ryujinx/Systems/AppHost.cs
+++ b/src/Ryujinx/Systems/AppHost.cs
@@ -659,15 +659,24 @@ namespace Ryujinx.Ava.Systems
// We only need to wait for all commands submitted during the main gpu loop to be processed.
// If the GPU has no work and is cancelled, we need to handle that as well.
- WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle });
+ int doneIndex = WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle });
if (_renderingStarted)
{
// Waiting for work to be finished before we dispose.
Device.Gpu.WaitUntilGpuReady();
}
-
- _gpuDoneEvent.Dispose();
+
+ // Dispose the event only when the render loop actually signalled it. When WaitAny
+ // returned on CANCELLATION, the loop's finally is still flushing and will Set() this
+ // event afterwards -- disposing here crashed the process at every "unlucky" close
+ // (ObjectDisposedException on the render thread, the long-standing close crash,
+ // stack finally captured 13/07). Skipping the dispose on that path leaks one handle
+ // into process exit, which the runtime reclaims.
+ if (doneIndex == 0)
+ {
+ _gpuDoneEvent.Dispose();
+ }
_gpuCancellationTokenSource.Dispose();
DisposeGpu();
@@ -1219,7 +1228,16 @@ namespace Ryujinx.Ava.Systems
threaded.FlushThreadedCommands();
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
}
- _gpuDoneEvent.Set();
+
+ try
+ {
+ _gpuDoneEvent.Set();
+ }
+ catch (ObjectDisposedException)
+ {
+ // Close race second belt: the disposer stopped waiting (cancellation) and
+ // already tore the event down; there is nobody left to signal.
+ }
}
});
diff --git a/src/Ryujinx/Systems/DlssRestart.cs b/src/Ryujinx/Systems/DlssRestart.cs
index d45e8b4bb..4056f1c7a 100644
--- a/src/Ryujinx/Systems/DlssRestart.cs
+++ b/src/Ryujinx/Systems/DlssRestart.cs
@@ -122,9 +122,13 @@ namespace Ryujinx.Ava.Systems
// fact, whether the clean exit hung (watchdog fired) or succeeded (it did not).
Thread watchdog = new(() =>
{
- Thread.Sleep(1000);
+ // 5 s, not 1 s: the clean exit is also what flushes the shader cache TOC and the
+ // config to disk. A 1 s fuse could Kill() mid-write and corrupt them; the hang
+ // this watchdog exists for is indefinite, so the extra seconds only cost time in
+ // the rare stuck case.
+ Thread.Sleep(5000);
Logger.Warning?.Print(LogClass.Application,
- "DLSS restart: clean exit hung >1s -- force-terminating the old process to complete the relaunch.");
+ "DLSS restart: clean exit hung >5s -- force-terminating the old process to complete the relaunch.");
Process.GetCurrentProcess().Kill();
})
{
diff --git a/src/Ryujinx/Systems/DlssUiSettings.cs b/src/Ryujinx/Systems/DlssUiSettings.cs
index 09c5d4ef4..b13854a56 100644
--- a/src/Ryujinx/Systems/DlssUiSettings.cs
+++ b/src/Ryujinx/Systems/DlssUiSettings.cs
@@ -195,6 +195,11 @@ namespace Ryujinx.Ava.Systems
Environment.SetEnvironmentVariable("RYUJINX_MVPP_FLIPY", "1");
Environment.SetEnvironmentVariable("RYUJINX_MVPP_VPFIFO", "1");
Environment.SetEnvironmentVariable("RYUJINX_MVPP_SKYROT", "1");
+ // Leading-edge disocclusion = clamp to nearest valid history (user A/B 13/07 at x3:
+ // 0 zero-MV = fine black lines on pans, 1 clamp = far less, >=2 raw = three lines).
+ // The engine default stays 0 so diagnostic baselines are unchanged; only the shipped
+ // UI profile opts in.
+ Environment.SetEnvironmentVariable("RYUJINX_MVPP_EDGE", "1");
int fgMultiplier = LoadFgMultiplier();
if (fgMultiplier > 1)
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index 6e8c1cbb6..5dc04db90 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -567,10 +567,17 @@ namespace Ryujinx.Ava.UI.ViewModels
set
{
_dlssPresetIndex = value;
+ _dlssPresetWasUnset = false; // an explicit pick replaces "engine default"
OnPropertyChanged();
}
}
+ // True when the saved preset token was absent/0 (engine default) or unknown: the combo
+ // displays K (which IS the engine default) but saving without touching it must preserve
+ // the default token -- silently writing an explicit K also tripped a false restart
+ // prompt for every pre-preset-era user (save wrote 11, the file said 0).
+ private bool _dlssPresetWasUnset;
+
// Set true by SaveSettings when the DLSS mode actually changed, so the settings window can offer a
// one-click self-restart (the DLSS env vars are only read at app startup).
public bool DlssModeRestartPending { get; set; }
@@ -957,7 +964,9 @@ namespace Ryujinx.Ava.UI.ViewModels
ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value;
DlssMode = dlssSaved;
int presetSaved = Ryujinx.Ava.Systems.DlssUiSettings.LoadPresetValue();
- DlssPresetIndex = Math.Max(0, Array.IndexOf(Ryujinx.Ava.Systems.DlssUiSettings.PresetValues, presetSaved));
+ int presetIndex = Array.IndexOf(Ryujinx.Ava.Systems.DlssUiSettings.PresetValues, presetSaved);
+ DlssPresetIndex = Math.Max(0, presetIndex);
+ _dlssPresetWasUnset = presetIndex < 0; // after the property write (the setter clears it)
DlssFgIndex = Math.Clamp(Ryujinx.Ava.Systems.DlssUiSettings.LoadFgMultiplier() - 1, 0, 2);
EnableForceMips = Ryujinx.Ava.Systems.DlssUiSettings.LoadForceMips();
@@ -1109,8 +1118,10 @@ namespace Ryujinx.Ava.UI.ViewModels
}
config.Graphics.ScalingFilterLevel.Value = ScalingFilterLevel;
- int dlssPresetValue = Ryujinx.Ava.Systems.DlssUiSettings.PresetValues[
- Math.Clamp(DlssPresetIndex, 0, Ryujinx.Ava.Systems.DlssUiSettings.PresetValues.Length - 1)];
+ int dlssPresetValue = _dlssPresetWasUnset && DlssPresetIndex == 0
+ ? 0 // untouched engine default stays the engine default
+ : Ryujinx.Ava.Systems.DlssUiSettings.PresetValues[
+ Math.Clamp(DlssPresetIndex, 0, Ryujinx.Ava.Systems.DlssUiSettings.PresetValues.Length - 1)];
DlssModeRestartPending = dlssMode != Ryujinx.Ava.Systems.DlssUiSettings.Load() ||
dlssPresetValue != Ryujinx.Ava.Systems.DlssUiSettings.LoadPresetValue() ||
DlssFgIndex + 1 != Ryujinx.Ava.Systems.DlssUiSettings.LoadFgMultiplier() ||