v1.2.1: stability update. Fixes the long standing crash on game close, a GPU device lost crash with frame generation and dynamic resolution, the TAA plus DLSS startup crash, the DLSS Quality startup zoom and black line, the brief ghost image at game start, and greatly reduces panning lines. Internal diagnostics are now fully off in normal play.
This commit is contained in:
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="ConsumePresent"/> and read on the render thread only; the
|
||||
/// GPU thread hands values across through the present ring below, never directly.
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues the camera paired with the frame being enqueued for presentation (GPU thread).
|
||||
/// </summary>
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues the camera paired with the present being executed and republishes it on
|
||||
/// <see cref="PresentVp"/>/<see cref="PresentVpValid"/>. 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.
|
||||
/// </summary>
|
||||
public static void ConsumePresent()
|
||||
{
|
||||
if (_presentRing.TryDequeue(out (Matrix4x4 Vp, bool Valid) entry))
|
||||
{
|
||||
PresentVp = entry.Vp;
|
||||
PresentVpValid = entry.Valid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the view-projection captured for the frame being rendered (GPU thread).
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class MvppDev
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEV") == "1";
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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<byte> 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,
|
||||
|
||||
@@ -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<int>(x, y, width, height);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// [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.
|
||||
/// </summary>
|
||||
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<long> _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<int>();
|
||||
return;
|
||||
}
|
||||
|
||||
List<int> 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<int> or y<int>)");
|
||||
}
|
||||
|
||||
_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;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Names the first watched coordinate an edge lands on, or null.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,7 +557,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
if (nowMs - _forceMipsSummaryMs > 5000)
|
||||
if (GAL.MvppDev.Enabled && nowMs - _forceMipsSummaryMs > 5000)
|
||||
{
|
||||
_forceMipsSummaryMs = nowMs;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -46,6 +46,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public float RenderTargetScaleY { get; private set; } = 1f;
|
||||
|
||||
// [COUTURE PROBE -- TEMPORARY, DISPOSABLE] throttle for the edge-seam hunt log.
|
||||
private long _coutureLogMs;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the texture manager.
|
||||
/// </summary>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -200,6 +205,22 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
return texture != null && !(texture.ScaleMode == TextureScaleMode.Blacklisted || texture.ScaleMode == TextureScaleMode.Undesired) && texture.ScaleFactor != GraphicsConfig.ResScale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [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).
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture being bound as a render target</param>
|
||||
/// <returns>True if the texture's exact ratio disagrees with the current group ratio</returns>
|
||||
private bool EffectiveScaleMismatch(Texture texture)
|
||||
{
|
||||
return TextureCache.DlssSrSpike &&
|
||||
texture.ScaleMode == TextureScaleMode.Scaled &&
|
||||
(texture.EffectiveScaleX != RenderTargetScaleX || texture.EffectiveScaleY != RenderTargetScaleY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the render target color buffer.
|
||||
/// </summary>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
/// <param name="program">Cached program</param>
|
||||
/// <param name="hostCode">Optional host binary code</param>
|
||||
/// <param name="streams">Output streams to use</param>
|
||||
// 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<byte> 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++)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the path to a usable nvngx_dlss.dll, or null if none could be found.
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,26 @@ namespace Ryujinx.Graphics.Vulkan.Dlss
|
||||
/// <summary>True when the user opted into DLSS via RYUJINX_DLSS=1.</summary>
|
||||
public static bool IsEnabled => _enabled;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static bool SrFractional => _srFractional;
|
||||
|
||||
/// <summary>
|
||||
/// 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)?
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
@@ -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<uint> statsZero = stackalloc uint[MvppStatsCount];
|
||||
_gd.BufferManager.SetData<uint>(_mvppStatsBuffer, 0, statsZero);
|
||||
if (MvppDev.Enabled)
|
||||
{
|
||||
Span<uint> statsZero = stackalloc uint[MvppStatsCount];
|
||||
_gd.BufferManager.SetData<uint>(_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
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -150,7 +150,13 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
_allocationAuto = new Auto<MemoryAllocation>(allocation);
|
||||
_imageAuto = new Auto<DisposableImage>(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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() ||
|
||||
|
||||
Reference in New Issue
Block a user