The Beast Roofer Edition v1.2.5

This commit is contained in:
2026-08-05 20:56:37 -04:00
parent 6634d3ef87
commit a4fb32b727
240 changed files with 2279 additions and 11508 deletions
+3
View File
@@ -3,3 +3,6 @@
###############################################################################
* text=auto eol=lf
*.json text eol=lf
# Les bats du coffre doivent garder leurs fins de ligne Windows (CRLF)
docs/bats-valides/*.bat text eol=crlf
+49 -13
View File
@@ -10,12 +10,12 @@ I am shipping this update because I caught a bug I had introduced myself in v1.2
one that turns the screen green in some games. It could in principle affect any game, so
please take this update even if yours looked fine.
One correction while I am at it: the black block artifacts in Xenoblade Chronicles 2, the
ones I had described as a long-standing emulation defect and worked around by skipping the
One correction while I am at it: the black block artifacts some of you saw in
motion-blur scenes, the ones I had described as a long-standing emulation defect and worked around by skipping the
motion-blur pass, were caused by that same bug of mine. They were never an old emulator
problem, they were mine. They are gone now, and the motion blur stays on.
### Fixed: green screen in several games (GYLT, Ghostbusters: Spirits Unleashed, and others)
### Fixed: green screen in several games
Some games — several Unreal Engine titles among them — rendered their menus and videos
correctly but showed a green (or corrupted) screen the moment 3D gameplay started. The
@@ -47,9 +47,26 @@ can make halos and shimmer more visible in motion. Applied on the next launch, l
other DLSS settings.
An experimental `Advanced motion stack` checkbox is also present, **off by default**. It
is validated on one engine family only and can add trembling in motion elsewhere — leave
improves camera tracking: it reads the camera even when a game stores it in an unusual place,
it rejects misleading matrices such as sky or reflections, and it stops mistaking the game's
own sampling jitter for real motion. It removes nothing from the image. It has only been
tested on Xenoblade Chronicles 2, and it can add trembling in motion on other games, so leave
it off unless you want to experiment.
### Requirements
DLSS in all its modes — DLAA, Quality, Balanced and Performance — needs an NVIDIA RTX card,
RTX 20 series or newer. Frame Generation needs an RTX 40 or 50 series. If you do not have an
RTX card, NIS is included in this fork and runs on any GPU, and the green screen fix above
matters to you either way.
The DLSS engine files are not in the package, because they are NVIDIA's proprietary DLLs and I
will not redistribute them. What the package does ship is the `dlss` folder next to the
executable, with NVIDIA's six Streamline files, which are MIT licensed. In that same folder you
add your own, taken from a DLSS game you own: `nvngx_dlss.dll` for upscaling, `nvngx_dlssg.dll`
for Frame Generation, which the NVIDIA driver usually provides already, and `NvLowLatencyVk.dll`
if your driver does not.
### Known issue: Frame Generation still produces ghost images
With Frame Generation enabled, ghost or "temporal" images can still appear — doubled
@@ -59,9 +76,9 @@ motion vectors we reconstruct, and those describe the camera better than they de
individual moving objects. If ghosting bothers you more than the extra smoothness helps,
set Frame Generation to Off in the graphics settings.
### Known issue: Xenoblade Chronicles 2 can hang while loading
### Known issue: loading can hang on some games
On Xenoblade Chronicles 2, loading (or the menu) occasionally stops progressing — the
On some games, loading (or the menu) occasionally stops progressing — the
picture keeps drawing, but the game stops advancing. Closing and reopening the emulator
clears it. We captured this live several times and it is **not solved yet**: the freeze
happens on the game's own side, and it predates the changes in this release. If you hit
@@ -75,12 +92,12 @@ J'envoie cette mise à jour parce que j'ai attrapé un bug que j'avais moi-même
v1.2.3, celui qui rend l'écran vert dans certains jeux. Il pouvait en principe toucher
n'importe quel jeu, alors prenez la mise à jour même si le vôtre semblait correct.
Une correction au passage : les blocs noirs dans Xenoblade Chronicles 2, ceux que j'avais
décrits comme un vieux défaut d'émulation et que je contournais en désactivant la passe de
Une correction au passage : les blocs noirs que certains ont vus dans les scènes avec
flou de mouvement, ceux que j'avais décrits comme un vieux défaut d'émulation et que je contournais en désactivant la passe de
flou de mouvement, venaient du même bug — le mien. Ça n'a jamais été un problème ancien de
l'émulateur, c'était moi. Ils sont partis, et le flou de mouvement reste actif.
### Corrigé : écran vert dans plusieurs jeux (GYLT, Ghostbusters: Spirits Unleashed, et d'autres)
### Corrigé : écran vert dans plusieurs jeux
Certains jeux — plusieurs titres Unreal Engine notamment — affichaient correctement leurs
menus et vidéos, mais montraient un écran vert (ou corrompu) dès que la 3D commençait.
@@ -116,8 +133,27 @@ mouvement deviennent plus visibles. Appliqué au prochain lancement, comme les a
réglages DLSS.
Une case `Advanced motion stack` expérimentale est également présente, **désactivée par
défaut**. Elle n'est validée que sur une famille de moteur et peut au contraire ajouter du
tremblement en mouvement ailleurs — laissez-la éteinte sauf si vous voulez expérimenter.
défaut**. Elle améliore le suivi de la caméra : elle lit la caméra même quand un jeu la range
dans un endroit inhabituel, elle rejette les matrices trompeuses comme le ciel ou les reflets,
et elle ne confond plus le tremblement d'échantillonnage du jeu avec du mouvement réel. Elle ne
retire rien de l'image. Elle n'a été testée que sur Xenoblade Chronicles 2, et elle peut au
contraire ajouter du tremblement en mouvement sur d'autres jeux, alors laissez-la éteinte sauf
si vous voulez expérimenter.
### Prérequis
Le DLSS dans tous ses modes — DLAA, Quality, Balanced et Performance — exige une carte NVIDIA
RTX, série 20 ou plus récente. La Frame Generation exige une RTX série 40 ou 50. Si vous n'avez
pas de carte RTX, le NIS est inclus dans ce fork et fonctionne sur n'importe quel GPU, et de
toute façon le correctif de l'écran vert ci-dessus vous concerne.
Les fichiers du moteur DLSS ne sont pas dans le paquet, parce que ce sont des DLL propriétaires
de NVIDIA et que je ne les redistribue pas. Ce que le paquet contient, c'est le dossier `dlss`
à côté de l'exécutable, avec les six fichiers Streamline de NVIDIA, sous licence MIT. Dans ce
même dossier vous ajoutez les vôtres, pris dans un jeu DLSS que vous possédez :
`nvngx_dlss.dll` pour la mise à l'échelle, `nvngx_dlssg.dll` pour la Frame Generation, que le
pilote NVIDIA fournit généralement déjà, et `NvLowLatencyVk.dll` si votre pilote ne le fournit
pas.
### Limite connue : la Frame Generation produit encore des images fantômes
@@ -129,9 +165,9 @@ ceux-ci décrivent mieux la caméra que les objets qui bougent individuellement.
fantômes vous gênent plus que la fluidité supplémentaire ne vous apporte, mettez la Frame
Generation sur Off dans les paramètres graphiques.
### Limite connue : Xenoblade Chronicles 2 peut se bloquer au chargement
### Limite connue : le chargement peut se bloquer sur certains jeux
Sur Xenoblade Chronicles 2, le chargement (ou le menu) cesse parfois d'avancer — l'image
Sur certains jeux, le chargement (ou le menu) cesse parfois d'avancer — l'image
continue de s'afficher, mais le jeu n'avance plus. Fermer et rouvrir l'émulateur règle le
problème. Nous l'avons capturé en direct à plusieurs reprises et il **n'est pas encore
résolu** : le blocage se produit du côté du jeu lui-même, et il est antérieur aux
@@ -16,7 +16,7 @@ namespace Ryujinx.Common.Configuration
VSyncMode.Switch => VSyncMode.Unbounded,
VSyncMode.Unbounded => VSyncMode.Vrr,
VSyncMode.Vrr => VSyncMode.Switch,
VSyncMode.Custom => VSyncMode.Switch, // legacy mode, no longer in the cycle
VSyncMode.Custom => VSyncMode.Switch,
_ => VSyncMode.Switch
};
}
@@ -39,11 +39,6 @@ namespace Ryujinx.Common.Utilities
public static void SerializeToFile<T>(string filePath, T value, JsonTypeInfo<T> typeInfo)
{
// [1.2.3] Écriture ATOMIQUE : File.Create tronquait le fichier AVANT d'écrire — un
// crash/kill pendant la sérialisation laissait un JSON tronqué (Config.json vidé,
// liste de jeux perdue : 2 occurrences le 18/07 pendant les bancs d'essai, et tout
// user dont l'émulateur meurt pendant une sauvegarde de config y est exposé).
// Le vrai fichier n'est remplacé qu'une fois le nouveau contenu ÉCRIT EN ENTIER.
string tmpPath = filePath + ".tmp";
using (FileStream file = File.Create(tmpPath, DefaultFileWriteBufferSize, FileOptions.WriteThrough))
-128
View File
@@ -1,6 +1,3 @@
// 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 System.Collections.Concurrent;
using System.Collections.Generic;
@@ -8,20 +5,6 @@ using System.Numerics;
namespace Ryujinx.Graphics.GAL
{
/// <summary>
/// Cross-layer hand-off for MV++ camera matrices (Phase 1: camera-reprojection motion
/// vectors). The GPU layer detects the game's camera block in the bound constant buffers
/// (validated structurally: orthonormal view x perspective proj == the view-proj next to
/// them) and publishes the view-projection of the frame it is RENDERING. The value is
/// snapshotted onto each frame at presentation enqueue and republished at dequeue - the
/// same carrier pattern as <see cref="DlssJitterState"/>, because the frame being presented
/// is not the frame being rendered - so the DLSS backend always reads the matrix matching
/// the frame on screen.
///
/// Matrices are stored exactly as the game stores them: row-major rows, column-vector
/// convention (clip = M * world). Matrix4x4.M11..M14 = the matrix's first ROW.
/// Lives in GAL because the GPU layer cannot reference the Vulkan backend.
/// </summary>
public static class DlssCameraState
{
public static bool Enabled;
@@ -30,58 +13,26 @@ namespace Ryujinx.Graphics.GAL
private static Matrix4x4 _currentVp;
private static bool _currentValid;
/// <summary>
/// View-projection of the frame currently being presented, republished at dequeue.
/// 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;
/// <summary>
/// [GAMEJITTER 28/07] Le decalage sous-pixel que LE JEU appliquait a l'image presentee, en
/// NDC (x_ndc += Jx). Republie au dequeue, comme <see cref="PresentVp"/>, donc apparie a la
/// meme image par le meme ordre. En pixels : Jx * largeurRendu / 2.
/// </summary>
public static float PresentJitterX;
public static float PresentJitterY;
// 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, float Jx, float Jy)> _presentRing = new();
public static void PublishPresent(in Matrix4x4 vp, bool valid) => PublishPresent(in vp, valid, 0f, 0f);
/// <summary>
/// Enqueues the camera paired with the frame being enqueued for presentation (GPU thread).
/// [GAMEJITTER] Le decalage sous-pixel voyage dans le MEME anneau, donc il ne peut pas se
/// desynchroniser de la camera ni de l'image. La surcharge sans decalage pousse zero.
/// </summary>
public static void PublishPresent(in Matrix4x4 vp, bool valid, float jx, float jy)
{
_presentRing.Enqueue((vp, valid, jx, jy));
// 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, float Jx, float Jy) entry))
@@ -93,9 +44,6 @@ namespace Ryujinx.Graphics.GAL
}
}
/// <summary>
/// Publishes the view-projection captured for the frame being rendered (GPU thread).
/// </summary>
public static void PublishCurrent(in Matrix4x4 vp)
{
lock (_lock)
@@ -105,9 +53,6 @@ namespace Ryujinx.Graphics.GAL
}
}
/// <summary>
/// Snapshots the current view-projection at frame enqueue (any thread).
/// </summary>
public static bool SnapshotCurrent(out Matrix4x4 vp)
{
lock (_lock)
@@ -118,91 +63,26 @@ namespace Ryujinx.Graphics.GAL
}
}
// Ordered hand-off (RYUJINX_MVPP_VPFIFO=1). The enqueue-time snapshot above races the
// GPU thread (measured on TOTK: 5-20% of frames mis-window their camera -> the brief
// image slide on a hard camera stop). Instead, the GPU thread pushes each DISTINCT
// camera it sees, in order, and the present dequeue - same thread, after the frame's
// fence - consumes the oldest: order does the pairing, timing no longer matters.
// Producer and consumer are both the GPU main thread, so a plain queue suffices.
public static bool FifoEnabled;
/// <summary>
/// Window-depth convention of the guest's main pass (from its viewport transform):
/// true = GL-style, the buffer stores (ndcZ + 1) / 2 and the reprojection shader must
/// undo it. Written by the GPU thread's state updater, read at reprojection time.
/// </summary>
public static bool GuestDepthMinusOneToOne;
/// <summary>
/// [CUTONJUMP 27/07] Incremented by the camera source whenever the guest's camera makes a
/// jump no continuous movement can explain - a warp, a zone load, a cutscene taking over.
/// The upscaler compares it against the value it last saw and, on a change, resets its
/// history for that frame.
///
/// It is a SEQUENCE and not a flag on purpose: a flag can be set and cleared between two
/// reads and vanish, and it also makes "how many jumps happened" unanswerable. A counter
/// only ever moves forward, so the consumer cannot miss one and the count is a diagnostic
/// on its own.
///
/// This carries no world units and no threshold: the decision is taken at the source,
/// where the camera's own recent motion provides the scale. See MvppSoloCamera.
/// </summary>
public static int TeleportSeq;
/// <summary>
/// [DUPSKIP 02/08] Séquence des présentations invitées portant AU MOINS UN dessin (une
/// « vraie » image rendue). Producteur : MvppCameraCapture à la frontière de present
/// (Interlocked, gaté RYUJINX_DLSS_DUPSKIP). Consommateur : DlssUpscaler.TryRun — si la
/// séquence n'a pas bougé depuis son dernier Evaluate, la présentation courante est un
/// DOUBLON (le jeu re-présente sans avoir rendu) : on re-blitte la sortie précédente au
/// lieu de ré-accumuler le temporel dessus (journal (348)-(350) : BOTW ~30 vraies
/// images/s en rotation caméra, présentées ~60 → « images fantômes »). Même patron que
/// TeleportSeq — une séquence, pas un drapeau (et PAS la FIFO : voir l'avertissement
/// du 28/07 ci-dessous). 0 = producteur jamais armé → le consommateur reste inerte
/// (garde-fou anti-gel).
/// </summary>
public static long RenderedFrameSeq;
// ⛔ NE PAS CHANGER LE TYPE D'ELEMENT DE CETTE FILE. Essaye le 28/07 : j'y avais ajoute deux
// flottants pour faire voyager le decalage sous-pixel avec la matrice. Resultat mesure a la
// trace : la capture poussait bien (etape A non nulle) et le present trouvait la file VIDE
// (etape B, valide=False, frais=False), alors que la meme file donnait pushes=30 fresh=30
// holds=0 quelques heures plus tot. `Queue<T>` n'est pas protegee contre les acces
// simultanes -- le commentaire ci-dessus affirme un seul thread, mais les journaux montrent
// OnDrawImpl d'un cote et Present de l'autre. Grossir l'element d'une structure deja limite
// suffit a la faire rendre du vide, et l'appariement camera s'effondre avec.
//
// Le decalage passe donc par l'ANNEAU DE PRESENTATION, qui est une ConcurrentQueue.
private static readonly Queue<Matrix4x4> _fifo = new();
private static Matrix4x4 _fifoLast;
private static bool _fifoHasLast;
// Health stats, logged by the consumer ~1/5s: a healthy 1:1 pairing consumes fresh
// nearly every present with depth <= 2 and never drops. Frequent holds WHILE pushes
// flow, or drops, mean the one-camera-per-frame assumption does not hold in order.
public static int StatPushes;
public static int StatFresh;
public static int StatHolds;
public static int StatDrops;
public static int StatMaxDepth;
// [28/07] NE RIEN AJOUTER ICI SANS Y PENSER A DEUX FOIS. Ce fichier vit dans
// Ryujinx.Graphics.GAL, et la copie de test tourne sur un GAL.dll du 10/07 : y ajouter un
// champ oblige a redeployer cette DLL, donc a injecter d'un coup tous les changements de
// GAL accumules depuis. Fait le 28/07 a 10h07 pour une simple sonde de journal, suivi d'un
// defaut neuf en jeu. Faire transiter les sondes autrement.
/// <summary>[JITTERVAL temporal probe, 10/07] Result of the most recent
/// <see cref="TryConsumeOrdered"/>: true = fresh dequeue, false = hold of the last camera.
/// Log-only (read by the gated JITTERVAL-PRES/INJ lines); nothing in the render path reads it.</summary>
public static bool LastConsumeFresh;
/// <summary>
/// Pushes a newly observed distinct camera view-projection, in draw order (GPU thread).
/// </summary>
// [28/07] La surcharge a decalage est SUPPRIMEE : voir le commentaire de _fifo. Elle reste
// acceptee pour ne pas casser les appelants, mais elle ignore le decalage.
public static void PushOrdered(in Matrix4x4 vp, float jx, float jy) => PushOrdered(in vp);
public static void PushOrdered(in Matrix4x4 vp)
@@ -215,8 +95,6 @@ namespace Ryujinx.Graphics.GAL
StatMaxDepth = _fifo.Count;
}
// A backlog deeper than the game's own pipelining means presents were missed
// (loading hitch): resync by dropping the oldest entries.
while (_fifo.Count > 4)
{
_fifo.Dequeue();
@@ -224,12 +102,6 @@ namespace Ryujinx.Graphics.GAL
}
}
/// <summary>
/// Consumes the camera paired with the frame being presented (GPU thread). With a
/// static camera no new value was written, so the last consumed one still describes
/// this frame.
/// </summary>
// [28/07] Restauree A L'IDENTIQUE de la version qui donnait pushes=30 fresh=30 holds=0.
public static bool TryConsumeOrdered(out Matrix4x4 vp)
{
if (_fifo.TryDequeue(out vp))
@@ -1,79 +1,34 @@
namespace Ryujinx.Graphics.GAL
{
/// <summary>
/// Cross-layer hand-off for DLSS "Mode B" sub-pixel jitter. The DLSS backend generates the Halton
/// offset and publishes the NEXT frame's value in <see cref="OffsetX"/>/<see cref="OffsetY"/>; the GPU
/// layer (StateUpdater) reads it to shift the resolution-scaled viewport of the frame it is rendering.
///
/// The hard part is handing DLSS the offset the PRESENTED frame was actually rendered with. Matching by
/// texture reference failed (the presented framebuffer is a different texture than the jittered 3D
/// target), so instead the offset is carried THROUGH the present frame queue: it is snapshotted onto
/// each frame when it is enqueued for presentation, then written to <see cref="PresentX"/>/<see
/// cref="PresentY"/> when that exact frame is dequeued and presented. The DLSS backend reads those, so
/// the offset always matches the frame on screen regardless of frame-queue depth or frames-in-flight.
///
/// When <see cref="Enabled"/> is false the GPU layer skips all of this, so the path is byte-identical.
/// This lives in GAL because the GPU layer cannot reference the Vulkan backend.
/// </summary>
public static class DlssJitterState
{
public static bool Enabled;
/// <summary>Diagnostic split (jitter dossier 05/07): when false, the GPU layer does NOT shift the
/// viewport (no image jitter) while the declaration path keeps running -- isolates "does the DLL
/// consume our jitterOffset at all". True = normal Mode B.</summary>
public static bool Inject = true;
/// <summary>[JITTER-UI] Also jitter the full-screen scale-1 color passes (UI/compositing) so DLSS
/// stabilizes the UI like the world instead of shaking it (see StateUpdater).</summary>
public static bool JitterUi;
/// <summary>[JITTER-NOSHADOW] Exclude square scaled passes (shadow atlases, light space) from the
/// camera jitter injection (see StateUpdater).</summary>
public static bool NoShadowJitter;
/// <summary>[JITTER-CAMPASS] By-the-book mode: inject ONLY the main camera geometry pass (16:9 +
/// depth bound), amplitude in final DLSS-input pixels. See StateUpdater.</summary>
public static bool CamPassOnly;
/// <summary>The DLSS input (final composited frame) dimensions, published by the upscaler each
/// frame; the CAMPASS amplitude denominator.</summary>
public static int InputWidth;
public static int InputHeight;
/// <summary>[SUBRECT measurement] The most recent CAMERA-pass host viewport seen by the jitter
/// injector (StateUpdater cameraPass branch) -- i.e. the game's CURRENT dynamic-resolution step.
/// Diagnostic only: read by the inject dump gate to classify low/full-res frames. Written on the
/// GPU thread, read at replay -- a 1-frame skew is tolerable for this one-shot measurement (the
/// dynres steps are stable for seconds at a time).</summary>
public static float CamVpW;
public static float CamVpH;
/// <summary>The next frame's jitter offset, in internal-render pixels, published by the DLSS backend
/// and read by the GPU layer to jitter the scaled viewport.</summary>
public static float OffsetX;
public static float OffsetY;
/// <summary>The jitter offset the frame currently being presented was rendered with. Set by the GPU
/// present from the value carried alongside that frame through the queue; read by the DLSS backend.</summary>
public static float PresentX;
public static float PresentY;
/// <summary>[JITTERVAL probe, 07/07] Monotonic frame id, bumped once per DlssJitter.Advance in
/// lockstep with OffsetX/Y. Snapshotted at enqueue and delivered as PresentFrameId at present, so
/// the geometry-side probe (StateUpdater) and the DLSS-side probe (DlssUpscaler) can be joined per
/// frame offline. Diagnostic only; nothing in the render path reads these.</summary>
public static long FrameId;
public static long PresentFrameId;
/// <summary>[ALIGN0 probe] Frame id (= FrameId) at the moment MvppCameraCapture pinned the scene
/// depth. Read at the DLSS eval site to compare with the color/present frame id. Diagnostic only;
/// nothing in the render path reads it.</summary>
public static long SceneDepthFrameId;
/// <summary>[ALIGN0 fix] Frame id (= FrameId) at the moment MvppCameraCapture pinned the scene
/// COLOR, stamped in the SAME StashSceneDepth with the SAME counter as SceneDepthFrameId — so
/// nativeVsDepthSameFrame is a valid same-counter comparison. Diagnostic only.</summary>
public static long SceneColorFrameId;
}
}
-4
View File
@@ -7,10 +7,6 @@ namespace Ryujinx.Graphics.GAL
{
void Barrier();
// [INJECT étape C] Mid-frame DLSS upscale injection hook. The source + destination textures are carried
// through the command (captured at detection); host dimensions are read from the TextureViews at
// execution. Default no-op so only the Vulkan backend implements it; other backends (and the flag-off
// path) are unaffected.
void MvppInjectDlssUpscale(ITexture src, ITexture dst, ITexture depth) { }
void BeginTransformFeedback(PrimitiveTopology topology);
@@ -62,7 +62,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register<BufferDisposeCommand>(CommandType.BufferDispose);
Register<BufferGetDataCommand>(CommandType.BufferGetData);
Register<BufferSetDataCommand>(CommandType.BufferSetData);
Register<BufferSetDataBatchCommand>(CommandType.BufferSetDataBatch); // [BUFBATCH]
Register<BufferSetDataBatchCommand>(CommandType.BufferSetDataBatch);
Register<CounterEventDisposeCommand>(CommandType.CounterEventDispose);
Register<CounterEventFlushCommand>(CommandType.CounterEventFlush);
@@ -1,16 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Graphics.GAL.Multithreading.Model;
using System;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer
{
/// <summary>
/// [BUFBATCH 02/08, journal (368)-(369)] Une entree du lot : ou ecrire (buffer, offset) et
/// combien d'octets consommer dans l'arene du lot (les donnees sont concatenees dans l'ordre).
/// </summary>
struct BufferSetDataBatchEntry
{
public BufferHandle Buffer;
@@ -18,13 +11,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer
public int Size;
}
/// <summary>
/// [BUFBATCH] Lot de SetBufferData consecutifs : K entrees + une arene de donnees concatenees,
/// portees par DEUX SpanRef (l'element de queue ne grossit pas — voir l'avertissement
/// « grossir l'element de la Queue la brise »). Ordre pool FIFO : le producteur insere les
/// ENTREES puis l'ARENE ; le consommateur consomme dans le meme ordre (copie des entrees en
/// pile avant Dispose, car Get/Dispose du pool sont strictement sequentiels).
/// </summary>
struct BufferSetDataBatchCommand : IGALCommand, IGALCommand<BufferSetDataBatchCommand>
{
public readonly CommandType CommandType => CommandType.BufferSetDataBatch;
@@ -41,8 +27,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer
public static void Run(ref BufferSetDataBatchCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
// Copie des entrees AVANT Dispose : le pool est un anneau, Dispose libere la zone
// pour le producteur — on ne garde jamais un Span pool au-dela de son Dispose.
Span<BufferSetDataBatchEntry> entries = command._count <= 64
? stackalloc BufferSetDataBatchEntry[64]
: new BufferSetDataBatchEntry[command._count];
@@ -1,20 +1,9 @@
// 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.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
/// <summary>
/// [INJECT étape C] Threaded-GAL passthrough for the mid-frame DLSS upscale injection. The GPU layer
/// captures the detected pass's SOURCE (scene colour) and DESTINATION (render target) textures at
/// detection time and enqueues this command right before the guest upscale draw. On the render thread it
/// runs IN ORDER at exactly that point, carrying the frame's OWN handles (via TableRef, like
/// SetImageCommand) -- no mutable side-channel, no data from a later frame. Host dimensions are read from
/// the TextureViews at execution (host space). Backends without an override fall back to the empty default.
/// </summary>
struct MvppInjectDlssCommand : IGALCommand, IGALCommand<MvppInjectDlssCommand>
{
public readonly CommandType CommandType => CommandType.MvppInjectDlss;
@@ -162,16 +162,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return _spanPool.Insert(data);
}
// [BUFBATCH 02/08, journal (368)-(369)] Lots de SetBufferData consecutifs
// (RYUJINX_BUFBATCH=1, OFF par defaut = chemin stock). Mesure (362)-(368) : le jeu ecrit
// ses buffers au tick invite (~40-50 000 uploads/s CONSTANTS) ; a ~6 us de machinerie par
// commande, le cout par IMAGE RENDUE explose quand la cadence chute (boucle de
// retroaction, marches 12-20 img/s). Ce gate accumule les SetBufferData CONSECUTIFS dans
// une arene productrice privee et n'emet qu'UNE commande par lot. L'ordre global est
// preserve : TOUTE commande d'un autre type flushe d'abord le lot en attente (crochet
// unique dans New<T>). Le lot ne detient AUCUNE ressource du pool avant son flush.
// Serialisation : memes hypotheses que le producteur stock (New/_producerPtr non
// verrouilles) — quiconque a le droit d'enqueue a le droit d'accumuler.
private static readonly bool _bufBatchEnabled =
Environment.GetEnvironmentVariable("RYUJINX_BUFBATCH") == "1";
@@ -195,7 +185,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
$"[BUFBATCH] ARME (RYUJINX_BUFBATCH=1) - lots de SetBufferData (max {BufBatchMaxEntries} entrees / {BufBatchArenaBytes / 1024} Ko).");
}
// Trop gros pour l'arene : flush du lot puis chemin stock (l'ordre reste correct).
if (data.Length > BufBatchArenaBytes)
{
FlushBufferBatch();
@@ -220,8 +209,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_bufBatchArenaUsed += data.Length;
}
/// <summary>[BUFBATCH] Emet le lot en attente : les ENTREES puis l'ARENE dans le pool
/// (ordre FIFO consomme a l'identique par la commande), une seule commande.</summary>
private unsafe void FlushBufferBatch()
{
if (_bufBatchCount == 0)
@@ -248,9 +235,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
internal unsafe T* New<T>() where T : unmanaged, IGALCommand
{
// [BUFBATCH] point d'ordre unique : toute commande d'un AUTRE type vide d'abord le
// lot de SetBufferData en attente. typeof(T) est resolu par le JIT par
// instanciation ; gate OFF => _bufBatchCount reste 0 => branche morte.
if (_bufBatchEnabled && _bufBatchCount != 0 && typeof(T) != typeof(BufferSetDataBatchCommand))
{
FlushBufferBatch();
@@ -1,45 +1,21 @@
// 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>
/// [COLORCAP snapshot prototype, 07/07] Cross-layer bridge that carries the pinned NATIVE scene
/// COLOR host texture from the GPU layer (MvppCameraCapture) to the Vulkan present, parallel to the
/// depth held path. The Vulkan present makes a GPU copy into a held texture and logs. Gated on
/// RYUJINX_MVPP_COLORCAP_SNAP=1; NOTHING is wired to DLSS and no default behavior changes.
/// </summary>
public static class MvppColorSnapshot
{
public static readonly bool Enabled =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORCAP_SNAP") == "1";
/// <summary>Host texture of this frame's pinned HDR scene color (same framebuffer as the pinned
/// scene depth). Published at present; read by the Vulkan snapshot. Null on gap frames.</summary>
public static ITexture SceneColorHost;
public static long FrameId;
/// <summary>[FLASHCAP gate] Wall-clock ms (Environment.TickCount64) of the last successful mid-frame
/// DLSS inject = we are rendering the real 3D scene. Loading/map/menu screens do NOT run the inject,
/// so this goes stale there -> the present-side flash detector uses it to skip non-gameplay frames.</summary>
public static long LastInjectMs;
/// <summary>[DUMPCOLOR sync] Set true by the mid-frame dump ONLY once it has captured a real, bright
/// gameplay frame (not a black/loading frame). The present-side COLORDUMP waits for this so both
/// dumps come from the same confirmed-gameplay moment. Diagnostic only.</summary>
public static bool DumpArmed;
/// <summary>The pinned scene color UNWRAPPED to its backend texture (ThreadedTexture.Base), so the
/// Vulkan present can cast it to the real TextureView. The GPU layer publishes a ThreadedTexture
/// wrapper; mirrors <see cref="MvppVelState.CoverageBackend"/>. Null-safe.</summary>
public static ITexture SceneColorBackend =>
SceneColorHost is Multithreading.Resources.ThreadedTexture t ? t.Base : SceneColorHost;
/// <summary>[HELDCOLOR] The GPU held COPY of the PRESENT-time (tonemapped) native scene colour,
/// published by the Vulkan present after its copy. Measured 09/07: the present colour is tonemapped
/// (balanced) vs the mid-frame raw (crushed HDR). The mid-frame inject can feed THIS instead of the
/// live raw scene (1-frame stale). Already a backend TextureView. Null until the first present copy.</summary>
public static ITexture SceneColorHeld;
}
}
-9
View File
@@ -1,15 +1,6 @@
// 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 =
@@ -1,17 +1,6 @@
// 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>
/// [INJECT étape C] Feature flag for the "inject DLSS before the game composite" chantier.
/// This holds ONLY the immutable enable flag. The per-frame resources (scene source, destination dims)
/// are NOT stored here: they are captured at the exact moment of detection on the GPU thread and carried
/// THROUGH the GAL command (MvppInjectDlssCommand), so the render thread receives exactly the frame's own
/// handles, in command order, with no cross-thread mutable side-channel and no data from a later frame.
/// Gated on RYUJINX_DLSS_UPSCALE_PASS; default OFF => the feature is inert.
/// </summary>
public static class MvppInjectState
{
public static readonly bool Enabled =
-41
View File
@@ -1,69 +1,28 @@
// 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>
/// MV++ Phase 2.2: bridge between the velocity pass (GPU side, DrawManager graft) and the
/// backend consumers (debug view / DLSS integration) -- the same static-bridge pattern as
/// DlssCameraState. Written on the GPU thread; the render thread only reads the texture
/// reference for the debug overlay (a benign racy read: worst case one frame late).
/// </summary>
public static class MvppVelState
{
/// <summary>Coverage render target (R8, scene-depth-sized), owned by the graft.</summary>
public static ITexture Coverage;
/// <summary>
/// Coverage unwrapped to the backend-level texture: with the threaded GAL the graft
/// stores a ThreadedTexture (internal to this assembly), but the debug view runs on
/// the render thread against backend types. Null until the queued create ran.
/// </summary>
public static ITexture CoverageBackend =>
Coverage is Multithreading.Resources.ThreadedTexture t ? t.Base : Coverage;
public static int Width;
public static int Height;
/// <summary>Set at frame boundary; the first graft of the frame clears the coverage.</summary>
public static bool FrameClearPending;
/// <summary>
/// True when the coverage received at least one graft since the last present -- the
/// debug overlay uses it (RYUJINX_MVPP_VEL_FRESH=1) to SKIP compositing a stale
/// coverage instead of freezing it on screen (the artifact that sabotaged the
/// TMIN and v4 run readings). Display-only; benign racy bool like the rest.
/// </summary>
public static bool CoverageFresh;
/// <summary>
/// MV++ double-transform (b1) sub-step 2: the host program of the MvppVariant of the
/// currently-bound graphics shaders, published by StateUpdater.UpdateShaderState so the
/// graft can SetProgram it for the re-draw. Null when no variant was generated (the
/// graft then logs a silent-fallback and keeps the base). GPU thread only.
/// </summary>
public static IProgram VariantProgram;
/// <summary>Set by the graft when it bound the variant; the next UpdateShaderState's
/// unconditional SetProgram(base) is the restore -- it logs (c) once and clears this.</summary>
public static bool VariantBound;
/// <summary>Per-window count of base-program restores after a variant bind (incremented
/// by StateUpdater, read+reset by the velocity window log as vRestore) -- proves the
/// restore fires per graft WITHOUT the 272k-line spam the one-shot (c) replaced.</summary>
public static int VariantRestores;
/// <summary>
/// MV++ (b1) sub-step 5a: host binding of the N-1 palette cbuf that the injector added to
/// the variant VS (the cloned clip_{N-1} slice reads THIS binding instead of cb5). Resolved
/// by StateUpdater from the variant's actual BufferDescriptors (Slot present in the variant
/// but not the base) -- NEVER guessed (loc-31 lesson). -1 = not resolved -> the graft skips
/// the N-1 bind and the clone falls back to reading the current cb5 (MV = 0). GPU thread.
/// </summary>
public static int VariantN1Binding = -1;
/// <summary>Descriptor set of the N-1 palette cbuf (paired with VariantN1Binding).</summary>
public static int VariantN1Set;
}
}
@@ -8,11 +8,6 @@ namespace Ryujinx.Graphics.GAL
public int Width { get; }
public int Height { get; }
// Active (rendered) sub-rect inside the allocated Width/Height, for the DLSS-SR
// fractional-render path: the target is allocated at an integer (mip-safe) scale but the
// scene renders into this smaller sub-rect. Falls back to Width/Height when constructed
// with 0. Derived metadata -- intentionally NOT part of Equals/GetHashCode so it never
// perturbs texture-cache identity (two infos that differ only here stay equal).
public int ActiveWidth { get; }
public int ActiveHeight { get; }
@@ -203,8 +203,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
_context.Renderer.Pipeline.DispatchCompute(qmd.CtaRasterWidth, qmd.CtaRasterHeight, qmd.CtaRasterDepth);
MvppBlitProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); // read-only (gated)
Threed.MvppDrawStepProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); // read-only (gated)
MvppBlitProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa);
Threed.MvppDrawStepProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa);
_3dEngine.ForceShaderUpdate();
}
@@ -216,7 +216,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
_3dEngine.CreatePendingSyncs();
_3dEngine.FlushUboDirty();
// [TWINXFER] Raw copy-engine tap, BEFORE any branch (buffer-domain blind spot). Self-gated.
Image.MvppTwinXferProbe.OnDma(srcGpuVa, dstGpuVa, xCount, yCount, copy2D, srcLinear, dstLinear);
if (copy2D)
@@ -328,7 +327,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
target.PropagateScale(source);
}
Image.MvppTwinXferProbe.OnCopy("DMA", source, target); // [TWINXFER] read-only, self-gated
Image.MvppTwinXferProbe.OnCopy("DMA", source, target);
source.HostTexture.CopyTo(target.HostTexture, 0, 0);
target.SignalModified();
return;
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,32 +6,18 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine
{
/// <summary>
/// MV++ 2D/compute composite tracer (RYUJINX_MVPP_BLITPROBE=1). READ-ONLY, default off, nothing in
/// the render path branches on it. The 3D DrawEnd census proved the final 3200x1800 R8G8B8A8 buffer
/// and the HUD are NOT produced by 3D draws (only HDR scene buffers are). This traces the OTHER two
/// paths -- the 2D blit engine (TwodClass.PixelsFromMemory) and compute dispatch -- to find which op
/// writes the final framebuffer, and whether the HUD/UI arrives from a SEPARATE source surface that
/// is composited in (=> capturable => GO) or the final image is produced by a single op (=> NO-GO /
/// compute-fused). Source/destination addresses are the SAME address space the 3D probe logged, so a
/// blit whose src == the scene RT (0x65799C4000 last run) is the scene upscale, and any OTHER source
/// feeding the same final destination is the HUD/UI layer.
/// </summary>
static class MvppBlitProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_BLITPROBE") == "1";
private const int MaxSigs = 512; // stop logging new shapes past this (bounds a pathological run)
private const int MaxSigs = 512;
private static readonly HashSet<long> _seenBlitSigs = new();
private static readonly HashSet<long> _seenDispatchSigs = new();
private static int _op;
private static int _dispatchCount;
// Final-framebuffer tracking, by SIZE CLASS (double/triple buffering rotates the address, so we
// key on the largest LDR destination dims and accumulate the distinct source + destination
// addresses that hit that size). distinctSrcs >= 2 => the HUD is composited from a separate source.
private static int _finalW, _finalH;
private static readonly HashSet<ulong> _finalSrcs = new();
private static readonly HashSet<ulong> _finalDsts = new();
@@ -47,7 +30,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
return;
}
// A diagnostic on the GPU thread must never take the process down.
try
{
OnBlitImpl(src, dst, duDx, dvDy);
@@ -70,13 +52,9 @@ namespace Ryujinx.Graphics.Gpu.Engine
int sW = src.Info.Width, sH = src.Info.Height;
int dW = dst.Info.Width, dH = dst.Info.Height;
// duDx/dvDy are 32.32 fixed-point SOURCE pixels per DESTINATION pixel; the upscale factor is
// the reciprocal (dst/src). ~1.0 = copy, <1 in duDx (=> >1 here) = upscale.
float sx = duDx != 0 ? (float)(4294967296.0 / duDx) : 0f;
float sy = dvDy != 0 ? (float)(4294967296.0 / dvDy) : 0f;
// Log each distinct blit SHAPE once: enumerates the whole composite structure compactly
// (scene->output upscale, UI->output composite, bloom chain, ...) without flooding.
long sig = Fnv(sAddr, dAddr, (int)sFmt, (int)dFmt, sW, sH, dW, dH);
if (_seenBlitSigs.Count < MaxSigs && _seenBlitSigs.Add(sig))
{
@@ -93,7 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
return;
}
// A strictly larger LDR destination resets the size class (first real frame settles it).
if ((long)dW * dH > (long)_finalW * _finalH)
{
_finalW = dW;
@@ -129,10 +106,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
{
_dispatchCount++;
// Log each distinct (grid, shader) ONCE, with its full compute I/O (inputs it samples +
// storage images it writes). The pass that writes the largest LDR store image is the final
// composite; its INPUT list answers the decisive question -- a separate LDR (HUD-sized)
// input texture distinct from the HDR scene = the UI is a separable surface (GO).
long sig = ((long)(gridX & 0xFFFF) << 32) ^ ((long)(gridY & 0xFFFF) << 16) ^ (long)(shaderVa & 0xFFFFFFF);
if (_seenDispatchSigs.Count >= MaxSigs || !_seenDispatchSigs.Add(sig))
{
@@ -176,13 +149,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
Logger.Info?.Print(LogClass.Gpu, $"MVPP-CIO s@{sh} IN @0x{ins[i].Addr:X} {ins[i].Fmt} {ins[i].W}x{ins[i].H}");
}
// Decisive flag: does a pass that WRITES a large LDR output also SAMPLE a separate LDR
// input that is not the HDR scene? (= the HUD as its own surface.)
foreach ((ulong oa, Format of, int ow, int oh) in outs)
{
if (!IsLdr(of) || (long)ow * oh < 2_000_000L)
{
continue; // only the ~full-res LDR output(s) matter
continue;
}
int ldrInputs = 0;
@@ -105,14 +105,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [Beast Roofer diag] RYUJINX_UBO_FORCEDIRTY=1 (EXP 11, gated OFF by default): always mark
// the inline-updated UBO range dirty, ignoring the redundancy check's verdict. The XC2
// velocity chain's only per-frame data path is the constant buffers (journal 141: every
// vertex SSBO is static); if the redundancy check ever wrongly reports "unchanged" (e.g. a
// game double-write making guest memory match while the HOST copy is still old), the GPU
// reads one-frame-stale matrices -- garbage velocities, only in motion, per draw. Forcing
// the dirty flag costs a redundant upload but cannot be wrong. Artifact gone with this on
// => the redundancy-check family is the root; see journal (142).
private static readonly bool _uboForceDirty =
Environment.GetEnvironmentVariable("RYUJINX_UBO_FORCEDIRTY") == "1";
@@ -138,8 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (!changed)
{
// Only reached with the experiment on: these are exactly the flushes the
// redundancy check would have skipped. Their count is the witness.
_uboForceDirtyHits++;
long now = Environment.TickCount64;
if (now - _uboForceDirtyLogMs >= 3000)
@@ -138,8 +138,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
MvppUiProbe.OnDraw(_channel, ref _state.State, _context);
MvppCompositeProbe.OnDraw(_channel, ref _state.State);
// [DOFSKIP] Compatibility option, inert unless RYUJINX_DOF_SCATTER_SKIP=1. Matched by
// pipeline shape, so it survives game versions AND a warm shader cache.
if (MvppDofSkip.ShouldSkip(_channel))
{
_drawState.DrawIndexed = false;
@@ -241,9 +239,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_context.Renderer.Pipeline.EndHostConditionalRendering();
}
// [POST 21/07] Capture la cible APRES l'ecriture de ce draw (chemin normal non-instancie).
// La sonde pre-draw (l.133) montre le contenu AVANT ; comparer les deux dit si ce draw
// ECRIT la corruption ou la trouve deja la.
MvppDrawStepProbe.OnDrawPost(_channel);
}
@@ -502,7 +497,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
(Image.Texture texture, Sampler sampler) = _channel.TextureManager.GetGraphicsTextureAndSampler(textureId, samplerId);
MvppFinalDrawProbe.OnDrawTexture(_channel, texture); // read-only: DrawTexture bypasses DrawEnd (gated)
MvppFinalDrawProbe.OnDrawTexture(_channel, texture);
float gSrcX0 = srcX0, gSrcY0 = srcY0, gSrcX1 = srcX1, gSrcY1 = srcY1;
float gDstX0 = dstX0, gDstY0 = dstY0, gDstX1 = dstX1, gDstY1 = dstY1;
@@ -525,7 +520,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (anyScaled)
{
Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated); the bound RT is not resolvable here
Image.MvppSeamProbe.OnScaledCopy(
"DRAWTEX",
texture, null,
gSrcX0, gSrcY0, gSrcX1, gSrcY1,
@@ -635,9 +630,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
int firstInstance,
bool indexed)
{
MvppFinalDrawProbe.OnDraw(_channel, ref _state.State); // universal draw choke point (gated, read-only)
MvppHdrCensusProbe.OnDraw(_channel, ref _state.State, count, indexed); // HDR-1600 producer census (gated, read-only)
MvppInjectGate.OnDraw(_channel, ref _state.State, count, indexed); // DLSS-injection gate validation, STEP A (gated, read-only)
MvppFinalDrawProbe.OnDraw(_channel, ref _state.State);
MvppHdrCensusProbe.OnDraw(_channel, ref _state.State, count, indexed);
MvppInjectGate.OnDraw(_channel, ref _state.State, count, indexed);
if (instanceCount > 1)
{
@@ -668,16 +663,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ForceStateDirty();
}
}
// [INJECT étape C] Generic scene-upscale substitution. Decide detects the pass (pipeline-shape only)
// and captures its real src/dst host textures. When it matches, the mid-frame DLSS hook is queued
// IN ORDER right before the guest draw. The guest draw is ALWAYS queued here; the backend skips it
// in order iff the inject's DLSS evaluate succeeded (C3b) -- otherwise the guest draw runs = fallback.
else
{
if (MvppDlssUpscale.Decide(_channel, ref _state.State, count, indexed, out var injectSrc, out var injectDst, out var injectDepth))
{
// The source + destination + depth handles captured THIS frame travel with the command,
// reaching the render thread in order (no mutable side-channel, no later-frame data).
_context.Renderer.Pipeline.MvppInjectDlssUpscale(injectSrc, injectDst, injectDepth);
}
@@ -690,8 +679,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_context.Renderer.Pipeline.Draw(count, instanceCount, firstVertex, firstInstance);
}
// MV++ velocity pass graft point: right after the real draw, all guest state
// still bound (no-op without RYUJINX_MVPP_VEL=1).
if (MvppVelocityPass.Enabled)
{
MvppVelocityPass.AfterDraw(this, engine, _context, _channel, ref _state.State,
@@ -960,7 +947,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
engine.UpdateState(updateMask);
int probeScX = -1, probeScY = -1, probeScW = -1, probeScH = -1; // [MVBUF] read-only capture of the effective clear scissor
int probeScX = -1, probeScY = -1, probeScW = -1, probeScH = -1;
if (needsCustomScissor)
{
@@ -994,7 +981,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
new(scissorX, scissorY, scissorW, scissorH)
];
probeScX = scissorX; // [MVBUF]
probeScX = scissorX;
probeScY = scissorY;
probeScW = scissorW;
probeScH = scissorH;
@@ -1010,7 +997,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ColorF color = new(clearColor.Red, clearColor.Green, clearColor.Blue, clearColor.Alpha);
// [MVBUF] Read-only (gated): record engine clears hitting the object-MV buffer.
Image.MvppMvBufProbe.OnClear(
_channel.TextureManager.GetColorTarget(index),
index,
@@ -1022,7 +1008,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
probeScW,
probeScH);
// [TWINMAP v5] per-twin clear attribution (self-gated).
Image.MvppTwinMapProbe.OnClear(
_channel.TextureManager.GetColorTarget(index),
componentMask,
@@ -1,58 +1,16 @@
// 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;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// IDENTITY TRACE of the camera actually read each frame. Gate: RYUJINX_MVPP_CAMTRACE=1.
/// LOGGING ONLY - one line per accepted read, nothing is altered, nothing is fed downstream.
///
/// WHY IT EXISTS (27/07, XC2). Alex reports the geometry sliding even when NOTHING moves.
/// Measured on his run: at the screen centre the motion vector is ~0.5 to 1.2 px and FLIPS
/// SIGN frame to frame while the scene is still, and the projective audit shows the camera
/// position returning, in a loop, to five values that repeat EXACTLY to three decimals on all
/// three axes, spread over 0.14 world units. Noise never repeats exactly, so those are not
/// measurement error: they are DISTINCT cameras, and the per-frame read lands on a different
/// one from frame to frame.
///
/// That was already anticipated in MvppSoloCamera's own notes - the game runs its adjustable
/// gameplay camera and renders the minimap from another - and CAMGUARD was written for it.
/// But CAMGUARD tests a TELEPORT budget (150 world units per second, floor 5), sized on the
/// intruders seen so far (150 and 34699 units). A rival camera 0.14 units away passes that
/// test untouched, and a genuine camera moving at the measured median (5.8 units per second =
/// ~0.1 per frame) is the SAME order of magnitude as the gap between the rivals. So distance
/// alone can never separate them, whatever the threshold: the discriminator has to be
/// IDENTITY, not metric.
///
/// WHAT THIS MEASURES. The per-frame read is keyed on the SLOT, deliberately, because the
/// camera's ADDRESS rotates through a small ring buffer. Nothing checks that the address
/// behind the slot still belongs to THAT ring. This trace prints, for every accepted read,
/// the address it came from and the position it yielded.
///
/// HOW TO READ IT. Group the lines by address:
/// - a handful of addresses, each always giving the SAME position, cycling => the slot is
/// shared by several cameras and the fix is to pin the read to the elected ring;
/// - one address giving positions that jitter => the ring holds different FRAMES of one
/// camera and the fix is a freshness rule, not an identity one;
/// - addresses and positions both stable => this whole theory is wrong and the false motion
/// is produced downstream of the camera.
/// The three outcomes ask for three different fixes, which is the point of measuring first.
/// </summary>
static class MvppCamTrace
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAMTRACE") == "1";
// ~10 seconds at 60 fps. Long enough to see a cycle repeat many times, short enough that
// the log stays readable and the run stays fast.
private const int MaxLines = 600;
// Ordinary reads measured at 4.8 max once the three fixes are armed; the intruders they
// removed were 250. Ten sits between the two by two orders of magnitude on either side.
private const float JumpThreshold = 50f;
private static int _lines;
@@ -61,10 +19,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static float _prevX, _prevY, _prevZ;
private static bool _hasPrev;
/// <summary>
/// Called on the success path of the per-frame read, with the address currently behind the
/// elected slot and the position that read produced.
/// </summary>
public static void Note(ulong address, float px, float py, float pz, float jx, float jy, bool deJittered)
{
if (!Enabled)
@@ -72,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// [27/07] The first version stopped after MaxLines and went silent for the rest of the
// session - so Alex's one remaining flick, which happened during a fight nine minutes
// in, left no trace at all. Past the opening window the probe keeps watching but only
// speaks when a read jumps: measured on the run that followed the three fixes, ordinary
// reads peak at 4.8 while the intruders were 250, so a jump this size is an event and
// not a busy log. Cost outside an event: three subtractions.
bool opening = _lines < MaxLines;
if (!opening)
@@ -106,8 +54,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_lines++;
// The step since the previous accepted read is what the reprojection turns into motion
// vectors, so it is printed rather than left to be recomputed by hand afterwards.
float step = 0f;
bool addrChanged = false;
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -12,67 +9,38 @@ using System.Threading;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ Phase 1, camera capture (RYUJINX_MVPP_CAP=1). Once per presented frame, on a draw
/// of the scaled 3D pass, finds the game's camera block in the vertex-stage constant
/// buffers and publishes its view-projection to <see cref="DlssCameraState"/>.
///
/// Detection is structural, not hardcoded: a location qualifies when an orthonormal VIEW,
/// a perspective PROJ and their PRODUCT sit side by side (View@o, Proj@o+0x40,
/// ViewProj@o+0x80, row-major - the layout found on TOTK's cbuf8, and a natural one for
/// any engine). The product check makes false positives implausible, and rejecting
/// orthographic projections filters shadow-cascade cameras. The found location is cached
/// and revalidated every frame for pennies; a full rescan runs on a miss.
///
/// Fenced like MvppProbe: this runs on the GPU thread, where an escaped exception kills
/// the process silently - on any error the capture disables itself and says why.
/// </summary>
static class MvppCameraCapture
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAP") == "1";
// [CAPWHY 02/08] Compteurs de branches du chemin de capture, lecture seule, gate OFF par
// defaut. Question (journal (347)) : sur les images AFFAMEES du village BOTW, ou meurt le
// chemin — OnDrawImpl jamais appelee, cache reussi (?!), relocalisation, ou repli solo ?
// Meme patron que les stats de fenetre : ecrits sur le fil GPU, vides au meme point de
// flush que la ligne "MVPP capture window" (approximation identique a l'existant).
private static readonly bool _capWhy =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPWHY") == "1";
// [DUPSKIP 02/08] Producteur du signal anti-doublons : a chaque present invite portant au
// moins un dessin, incremente GAL.DlssCameraState.RenderedFrameSeq (voir sa doc). Reutilise
// le verrou par image de CAPWHY v2 ; ferme = aucun comportement.
internal static readonly bool Verbose =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VERBOSE") == "1";
private static readonly bool _dupSkip =
Environment.GetEnvironmentVariable("RYUJINX_DLSS_DUPSKIP") == "1";
// [FGFEED 02/08] Meme signal, autre consommateur : Vulkan Window.Present AVALE les
// presentations-doublons pour que Streamline/FG ne voie que les vraies images et
// interpole entre elles (journal (353)). Le producteur est strictement identique a
// DUPSKIP ; seul le consommateur change. Ferme = aucun comportement.
private static readonly bool _fgFeed =
Environment.GetEnvironmentVariable("RYUJINX_DLSS_FGFEED") == "1";
private static int _cwQual; // dessins ayant passe le prefiltre (OnDraw)
private static int _cwNeed; // ... dont l'image n'avait pas encore capture
private static int _cwImpl; // entrees dans OnDrawImpl
private static int _cwCachedOk; // lecture au cache reussie (publie)
private static int _cwHeld; // quarantaine gate (slot tenu ouvert)
private static int _cwRescanOk; // relocalisation reussie (publie)
private static int _cwSoloCall; // repli solo atteint
private static int _cwSoloOk; // repli solo a publie
// "rien" = impl - cachedOk - held - rescanOk - soloOk, derivable, pas de compteur dedie.
private static int _cwQual;
private static int _cwNeed;
private static int _cwImpl;
private static int _cwCachedOk;
private static int _cwHeld;
private static int _cwRescanOk;
private static int _cwSoloCall;
private static int _cwSoloOk;
// [CAPWHY v2 02/08] Discriminant demande par la correction d'Alex (« les FPS ne descendent
// pas ») : par PRESENTATION, l'image avait-elle des dessins DU TOUT (avant prefiltre), et
// en avait-elle apres ? Separe « le jeu n'a pas rendu » (monde 1) de « tout est mort au
// prefiltre » (monde 2). Verrous par image poses sur le fil GPU, releves au present.
private static int _cwRaw; // dessins vus a l'entree de OnDraw (avant prefiltre)
private static int _cwFrameHadRaw; // verrou : cette image a vu >= 1 dessin brut
private static int _cwFrameHadQual; // verrou : cette image a vu >= 1 dessin qualifie
private static int _cwFramesNoDraw; // presentations sans AUCUN dessin brut
private static int _cwFramesPrefiltred; // presentations avec des bruts mais 0 qualifie
private static int _cwFramesOk; // presentations avec >= 1 dessin qualifie
private static int _cwRaw;
private static int _cwFrameHadRaw;
private static int _cwFrameHadQual;
private static int _cwFramesNoDraw;
private static int _cwFramesPrefiltred;
private static int _cwFramesOk;
private static int _capturedThisFrame;
private static int _cachedSlot = -1;
@@ -81,13 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static ulong _captures;
private static ulong _rescans;
// Publisher-side cold-start forensics (J2): correlate WHAT gets published with the
// classifier windows. Per 5 s window: frames enqueued vs frames that actually
// published (a gap means SnapshotCurrent serves an old value), whether the
// once-per-frame WINNER draw had the published SCENE depth bound or an AUX one
// (the capture slot is first-winner-takes-all), and how many DISTINCT VP values
// were published (a still camera should publish ONE; the 05:59 run showed wildly
// different world positions while the user stood still).
private static long _pubWindowMs;
private static int _statEnqueues;
private static int _statPubFrames;
@@ -99,36 +60,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _vpTransitionLogs;
private const int VpTransitionLogCap = 200;
// Run 06:34 lesson: the full-block fingerprint changes EVERY frame (sub-millidecimal
// camera drift) -- the transition cap burned in one second on noise. The datum that
// actually names mechanism B2 is the TELEPORT: a published VP whose bottom row moves
// by more than any pan could in one frame (foreign camera winning the slot). Only
// those get a log line; pans and micro-noise stay silent.
private static float _lastR3X, _lastR3Y, _lastR3Z, _lastR3W;
private static bool _hasR3;
private static int _statVpJumps;
// Publication gate (RYUJINX_MVPP_CAP_GATE=1, default OFF -- daily driver intact).
// The teleport run named mechanism B2: an iterating family of structurally valid
// cameras from a half-res pass (impostor/far-terrain updates) hijacks the once-per-
// frame slot, and the lottery FREEZES on whatever won last when the player stops.
// The gate publishes only the LINEAGE: candidates continuous with the last published
// VP (per-frame thresholds below -- measured on the 06:51 run: pans stay under,
// teleports exceed). A discontinuous candidate is a RIVAL: never published, the slot
// stays OPEN so the lineage can still draw later in the same frame; a rival takes
// over only after persisting ALONE for GateResyncFrames consecutive frames (a real
// camera cut) -- the proven edge+cooldown pattern. Degradation = today's behavior:
// no stable candidate means nothing new is published (principle #1: never worse).
private static readonly bool _gateEnabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAP_GATE") == "1";
// Gate v2 (run 07:08 falsified v1's |dW| <= 50: w = -dir.pos is AMPLIFIED by |pos|
// (~1500-2000 at the village), so a plain pan hits 40-80/frame and the gate self-
// quarantined the REAL camera -- 40 false "cut" resyncs, frozen overlay through
// every pan). v2 tests the rotation-INVARIANT identity instead: camera POSITION
// (pos = -R^T t from the view matrix, already in hand) + view direction. Measured
// margins: pan dir ~0.05/frame vs 0.35 allowed; travel <= 2 units/frame vs 30
// allowed; impostor jumps = hundreds of units, orders of magnitude above.
private const float GateDirEps = 0.35f;
private const float GatePosEps = 30f;
private const int GateResyncFrames = 30;
@@ -144,25 +82,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _statLineageLate;
private static int _statResyncs;
// Movement/alternation proxy (user observation 03/07: green only appears while the
// camera MOVES): cross-draw churn of the cached camera block, counted where the FIFO
// probe already fingerprints it. Still user + still publish + LOW churn = one static
// camera everywhere; HIGH churn while still = several cameras ALTERNATING across
// draws (the once-per-frame winner is then a lottery). Read against the user's noted
// movement phases.
private static int _statBlockChanges;
// RYUJINX_MVPP_PAIRPROBE=1: count DISTINCT camera-block values per enqueue interval,
// across every scaled draw. Precondition check for order-based (FIFO) VP pairing --
// the fix for the enqueue-snapshot race is only safe if the scaled passes write
// exactly ONE camera per frame (a scaled reflection pass carrying its own
// structurally-valid camera would make an ordered queue drift).
private static readonly bool _pairProbe =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_PAIRPROBE") == "1";
// 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;
@@ -170,17 +94,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _probeFrames;
private static long _probeLogMs;
// RYUJINX_MVPP_VPFIFO=1: order-based camera pairing. Each DISTINCT camera value seen on
// the scaled passes is pushed in draw order and consumed one-per-present at dequeue --
// immune to the enqueue-snapshot race the pair probe measured (its precondition, exactly
// one camera per frame while moving, was confirmed on TOTK: histogram all 1s, the
// 0/2 pairs being the measurement window racing, not a second camera).
private static readonly bool _fifoMode =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VPFIFO") == "1";
// [VPSOLO] Last matrix pushed into the ordered FIFO by the solo fallback, so it pushes
// only on change (the triplet producer gates on its block changing; without an
// equivalent here the queue would be fed an identical matrix every single frame).
private static bool _hasSoloPushed;
private static Matrix4x4 _lastSoloPushed;
@@ -207,10 +123,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_cwFrameHadRaw = 1;
}
// [VPSOLO] Aspect reference for the solo fallback, fed on EVERY draw with a bound
// depth -- both regimes, before any filtering. Feeding it only from the native arm
// left a scaled config (res_scale > 1) with no reference at all, which silently
// blocks the election for ever. Guarded: nothing runs unless the fallback is on.
if (MvppSoloCamera.Enabled)
{
Image.Texture aspectDs = channel.TextureManager.RenderTargetDepthStencil;
@@ -221,17 +133,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Two regimes for recognizing the main 3D scene pass (everything BEHIND this
// pre-filter -- square guard, largest-wins vote, structural camera validation --
// is scale-agnostic and unchanged):
// - scaled config (res_scale > 1): the scaled pass IS the main pass. Historic
// path, byte-identical: native-res draws (UI/2D) are skipped here.
// - native config (res_scale == 1, e.g. in-engine 4K render mods): NOTHING is
// ever scaled, so the scale test alone left the capture blind for the whole
// session (proven 19/07: 4K-native sonde, pushes 0 over the full run, MV++
// silent, clouds ghosting raw). Recognize scene draws by a bound NON-SQUARE
// depth-stencil instead. The square skip here also keeps the probe path away
// from shadow-pass draws -- the same invariant the scaled filter provided.
if (channel.TextureManager.RenderTargetScale == 1f)
{
if (GraphicsConfig.ResScale != 1f)
@@ -253,9 +154,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [CTXPROBE 29/07 SOIR] Lecture seule. Recense TOUTES les passes qui arrivent jusqu'ici, pas
// seulement la premiere : c'est ce chiffre qui dit s'il y a un choix a faire, puisque la
// capture ci-dessous prend la premiere venue et se tait ensuite. Voir MvppCtxProbe.
if (MvppCtxProbe.Enabled)
{
MvppCtxProbe.NoteQualifyingDraw(channel);
@@ -275,21 +173,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [SCENEPASS 29/07 SOIR] Le pre-filtre ci-dessus reconnait "une passe 3D", pas "LA passe
// de la scene", et la capture prend la premiere venue. Mesure CTXPROBE du soir : la passe
// 8 bits porte 67 % des intruses, la couleur HDR de scene 1,7 %. On saute donc la
// tentative sur les passes qui ne sont pas la scene -- la capture se fera plus loin dans
// la MEME image. Filet de securite et auto-desarmement : voir MvppScenePass.
if (needCapture && MvppScenePass.Enabled && !MvppScenePass.Allows(channel))
{
needCapture = false;
}
// [NOLDR 29/07 SOIR] L'inverse de SCENEPASS, et c'est la mesure qui a impose l'inversion :
// la passe de scene HDR existe dans 98,9 % des images mais la camera n'y est lisible que
// dans 36,5 % des cas, donc n'autoriser QUE celle-la affame la camera. On ECARTE plutot
// la pire passe -- la 8 bits, 19,6 % d'intruses, quatre intruses sur cinq -- et on garde
// toutes les autres comme occasions de capture. Voir MvppLdrSkip.
if (needCapture && MvppLdrSkip.Active && !MvppLdrSkip.Allows(channel))
{
needCapture = false;
@@ -299,12 +187,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
StashSceneDepth(channel);
// [CAPTIME_FIX 31/07] Mesure du 31/07 : sur 55 % des images le jeu reecrit sa camera
// APRES notre capture, vers le 39e dessin sur ~160. On laisse donc passer les
// dessins tant que le lieu elu porte encore la valeur deja publiee, et la capture
// normale se fait des qu'une valeur NOUVELLE apparait. Une seule capture par image,
// par le chemin normal : SNAPGUARD, la file et les gardes gardent leur hypothese.
// Eteint par defaut ; le doute profite toujours au comportement existant.
if (needCapture && MvppSoloCamera.StaleAtElectedLocation(channel))
{
needCapture = false;
@@ -316,10 +198,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else if (MvppSoloCamera.CapTime && Volatile.Read(ref _capturedThisFrame) == 1)
{
// [CAPTIME 31/07] LA SEULE ligne ajoutee hors de MvppSoloCamera, et elle est
// inevitable : le chemin solo n'est appele qu'au-dessus, sous `needCapture`,
// donc une fois l'image capturee MvppSoloCamera ne serait plus jamais sollicite.
// Lecture pure, eteinte par defaut, aucun effet sur la capture ni sur l'election.
MvppSoloCamera.NoteLateDraw(channel);
}
@@ -336,60 +214,26 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// The depth published by SetRenderTargetDepthStencil is "the last depth bound by ANY
// pass" -- a roulette that lands on shadow maps or bloom-chain buffers depending on
// where the present falls (measured: 487 source flips in ~2 min = the mode 2
// whole-image tremble; DLSS was fed real/zero/garbage MVs in alternation). The scene
// depth is pinned HERE instead: the depth bound while the SCALED main pass draws,
// keeping the largest one per present interval.
private static Image.Texture _sceneDepth;
// Plumbing counters for the depth-pinning path (read+reset by the present probe):
// scaled draws seen, of which how many had a depth-stencil bound.
public static int StatScaledDraws;
public static int StatDepthBound;
// Host dims of the pinned scene depth, kept across frames (dyn-res updates them a
// frame late at worst). The velocity pass uses them to tell scene-pass grafts apart
// from shadow/aux passes whose DS sizes differ (3b re-draw + debug-bridge gate).
public static int SceneDepthHostWidth;
public static int SceneDepthHostHeight;
// (b1) aspect-guard instrumentation: how often and when the guard fires (must hit
// jumps/climbs, never normal scenes -- readable straight off the log).
private static int _guardRejects;
private static long _guardLogMs;
// [native arm, 19/07] one-shot marker that the res_scale==1 recognition path engaged
// this session (reads user logs apart: native-mod configs vs scaled configs).
private static bool _nativeArmLogged;
// RYUJINX_MVPP_DEPTHPROBE=1 (depth-source verdict session, 05/07): in the "garbage"
// frames, is the real D32 scene depth STILL bound during scaled draws (out-voted at the
// size tie) or has it VANISHED? Largest-wins is format-blind AND same-dims-blind, so the
// dumps (one depth per frame) can't answer. This census logs EVERY DS candidate per
// present -- deduped by (format, dims), IN binding order (which breaks the tie), plus how
// many DISTINCT depth-target identities alternated under each (fmt, dims) bucket -- and
// marks the winner. Default OFF; the non-gated path is byte-identical.
private static readonly bool _depthProbe =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHPROBE") == "1";
private static bool _depthProbeFaulted;
// RYUJINX_MVPP_DEPTHFIX (05/07): at EXACTLY equal area, break the depth-capture tie by
// precision (D32 > D24 > D16) instead of bind order. GRAVED DEFAULT-ON. Verdict A: the real
// D32Float scene depth was bound every frame but lost a ~50/50 same-dims tie to a D16Unorm
// 8-level buffer -> DLSS ate a posterized depth ~half the time. Measured 0 D16 winners with
// this on. Pure correctness win (DLSS always gets the true depth, all modes); it did NOT fix
// the far shimmer -- that lives elsewhere (open). RYUJINX_MVPP_DEPTHFIX=0 reverts to the
// strict-> behavior (one-flag rollback). (GAL ScalarSize conflates D24/D32 at 4 bytes, so
// DepthRank below maps the precision ourselves.)
private static readonly bool _depthFix =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHFIX") != "0";
// [COLORCAP probe, 07/07] Prototype: is the scene-pass COLOR attachment capturable EXACTLY like
// the depth? Reads color[0] bound at the SAME instant as a scene depth candidate (same framebuffer,
// same scaled pass), logs dims/format/address vs the depth's, deduped by (fmt,dims). Gated on
// RYUJINX_MVPP_COLORCAP_PROBE=1 (requires RYUJINX_MVPP_CAP=1). Reads only, NOTHING wired to DLSS.
private static readonly bool _colorCapProbe =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORCAP_PROBE") == "1";
private static readonly System.Collections.Generic.HashSet<long> _colorCapSeen = new();
@@ -401,17 +245,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly int[] _censusH = new int[DepthCensusMax];
private static readonly int[] _censusDraws = new int[DepthCensusMax];
private static readonly bool[] _censusSq = new bool[DepthCensusMax];
// Per-bucket same-dims competition: last identity seen (guest base address) + the
// increment-on-change count (the _probeChanges pattern). >1 on the WINNER line = several
// depth targets alternate under one (fmt, dims) -- the hijack a dims-only census misses.
private static readonly ulong[] _censusIdent = new ulong[DepthCensusMax];
private static readonly int[] _censusIdentN = new int[DepthCensusMax];
private static int _censusCount;
private static int _censusOverflow;
// Edge-trigger: a census line prints only when the signature (ordered buckets + whether
// each alternates + winner + overflow) changes. Capped; the 5 s window keeps counting
// flips so a "487 flips" flip-flop stays visible after the cap is spent.
private static ulong _depthSigLast;
private static bool _hasDepthSig;
private static int _depthTransitionLogs;
@@ -420,11 +258,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _statDepthPresents;
private static int _statDepthCensusChanges;
// [COLORCAP snapshot] the pinned HDR scene color (same framebuffer as the pinned scene depth),
// consumed at present by TakeSceneColor. Parallel to _sceneDepth. Set only when the snapshot is on.
private static Image.Texture _sceneColor;
/// <summary>[COLORCAP snapshot] Consumes the pinned scene color since the last present (GPU thread).</summary>
public static Image.Texture TakeSceneColor()
{
Image.Texture c = _sceneColor;
@@ -432,14 +267,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return c;
}
/// <summary>[COLORCAP probe] Monotonic present frame id, exposed for the snapshot log.</summary>
public static long ColorCapFrameId => _colorCapFrameId;
/// <summary>
/// [MV++ UI audit, read-only] GPU address of the pinned HDR scene color, or 0 if none is pinned
/// (requires RYUJINX_MVPP_COLORCAP_SNAP). The gated composite-locator probe compares this against
/// each graphics draw's sampled textures to find the draw that consumes the scene.
/// </summary>
public static ulong SceneColorAddress => _sceneColor != null ? _sceneColor.Range.GetSubRange(0).Address : 0UL;
private static void StashSceneDepth(GpuChannel channel)
@@ -461,12 +290,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ds.Info.Width == ds.Info.Height, ds.Range.GetSubRange(0).Address);
}
// (b1) FIX carres-saut -- aspect guard, PROVEN by the 04/07 log census: no TOTK
// scene is ever SQUARE (all 16:9 -- 2666x1500 / 3200x1800 / 1332x750 / 1600x900);
// square DS = shadow atlas (8192^2 and one 4096^2 observed). The largest-wins
// pick below let the 67 Mpx atlas beat the 4 Mpx scene whenever the cascades
// re-render (every jump / some climb angles) -> polluted scene publication ->
// graft + 8192 coverage -> on-screen squares.
if (ds.Info.Width == ds.Info.Height)
{
_guardRejects++;
@@ -482,9 +305,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// [COLORCAP PROBE] Non-square scene-pass depth candidate: read the color[0] attachment bound
// at this SAME instant (same framebuffer, same scaled pass). Prove it exists, matches the depth
// dims (2666x1500), and is reached BEFORE the game's composite/present. Deduped by (fmt,dims).
if (_colorCapProbe)
{
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
@@ -507,10 +327,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [COLORCAP snapshot] Independent color pin: keep the LARGEST non-square HDR color seen this
// present interval (its OWN largest-wins, decoupled from the depth pin -- the HDR scene color
// is bound on different draws than the depth winner, so tying it to the depth pin missed it).
// Consumed at present by TakeSceneColor. HDR formats only; LDR / half-res lose the size race.
if (MvppColorSnapshot.Enabled)
{
Image.Texture c0 = channel.TextureManager.RenderTargetColor0;
@@ -522,8 +338,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
(long)c0.Info.Width * c0.Info.Height > (long)_sceneColor.Info.Width * _sceneColor.Info.Height))
{
_sceneColor = c0;
// [ALIGN0 fix] stamp the color pin with the SAME counter as the depth pin
// (DlssJitterState.FrameId), so nativeVsDepthSameFrame compares like-for-like.
DlssJitterState.SceneColorFrameId = DlssJitterState.FrameId;
}
}
@@ -531,19 +345,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_sceneDepth == null ||
(long)ds.Info.Width * ds.Info.Height > (long)_sceneDepth.Info.Width * _sceneDepth.Info.Height ||
// [DEPTHFIX] Verdict A: the real D32 is bound every frame but loses a same-dims tie
// to a D16Unorm coarse buffer (first-bound wins the format-blind >). ONLY at exactly
// equal area, prefer the higher depth precision. The `_sceneDepth == null` clause
// above short-circuits, so this never dereferences null; OFF => `false &&` => the
// strict-> behavior byte-for-byte.
(_depthFix &&
(long)ds.Info.Width * ds.Info.Height == (long)_sceneDepth.Info.Width * _sceneDepth.Info.Height &&
DepthRank(ds.Info.FormatInfo.Format) > DepthRank(_sceneDepth.Info.FormatInfo.Format)))
{
_sceneDepth = ds;
// [ALIGN0 probe] stamp the frame this depth was pinned in (= the jitter FrameId), to
// compare against the color/present frame id at the DLSS eval site. Diagnostic only.
DlssJitterState.SceneDepthFrameId = DlssJitterState.FrameId;
GAL.ITexture host = ds.HostTexture;
@@ -555,9 +362,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [DEPTHFIX] Depth precision rank for the same-dims tie-break. No GAL accessor separates
// D24 from D32 (ScalarSize conflates both at 4 bytes), so map it here. Higher = keep; a
// depth-stencil target always ranks > 0, so this only ever orders real depth candidates.
private static int DepthRank(Format fmt) => fmt switch
{
Format.D32Float or Format.D32FloatS8Uint => 32,
@@ -566,21 +370,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_ => 0,
};
/// <summary>
/// Consumes the scene depth seen since the last present dequeue (GPU thread).
/// </summary>
/// <summary>[INJECT étape C4-1, read-only] GPU address of the currently-pinned scene depth (0 if none).
/// Parallel to <see cref="SceneColorAddress"/>; used to identify/log the depth candidate at the injection
/// point and to confirm it belongs to the same framebuffer as the scene colour.</summary>
public static ulong SceneDepthAddress => _sceneDepth != null ? _sceneDepth.Range.GetSubRange(0).Address : 0UL;
/// <summary>[INJECT étape C4-1, read-only] Format of the currently-pinned scene depth.</summary>
public static Ryujinx.Graphics.GAL.Format SceneDepthFormat =>
_sceneDepth != null ? _sceneDepth.Info.FormatInfo.Format : default;
/// <summary>[INJECT étape C4, read-only] Host texture handle of the currently-pinned scene depth (null
/// if none). Carried through the GAL command (like the scene colour) so the mid-frame DLSS gets this
/// frame's own depth, in order. Null-safe.</summary>
public static Ryujinx.Graphics.GAL.ITexture SceneDepthHost => _sceneDepth?.HostTexture;
public static Image.Texture TakeSceneDepth()
@@ -590,9 +384,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_depthProbe && !_depthProbeFaulted)
{
// Called OUTSIDE the OnDraw fence (Window.cs present) -- own guard so an emission
// fault disables the probe instead of killing the present. Reset lives inside the
// gate so the non-gated path stays byte-identical.
try
{
EmitDepthCensus(d);
@@ -610,10 +401,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return d;
}
// Depth-probe census recorder (RYUJINX_MVPP_DEPTHPROBE). Append-on-first-sighting keeps
// binding ORDER (which breaks the largest-wins tie) and dedups by (format, dims); per
// bucket, tracks how many DISTINCT depth-target identities alternated (increment-on-
// change, like _probeChanges). Fixed arrays, zero alloc per draw.
private static void RecordDepthCensus(Format fmt, int w, int h, bool sq, ulong ident)
{
for (int i = 0; i < _censusCount; i++)
@@ -649,9 +436,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_censusIdentN[n] = 1;
}
// Emitted from TakeSceneDepth (once per present, outside the fence -- the caller guards
// it). Edge-triggered per-change line + a 5 s window that counts flips regardless of the
// per-change cap, so a "487 flips" flip-flop stays visible once the cap is spent.
private static void EmitDepthCensus(Image.Texture winner)
{
bool hasWinner = winner != null;
@@ -665,7 +449,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
wH = winner.Info.Height;
}
// Signature over ordered buckets (+ whether each alternates) + the winner + overflow.
ulong sig = 14695981039346656037UL;
for (int i = 0; i < _censusCount; i++)
{
@@ -780,8 +563,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_fifoMode)
{
// Same structural validation as TryCapture before trusting the new value:
// the block could be repurposed under our cached location.
ReadOnlySpan<float> f = MemoryMarshal.Cast<byte, float>(block);
ReadOnlySpan<float> view = f.Slice(0, 16);
ReadOnlySpan<float> proj = f.Slice(16, 16);
@@ -807,21 +588,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Called when a frame is enqueued for presentation: re-arms the once-per-frame capture.
/// </summary>
public static void OnFrameEnqueued()
{
_colorCapFrameId++; // [COLORCAP probe] one id per presented frame, for the capture prototype log
_colorCapFrameId++;
// [HANGWATCH 29/07] Une ecriture d'entier, sur un fil de fond independant qui, lui,
// survivra au gel du fil graphique. Voir MvppHangWatch.
if (MvppHangWatch.Enabled)
{
MvppHangWatch.Ping();
}
// [CTXPROBE 29/07 SOIR] Re-arme le rang de dessin, au meme endroit que la capture elle-meme.
if (MvppCtxProbe.Enabled)
{
MvppCtxProbe.OnFrame();
@@ -831,22 +606,17 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
if (Volatile.Read(ref _capturedThisFrame) == 1)
{
// Lineage published this frame: rivals evaporate.
_rivalStreak = 0;
_hasRival = false;
}
else if (_rivalSeenThisFrame)
{
// Rival-only frame: the persistence clock ticks.
_rivalStreak++;
}
_rivalSeenThisFrame = false;
}
// [SCENEPASS 29/07 SOIR] ICI, avant le re-armement : c'est le seul endroit ou l'on sait
// si l'image a fini par obtenir une camera. Une image restee sans camera alors qu'elle
// avait des passes 3D est une image potentiellement perdue -- le filet la compte.
if (MvppScenePass.Enabled)
{
MvppScenePass.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1);
@@ -857,16 +627,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
MvppLdrSkip.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1);
}
// [CAPTIME_AB 31/07] Meme endroit, meme raison que les deux au-dessus : c'est le seul
// point ou l'on sait si l'image a fini par obtenir une camera. Inerte tant que
// RYUJINX_MVPP_CAPTIME_AB n'est pas renseigne.
MvppSoloCamera.OnPresentBoundary(Volatile.Read(ref _capturedThisFrame) == 1);
MvppSoloCamera.LogWhy();
Volatile.Write(ref _capturedThisFrame, 0);
// [CAPWHY v2] Classement de la presentation qui se termine, puis reset des verrous.
// [DUPSKIP] La meme frontiere nourrit la sequence des vraies images rendues.
if (_capWhy || _dupSkip || _fgFeed)
{
if ((_dupSkip || _fgFeed) && _cwFrameHadRaw == 1)
@@ -894,14 +659,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_cwFrameHadQual = 0;
}
// Publisher window: ticks on EVERY present so a zero-publish phase still logs
// (the in-capture heartbeat is silent exactly when things go wrong).
_statEnqueues++;
long nowPub = Environment.TickCount64;
if (nowPub - _pubWindowMs >= 5000)
{
if (_pubWindowMs != 0 && _enabled)
if (_pubWindowMs != 0 && _enabled && Verbose)
{
Logger.Info?.Print(LogClass.Gpu,
$"MVPP capture window: enq {_statEnqueues}, pub {_statPubFrames} " +
@@ -964,16 +727,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [DUPPAIR 28/07] Sonde du POINT DE PRODUCTION de la camera, cadencee a 1 Hz et sans
// dependance au mode dev (qui coute des images). Mesure du 28/07 cote consommation :
// pendant les pans, 6 a 15 valeurs distinctes par seconde pour 30 images -- et le
// manque est INVERSEMENT correle a la vitesse de la camera. Rien n'est perdu en route
// (drops=0), donc la valeur n'est jamais produite. Reste a savoir si on lit et que la
// valeur ne bouge pas, ou si la lecture echoue -- et alors pour quel motif.
private static readonly bool _dupPair =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUPPAIR") == "1";
// [JITTRACE 28/07] Voir les etapes A / B / C. Journal seul, une ligne par seconde par etape.
internal static readonly bool JitTrace =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_JITTRACE") == "1";
@@ -1045,7 +801,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_gateHeldSlot = false;
// Cached location first: one read + three cheap checks per frame in steady state.
if (_cachedSlot >= 0 && TryCapture(channel, _cachedSlot, _cachedOffset))
{
if (_capWhy)
@@ -1058,9 +813,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_gateHeldSlot)
{
// The cached block validated but the gate quarantined it (rival camera):
// the lineage lives at the SAME location on a LATER draw -- a full rescan
// would only re-find the rival. Keep the slot open, skip the scan.
if (_capWhy)
{
_cwHeld++;
@@ -1069,7 +821,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Full rescan of the vertex-stage cbufs (first frame, or the game moved its block).
uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(0);
for (int slot = 0; mask != 0; slot++, mask >>= 1)
@@ -1102,14 +853,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [VPSOLO fallback, 21/07 -- RYUJINX_MVPP_VPSOLO=1, off by default]
// Reached ONLY when the triplet scan above found nothing, so a game whose camera the
// historic contract recognises never gets here and its behaviour is unchanged by
// construction rather than by testing. Games that store their view-projection ALONE
// (Xenoblade 2: view and proj in separate buffers, column-major, canonical triplet
// count 0 across every measured run) had MV++ silently disarmed for the whole
// session; this gives them a camera. See MvppSoloCamera for how the matrix is
// recognised and how the location is elected.
if (_capWhy && MvppSoloCamera.Enabled)
{
_cwSoloCall++;
@@ -1127,40 +870,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_captures++;
_statPubFrames++;
// Publishing is only half the pipe. In VPFIFO mode the present side pairs the
// camera to the presented frame by CONSUMING an ordered queue, and the only
// producer for that queue is ProbeDistinctValues -- which requires _cachedSlot,
// i.e. a triplet that was found. On a game where the triplet does not exist the
// queue therefore stayed empty for ever: measured 21/07 on XC2, pub 150/151 on
// the GPU side while the consumer logged "pairOk=0" every 5 s for a whole
// 3-minute run, so the reprojection pass never once ran. Push here too, and only
// on a CHANGED value, exactly like the triplet producer does -- a queue fed the
// same matrix every frame would just grow.
if (_fifoMode && (!_hasSoloPushed || soloVp != _lastSoloPushed))
{
_hasSoloPushed = true;
_lastSoloPushed = soloVp;
// [GAMEJITTER 28/07] Le decalage sous-pixel MESURE SUR CETTE LECTURE part avec
// la matrice : c'est la seule facon qu'il reste apparie a la bonne image.
float pushJx = MvppSoloCamera.LastJitterX;
float pushJy = MvppSoloCamera.LastJitterY;
// ⛔ [28/07 17h40] Surcharge a 3 parametres NEUTRALISEE, meme dette que dans
// Window.cs : PushOrdered(vp, jx, jy) a ete ajoutee au GAL a 11:46, le GAL.dll
// de l'installation date de 10:07 et ne la contient pas -> MissingMethodException
// des que le projet Gpu est recompile. La surcharge a 1 parametre existe des
// deux cotes. Zero perte : GAMEJITTER ne transporte rien (mesure du 28/07,
// `brut (0,000;0,000)` cote DLSS). A retablir avec la reconstruction du GAL.
_ = pushJx;
_ = pushJy;
DlssCameraState.PushOrdered(in soloVp);
MvppFamHold.NotePush();
_soloDistinct++;
// [JITTRACE 28/07] ETAPE A. La valeur est mesuree d'un cote et vaut zero de
// l'autre ; deux explications de ma part se sont deja revelees fausses. On
// trace donc a CHAQUE saut au lieu de raisonner. Ici : ce qu'on POUSSE.
if (_jitTrace && Environment.TickCount64 - _jitTraceMs >= 1000)
{
_jitTraceMs = Environment.TickCount64;
@@ -1171,16 +894,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else if (_fifoMode)
{
// [DUPPAIR 28/07] Lecture reussie qui rend la MEME matrice que la derniere
// poussee. C'est la moitie manquante du diagnostic : une image sans nouvelle
// camera vient soit d'ici (on lit, mais la valeur ne bouge pas), soit d'un
// echec de lecture (compte par motif dans MvppSoloCamera). Les deux ne se
// corrigent pas au meme endroit.
_soloSame++;
}
// Same scene/aux attribution as the triplet path: did the draw that carried this
// camera have the pinned scene depth bound?
GAL.ITexture soloDs = channel.TextureManager.RenderTargetDepthStencil?.HostTexture;
if (soloDs != null && soloDs.Width == SceneDepthHostWidth && soloDs.Height == SceneDepthHostHeight)
@@ -1194,13 +910,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Center-pixel MV spikes of +-40 px at 1-3 Hz with a near-still camera can only come
// from a FOREIGN camera entering the reprojection pair: the math pins |mv| ~ 0 when
// the two matrices are near-identical, whatever the depth does. The episodic
// perspective camera in TOTK is the environment-cubemap update -- perspective,
// orthonormal, exact product, structurally indistinguishable -- but its faces are
// SQUARE (m00 == m11), while a real presentation camera carries the screen aspect.
// Rejects are counted to prove the gate fires at the spike frequency.
public static int StatSquareRejects;
private static bool IsSquareProjection(ReadOnlySpan<float> proj)
@@ -1250,7 +959,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return false;
}
// Camera position (rotation-invariant): view = [R|t] row-major, pos = -R^T t.
float camPosX = -(view[0] * view[3] + view[4] * view[7] + view[8] * view[11]);
float camPosY = -(view[1] * view[3] + view[5] * view[7] + view[9] * view[11]);
float camPosZ = -(view[2] * view[3] + view[6] * view[7] + view[10] * view[11]);
@@ -1280,8 +988,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (!sameRival)
{
// A different rival restarts the persistence clock: the impostor
// rafale (a new camera every few frames) can never build a streak.
_hasRival = true;
_rivalStreak = 0;
}
@@ -1295,15 +1001,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_rivalStreak < GateResyncFrames)
{
// Quarantined: not published, and the slot stays OPEN -- the
// lineage may still draw later in this frame.
_gateHeldSlot = true;
return false;
}
// A rival that persisted ALONE for the full window is the camera now
// (real cut): adopt it as the new lineage.
_statResyncs++;
_hasRival = false;
_rivalStreak = 0;
@@ -1314,8 +1016,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else if (_rivalSeenThisFrame)
{
// The impostor drew first, the gate held the slot open, and the real
// lineage arrived later in the same frame -- the exact rescue case.
_statLineageLate++;
}
}
@@ -1331,8 +1031,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_captures++;
_statPubFrames++;
// Who won the once-per-frame slot: a draw of the published scene pass, or an
// auxiliary scaled pass carrying its own structurally-valid camera?
Image.Texture winnerDs = channel.TextureManager.RenderTargetDepthStencil;
GAL.ITexture winnerDsHost = winnerDs?.HostTexture;
bool sceneWinner = winnerDsHost != null &&
@@ -1348,7 +1046,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_statCapAux++;
}
ulong pubFp = 14695981039346656037UL; // FNV-1a over the published VP block
ulong pubFp = 14695981039346656037UL;
foreach (byte b in MemoryMarshal.AsBytes(vp))
{
pubFp = (pubFp ^ b) * 1099511628211UL;
@@ -1361,9 +1059,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_statVpChanges++;
}
// Teleport detector, same metric as the gate (v2: position + direction, both
// rotation-safe): with the gate ON every TELEPORT line must pair with a resync
// line -- an orphan TELEPORT is a gate hole.
bool teleport = _hasR3 &&
(MathF.Abs(camPosX - _lastPosX) > GatePosEps ||
MathF.Abs(camPosY - _lastPosY) > GatePosEps ||
@@ -1413,10 +1108,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return true;
}
/// <summary>
/// World-to-camera view: bottom row [0,0,0,1], upper 3x3 orthonormal (unit rows,
/// mutually orthogonal).
/// </summary>
private static bool IsOrthonormalView(ReadOnlySpan<float> m)
{
if (MathF.Abs(m[12]) > 1e-3f || MathF.Abs(m[13]) > 1e-3f ||
@@ -1451,10 +1142,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return true;
}
/// <summary>
/// Perspective projection: diagonal focal terms, zeros elsewhere, w-carry row
/// [0, 0, +/-1, 0]. Orthographic (shadow cascades) fails the +/-1 w-carry on purpose.
/// </summary>
private static bool IsPerspectiveProj(ReadOnlySpan<float> m)
{
return m[0] > 0.05f && MathF.Abs(m[1]) < 1e-4f && MathF.Abs(m[2]) < 1e-4f && MathF.Abs(m[3]) < 1e-4f &&
@@ -1465,10 +1152,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
MathF.Abs(m[15]) < 1e-3f;
}
/// <summary>
/// The clincher: the third matrix must be the product of the first two (proj x view),
/// element by element, within float tolerance.
/// </summary>
private static bool ProductMatches(ReadOnlySpan<float> p, ReadOnlySpan<float> v, ReadOnlySpan<float> vp)
{
for (int r = 0; r < 4; r++)
@@ -1,6 +1,3 @@
// 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;
@@ -8,16 +5,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Recensement du tuilage GOB de toutes les textures pleine résolution (RYUJINX_COMPIN=1). READ-ONLY.
///
/// La corruption XC2 a une période de 8 lignes = hauteur d'un GOB ⇒ erreur de tuilage block-linear.
/// Plutôt que de traquer la passe exacte (mes filtres étaient tour à tour trop larges puis trop serrés),
/// on recense directement le gobBlocksInY de CHAQUE texture aux dimensions d'un G-buffer (1280x720,
/// 640x360, 512x288), cibles de rendu ET entrées échantillonnées. Une texture dont le gobBlocksInY
/// détonne de ses voisines de MÊME taille est lue/écrite de travers : c'est le bug.
/// Chaque forme distincte (dims + format + gobY + rôle) est logguée une seule fois.
/// </summary>
static class MvppCompInputsProbe
{
private static bool _enabled =
@@ -53,8 +40,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// On logue le gobBlocksInY brut : l'anomalie = deux textures de MÊME taille avec des gobY
// différents. Le stride est ajouté car un pitch block-linear faux est l'autre forme du bug.
string sig = $"{role} {i.Width}x{i.Height} {i.FormatInfo.Format} gobY={i.GobBlocksInY} lin={i.IsLinear} stride={i.Stride}";
if (_seen.Add(sig))
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,25 +6,16 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ composite-locator probe (RYUJINX_MVPP_COMPOSITEPROBE=1). READ-ONLY, default off, nothing in the
/// render path branches on it. Proves or disproves that the final composite is a GRAPHICS draw that
/// samples the captured HDR scene texture. For each draw it compares every bound sampled texture's GPU
/// address to MvppCameraCapture.SceneColorAddress; on a match it logs the 8 colour render targets
/// (address/format/dims/slot), blend/depth/projection/shader, then the next 20 draws to see whether the
/// HUD is drawn afterwards. A 2 s summary counts scene-sampling draws per window (single vs multiple
/// composite). Requires RYUJINX_MVPP_COLORCAP_SNAP so the scene is pinned.
/// </summary>
static class MvppCompositeProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_COMPOSITEPROBE") == "1";
private const int MaxFullDumps = 4; // fully characterize the first few composite draws, then only summarize
private const int MaxFullDumps = 4;
private static int _drawNo;
private static int _fullDumps;
private static int _following; // countdown: log the next N draws after a composite hit (HUD check)
private static int _following;
private static long _lastSummaryMs;
private static int _sceneReadsSinceSummary;
@@ -55,7 +43,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
// Does THIS draw sample the scene texture? (address match against any bound input)
bool samplesScene = false;
if (sceneAddr != 0)
{
@@ -91,7 +78,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"MVPP-COMPOSITE OUT slot{slot} @0x{rt.Range.GetSubRange(0).Address:X} {rt.Info.FormatInfo.Format} {rt.Info.Width}x{rt.Info.Height}");
});
_following = 20; // characterize what is drawn AFTER the composite (HUD?)
_following = 20;
}
}
else if (_following > 0)
@@ -133,8 +120,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Best-effort projection hint from stage-0 cbuf5 matrix0 (same slot MvppP2Probe reads). A fullscreen
// composite quad may have no real projection; the raw ortho/persp/none hint is advisory only.
private static string ClassifyProj(GpuChannel channel)
{
try
@@ -1,86 +1,21 @@
// 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;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// [CTXPROBE 29/07 SOIR] Sonde de CONTEXTE DE BRANCHEMENT (RYUJINX_MVPP_CTXPROBE=1, coupee par
/// defaut). LECTURE SEULE : ne touche ni la matrice rendue, ni l'election, ni un garde-fou.
/// Aucune ecriture disque. Deux appels d'instrumentation, rien d'autre.
///
/// LA QUESTION QU'ELLE POSE. Identifier l'intruse par son CONTENU est ferme depuis le 27/07
/// (commentaire de SNAPGUARD) : elle n'est pas a l'origine, elle a le bon rapport d'image, et
/// c'est une vue-projection structurellement PARFAITE. Rien dans ses chiffres ne la distingue
/// d'une vraie camera. Reste une chose qu'on n'a jamais regardee : non pas a quoi elle
/// ressemble, mais OU et QUAND le jeu la branche.
///
/// LE MECANISME SOUPCONNE. <see cref="MvppCameraCapture.OnDraw"/> capture la camera au PREMIER
/// dessin de l'image qui passe le pre-filtre (profondeur non carree / cible mise a l'echelle),
/// puis se tait jusqu'a l'image suivante (_capturedThisFrame). Ce pre-filtre reconnait "une
/// passe 3D", pas "LA passe de la scene principale". Si le jeu dessine une passe secondaire
/// (reflet, miroir, carte, vue fixe) AVANT la scene, on lit SA camera -- une vraie camera, a un
/// siege fixe du monde, structurellement parfaite. Ce qui collerait a tout le dossier : siege
/// fixe, matrice parfaite, entree par le slot ELU (meme slot, autre passe), salves de plus de
/// 20 images, dependance a l'angle et a l'endroit, et le fait qu'un filtre d'amplitude attenue
/// sans jamais tuer -- on filtre apres coup un choix fait trop tot.
///
/// CE QU'ELLE MESURE. Deux recensements, tous deux a GROS VOLUME (lecon du 29/07 : ne jamais
/// juger sur des evenements rares) :
/// 1. CONTEXTE DE LECTURE -- pour chaque lecture de la camera, la cible couleur et la
/// profondeur branchees a cet instant, plus le rang du dessin dans l'image. Chaque
/// contexte compte ses lectures ET ses intruses.
/// 2. RECENSEMENT DES PASSES QUALIFIANTES -- toutes les passes qui passent le pre-filtre dans
/// l'image, pas seulement la premiere. Ce chiffre decide si un correctif est meme
/// POSSIBLE : s'il n'y a qu'une passe qualifiante par image, il n'y a rien a choisir et
/// l'hypothese meurt ici.
///
/// COMMENT LA LIRE. Si les intruses se concentrent sur un contexte distinct des lectures
/// saines, on tient un discriminant que le contenu ne donnait pas. Si tous les contextes sont
/// identiques, l'hypothese est morte -- et c'est un resultat, pas un echec.
///
/// Minuteur PROPRE, delibere : celui d'ALTPROBE etait enferme dans le bloc d'une autre sonde et
/// n'a jamais ete emis une seule fois de tout le dossier.
/// </summary>
static class MvppCtxProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CTXPROBE") == "1";
/// <summary>
/// [29/07 soir] La lecture d'ESSAI sur la passe HDR, derriere SA PROPRE variable
/// (RYUJINX_MVPP_HDRTRY=1). Pourquoi separee : le run qui l'a introduite a rendu une
/// publication de 60,4 % contre 83,2 % de reference, alors que rien d'autre n'avait change
/// -- une sonde en lecture seule ne devrait pas deplacer ce chiffre. Soit c'est l'endroit
/// (la reference etait mesuree ailleurs), soit CETTE lecture perturbe le chemin de lecture.
/// Un interrupteur separe permet de trancher au meme endroit, sans perdre le recensement.
/// 📌 Dans ce dossier, un ecart spectaculaire est d'abord un bug d'instrument.
/// </summary>
public static readonly bool TrialRead =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HDRTRY") == "1";
/// <summary>
/// Seuil de separation, pris des mesures du 29/07 et non d'un choix : le plus grand pas
/// LEGITIME observe vaut 4,9 u (pas ordinaire 0,2, 99e centile 0,7), la plus petite
/// INTRUSE 415 u. 100 u tombe entre deux ordres de grandeur ou aucune lecture n'a jamais
/// ete mesuree. Ce seuil ne SERT a rien d'autre qu'a etiqueter une ligne de journal.
/// </summary>
private const float IntruderDist = 100f;
/// <summary>
/// Au-dela de cet age, la derniere position acceptee ne peut plus servir de reference : le
/// joueur a eu le temps de se deplacer, donc l'ecart mesure n'est plus un saut de camera.
/// 60 ms ≈ deux images. Mesure du 29/07 au soir : les lectures saines ont un age moyen de
/// 37 ms, les faux positifs 2 443 ms -- les deux populations sont separees par presque deux
/// ordres de grandeur, donc la valeur exacte du seuil n'est pas critique.
/// </summary>
private const long FreshRefMs = 60;
// Releve a 32 le 29/07 au soir : l'adresse de la profondeur est entree dans la cle, donc une
// meme forme peut desormais occuper plusieurs lignes. Le debordement reste journalise.
private const int MaxCtx = 32;
private const int ReportMs = 5000;
@@ -104,11 +39,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly Ctx[] _readCtx = new Ctx[MaxCtx];
private static readonly Ctx[] _qualCtx = new Ctx[MaxCtx];
// [RANG REPARE 29/07 soir] La v1 remettait _rank a zero depuis OnFrame, c'est-a-dire depuis
// le fil de PRESENTATION, pendant que OnDraw l'incrementait depuis le fil GPU -- d'ou des
// "rang 0" impossibles et une colonne inutilisable. Ici OnFrame ne touche plus qu'un JETON,
// et la remise a zero se fait paresseusement sur le fil GPU, seul ecrivain de _rank. Plus de
// course : le seul echange entre fils est un int, indechirable.
private static int _rank;
private static int _frameMark;
private static int _seenMark;
@@ -122,17 +52,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _intruders;
private static int _ctxOverflow;
// [VERDICT PASSE HDR, 29/07 soir] La question qui decide du chantier SCENEPASS. Le
// recensement a montre OU sont les intruses (8,1 % sur la passe 8 bits, 0,08 % sur la
// couleur HDR) ; deplacer la capture n'a de sens que si la camera est LISIBLE au moment de
// la passe HDR. Une lecture d'essai par image, a la premiere passe HDR rencontree.
// [IDENTITE DES INTRUSES, 29/07 soir, 3e passe] La question qui peut FERMER le dossier. Le
// residu vaut 20 intruses par run, et toutes plafonnent a 415,2 u -- or (272) a etabli que
// "415,2 depuis [403,3 -28 436,4]" est la transition de camera au CHARGEMENT : deterministe,
// une fois par lancement, PAS un symptome. Si les 20 sont toutes de cette famille, il ne
// reste rien a corriger. On les recense donc une par une, dedupliquees par distance arrondie
// au dixieme, avec la position d'ou elles viennent : une valeur ET une position identiques
// au chiffre pres d'un lancement a l'autre = evenement deterministe (regle de (272)).
private const int MaxIntr = 16;
private static readonly float[] _intrDist = new float[MaxIntr];
private static readonly int[] _intrCount = new int[MaxIntr];
@@ -142,12 +61,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _intrN;
private static int _intrOverflow;
// [RANG DES INTRUSES, 29/07 soir, 4e passe] Le seul axe jamais regarde, et disponible
// uniquement depuis que la colonne "rang" est reparee. La question : les intruses arrivent-
// elles a un rang systematiquement PLUS TOT que les lectures saines ? Si oui, "ne pas
// capturer dans les N premiers dessins qualifiants" est un correctif generique, simple et
// testable -- et ce serait le meme genre de discriminant que celui qui a tue la passe 8 bits.
// Moyennes + histogramme grossier : on cherche un ecart franc, pas une decimale.
private static long _rankSumClean;
private static int _rankNClean;
private static long _rankSumIntr;
@@ -170,11 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static long _lastReportMs;
/// <summary>
/// Appelee pour CHAQUE dessin qui passe le pre-filtre de <see cref="MvppCameraCapture"/>,
/// que la camera y soit lue ou non. C'est ce recensement qui dit combien de passes
/// qualifiantes existent par image, donc s'il y a un choix a faire.
/// </summary>
public static void NoteQualifyingDraw(GpuChannel channel)
{
int mark = _frameMark;
@@ -187,10 +95,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_rank++;
// [VERDICT PASSE HDR] Une seule lecture d'essai par image, a la PREMIERE passe de scene
// HDR rencontree -- c'est exactement l'instant ou SCENEPASS voudrait capturer. Lecture
// seule et sans effet de bord (voir MvppSoloCamera.TryProbeRead) : rien n'est publie,
// aucun garde n'est traverse, la paire de jitter est restauree.
if (TrialRead && !_probedThisFrame && MvppScenePass.IsSceneColorPass(channel))
{
_probedThisFrame = true;
@@ -217,30 +121,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Record(_qualCtx, fmt, cw, ch, dw, dh, cAddr, dAddr, _rank, 0f, false);
}
/// <summary>
/// Appelee sur la lecture principale de la camera, juste apres qu'elle a reussi
/// structurellement et AVANT que les gardes ne tranchent -- sinon les intruses, que
/// SNAPGUARD refuse, seraient invisibles et la sonde ne verrait que les lectures saines.
/// <paramref name="dist"/> est le pas depuis la derniere position ACCEPTEE, c'est-a-dire
/// exactement la distance que mesure SNAPGUARD : les chiffres des deux se comparent.
/// </summary>
public static void NoteRead(GpuChannel channel, float dist, float px, float py, float pz, long ageMs)
{
_reads++;
// [DEFINITION CORRIGEE, 29/07 soir, 7e passe] MESURE : les "intruses" avaient un age de
// reference de 2 443 ms de moyenne (max 2 875) contre 37 ms pour les lectures saines.
// 🐛 Donc elles n'etaient PAS des cameras rivales : c'etait ma definition qui se
// declenchait sur une reference PERIMEE. Pendant 2,4 s le joueur se deplace, et la
// lecture legitime suivante ressemble forcement a un saut de 400 u. Ca explique tout ce
// qui m'intriguait : distances toujours vers 415 (ce qu'on parcourt en 2,4 s), "familles"
// changeant de run en run, "position fixe" revue 19 fois (l'endroit ou les lectures
// reprennent).
//
// Une intruse n'est donc comptee que si le saut est mesure contre une reference FRAICHE.
// Les sauts a reference perimee sont comptes A PART : ils ne disparaissent pas du
// journal, ils changent de nom -- ils mesurent les TROUS d'acceptation, ce qui est une
// vraie grandeur, mais pas celle qu'on croyait.
bool freshRef = ageMs <= FreshRefMs;
bool bigJump = dist >= IntruderDist;
bool intruder = bigJump && freshRef;
@@ -250,15 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_staleJumps++;
}
// [AGE DE LA REFERENCE, 29/07 soir, 6e passe] LE DOUTE SUR MA PROPRE DEFINITION.
// "Intruse" = pas de 100 u ou plus depuis la DERNIERE POSITION ACCEPTEE. Or quand
// SNAPGUARD refuse un moment, cette reference VIEILLIT pendant que le joueur avance :
// une lecture parfaitement legitime peut alors ressembler a un saut de 400 u. Ce qui
// collerait avec la famille qui DEFILAIT au plan Y du joueur ([115,1 -11,5 142,1] ->
// [117,0 -11,5 143,7]) : peut-etre sa propre camera, lue apres une tenue longue.
// Si les intruses ont un age de reference bien plus eleve que les lectures saines, une
// partie du "residu" est un artefact de MA sonde, pas un defaut -- et il ne faut surtout
// pas coder un correctif contre ca.
if (intruder)
{
_ageSumIntr += ageMs;
@@ -296,10 +171,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_rankHistClean[bucket]++;
}
// [NOLDR A/B] Etiquette la lecture avec la moitie d'experience en cours. C'est le seul
// endroit qui sait si une lecture est une intruse, et MvppLdrSkip le seul qui sache
// quelle moitie tourne : les deux moities se comparent alors sur la MEME scene, ce qui
// supprime la variable qui a fait juger trois correctifs sur du hasard ce matin.
MvppLdrSkip.NoteRead(intruder);
Describe(channel, out int fmt, out int cw, out int ch, out int dw, out int dh,
@@ -316,7 +187,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>Re-arme le rang de dessin, une fois par image presentee.</summary>
public static void OnFrame()
{
if (_rank > 0)
@@ -335,29 +205,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// On ne touche PLUS a _rank ici (fil de presentation) : on avance un jeton, et le fil GPU
// fera la remise a zero lui-meme. C'est la reparation de la colonne "rang".
_frameMark++;
_probedThisFrame = false;
}
/// <summary>
/// Recense une intruse, dedupliquee par sa POSITION arrondie a l'unite.
///
/// 🐛 [Correction 29/07 soir] La v1 dedupliquait par DISTANCE, et c'etait faux : quand le
/// joueur bouge, chaque image donne une distance neuve pour une intruse pourtant immobile
/// (mesure : 412,1 · 411,9 · 411,7 · 411,4 … depuis des positions qui defilent). La table de
/// 16 lignes debordait donc en quelques secondes -- 23 valeurs perdues -- et le compteur
/// "vue N fois" ne voulait rien dire.
///
/// La position est la bonne cle, et elle repond a LA question qui reste : combien du residu
/// est-il STATIONNAIRE ? Un siege immobile pendant que la camera acceptee bouge n'est pas la
/// camera de scene, et ca serait un critere GENERIQUE -- contrairement a un seuil sur une
/// hauteur du monde. Les mesures disent que les deux especes coexistent : la famille a 550 u
/// tient une position quasi fixe ([53,8 1,0 337,6]) alors que celle a 410 u DEFILE avec le
/// joueur, au meme plan Y que lui. Le compte par position dira laquelle domine, donc ce qu'un
/// tel critere pourrait gagner -- AVANT d'ecrire la moindre ligne de correctif.
/// </summary>
private static void NoteIntruder(float dist, float px, float py, float pz)
{
for (int i = 0; i < _intrN; i++)
@@ -400,7 +251,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil;
// Dimensions HOTES : c'est ce que le rendu occupe vraiment, mise a l'echelle comprise.
GAL.ITexture colHost = col0?.HostTexture;
GAL.ITexture dsHost = ds?.HostTexture;
@@ -413,24 +263,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
dAddr = ds != null ? ds.Range.GetSubRange(0).Address : 0UL;
}
/// <summary>
/// Regroupe par (format couleur, dimensions couleur, dimensions profondeur, ADRESSE de la
/// profondeur).
///
/// [29/07 soir, 2e passe] L'adresse de la PROFONDEUR est entree dans la cle, et elle seule.
/// Pourquoi : une fois `NOLDR` arme, le residu d'intruses s'est concentre dans le contexte
/// "aucune couleur attachee" -- 21 intruses sur 5 146 lectures -- devenu le contexte
/// DOMINANT. Or les journaux montrent DEUX cibles de profondeur stables qui reviennent run
/// apres run (…52C0000 et …5680000) : si les intruses se concentrent sur l'une des deux, on
/// tient le meme genre de discriminant que celui qui a tue la passe 8 bits. C'est la seule
/// facon de subdiviser ce contexte, puisqu'il n'a pas de couleur a montrer.
///
/// L'adresse de la COULEUR reste hors de la cle : le jeu fait tourner ses tampons (XC2 :
/// …ED00/EE00/EF00, une seule trajectoire), donc l'y mettre eclaterait un meme contexte en
/// plusieurs lignes. Elle est conservee a titre indicatif (derniere vue). Si la profondeur
/// tourne elle aussi, la table debordera -- et le compteur de debordement le DIRA, il est
/// journalise : une sonde qui tronque en silence se lit comme une sonde qui a tout vu.
/// </summary>
private static void Record(
Ctx[] table,
int fmt,
@@ -492,8 +324,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Table pleine : compte l'oubli plutot que de le taire. Une sonde qui tronque en
// silence se lit comme une sonde qui a tout vu.
_ctxOverflow++;
}
@@ -507,10 +337,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"max {_qualMax} / moyenne {(_frames > 0 ? (float)_qualSum / _frames : 0f):0.##} sur {_frames} images" +
$"{(_ctxOverflow > 0 ? $" · TABLE PLEINE, {_ctxOverflow} contextes non comptes" : "")}");
// LA LIGNE QUI DECIDE. Si "passe HDR" est proche de 100 % des images 3D ET que la camera
// y est lisible presque toujours, alors deplacer la capture est jouable et c'est mon
// garde-fou qui etait mal regle. Si l'un des deux s'effondre, l'approche est morte -- et
// c'est un resultat, pas un echec.
Logger.Info?.Print(LogClass.Gpu,
$"MVPP CTXPROBE VERDICT PASSE HDR : {_frames} images 3D · " +
$"{_hdrFrames} avec une passe HDR ({(_frames > 0 ? 100f * _hdrFrames / _frames : 0f):0.#} %) · " +
@@ -520,18 +346,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"intruses parmi les lisibles : {_hdrIntruders} " +
$"({(_hdrReadable > 0 ? 100f * _hdrIntruders / _hdrReadable : 0f):0.##} %)");
// L'AGE DE LA REFERENCE : le test qui dit si mes "intruses" sont reelles ou si c'est ma
// definition qui les fabrique. Ages comparables => intruses reelles. Age des intruses
// bien plus eleve => une partie du residu est un artefact de la sonde.
Logger.Info?.Print(LogClass.Gpu,
$"MVPP CTXPROBE AGE DE LA REFERENCE · saines : " +
$"{(_ageNClean > 0 ? (float)_ageSumClean / _ageNClean : 0f):0.#} ms de moyenne sur {_ageNClean} · " +
$"intruses : {(_ageNIntr > 0 ? (float)_ageSumIntr / _ageNIntr : 0f):0.#} ms sur {_ageNIntr} " +
$"(max {_ageMaxIntr} ms)");
// LE RANG : intruses contre lectures saines. Un ecart franc ici = un correctif generique
// ("ne pas capturer trop tot dans l'image"). Des profils identiques = axe mort, et c'est
// un resultat aussi.
Logger.Info?.Print(LogClass.Gpu,
$"MVPP CTXPROBE RANG · saines : moyenne " +
$"{(_rankNClean > 0 ? (float)_rankSumClean / _rankNClean : 0f):0.#} sur {_rankNClean} " +
@@ -546,9 +366,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"4-10 {Pct(_rankHistIntr[2], _rankNIntr)} · 11-50 {Pct(_rankHistIntr[3], _rankNIntr)} · " +
$"51+ {Pct(_rankHistIntr[4], _rankNIntr)}]");
// LA LIGNE QUI PEUT FERMER LE DOSSIER : si toutes les intruses tiennent sur une ou deux
// valeurs, depuis une position fixe, ce sont des evenements DETERMINISTES (transition de
// chargement) et non le symptome qu'on chasse.
for (int i = 0; i < _intrN; i++)
{
Logger.Info?.Print(LogClass.Gpu,
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -8,58 +5,23 @@ using System;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// [INJECT étape C] GENERIC detection + capture for replacing a game's scene-upscale pass with DLSS.
/// Deliberately game-agnostic: it keys ONLY on graphics-pipeline characteristics --
/// - exactly one sampled input texture,
/// - that input is an HDR-colour format (source scene colour),
/// - a fullscreen pass (indexed triangle/quad),
/// - depth test off and blend off (the pass replaces, it does not composite),
/// - the render target slot 0 is an HDR-colour format LARGER than the source (a spatial upscale),
/// - coherence with the engine-captured scene colour.
/// NO shader hash, NO GPU address literal, NO fixed resolution, NO game name, NO Zelda-specific branch.
/// It is a SEPARATE class from the read-only <see cref="MvppInjectGate"/> (which stays untouched, still
/// used for validation). Gated by RYUJINX_DLSS_UPSCALE_PASS; OFF => early no-op, daily byte-identical.
///
/// STEP C1: detect the pass, capture the real source/destination host textures and publish them to
/// <see cref="MvppInjectState"/>. The guest draw is NOT skipped and no DLSS runs yet
/// (<see cref="SubstituteActive"/> = false) -- this only proves the generic detection + cross-layer
/// resource capture. C2 adds the mid-frame DLSS into an intermediate; C3 flips SubstituteActive to write
/// the real destination and skip the guest draw.
/// </summary>
static class MvppDlssUpscale
{
// The GPU-thread decision is only "is this the scene-upscale pass, and is its source scene-coherent?"
// (=> queue the mid-frame inject command). Whether the guest draw is actually SKIPPED is decided on the
// render thread, in command order, gated on the real DLSS evaluate result (see PipelineBase / the
// SubstituteActive switch in DlssUpscaler). This keeps the skip tied to evaluate==OK with no side-channel.
private static readonly bool _enabled = MvppInjectState.Enabled;
// RYUJINX_MVPP_INJECT_QUIET=1: silence the per-frame INJECT-C detector log (perf cleanup; render unchanged).
private static readonly bool _quiet = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_QUIET") == "1";
private static long _statusMs;
private static long _statusSig = -1;
private static int _diagCount; // [DIAG C2b vertical-offset] one-shot capture-side detail counter
private static int _diagCount;
// HDR colour formats a scene-upscale pass realistically produces. Role-based, not a game constant:
// an SDR/8-bit or depth format never qualifies. Extend if a future backend exposes more HDR formats.
private static bool IsHdrColor(Format f) =>
f == Format.R11G11B10Float ||
f == Format.R16G16B16A16Float;
/// <summary>
/// Called at the universal draw choke point. Returns true when this draw IS the scene-upscale pass and
/// its source is scene-coherent => the caller queues the mid-frame inject command (and still queues the
/// guest draw). The actual SKIP of the guest draw is decided on the render thread, gated on the real
/// DLSS evaluate result. Also outputs the frame's own source + destination handles to travel with the
/// command. Purely generic pipeline-shape detection; no shader hash / address literal / resolution / game.
/// </summary>
public static bool Decide(GpuChannel channel, ref ThreedClassState state, int count, bool indexed,
out ITexture src, out ITexture dst, out ITexture depth)
{
// The caller passes these straight into the GAL command (captured this frame, carried in order).
// Host dimensions are read from the TextureViews at execution; only the handles travel.
src = null;
dst = null;
depth = null;
@@ -69,14 +31,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return false;
}
// Cheap pipeline-shape gate first (skips the vast majority of draws before any enumeration).
bool fullscreen = indexed && (count == 3 || count == 6); // fullscreen triangle or quad
bool fullscreen = indexed && (count == 3 || count == 6);
if (!fullscreen || state.DepthTestEnable || state.BlendEnable[0])
{
return false;
}
// Destination = render target slot 0.
ulong dstAddr = 0;
Format dstFmt = default;
int dw = 0, dh = 0;
@@ -95,7 +55,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
});
// Source = the single sampled input.
int texCount = 0;
ulong srcAddr = 0;
Format srcFmt = default;
@@ -116,36 +75,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
});
bool detected =
texCount == 1 && // exactly one sampled texture
texCount == 1 &&
haveDst && srcHost != null && dstHost != null &&
IsHdrColor(srcFmt) && IsHdrColor(dstFmt) &&
sw > 0 && sh > 0 && dw > sw && dh > sh; // dst strictly larger than src = spatial upscale (relative dims only)
sw > 0 && sh > 0 && dw > sw && dh > sh;
if (!detected)
{
return false;
}
// Coherence with the engine-captured scene colour. This is the ALLOWED "coherence with already
// captured buffers" criterion -- a comparison against a dynamically-pinned buffer, NOT a hardcoded
// address. It disambiguates the scene upscale from other fullscreen upscales (e.g. bloom), and
// substitution requires it so the wrong pass is never replaced -- any miss falls back to the draw.
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
bool inject = sceneAddr != 0 && srcAddr == sceneAddr;
if (inject)
{
// Hand the caller THIS frame's own source + destination handles; they travel with the command
// in order. Host dimensions are read from the TextureViews on the render thread (host space),
// so guest Info dims never mix with host dims. Detection above stays in guest space (Info),
// self-consistent.
src = srcHost;
dst = dstHost;
depth = MvppCameraCapture.SceneDepthHost; // this frame's pinned scene depth (may be null -> fallback)
depth = MvppCameraCapture.SceneDepthHost;
// [DIAG, read-only] Capture-side picture (GPU thread): the exact moment of the detected draw +
// source/destination in BOTH guest (Info) and host (ITexture) spaces + the guest addresses
// (buffer identity / scene-pin coherence / real dst RT address). Correlate with the replay line.
if (_diagCount < 4)
{
_diagCount++;
@@ -154,11 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"srcGuest={sw}x{sh} srcHost={srcHost.Width}x{srcHost.Height} srcAddr=0x{srcAddr:X} sceneAddr=0x{sceneAddr:X} " +
$"dstGuest={dw}x{dh} dstHost={dstHost.Width}x{dstHost.Height} dstAddr=0x{dstAddr:X} dstFmt={dstFmt}");
// [C4 Étape 1, read-only] Identify + log the scene DEPTH candidate the engine already pins
// (MvppCameraCapture, scaled main pass, non-square, DEPTHFIX precision tie-break). Address /
// format / host dims + timing vs the scene colour (same DlssJitterState.FrameId = aligned).
// Nothing is wired to DLSS yet -- Evaluate still uses the zero depth. depthMatchesSrcHost
// confirms it is at the same host resolution as the DLSS colour input.
Logger.Info?.Print(LogClass.Gpu,
$"MVPP-INJECT-DEPTH candidate: depthAddr=0x{MvppCameraCapture.SceneDepthAddress:X} " +
$"depthFmt={MvppCameraCapture.SceneDepthFormat} " +
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu.Image;
@@ -8,44 +5,8 @@ using System;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// [DOFSKIP] (RYUJINX_MVPP_DOF_SCATTER_SKIP=1, inert unless set). Compatibility option.
///
/// The name MUST keep the RYUJINX_MVPP_ prefix: DlssRestart only carries variables matching
/// RYUJINX_DLSS* / RYUJINX_MVPP_* / RYUJINX_HDR_PQ across a cold restart, so a gate named
/// outside those prefixes is silently dropped the moment a DLSS setting changes in the UI -
/// the same partial-state bug already caught once on SKYDRIFT/SKYGRID/EDGE/BORDERFIX.
///
/// Skips the scatter/bokeh accumulation pass of the Xenoblade engine's motion blur. On
/// Vulkan that pass renders a block artifact this fork has not been able to explain -
/// journal 226..246 eliminated, by measurement, every shader of the chain (all bit-identical
/// to OpenGL on an offline bench), its inputs, its constants, the sin precision, the LOD,
/// the blit path and the synchronisation. The effect it produces exists to hide the console's
/// 30 fps; at emulated framerates it buys nothing, so switching it off is a defensible
/// trade rather than a mutilation. It IS a trade: the blur is gone, not fixed.
///
/// Identified by PIPELINE SHAPE, never by a hardcoded guest address or shader hash:
/// the pass is the only one in the frame whose colour target is a 512x288-equivalent
/// RGBA16F surface. Two consequences matter:
/// - it keeps working across game versions and regions, where an address does not;
/// - it works with the shader cache ENABLED, unlike the translation-time gate, which
/// never matches on a cached shader (the guest address comes back as 0) and therefore
/// required playing with the cache off - unusable in practice.
///
/// Matched by PROPORTION of the render width, so it survives DLSS and ResScale alike.
/// </summary>
static class MvppDofSkip
{
/// <summary>
/// 0 = off (default), 1 = skip the pass, 2 = DRY RUN: detect and report, skip NOTHING.
///
/// Mode 2 exists because the shape rule was validated against a 400-target census taken in
/// GAMEPLAY only - cutscenes were never censused. If a cutscene pass falls inside the same
/// proportions it would be skipped while legitimate, leaving its surface untouched, which
/// reads on screen as coloured blocks. Mode 2 answers "which targets would I have skipped"
/// during a single cutscene without altering a single pixel, so the visual verdict and the
/// measurement come from the SAME run.
/// </summary>
public static readonly int Mode = ParseMode();
public static bool Enabled => Mode != 0;
@@ -60,13 +21,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
};
}
// The pass renders at 512x288 when the game renders at 1280x720 - that is 40% of the
// render width, in 16:9. Matching those PROPORTIONS instead of an absolute pixel size is
// what makes the option survive every scaling path: DLSS multiplies the whole frame
// (quality made 512x288 come through as 768x432, x1.5, which an absolute match missed
// entirely) and ResScale multiplies it again, neither of them reported by
// RenderTargetScale alone.
private const float WidthRatio = 0.4f; // 512 / 1280
private const float WidthRatio = 0.4f;
private const float AspectRatio = 16f / 9f;
private const float Tolerance = 0.02f;
@@ -75,19 +30,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static long _lastLogMs;
private static int _renderWidth;
// Census of the DISTINCT target shapes this rule matches, and of every render-width change.
// Both are silent in mode 1 beyond the first announcement; mode 2 reports them. A second
// distinct shape appearing only during cutscenes IS the false positive we are hunting: the
// rule cannot tell it apart from the bokeh pass, so it skips a legitimate draw and leaves
// its surface untouched.
private const int CensusCap = 16;
private static readonly long[] _shapes = new long[CensusCap];
private static int _shapeCount;
private static readonly object _censusLock = new();
/// <summary>
/// True when this draw is the scatter/bokeh accumulation and should not be issued.
/// </summary>
public static bool ShouldSkip(GpuChannel channel)
{
if (!Enabled || channel == null)
@@ -114,15 +61,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
int rawW = target.Info.Width;
int rawH = target.Info.Height;
// Track the widest colour target seen: that is the frame's render width, whatever
// DLSS and ResScale multiplied it by. Everything else is judged relative to it.
if (rawW > _renderWidth)
{
int previous = _renderWidth;
_renderWidth = rawW;
// The reference only ever grows, so a wider target appearing in a cutscene silently
// moves the 40% goalpost for every later frame. Worth seeing, not just inferring.
if (Mode == 2 && previous != 0)
{
Logger.Warning?.Print(LogClass.Gpu,
@@ -147,9 +90,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_skipped++;
// Report every DISTINCT shape the rule claims, not just the first one. The original
// code announced once and stayed silent forever, so a second matching shape appearing
// later in the session - exactly the cutscene case - was invisible in the log.
ReportShape(rawW, rawH, widthRatio);
if (!_announced)
@@ -170,15 +110,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"[DOFSKIP{(Mode == 2 ? "/DRY" : "")}] draws matched so far: {_skipped}");
}
// Dry run: everything above is measurement, nothing is skipped. The frame renders exactly
// as it would with the option off, so the eye verdict and the census come from one run.
return Mode != 2;
}
/// <summary>
/// Logs a matched target shape the first time it is seen. Deduplicated and capped, so a
/// pass running once per frame costs one line for the whole session.
/// </summary>
private static void ReportShape(int width, int height, float widthRatio)
{
long key = ((long)width << 32) | (uint)height;
@@ -1,6 +1,3 @@
// 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;
@@ -8,70 +5,29 @@ using System.IO;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Capture PAS À PAS d'une seule image (RYUJINX_DRAWSTEP=1). READ-ONLY, off par défaut.
///
/// POURQUOI. RenderDoc refuse de tourner sur cet émulateur (deux tentatives, deux plantages).
/// Or ce qu'il apporte tient en une chose : voir le contenu de la cible APRÈS CHAQUE DRAW, pour
/// repérer l'instant exact où les pixels deviennent faux. Cette sonde fait la même chose en plus
/// rustique : sur UNE image et une seule, elle écrit la cible couleur principale tous les N draws.
/// On obtient une séquence qui montre la scène se construire, et on lit à quel moment le
/// quadrillage ou les traînées apparaissent -- puis on remonte au draw responsable.
///
/// Le dossier XC2 au 21/07 : le défaut est DANS l'image que le jeu produit (établi sur une paire
/// même-image), et aucun état de rastérisation n'est en cause (viewport, scissor, screen scissor,
/// clip, miroir, swizzle : six mesures, zéro anomalie sur ~90 000 draws). Restent les draws
/// eux-mêmes, et personne ne les a jamais regardés un par un.
///
/// COÛT ASSUMÉ : chaque capture est une lecture GPU->CPU synchrone qui vide le pipeline. Une
/// trentaine d'affilée fige le jeu quelques secondes. C'est pour ça que ça ne tourne QUE sur une
/// image, et jamais par défaut.
/// </summary>
static class MvppDrawStepProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP") == "1";
/// <summary>Secondes avant de capturer l'image (RYUJINX_DRAWSTEP_START, défaut 45).</summary>
private static readonly int _startSeconds =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_START"), out int st) && st > 0 ? st : 45;
/// <summary>Un cliché tous les N draws (RYUJINX_DRAWSTEP_EVERY, défaut 50).</summary>
private static readonly int _everyDraws =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_EVERY"), out int ev) && ev > 0 ? ev : 50;
/// <summary>
/// Ne capturer qu'à partir de ce numéro de draw (RYUJINX_DRAWSTEP_FROM, défaut 0 = dès le début).
/// [21/07] La frame artefactée a ~54 draws et le budget s'épuisait au draw 33, ratant la
/// composition finale (34-54) où la corruption naît. Ce gate saute le G-buffer du début pour
/// dépenser tout le budget DENSÉMENT sur la fin de frame.
/// </summary>
private static readonly int _fromDraw =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FROM"), out int fdr) && fdr > 0 ? fdr : 0;
/// <summary>Plafond de clichés PAR image (RYUJINX_DRAWSTEP_MAX, défaut 25).</summary>
private static readonly int _maxSteps =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_MAX"), out int mx) && mx > 0 ? mx : 25;
/// <summary>
/// Nombre d'images instrumentées (RYUJINX_DRAWSTEP_FRAMES, défaut 5), espacées de _gapMs.
/// Une seule image obligerait le joueur à tomber pile sur l'instant où l'artefact est là ;
/// avec plusieurs tentatives espacées, il lui suffit de tourner en continu.
/// </summary>
private static readonly int _frames =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FRAMES"), out int fr) && fr > 0 ? fr : 5;
/// <summary>Pause entre deux images instrumentées (RYUJINX_DRAWSTEP_GAP, défaut 3000 ms).</summary>
private static readonly int _gapMs =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_GAP"), out int gp) && gp > 0 ? gp : 3000;
/// <summary>
/// Déclenchement AU CLAVIER (F10 par défaut, RYUJINX_DRAWSTEP_VKEY pour changer le code).
///
/// La version à minuterie était injouable : elle demandait d'avoir chargé la partie, d'être au
/// bon endroit ET que l'artefact soit visible à la seconde près. Ici c'est l'inverse -- Alex joue
/// normalement, et quand il VOIT le défaut il appuie. Même principe que le F12 de RenderDoc.
/// </summary>
private static readonly int _vkey =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_VKEY"), out int vk) && vk > 0 ? vk : 0x79;
@@ -106,10 +62,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _stepNo;
private static string _dir;
/// <summary>
/// Frontière d'image, appelée depuis Gpu/Window.Present. Ouvre la capture sur l'image suivante,
/// puis la referme définitivement : une seule image est instrumentée par session.
/// </summary>
public static void OnPresent(Image.Texture presented)
{
if (!_enabled || _done)
@@ -137,9 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_capturing)
{
// L'image instrumentée vient de se terminer. On écrit d'abord CE QUE LE JOUEUR VOIT :
// sans ça la séquence s'arrête sur un buffer HDR intermédiaire, qu'Alex ne peut pas
// reconnaître -- c'est ce qui a rendu les 5 premières séquences inexploitables.
if (presented?.HostTexture != null)
{
try
@@ -173,7 +122,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Anti-rebond seulement : la décision appartient entièrement au joueur.
if (now < _nextMs || !TriggerHeld())
{
return;
@@ -196,16 +144,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Dispatch COMPUTE, appelé depuis ComputeClass. Mesuré le 21/07 : au dernier DRAW de l'image la
/// scène est complète et propre, et dans l'image présentée la moitié du décor a disparu sous une
/// zone verte à bords droits. Comme cette sonde voit TOUS les draws et rien d'autre, ce qui reste
/// entre les deux, c'est le compute et les copies -- un étage jamais instrumenté dans ce dossier.
///
/// On capture ici les IMAGES DE SORTIE du dispatch (bindings image en écriture), puisqu'un compute
/// n'écrit pas dans une cible de rendu. Le nom porte le rang du dispatch DANS la séquence de draws,
/// pour qu'on puisse replacer l'événement au bon endroit de la frame.
/// </summary>
public static void OnDispatch(Image.TextureManager tm, int gridX, int gridY, int gridZ, ulong shaderVa)
{
if (!_enabled || !_capturing || _stepNo >= _maxSteps || tm == null)
@@ -217,8 +155,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
int idx = 0;
// Compteurs VISIBLES : sans eux, "aucune capture" ne distingue pas "aucun dispatch" de
// "dispatch sans image en ecriture". C'est la regle du projet, et je l'ai deja violee une fois.
_csDispatches++;
tm.MvppEnumerateComputeBindings((isImage, isStore, tex) =>
@@ -278,9 +214,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// [21/07] TOUS les slots couleur, plus seulement le 0 : la texture PRÉSENTÉE est un
// R8G8B8A8 alors que les derniers draws visaient le HDR R11G11B10 en slot 0. En ne
// regardant que le slot 0 on capturait la mauvaise cible et on croyait la scène propre.
int localDraw = _drawNo;
channel.TextureManager.MvppEnumerateRenderTargets((slot, rt) =>
@@ -300,12 +233,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: {name} ({bytes.Length} octets).");
});
// [DRAWSTEP-INPUTS 21/07] Dump aussi CE QUE CE DRAW ECHANTILLONNE. Trancher hérité vs né :
// si une sortie capturée est striée ALORS QUE ses entrées le sont déjà -> corruption
// HERITEE (remonter à qui a écrit cette entrée). Si les entrées sont propres et la sortie
// striée -> corruption NEE à ce draw (shader / mise en place de la cible). C'est la
// bifurcation que la capture par-slot du 21/07 ne pouvait pas résoudre : elle ne voyait
// que les sorties.
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) =>
{
if (tex?.HostTexture == null || _stepNo >= _maxSteps)
@@ -330,11 +257,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Capture APRÈS l'écriture du draw (appelée en fin de DrawEnd, chemin normal). Même grille que
/// OnDraw mais SANS ré-incrémenter _drawNo (déjà fait au pré). Nom suffixé _POST pour apparier
/// pré et post et dire si CE draw écrit la corruption ou la trouve déjà présente dans la cible.
/// </summary>
public static void OnDrawPost(GpuChannel channel)
{
if (!_enabled || !_capturing || _stepNo >= _maxSteps)
@@ -0,0 +1,216 @@
using Ryujinx.Common.Logging;
using System;
using System.Numerics;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
static class MvppExtProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_EXTPROBE") == "1";
private const float FloorPos = 1f;
private const float FloorRot = 0.01f;
private const float CeilPos = 10f;
private const float CeilRot = 0.5f;
private const int MaxLinesPerSecond = 12;
private static Matrix4x4 _m1, _m2;
private static bool _has1, _has2Consec, _lastWasFresh;
private static int _chain;
private static int _lastTpSeq = int.MinValue;
private static int _tpAge = 99;
private static bool _pending;
private static Matrix4x4 _pred, _held;
private static float _pStepPos, _pStepRot;
private static bool _pTpNear;
private static long _wLogMs;
private static int _wHolds, _wQualifies, _wResolus, _wSousPlancher, _wSurPlafond,
_wSansPaire, _wEnchaines, _wTp, _wInvRatees, _wChaines2;
private static long _lineWindowMs;
private static int _lineCount;
public static void OnPresent(bool fresh, in Matrix4x4 m, int teleportSeq)
{
if (teleportSeq != _lastTpSeq)
{
_lastTpSeq = teleportSeq;
_tpAge = 0;
}
else if (_tpAge < 99)
{
_tpAge++;
}
if (fresh)
{
if (_pending)
{
Resolve(in m, _chain);
_pending = false;
}
_has2Consec = _has1 && _lastWasFresh;
_m2 = _m1;
_m1 = m;
_has1 = true;
_lastWasFresh = true;
_chain = 0;
}
else
{
_chain++;
_lastWasFresh = false;
_wHolds++;
if (_chain == 2)
{
_wChaines2++;
}
if (_chain == 1)
{
Qualify();
}
else
{
_wEnchaines++;
}
}
long now = Environment.TickCount64;
if (now - _wLogMs >= 5000)
{
if (_wLogMs != 0 && _wHolds > 0)
{
Logger.Info?.Print(LogClass.Gpu,
$"MVPP extpred fenetre: holds={_wHolds} qualifies={_wQualifies} " +
$"resolus={_wResolus} sous-plancher={_wSousPlancher} " +
$"sur-plafond={_wSurPlafond} sans-paire={_wSansPaire} " +
$"enchaines={_wEnchaines} tp={_wTp} inv-ratees={_wInvRatees} " +
$"chaines2+={_wChaines2}");
}
_wLogMs = now;
_wHolds = _wQualifies = _wResolus = _wSousPlancher = _wSurPlafond = 0;
_wSansPaire = _wEnchaines = _wTp = _wInvRatees = _wChaines2 = 0;
}
}
private static void Qualify()
{
if (!_has1 || !_has2Consec)
{
_wSansPaire++;
return;
}
if (_tpAge <= 2)
{
_wTp++;
return;
}
float stepRot = RotDelta(in _m1, in _m2);
float stepPos = -1f;
if (MvppFamHold.PosFromVp(in _m1, out Vector3 p1) &&
MvppFamHold.PosFromVp(in _m2, out Vector3 p2))
{
stepPos = (p1 - p2).Length();
}
if ((stepPos < 0f || stepPos <= FloorPos) && stepRot <= FloorRot)
{
_wSousPlancher++;
return;
}
if (stepPos > CeilPos || stepRot > CeilRot)
{
_wSurPlafond++;
return;
}
if (!Matrix4x4.Invert(_m2, out Matrix4x4 inv2))
{
_wInvRatees++;
return;
}
_pred = Matrix4x4.Multiply(Matrix4x4.Multiply(_m1, inv2), _m1);
_held = _m1;
_pStepPos = stepPos;
_pStepRot = stepRot;
_pTpNear = false;
_pending = true;
_wQualifies++;
}
private static void Resolve(in Matrix4x4 real, int chainLen)
{
_wResolus++;
float errErot = RotDelta(in _pred, in real);
float errArot = RotDelta(in _held, in real);
string errEpos = "?", errApos = "?";
if (MvppFamHold.PosFromVp(in real, out Vector3 pr))
{
if (MvppFamHold.PosFromVp(in _pred, out Vector3 pe))
{
errEpos = (pe - pr).Length().ToString("0.###");
}
if (MvppFamHold.PosFromVp(in _held, out Vector3 pa))
{
errApos = (pa - pr).Length().ToString("0.###");
}
}
long now = Environment.TickCount64;
if (now - _lineWindowMs >= 1000)
{
_lineWindowMs = now;
_lineCount = 0;
}
if (++_lineCount <= MaxLinesPerSecond)
{
Logger.Info?.Print(LogClass.Gpu,
$"MVPP extpred: errE-pos={errEpos} errE-rot={errErot:0.####} " +
$"errA-pos={errApos} errA-rot={errArot:0.####} " +
$"step-pos={(_pStepPos >= 0f ? _pStepPos.ToString("0.###") : "?")} " +
$"step-rot={_pStepRot:0.####} chaine={chainLen} tp={(_pTpNear ? "oui" : "non")}");
}
}
private static float RotDelta(in Matrix4x4 a, in Matrix4x4 b)
{
float d = 0f;
d = MathF.Max(d, MathF.Abs(a.M11 - b.M11));
d = MathF.Max(d, MathF.Abs(a.M12 - b.M12));
d = MathF.Max(d, MathF.Abs(a.M13 - b.M13));
d = MathF.Max(d, MathF.Abs(a.M21 - b.M21));
d = MathF.Max(d, MathF.Abs(a.M22 - b.M22));
d = MathF.Max(d, MathF.Abs(a.M23 - b.M23));
d = MathF.Max(d, MathF.Abs(a.M31 - b.M31));
d = MathF.Max(d, MathF.Abs(a.M32 - b.M32));
d = MathF.Max(d, MathF.Abs(a.M33 - b.M33));
return d;
}
}
}
@@ -0,0 +1,312 @@
using Ryujinx.Common.Logging;
using System;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
static class MvppFamHold
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_FAMHOLD") == "1";
private const int MaxBytesPerBuffer = 4096;
private const int SampleStride = 8;
private const int MaxLinesPerSecond = 12;
private static readonly (string Nom, int Stage, int Slot, int Offset, bool T)[] _familles =
{
("s4c3+000", 4, 3, 0x000, false),
("s0c6+000", 0, 6, 0x000, false),
("s0c6+040", 0, 6, 0x040, false),
("s0c4+000T", 0, 4, 0x000, true),
("s0c3+040", 0, 3, 0x040, false),
};
private static readonly object _lock = new();
private static readonly Matrix4x4[] _cur = new Matrix4x4[5];
private static readonly bool[] _curSampled = new bool[5];
private static readonly Matrix4x4[] _prev = new Matrix4x4[5];
private static readonly int[] _prevAge = new int[5];
private static int _drawTick;
private static int _pushed;
private static long _wLogMs;
private static int _wHolds, _wFrais, _wRien, _wSansVerif, _wSupprimees;
private static readonly int[] _wParFamille = new int[5];
private static long _lineWindowMs;
private static int _lineCount;
public static void NotePush()
{
System.Threading.Interlocked.Exchange(ref _pushed, 1);
}
public static void Sample(GpuChannel channel)
{
if (_drawTick++ % SampleStride != 0)
{
return;
}
for (int i = 0; i < _familles.Length; i++)
{
if (TryReadFamille(channel, i, out Matrix4x4 m))
{
lock (_lock)
{
_cur[i] = m;
_curSampled[i] = true;
}
}
}
}
public static void OnPresent(bool fresh, in Matrix4x4 publiee)
{
bool pushed = System.Threading.Interlocked.Exchange(ref _pushed, 0) == 1;
lock (_lock)
{
if (!fresh && !pushed)
{
Evaluate(in publiee);
}
for (int i = 0; i < _familles.Length; i++)
{
if (_curSampled[i])
{
_prev[i] = _cur[i];
_prevAge[i] = 1;
_curSampled[i] = false;
}
else if (_prevAge[i] > 0)
{
_prevAge[i]++;
}
}
long now = Environment.TickCount64;
if (now - _wLogMs >= 5000)
{
if (_wLogMs != 0 && (_wHolds > 0 || _wSupprimees > 0))
{
Logger.Info?.Print(LogClass.Gpu,
$"MVPP famhold fenetre: holds={_wHolds} frais-ailleurs={_wFrais} " +
$"rien={_wRien} sans-verif={_wSansVerif} supprimees={_wSupprimees} | " +
$"{_familles[0].Nom}={_wParFamille[0]} {_familles[1].Nom}={_wParFamille[1]} " +
$"{_familles[2].Nom}={_wParFamille[2]} {_familles[3].Nom}={_wParFamille[3]} " +
$"{_familles[4].Nom}={_wParFamille[4]}");
}
_wLogMs = now;
_wHolds = _wFrais = _wRien = _wSansVerif = _wSupprimees = 0;
Array.Clear(_wParFamille, 0, _wParFamille.Length);
}
}
}
private static void Evaluate(in Matrix4x4 publiee)
{
_wHolds++;
int verifies = 0;
int fraisIdx = -1;
for (int i = 0; i < _familles.Length; i++)
{
if (!_curSampled[i] || _prevAge[i] != 1)
{
continue;
}
verifies++;
if (!_cur[i].Equals(_prev[i]) && LooksLikeVp(in _cur[i]))
{
_wParFamille[i]++;
if (fraisIdx < 0)
{
fraisIdx = i;
}
}
}
string ligne;
if (verifies == 0)
{
_wSansVerif++;
ligne = $"MVPP famhold: HOLD verifies=0/{_familles.Length} (aucune famille lisible cette image)";
}
else if (fraisIdx >= 0)
{
_wFrais++;
string dist = "?";
if (PosFromVp(in _cur[fraisIdx], out Vector3 pf) && PosFromVp(in publiee, out Vector3 pp))
{
dist = (pf - pp).Length().ToString("0.##");
}
ligne = $"MVPP famhold: HOLD frais=oui famille={_familles[fraisIdx].Nom} " +
$"dist={dist} verifies={verifies}/{_familles.Length}";
}
else
{
_wRien++;
ligne = $"MVPP famhold: HOLD frais=non verifies={verifies}/{_familles.Length}";
}
long now = Environment.TickCount64;
if (now - _lineWindowMs >= 1000)
{
_lineWindowMs = now;
_lineCount = 0;
}
if (++_lineCount <= MaxLinesPerSecond)
{
Logger.Info?.Print(LogClass.Gpu, ligne);
}
else
{
_wSupprimees++;
}
}
private static bool TryReadFamille(GpuChannel channel, int i, out Matrix4x4 m)
{
m = default;
(_, int stage, int slot, int offset, bool t) = _familles[i];
ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot);
int size = Math.Min(channel.BufferManager.GetGraphicsUniformBufferSize(stage, slot), MaxBytesPerBuffer);
if (address == 0 || address == ulong.MaxValue || size < offset + 64)
{
return false;
}
ReadOnlySpan<float> data;
try
{
data = MemoryMarshal.Cast<byte, float>(channel.MemoryManager.Physical.GetSpan(address, size));
}
catch
{
return false;
}
int idx = offset / 4;
if (idx + 16 > data.Length)
{
return false;
}
ReadOnlySpan<float> r = data.Slice(idx, 16);
for (int k = 0; k < 16; k++)
{
if (!float.IsFinite(r[k]))
{
return false;
}
}
m = t
? new Matrix4x4(r[0], r[4], r[8], r[12],
r[1], r[5], r[9], r[13],
r[2], r[6], r[10], r[14],
r[3], r[7], r[11], r[15])
: new Matrix4x4(r[0], r[1], r[2], r[3],
r[4], r[5], r[6], r[7],
r[8], r[9], r[10], r[11],
r[12], r[13], r[14], r[15]);
return true;
}
private static bool LooksLikeVp(in Matrix4x4 m)
{
Vector3 r0 = new(m.M11, m.M12, m.M13);
Vector3 r1 = new(m.M21, m.M22, m.M23);
Vector3 r2 = new(m.M31, m.M32, m.M33);
Vector3 r3 = new(m.M41, m.M42, m.M43);
float n3 = r3.Length();
if (n3 < 0.9f || n3 > 1.1f)
{
return false;
}
float n0 = r0.Length(), n1 = r1.Length(), n2 = r2.Length();
if (n0 < 1e-6f || n1 < 1e-6f || n2 < 1e-6f)
{
return false;
}
if (MathF.Abs(Vector3.Dot(r0, r1)) / (n0 * n1) > 0.05f ||
MathF.Abs(Vector3.Dot(r0, r3)) / n0 > 0.05f ||
MathF.Abs(Vector3.Dot(r1, r3)) / n1 > 0.05f)
{
return false;
}
if (Vector3.Cross(r2, r3).Length() / n2 > 0.05f)
{
return false;
}
return true;
}
internal static bool PosFromVp(in Matrix4x4 m, out Vector3 p)
{
p = default;
float a0 = m.M11, b0 = m.M12, c0 = m.M13, d0 = m.M14;
float a1 = m.M21, b1 = m.M22, c1 = m.M23, d1 = m.M24;
float a3 = m.M41, b3 = m.M42, c3 = m.M43, d3 = m.M44;
float det = a0 * (b1 * c3 - c1 * b3) - b0 * (a1 * c3 - c1 * a3) + c0 * (a1 * b3 - b1 * a3);
if (MathF.Abs(det) < 1e-12f)
{
return false;
}
float dx = -d0 * (b1 * c3 - c1 * b3) - b0 * (-d1 * c3 + c1 * d3) + c0 * (-d1 * b3 + b1 * d3);
float dy = a0 * (-d1 * c3 + c1 * d3) + d0 * (a1 * c3 - c1 * a3) + c0 * (-a1 * d3 + d1 * a3);
float dz = a0 * (-b1 * d3 + d1 * b3) - b0 * (-a1 * d3 + d1 * a3) - d0 * (a1 * b3 - b1 * a3);
p = new Vector3(dx / det, dy / det, dz / det);
float scale = 1f + p.Length();
float rw = MathF.Abs(a3 * p.X + b3 * p.Y + c3 * p.Z + d3);
float rx = MathF.Abs(a0 * p.X + b0 * p.Y + c0 * p.Z + d0);
if (rw > 1e-2f * scale || rx > 1e-1f * scale || !float.IsFinite(p.X + p.Y + p.Z))
{
p = default;
return false;
}
return true;
}
}
}
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,28 +6,17 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ composite-chain tracer (RYUJINX_MVPP_FINALPROBE=1). READ-ONLY, default off. Anchored on the
/// destination: the presented framebuffer address comes from MvppDestProbe. Hooked from DrawManager's
/// universal DrawImpl choke point (catches every draw path, unlike DrawEnd alone). For any draw whose
/// colour RT is a TRACED buffer -- the presented final, or an intermediate we are climbing towards --
/// it logs the draw's shader, RT, depth and ALL sampled inputs, and AUTOMATICALLY adds every HDR input
/// to the trace set so the producer of that intermediate is caught next. This walks the chain
/// final <- composite <- (HDR intermediate) <- ... back to the native scene, naming the DLSS injection
/// point (the pass that first reads the native scene + its depth).
/// </summary>
static class MvppFinalDrawProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_FINALPROBE") == "1";
private const int MaxPerRt = 40; // log many draws into each traced RT (the scene reader may be late)
private const int MaxTargets = 24; // bound the climb
private const int MaxPerRt = 40;
private const int MaxTargets = 24;
private static readonly HashSet<ulong> _targets = new(); // intermediate RT addresses being climbed
private static readonly HashSet<ulong> _targets = new();
private static readonly Dictionary<ulong, int> _loggedPerRt = new();
// Kept: DrawTexture never fired for TotK, but the hook is harmless and covers other titles.
private static int _texCount;
private const int MaxTexLogged = 40;
@@ -96,7 +82,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state)
{
// Is this draw writing a traced buffer? (the presented final, or an intermediate we are climbing.)
ulong rtAddr = 0;
int rtSlot = -1;
Format rtFmt = default;
@@ -130,7 +115,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Per-RT budget so each stage of the chain logs a few times, no flood.
_loggedPerRt.TryGetValue(rtAddr, out int n);
if (n >= MaxPerRt)
{
@@ -163,8 +147,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Logger.Info?.Print(LogClass.Gpu,
$"MVPP-CHAIN IN @0x{a:X} {f} {w}x{h}{(hdr ? " [HDR]" : "")}{(isScene ? " [=SCENE-PIN]" : "")}");
// Climb: trace the producer of every HDR input we have not seen yet -- both as a DrawImpl RT
// target (3D producers) AND via the destination-anchored probe (catches compute/DMA/2D too).
if (hdr && !isScene)
{
if (_targets.Count < MaxTargets)
@@ -175,8 +157,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
});
// Storage images (imageLoad/Store) -- the blind spot: a fullscreen upscale may read its source
// this way, not as a sampled texture.
channel.TextureManager.MvppEnumerateGraphicsImages((tex, isStore) =>
{
ulong a = tex.Range.GetSubRange(0).Address;
@@ -1,5 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
using Ryujinx.Common.Logging;
using System;
@@ -7,28 +5,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Light-glow clipping probe (RYUJINX_GLOW_PROBE=1, OFF by default, read-only).
///
/// WHERE THIS COMES FROM. Ten suspects were eliminated by measurement on the Xenoblade 2
/// artefact (shader cache, DLSS/MV++/FG, runtime mipmaps, recycled memory, history resets, the
/// fork's gobBlocksInZ clamp, sampling a bound target, the 1080p mod, dynamic resolution, and
/// the scene buffer itself which dumped perfectly clean). Capturing the actual render targets
/// then found it: one 1280x720 R11G11B10Float buffer holds the game's LIGHT HALOS, and every
/// halo sits inside a HARD-EDGED RECTANGLE -- the glow is sliced off at the box border instead
/// of fading out. Detected on six independent captures: ~450 columns and ~300 rows of abrupt
/// edges, where a normal scene image gives 0 to 3.
///
/// That matches every observation: the boxes follow the lights so they move when the camera
/// turns; more lights indoors means more boxes; it is absent when no light is in frame; and it
/// survived every switch because it is the GAME's own rendering, upstream of all of it.
///
/// WHAT THIS PROBE ANSWERS. Are those rectangles the SCISSOR the game sets for each glow draw?
/// If the logged scissor boxes match the rectangles seen in the dumps, the mechanism is
/// "content drawn larger than the box it is clipped to", and we know exactly what to look at
/// next. If the scissors are full-screen, the rectangles come from the geometry or the texture
/// instead, and that is a different fix. Read-only either way: it records and reports.
/// </summary>
static class MvppGlowProbe
{
private static bool _enabled =
@@ -60,8 +36,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state)
{
// Only the HDR float target the halos live in. Identified from the dumps, by FORMAT and
// shape rather than by any address or game-specific value.
Image.Texture c0 = channel.TextureManager.RenderTargetColor0;
if (c0 == null || !c0.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
@@ -83,8 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
_scissored++;
// Round to 8 px so the same box seen over consecutive frames aggregates instead of
// producing a line per pixel of camera drift.
string key = $"{sc.X1 / 8 * 8},{sc.Y1 / 8 * 8} -> {sc.X2 / 8 * 8},{sc.Y2 / 8 * 8} " +
$"({(sc.X2 - sc.X1) / 8 * 8}x{(sc.Y2 - sc.Y1) / 8 * 8})";
@@ -92,14 +64,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_boxes[key] = n + 1;
}
// The scissor answered "no" (0 boxes over the whole run), so the rectangles are the
// glow QUADS themselves -- which is normal geometry. What is NOT normal, proven
// against a TOTK control where the same buffer shows zero hard edges, is that the halo
// is still bright AT the quad border instead of having faded to nothing. So look at
// what these draws READ: the halo texture and, above all, its ADDRESSING MODE, which
// is exactly what decides the value returned past the texture edge. ClampToEdge repeats
// the last texel for ever (a bright rim stays bright); ClampToBorder returns the border
// colour (normally transparent black, which fades correctly).
channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) =>
{
if (tex == null || smp == null || _inputs.Count >= 24)
@@ -1,6 +1,3 @@
// 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;
@@ -8,52 +5,13 @@ using System.Threading;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// [HANGWATCH 29/07 fin de soirée] Surveillance qui SURVIT AU GEL
/// (RYUJINX_MVPP_HANGWATCH=1, coupée par défaut). Préparée à la demande d'Alex, à laisser
/// dormir : le chantier du gel n'est PAS ouvert, l'instrument est juste prêt.
///
/// LE PROBLÈME QU'ELLE RÉSOUT. Alex a un blocage fréquent : après un combat, sur un écran de
/// chargement, **l'image se figeauze mais la musique continue**, le processus reste vivant (214
/// fils, un seul qui tourne, la fenêtre répond), et fermer/rouvrir suffit à repartir. Vu sans
/// FG comme avec, et bien avant qu'on la rallume ⇒ **la FG est hors de cause**.
///
/// ⚠️ ET ON EST AVEUGLES À L'INSTANT EXACT : toutes les lignes du journal viennent du fil
/// graphique, donc quand il se coince, le journal se coince avec lui. On voit les dix minutes
/// d'avant, jamais le moment. Aucune sonde posée sur ce fil ne peut répondre -- il faut un
/// observateur EXTÉRIEUR. C'est tout l'objet de ce fichier.
///
/// CE QU'ELLE MESURE, ET POURQUOI CES DEUX CHIFFRES SUFFISENT À TRANCHER. Un fil de fond,
/// indépendant, compare deux horloges :
/// - la dernière PRÉSENTATION d'image (<see cref="Ping"/>, appelé depuis OnFrameEnqueued) ;
/// - le compteur de DESSINS de <see cref="MvppCameraCapture.StatScaledDraws"/>.
/// Deux familles, et elles ne se réparent pas au même endroit :
/// A. les présentations s'arrêtent MAIS les dessins continuent d'avancer ⇒ le fil graphique
/// est vivant, c'est la PRÉSENTATION qui est bloquée (chaîne d'affichage, file de
/// présentation, interposition d'un proxy) ;
/// B. les deux s'arrêtent ensemble ⇒ c'est le fil graphique LUI-MÊME qui est coincé (attente
/// GPU, compilation de shaders, verrou).
/// Un seul run avec cette surveillance armée donnera la lettre. Sans elle, on ne peut que
/// deviner -- et deviner est exactement ce qui a coûté des runs à Alex le 29/07.
///
/// ⛔ ELLE NE RÉPARE RIEN ET NE TOUCHE À RIEN : aucune écriture d'état de rendu, aucun réveil
/// forcé, aucune tentative de récupération. Elle observe et elle écrit une ligne. Le seul coût
/// sur le chemin chaud est une écriture d'entier par image présentée.
/// </summary>
static class MvppHangWatch
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HANGWATCH") == "1";
/// <summary>
/// Au-delà de ce silence, on considère l'affichage arrêté. 3 s = une centaine d'images
/// manquées à 30 Hz : bien au-delà d'un simple à-coup ou d'une compilation de shaders
/// ordinaire, et bien en dessous des dizaines de secondes qu'Alex laisse passer avant de
/// fermer. Ni faux positifs de micro-saccade, ni détection trop tardive.
/// </summary>
private const long StallMs = 3000;
/// <summary>Rappel pendant que le gel dure, pour voir si les dessins avancent ou non.</summary>
private const long RepeatMs = 5000;
private static long _lastPresentMs;
@@ -63,10 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _drawsAtStall;
private static bool _inStall;
/// <summary>
/// Appelée à chaque image présentée. Volontairement minuscule : une écriture d'entier, rien
/// d'autre, pas de verrou -- elle est sur le chemin chaud.
/// </summary>
public static void Ping()
{
Volatile.Write(ref _lastPresentMs, Environment.TickCount64);
@@ -111,7 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
continue;
}
// Le chiffre qui tranche : les dessins avancent-ils PENDANT le gel ?
int draws = MvppCameraCapture.StatScaledDraws;
if (!_inStall)
@@ -1,5 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
using Ryujinx.Common.Logging;
using System;
@@ -7,31 +5,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Read-while-written texture hazard probe (RYUJINX_TEX_HAZARD=1, OFF by default, read-only).
///
/// TARGET: the Xenoblade 2 artefact -- large rectangles of real-but-wrong content, appearing
/// when the camera TURNS, reportedly worse indoors where more objects are on screen.
///
/// Everything cheap has already been eliminated BY MEASUREMENT, each with its switch verified
/// live in the log (never assumed):
/// - shader cache purged, artefact unchanged
/// - DLSS / MV++ / FG log proved mode=0, zero "using mode", zero "DLSS: available"
/// - runtime mipmaps never applied when DLSS is off (checked in the code path)
/// - recycled memory 2000+ fresh allocations zeroed, artefact unchanged
/// - history-reset storm 4 resets in a minute (Kameleo20's storm was 85-98%)
/// - gobBlocksInZ clamp probe fired ZERO times on this game
///
/// What is left that displaces REAL content in RECTANGLES is a synchronisation hazard: the game
/// SAMPLES a texture that is at the same time a bound RENDER TARGET. It then reads a mix of
/// already-written and still-stale tiles -- blocky, made of genuine pixels, and worse when more
/// draws are in flight. Turning the camera is when a streaming game re-renders those buffers.
///
/// This probe changes NOTHING. Each draw it lists what is being written (colour render targets)
/// and what is being read (sampled textures, every stage), and reports the ones that are the
/// same memory. A hit names the texture: dimensions, format, address. No hit rules the whole
/// mechanism out, and we look elsewhere.
/// </summary>
static class MvppHazardProbe
{
private static bool _enabled =
@@ -55,7 +28,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Never take the process down from a diagnostic on the GPU thread.
try
{
OnDrawImpl(channel);
@@ -107,8 +79,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
hitThisDraw = true;
// Distinct shapes, not one line per draw: the same few buffers repeat
// thousands of times a second and what matters is WHICH ones.
string key =
$"{tex.Info.Width}x{tex.Info.Height} {tex.Info.FormatInfo.Format} " +
$"{tex.Info.Target} @{addr:X10} (rt{i}, stage{stage})";
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -10,15 +7,6 @@ using System.Numerics;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ HDR-1600 producer census (RYUJINX_MVPP_CENSUS=1). READ-ONLY, default off. Hooked from the
/// universal DrawImpl choke point. For EVERY draw whose colour RT is the pre-tonemap HDR buffer
/// (R11G11B10Float, width >= 1500), it censuses the passes by (vertex, fragment) shader pair and, on
/// first sight of each pair, logs the full geometry/pipeline/data-source metrics and an automatic
/// A/B/C classification. Answers definitively whether the HDR 1600 is: A) the real scene rendered at
/// 1600 (many material shaders, heavy geometry, depth), B) a fullscreen upscale/post that samples an
/// HDR source, or C) a fullscreen pass fed by buffers/bindless (no visible texture).
/// </summary>
static class MvppHdrCensusProbe
{
private static bool _enabled =
@@ -50,7 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state, int count, bool indexed)
{
// Target: the pre-tonemap HDR buffer -- a large R11G11B10Float colour RT.
ulong rtAddr = 0;
int rtW = 0, rtH = 0;
bool isHdrTarget = false;
@@ -90,17 +77,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
_pairLogged.Add(key);
// Geometry
int vtx = indexed ? -1 : count;
int idx = indexed ? count : 0;
// Pipeline
bool depthT = state.DepthTestEnable;
bool depthW = state.DepthWriteEnable;
bool blend = state.BlendEnable[0];
int vpW = (int)MathF.Round(MathF.Abs(state.ViewportTransform[0].ScaleX) * 2f);
// Vertex buffers
int vbCount = 0;
for (int i = 0; i < 16; i++)
{
@@ -110,7 +94,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Data sources
int texCount = 0;
channel.TextureManager.MvppEnumerateGraphicsInputs(_ => texCount++);
int imgCount = 0;
@@ -131,7 +114,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Classification
string cls;
bool geomLike = depthW && (idx > 100 || vtx > 100) && vbCount > 0;
bool fullscreen = (vtx >= 3 && vtx <= 6) || (idx >= 3 && idx <= 6);
@@ -155,8 +137,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"vtx={vtx} idx={idx} vb={vbCount} vpW={vpW} depthT={depthT} depthW={depthW} blend={blend} | " +
$"tex={texCount} arr={arrCount} img={imgCount} ubuf={ubufCount} ubufMax={ubufMaxSize} => {cls}");
// The decisive data: the ADDRESS of every sampled input, so we can name the pass that reads the
// native scene (0x65799C4000 class) and writes this HDR 1600 = the DLSS injection point.
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
bool readsLowerResHdr = false;
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) =>
@@ -166,7 +146,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
int w = tex.Info.Width, h = tex.Info.Height;
bool hdr = f == Format.R11G11B10Float || f == Format.R16G16B16A16Float;
bool isScenePin = sceneAddr != 0 && a == sceneAddr;
bool lowerResHdr = hdr && w < 1500; // an HDR source SMALLER than the 1600 output = candidate scene
bool lowerResHdr = hdr && w < 1500;
readsLowerResHdr |= lowerResHdr || isScenePin;
Logger.Info?.Print(LogClass.Gpu,
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,25 +6,13 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ DLSS-injection GATE validation (RYUJINX_MVPP_INJECTGATE=1). STRICTLY READ-ONLY, default off.
/// STEP A of the injection plan: prove that a robust multi-criteria gate matches ONLY the identified
/// scene-upscale pass (scene 1333 HDR -> HDR 1600), with zero false positives and ZERO change to the
/// rendered image. Nothing is cancelled, no DLSS is called, no texture/barrier is touched. The FS shader
/// id is a SECONDARY signal only; the decisive criteria are "reads exactly the captured SceneColor" and
/// "writes the HDR 1600 RT". Also tracks near-misses (draws that read the scene but fail the gate) to
/// prove there is no source/destination ambiguity.
/// </summary>
static class MvppInjectGate
{
// Verbose = the Étape A read-only validation run (CANDIDATE/SUMMARY/near-miss spam). The gate also
// runs -- silently -- under the Étape B inject flag, where its only job is to publish the identified
// upscale destination dims to MvppInjectState so the Vulkan present can size the intermediate target.
private static readonly bool _verbose =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECTGATE") == "1";
private static bool _enabled = _verbose || MvppInjectState.Enabled;
private const ulong TargetFs = 0x559E30; // secondary confirmation only
private const ulong TargetFs = 0x559E30;
private const int MaxFullLogged = 40;
private static long _total;
@@ -37,9 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static long _summaryMs;
private static readonly HashSet<(ulong, ulong)> _candidatePairs = new();
// RELAXED gate = the 5 stable criteria WITHOUT the fragile "src == flapping SceneColor pin" (c2). If
// this stays 1 distinct pair, it is both specific AND complete (fires every frame), and is the gate
// to use. Its distinct pairs are logged so we can see if dropping c2 introduces any ambiguity.
private static long _relaxedCandidates;
private static readonly HashSet<(ulong, ulong)> _relaxedPairs = new();
@@ -65,12 +47,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
_total++;
// Cheap pipeline-signature criteria first.
bool idx3 = indexed && count == 3;
bool depthOff = !state.DepthTestEnable;
bool blendOff = !state.BlendEnable[0];
// Destination RT slot 0.
ulong dstAddr = 0;
Format dstFmt = default;
int dw = 0, dh = 0;
@@ -87,7 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
});
// Sampled input textures (count + first).
int texCount = 0;
ulong srcAddr = 0;
Format srcFmt = default;
@@ -106,16 +85,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
// The six gate criteria.
bool c1 = texCount == 1; // exactly one sampled texture
bool c2 = sceneAddr != 0 && srcAddr == sceneAddr; // source == captured SceneColor
bool c3 = srcFmt == Format.R11G11B10Float; // source format
bool c4 = haveDst && dstFmt == Format.R11G11B10Float && dw >= 1500 && dw <= 1700; // dest == HDR 1600
bool c5 = sw > 0 && dw > 0 && sw < dw; // coherent dims (upscale)
bool c6 = idx3 && depthOff && blendOff; // fullscreen triangle, no depth/blend
bool c1 = texCount == 1;
bool c2 = sceneAddr != 0 && srcAddr == sceneAddr;
bool c3 = srcFmt == Format.R11G11B10Float;
bool c4 = haveDst && dstFmt == Format.R11G11B10Float && dw >= 1500 && dw <= 1700;
bool c5 = sw > 0 && dw > 0 && sw < dw;
bool c6 = idx3 && depthOff && blendOff;
bool gate = c1 && c2 && c3 && c4 && c5 && c6;
bool relaxed = c1 && c3 && c4 && c5 && c6; // no c2 (drops the flapping SceneColor pin match)
bool relaxed = c1 && c3 && c4 && c5 && c6;
ulong vs = (ulong)state.ShaderState[1].Offset;
ulong fs = (ulong)state.ShaderState[5].Offset;
@@ -141,7 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else if (c2)
{
// Reads the captured scene but is NOT the upscale gate -- proves specificity (should be rare/zero).
_nearMiss++;
if (_verbose && _nearMiss <= 30)
{
@@ -161,8 +138,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"strict(candidates={_candidates} pairs={_candidatePairs.Count}) " +
$"RELAXED(candidates={_relaxedCandidates} pairs={_relaxedPairs.Count}) nearMiss={_nearMiss}");
// If the relaxed gate found extra shader-pairs beyond the upscale, name them (would be
// false positives if we dropped c2).
if (_relaxedPairs.Count > 1)
{
foreach ((ulong pvs, ulong pfs) in _relaxedPairs)
@@ -1,23 +1,9 @@
// 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;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Comparaison tuilage demandé / tuilage fourni pour la texture présentée (RYUJINX_LAYOUT=1). READ-ONLY.
///
/// Établi le 21/07 : la texture remise au present arrive DÉJÀ corrompue, avec une mosaïque en blocs =
/// signature d'un détuilage block-linear raté. Ici on met face à face ce que le JEU a demandé
/// (pt.Info, construit dans EnqueueFrameThreadSafe à partir de stride/isLinear/gobBlocksInY que le jeu
/// fournit) et ce que le cache a réellement retrouvé/créé (texture.Info). Un écart sur isLinear,
/// gobBlocksInY ou stride est la cause exacte cherchée.
///
/// Log throttlé + une ligne à CHAQUE changement de signature, pour ne rien rater sans spammer.
/// </summary>
static class MvppLayoutProbe
{
private static bool _enabled =
@@ -1,127 +1,25 @@
// 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;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// [NOLDR 29/07 SOIR] Ne PAS capturer la camera sur une passe a cible couleur ENTIERE (8 bits),
/// tout en laissant passer les autres. Deux interrupteurs :
/// RYUJINX_MVPP_NOLDR=1 -- le correctif, arme apres calibrage (voir le filet plus bas)
/// RYUJINX_MVPP_NOLDR_AB=1 -- le MESUREUR : la restriction s'allume et s'eteint toute seule
/// par tranches de 600 images, dans le MEME run
/// Les deux sont coupes par defaut.
///
/// POURQUOI CE FICHIER REMPLACE <see cref="MvppScenePass"/>. Ce dernier n'autorisait QUE la
/// passe de scene HDR : la camera y etait mesuree lisible dans trop peu d'images, la publication
/// s'effondrait, la camera etait AFFAMEE -- et une camera manquee fait ecrire zero sur toute
/// l'image. ⛔ Ne pas le re-armer. Ici on n'ELIT pas une passe, on ECARTE la pire.
///
/// LA MESURE QUI L'A IMPOSE (runs SANS lecture d'essai, donc non contamines) -- la passe 8 bits
/// est la pire dans les TROIS runs propres, et la passe HDR la plus saine dans les trois :
/// run 1 (etalon) : 8 bits 119/1340 = 8,9 % · HDR 3/230 = 1,3 %
/// run SCENEPASS : 8 bits 100/1233 = 8,1 % · HDR 1/1280 = 0,08 %
/// run temoin : 8 bits 8/382 = 2,1 % · HDR 1/130 = 0,8 %
/// ✅ VERIFIE EN JEU : avec NOLDR arme, la passe 8 bits DISPARAIT de la table des lectures
/// (382 -> 0) et les captures se deplacent sur la passe HDR (130 -> 280). Le mecanisme fait ce
/// qu'il annonce ; c'est son COUT qui restait a mesurer.
///
/// LE CRITERE EST GENERIQUE -- forme de pipeline : une cible couleur ATTACHEE dont le format
/// n'est PAS flottant. Pas de couleur attachee ⇒ passe. Couleur flottante ⇒ passe. Aucune
/// adresse, aucun hash, aucune resolution, aucun nom de jeu.
///
/// ⚠️⚠️⚠️ MES DEUX FILETS PRECEDENTS ETAIENT FAUX, CHACUN A SA FACON. A RELIRE AVANT D'EN
/// ECRIRE UN TROISIEME.
/// 1. `SCENEPASS` declenchait sur "2 images d'affilee sans camera". Or la reference en rate
/// DEJA 17 % toute seule ⇒ deux rates d'affilee est un evenement a ~3 %, NORMAL au repos :
/// le filet sautait sur une condition de base, desarme en 92 secondes.
/// 2. La v1 de CE fichier se calibrait sur les 600 premieres images 3D -- c'est-a-dire pendant
/// le CHARGEMENT, ou le ratage vaut 90 %. Le seuil devenait 90 + 10 = 100 % :
/// **impossible a atteindre**. La restriction a tourne sans garde-fou tout un run.
/// 📌 La lecon commune : un detecteur qui ne peut pas se declencher est indiscernable d'un
/// detecteur casse. Avant de croire un compteur, verifier ce qu'il compte DANS LE REGIME OU ON
/// L'A MIS -- ici, ne rien calibrer avant que la camera soit chaude.
///
/// LE MESUREUR (mode A/B), ET POURQUOI IL EXISTE. Quatre runs ont rendu des taux de publication
/// de 83 / 70 / 63 / 60 % selon l'ENDROIT, et des comptes d'intruses variant d'un facteur 10.
/// Comparer deux runs differents ne prouve donc rien -- c'est ce qui a fait juger trois
/// correctifs sur du hasard le 29/07 au matin. Le mode A/B supprime la variable : la restriction
/// alterne DANS le meme run, sur la meme scene et le meme geste, et les deux moities se
/// comparent directement. Chauffe d'abord (200 captures reussies) pour ne jamais mesurer le
/// chargement.
///
/// Les compteurs sont approximatifs : OnDraw tourne sur le fil GPU, OnFrame sur celui de la
/// presentation. Ordres de grandeur seulement.
/// </summary>
static class MvppLdrSkip
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR") == "1";
/// <summary>
/// [NORANK1 29/07 soir, 5e passe] Ne pas capturer sur la PREMIERE tentative de l'image
/// (RYUJINX_MVPP_NORANK1=1, coupe par defaut).
///
/// LA MESURE. Une fois la colonne "rang" reparee, run de 4 min avec `NOLDR` deja arme (donc
/// les passes 8 bits deja ecartees -- l'effet mesure ici est INDEPENDANT) :
/// lectures saines : rang moyen 33,6 · rang 1 = 48 % · 2-3 = 4 % · 4-10 = 9 % · 51+ = 22 %
/// intruses : rang moyen 3,1 · rang 1 = 95 % · 2-3 = 0 % · 4-10 = 0 % · 51+ = 3 %
/// En taux, la seule lecture qui vaille : au rang 1 une lecture est intruse dans 1,62 % des
/// cas, au-dela du rang 1 dans 0,08 %. **Vingt fois moins.**
///
/// POURQUOI CA NE DEVRAIT PAS AFFAMER, contrairement a `MvppScenePass` : il y a ~153 passes
/// qualifiantes par image, donc sauter la premiere laisse 152 occasions derriere. SCENEPASS
/// n'avait que la passe HDR pour se rattraper, et la camera n'y etait pas toujours lisible.
/// Le meme filet auto-calibre couvre les deux criteres et tranchera par la mesure.
/// </summary>
public static readonly bool SkipRank1 =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NORANK1") == "1";
/// <summary>
/// Le MESUREUR : fait ALTERNER par tranches de 600 images le ou les criteres armes, dans le
/// meme run. Il est agnostique au critere -- il alterne ce qui est allume. C'est le seul
/// instrument de la soiree qui ait produit une comparaison fiable, parce qu'il supprime
/// l'endroit comme variable.
/// </summary>
public static readonly bool AbMode =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR_AB") == "1";
/// <summary>
/// [MISSWHY 29/07 fin de soirée] Pourquoi une image rate sa capture quand la restriction est
/// armée (RYUJINX_MVPP_MISSWHY=1, coupé par défaut).
///
/// LE FAIT À EXPLIQUER : sur huit runs, la restriction s'est suspendue DEUX fois, les deux
/// dans des scènes où le ratage calibré était déjà haut. Une suspension coûte 100 s de gain.
/// Deux explications possibles, et elles ne se réparent PAS de la même façon :
/// A. dans ces images il n'existait AUCUNE autre passe pour se rattraper — on a écarté la
/// 8 bits et il n'y avait rien derrière ⇒ la restriction est la cause ;
/// B. il y avait des alternatives, elles ont été tentées, mais la lecture a échoué ⇒ la
/// restriction n'est peut-être pas la cause (l'image aurait raté de toute façon), mais
/// les passes 8 bits écartées auraient peut-être réussi. **Cas AMBIGU, dit comme tel.**
/// C. rien n'a été écarté dans cette image ⇒ la restriction n'y est pour rien. C'est le
/// ratage de fond, celui qui existe sans nous.
/// Le compte des trois dit laquelle domine. ⛔ Je ne devine pas : A et B mènent à des
/// correctifs différents, et C n'en demande aucun.
///
/// Les deux compteurs par image sont écrits UNIQUEMENT par le fil GPU (dans Allows), remis à
/// zéro par lui aussi via le jeton, et seulement LUS par le fil de présentation à la fin de
/// l'image — donc après le dernier dessin. Pas de course sur une écriture. (Case 1 de
/// `docs/CHECKLIST-SONDE.md`.)
/// </summary>
public static readonly bool MissWhy =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_MISSWHY") == "1";
/// <summary>Le crochet de capture consulte ceci, pas les interrupteurs un par un.</summary>
public static bool Active => Enabled || SkipRank1 || AbMode;
/// <summary>
/// Captures reussies a attendre avant de mesurer OU d'armer quoi que ce soit. Sert
/// uniquement a sortir de la phase de chargement, ou le ratage vaut 90 % et ou toute
/// calibration est un mensonge (faute de la v1 de ce fichier).
/// </summary>
private const int WarmCaptures = 200;
private const int CalibrationFrames = 600;
@@ -129,19 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private const float MarginPoints = 0.10f;
private const int ReportMs = 5000;
/// <summary>
/// [SUSPENSION 29/07 soir, 4e passe] La v2 coupait la restriction pour TOUTE LA SESSION au
/// premier excès. Mesure du soir : sur cinq runs, un seul a déclenché -- et c'était celui
/// dont le ratage calibré était déjà le plus haut (11,2 %, fenêtre à 25,3 %) ⇒ la famine a
/// l'air LOCALE à une scène. Couper pour la session jetait donc le gain partout ailleurs
/// jusqu'à la fermeture du jeu.
///
/// Ici l'excès ne coupe plus définitivement : il SUSPEND pour 3 000 images (~100 s), puis on
/// RECALIBRE sur place -- la scène a changé, l'ancien étalon ne vaut plus -- et on réarme.
/// Abandon définitif seulement au 4e excès, avec une ligne de journal explicite. La borne qui
/// protège reste la même (le ratage ne peut pas dépasser le calibré de plus de 10 points
/// pendant plus d'une fenêtre), mais on récupère le gain dès que la scène le permet.
/// </summary>
private const int SuspendFrames = 3000;
private const int SuspendsBeforeGiveUp = 4;
@@ -169,18 +54,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _frameMark;
private static int _seenMark;
// Par image, ecrits par le fil GPU seulement (voir MissWhy).
private static int _frSkipped;
private static int _frAllowed;
// A : rien d'autre n'etait disponible · B : alternatives tentees et echouees (ambigu)
// C : rien n'a ete ecarte, ratage de fond.
private static int _missA;
private static int _missB;
private static int _missC;
private static long _lastReportMs;
// Mode A/B : indice 1 = restriction ACTIVE (avec), indice 0 = restriction LEVEE (sans).
private static bool _abArmed;
private static int _abWin;
private static bool _abStarted;
@@ -189,21 +70,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly int[] _abReads = new int[2];
private static readonly int[] _abIntruders = new int[2];
/// <summary>Vrai quand la restriction s'applique a cet instant. Sert a etiqueter les mesures.</summary>
internal static bool ArmedNow => AbMode ? _abArmed : _phase == PhaseArmed;
/// <summary>
/// Appelee sur un dessin qualifiant tant que l'image n'a pas encore sa camera. Rend false
/// pour SAUTER la tentative sur ce dessin : la capture pourra se faire plus loin dans la
/// MEME image, sur une passe qui n'est pas la passe 8 bits.
/// </summary>
public static bool Allows(GpuChannel channel)
{
_qualThisFrame = true;
// Rang de la TENTATIVE dans l'image. Meme mecanique que la reparation de MvppCtxProbe :
// OnFrame (fil de presentation) n'avance qu'un jeton, la remise a zero se fait ici, sur
// le fil GPU, seul ecrivain de _attempt. Pas de course.
int mark = _frameMark;
if (_seenMark != mark)
@@ -237,18 +109,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return false;
}
// Une tentative AUTORISEE : si l'image finit quand meme sans camera, c'est que la lecture
// a echoue ici, pas que la restriction a ferme la porte.
_frAllowed++;
return true;
}
/// <summary>
/// La passe a ecarter, reconnue a la FORME de sa cible : une couleur est attachee et son
/// format n'est pas flottant. Une passe sans couleur et une passe flottante restent des
/// occasions de capture -- c'est ce qui evite la famine qui a tue <see cref="MvppScenePass"/>.
/// </summary>
private static bool IsIntegerColorPass(GpuChannel channel)
{
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
@@ -281,10 +146,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Etiquette une lecture de camera avec la moitie d'experience en cours. Appelee par
/// <see cref="MvppCtxProbe"/>, qui est le seul a savoir si une lecture est une intruse.
/// </summary>
internal static void NoteRead(bool intruder)
{
if (!AbMode || !_abStarted)
@@ -302,19 +163,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>Une fois par image presentee, AVANT le re-armement du drapeau de capture.</summary>
public static void OnFrame(bool captured)
{
if (!_qualThisFrame)
{
// Image sans passe 3D (menu, chargement) : elle n'avait pas de camera a prendre.
return;
}
_qualThisFrame = false;
// [MISSWHY] Classement de l'image ratee, ICI : le dernier dessin est passe, les deux
// compteurs de l'image sont complets, et le jeton n'a pas encore ete avance.
if (MissWhy && !captured && ArmedNow)
{
if (_frSkipped == 0)
@@ -331,12 +188,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Jeton pour le rang de tentative : seul echange entre fils, un int, indechirable.
_frameMark++;
// CHAUFFE, commune aux deux modes : rien n'est mesure ni arme avant que la camera
// produise vraiment. C'est la correction de la faute de la v1, qui calibrait pendant le
// chargement et en tirait un seuil inatteignable.
if (_phase == PhaseWarming)
{
if (captured)
@@ -394,8 +247,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// SUSPENSION : la restriction est levee (ArmedNow est faux), on laisse la scene passer,
// puis on RECALIBRE ici meme au lieu de reprendre un etalon perime.
if (_phase == PhaseSuspended)
{
if (--_suspendLeft <= 0)
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -10,19 +7,6 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ Phase 2.1 run B v4 (RYUJINX_MVPP_P2B_PROBE=1): empirical transform hunt.
/// v3 verdict: cb9 holds NO world translation at any 16-byte-aligned offset (the broad
/// low-percent hits were the T~0 artifact: a zero translation "projects" whenever the
/// world origin is on screen), and cb10[0x20] is neither an int nor an integral float.
/// Current theory: TOTK's static world geometry is PRE-TRANSFORMED in the vertex data
/// (no model matrix exists -- which is exactly why camera reprojection already nails it),
/// and the transforms that matter for MV++ are the skinning bone palettes (cb5) of the
/// ACTORS. v4 therefore scans DENSELY (every 4-byte float offset, both the contiguous
/// vec3 and the spread row-major x/+4/+8 pattern), across slots {1, 5, 9, 13}, excluding
/// |T| &lt; 0.5 (the origin artifact), and dumps the cb9/cb10 headers as hex (v3 samples
/// looked like handles/addresses, not matrices). CPU-only, zero rendering change.
/// </summary>
static class MvppP2BProbe
{
private static bool _enabled =
@@ -30,10 +14,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private const int LogIntervalMs = 2000;
private const float FrustumMargin = 1.3f;
private const float MinTranslation = 0.5f; // kills the T~0 "origin visible" artifact
private const float MinTranslation = 0.5f;
private const int SlotCount = 18;
private const int ScanSampleShift = 4; // scan every 16th draw
private const int MaxScanFloats = 5120; // covers cb5's 20 KB palettes
private const int ScanSampleShift = 4;
private const int MaxScanFloats = 5120;
private static readonly int[] _scanSlots = { 1, 5, 9, 13 };
@@ -45,7 +29,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static bool _sampleDumped;
private static readonly int[] _slotBound = new int[SlotCount];
private static readonly int[] _scanned = new int[4];
// [slot][conv 0=contiguous vec3, 1=spread x/+4/+8][float offset]
private static readonly int[][][] _hits = CreateHits();
private static int[][][] CreateHits()
@@ -167,8 +150,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// One header dump per window: v3's cb9 samples looked like handles/addresses.
// Show cb9 and cb10 heads as raw hex so their true nature is on the record.
if (!_sampleDumped && _named >= 500)
{
_sampleDumped = true;
@@ -201,7 +182,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
(ax >= MinTranslation || ay >= MinTranslation || az >= MinTranslation);
}
/// <summary>Row-major, column-vector convention (clip = M * world), as the whole MV++ chain.</summary>
private static bool ProjectsIntoFrustum(in Matrix4x4 vp, float x, float y, float z)
{
float cw = vp.M41 * x + vp.M42 * y + vp.M43 * z + vp.M44;
@@ -1,6 +1,3 @@
// 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;
@@ -10,16 +7,6 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ Phase 2.0 feasibility probe (RYUJINX_MVPP_P2_PROBE=1): measures whether a scaled
/// draw can be re-identified from one frame to the next ("the same object") from the draw
/// state alone (guest shader offsets, vertex/index buffer addresses, draw range, occurrence
/// ordinal), with cbuf5 matrix 0 as the tracked payload. This is the foundation the
/// per-object-MV plan (velocity pass fed with previous-frame transforms) stands on: if
/// matching fails here, Phase 2 dies cheap. CPU-only reads, no GAL calls, zero rendering
/// change; completely off without the env var. Metric definitions and the per-scene success
/// thresholds live in docs/MVPP-JOURNAL.md (Phase 2.0 entry).
/// </summary>
static class MvppP2Probe
{
private static bool _enabled =
@@ -27,9 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private const int LogIntervalMs = 2000;
// Real per-frame motion produces small element deltas (a galloping actor moves about
// 0.25 world units per frame); a MISmatched pair jumps by world-position magnitudes
// (tens to hundreds). The gap between the two classes is what makes 1.0 a safe cut.
private const float CoherentMaxDelta = 1.0f;
private struct DrawRecord
@@ -37,8 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
public Matrix4x4 Mat0;
}
// Frame N-1 and frame N tables, swapped at each present enqueue. The enqueue may run on
// a different thread than the draws, so all state below is under one (uncontended) lock.
private static Dictionary<ulong, DrawRecord> _prev = new();
private static Dictionary<ulong, DrawRecord> _curr = new();
private static readonly Dictionary<ulong, int> _sigCounts = new();
@@ -53,13 +35,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _ambiguous;
private static int _matched;
private static int _coherent;
private static double _sumDtRow; // |delta translation| of coherent pairs, row-major candidate [12..14]
private static double _sumDtCol; // column-major candidate [3,7,11]
private static float _maxDelta; // max element delta over ALL matched pairs (mismatch witness)
private static double _sumDtRow;
private static double _sumDtCol;
private static float _maxDelta;
private static long _sumCbufBytes;
private static int _maxCbufBytes;
/// <summary>Present enqueue = frame boundary: close the frame, swap tables, maybe log.</summary>
public static void OnFrameEnqueued()
{
if (!_enabled)
@@ -69,8 +50,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
lock (_sync)
{
// Ambiguity is exact per frame: every draw whose signature occurred more than
// once this frame counts (including the first occurrence of each group).
foreach (KeyValuePair<ulong, int> kv in _sigCounts)
{
if (kv.Value > 1)
@@ -97,7 +76,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>Per scaled draw, called from DrawManager.DrawEnd on the GPU thread.</summary>
public static void OnDraw(
GpuChannel channel,
ref ThreedClassState state,
@@ -111,8 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Same hardening as the Phase 0 probe: an escaped exception on the GPU thread is a
// silent process kill. On any unexpected error: disable and say why.
try
{
OnDrawImpl(channel, ref state, firstIndex, indexCount, drawFirstVertex, drawVertexCount);
@@ -132,7 +108,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
int drawFirstVertex,
int drawVertexCount)
{
// Only the scaled 3D scene pass; UI/native passes stay at 1x.
if (channel.TextureManager.RenderTargetScale == 1f)
{
return;
@@ -145,7 +120,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
"MVPP P2 probe: active (frame-to-frame draw-identity matching, thresholds in docs/MVPP-JOURNAL.md).");
}
// Draw-identity signature: the exact field list is contractual (journal, Phase 2.0).
ulong sig = 14695981039346656037UL;
sig = FnvStep(sig, state.ShaderState[1].Offset);
sig = FnvStep(sig, state.ShaderState[5].Offset);
@@ -156,7 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
sig = FnvStep(sig, (ulong)(uint)drawFirstVertex);
sig = FnvStep(sig, (ulong)(uint)drawVertexCount);
// cbuf5 (vertex stage) coverage + matrix 0 snapshot.
ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(0, 5);
int size = channel.BufferManager.GetGraphicsUniformBufferSize(0, 5);
@@ -248,9 +221,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
_coherent++;
// Translation candidates for both storage layouts; which one carries
// the actual world motion (visible on actors, so best read in the
// combat scene) settles the layout question without a dump session.
float drx = b[12] - a[12], dry = b[13] - a[13], drz = b[14] - a[14];
float dcx = b[3] - a[3], dcy = b[7] - a[7], dcz = b[11] - a[11];
_sumDtRow += MathF.Sqrt(drx * drx + dry * dry + drz * drz);
@@ -1,6 +1,3 @@
// 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;
@@ -8,19 +5,6 @@ using System.IO;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Test décisif XC2 (RYUJINX_PRESYNC=1). READ-ONLY, off par défaut.
///
/// Capture la texture PRÉSENTÉE juste AVANT et juste APRÈS le `texture.SynchronizeMemory()` de
/// `Window.Present` (Gpu/Window.cs ~272). Hypothèse : le render target composé sur GPU est aussi suivi
/// comme mémoire invitée et marqué sale, donc cette resynchronisation recharge la mémoire invitée
/// (périmée) par-dessus l'image rendue propre, juste avant l'affichage. C'est le seul intervalle du
/// pipeline jamais instrumenté, et le seul compatible avec "propre au dernier draw, détruit à l'écran".
///
/// Déclenchement F10 (comme la sonde pas-à-pas), pour capturer quand l'artefact est visible. Écrit
/// AVANT et APRÈS, plus un booléen "identiques ?". Si before propre + after détruit => cause trouvée,
/// à la ligne près. Si identiques => l'hypothèse meurt et le coupable est ailleurs dans le present.
/// </summary>
static class MvppPreSyncProbe
{
private static bool _enabled =
@@ -60,13 +44,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Vidage de la mémoire INVITÉE brute de la texture présentée, sur F10. Comparé hors-ligne au
/// détuilage : si la mémoire invitée détuilée est PROPRE alors que la texture hôte est corrompue,
/// le bug est dans la lecture (détuilage) ; si la mémoire invitée est DÉJÀ corrompue, quelque chose
/// a écrit des blocs décalés en amont. Écrit aussi les octets bruts + les paramètres pour rejouer
/// le détuilage à la main.
/// </summary>
public static void DumpGuest(Image.Texture texture)
{
if (!_enabled || texture == null)
@@ -1,6 +1,3 @@
// 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;
@@ -9,22 +6,11 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ Phase 0 discovery probe (RYUJINX_MVPP_PROBE=1). Once every couple of seconds, on a
/// draw of the scaled 3D pass, scans the bound graphics uniform buffers for 4x4-matrix-shaped
/// float windows and fingerprints each candidate. Only candidates whose CONTENT CHANGED since
/// the previous sample are logged, so the in-game protocol is: hold the camera still (expect
/// silence), then pan it (the candidates that light up are camera-dependent - the
/// view/view-projection matrices MV++ needs). Read-only; completely off without the env var.
/// </summary>
static class MvppProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_PROBE") == "1";
// Dump mode (RYUJINX_MVPP_DUMP=<slot>): instead of scanning, print the first 512 bytes
// of stage-0 cbuf<slot> as float rows every sample, to map the block's exact layout
// (which matrix is view / proj / view-proj / inverses / previous frame).
private static readonly int _dumpSlot =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUMP"), out int s) ? s : -1;
@@ -43,9 +29,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// A diagnostic probe must never take the process down: it runs on the GPU thread,
// where an escaped exception is fatal and unlogged (crash of 2026-07-02 08:22,
// WER e0434352 with a silent Ryujinx log). On any error: disable and tell why.
try
{
OnDrawImpl(channel);
@@ -59,7 +42,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static void OnDrawImpl(GpuChannel channel)
{
// The main 3D scene pass is the scaled one; UI/native passes stay at 1x.
if (channel.TextureManager.RenderTargetScale == 1f)
{
return;
@@ -95,8 +77,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
continue;
}
// The bound ranges are already TRANSLATED: physical addresses, not GPU VAs
// (SetGraphicsUniformBuffer runs TranslateAndCreateBuffer). Read physical.
ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot);
int size = Math.Min(channel.BufferManager.GetGraphicsUniformBufferSize(stage, slot), MaxBytesPerBuffer);
@@ -122,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
continue;
}
// 16-byte aligned windows of 16 floats.
for (int i = 0; i + 16 <= data.Length; i += 4)
{
ReadOnlySpan<float> window = data.Slice(i, 16);
@@ -151,7 +130,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_lastFingerprint[key] = fp;
i += 12; // jump past this matrix (loop adds 4 -> next window starts right after it).
i += 12;
}
}
}
@@ -179,10 +158,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Cheap shape filter: 16 finite floats of sane magnitude with enough non-zero terms.
/// Material constants pass it too - the temporal changed-filter does the real sorting.
/// </summary>
private static bool LooksLikeMat4(ReadOnlySpan<float> m)
{
int nonZero = 0;
@@ -1,6 +1,3 @@
// 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;
@@ -8,60 +5,6 @@ using System.Numerics;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// PROJECTIVE AUDIT of the view-projection elected by <see cref="MvppSoloCamera"/>.
/// Gate: RYUJINX_MVPP_PROJAUDIT=1. LOGGING ONLY - reads the matrix that was already
/// accepted, computes scalars, prints one line a second. It changes no state, feeds no
/// shader and returns nothing, so with the gate off it is two boolean tests per frame and
/// with the gate on it cannot alter a single pixel.
///
/// WHY IT EXISTS (27/07, XC2). VPSOLO finally armed MV++ on Xenoblade 2 and the split in
/// Alex's verdict is the whole clue: the SKY is perfect, the GEOMETRY slides when the camera
/// moves. SKYROT reprojects the far plane with ROTATION ONLY - it needs neither depth nor the
/// projective part. Geometry needs the full matrix AND depth. Sky right + geometry wrong
/// therefore points at a matrix that is correct as a CAMERA and wrong as a PROJECTION.
///
/// And that is exactly what the election allows. MvppSoloCamera.IsViewProj checks the norm of
/// row3, the norms fx/fy, the mutual orthogonality of rows 0/1/3, the collinearity of row2
/// with row3, and rejects bare projections. NONE of those constrain the DEPTH MAPPING: the
/// terms usually written A and B (row2.xyz magnitude and row2.w) are never validated, and
/// ViewProjPos rebuilds the camera position from m[3], m[7] and m[15] without ever reading
/// m[11]. A matrix can pass every rule while mapping depth in a convention the reprojection
/// does not expect.
///
/// THE SUSPECT NUMBER. The in-game "MVPP center" probe reports worldW between 0.0002 and
/// 0.077 for real geometry - a ship and a building. If worldW is the view-space distance in
/// world units, those objects would sit four centimetres from the camera. The reprojection
/// divides by that w, so a w that is wrong by three orders of magnitude produces exactly what
/// was measured: vectors that flip sign frame to frame during a single continuous pan and
/// that peg to whatever the clamp is (128 with MAXMOTION=128, 512 with 512).
///
/// WHAT THIS PRINTS, AND HOW TO READ IT.
/// fx/fy : the focal scales. Their ratio must equal the render aspect (~1.778).
/// n3 : |row3.xyz|. IsViewProj FORCES this to 1 +/- 2%, so it is printed to confirm
/// the constraint is what makes the scale of w what it is.
/// A : |row2.xyz|. For a standard perspective this is the depth compression. A
/// near-zero A means an INFINITE far plane (or reverse-Z), and note that
/// IsViewProj SKIPS its collinearity test entirely when A < 1e-4 - such a
/// matrix is accepted without that check ever running.
/// B : row2.w - A*tz, the depth offset.
/// near/far : recovered from A and B under the OpenGL convention. If they come out
/// absurd (negative, inverted, astronomically large) the matrix does not map
/// depth the way the reprojection assumes, and that is the defect.
/// wCenter : the perspective w for a point at the screen centre at the depth given, in
/// the SAME units as the camera position. Compare it against camDist: they
/// describe the same distance and must agree in order of magnitude.
/// rt : round-trip error in pixels - unproject the screen centre with VP^-1 then
/// project it back with VP. This tests CONDITIONING only, never correctness:
/// a badly scaled matrix round-trips perfectly. A large rt means the matrix is
/// near-singular, which is a separate and worse problem.
///
/// HONEST LIMIT. This audit cannot say what the TRUE matrix is - there is no ground truth
/// available at this level. It can only say whether the accepted one is internally coherent
/// and whether its depth mapping is plausible. That is enough to decide where to look next:
/// a coherent matrix moves the search to the consumer (the reprojection shader), an incoherent
/// one keeps it here, in the election.
/// </summary>
static class MvppProjAudit
{
public static readonly bool Enabled =
@@ -71,10 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static long _lastMs;
/// <summary>
/// Called on the success path of MvppSoloCamera.TryGetViewProjection, with the matrix it
/// is about to hand out and the focal scales it recovered. Throttled to one line a second.
/// </summary>
public static void Audit(in Matrix4x4 vp, float camX, float camY, float camZ)
{
if (!Enabled)
@@ -91,41 +30,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_lastMs = now;
// Recovered here rather than passed in, so the hook at the call site stays a single
// line and TryReadAt keeps its signature untouched. Same definition as IsViewProj.
float fx = MathF.Sqrt(vp.M11 * vp.M11 + vp.M12 * vp.M12 + vp.M13 * vp.M13);
float fy = MathF.Sqrt(vp.M21 * vp.M21 + vp.M22 * vp.M22 + vp.M23 * vp.M23);
// Row layout, matching the constructor in MvppSoloCamera.TryReadAt:
// row0 = M11 M12 M13 M14 row2 = M31 M32 M33 M34
// row1 = M21 M22 M23 M24 row3 = M41 M42 M43 M44
float n3 = MathF.Sqrt(vp.M41 * vp.M41 + vp.M42 * vp.M42 + vp.M43 * vp.M43);
float a = MathF.Sqrt(vp.M31 * vp.M31 + vp.M32 * vp.M32 + vp.M33 * vp.M33);
// Documented shape: row2.xyz = A*R.row2 and row3.xyz = s*R.row2 with s = +/-1, so the
// sign is read off the dot product rather than assumed.
float dot23 = vp.M31 * vp.M41 + vp.M32 * vp.M42 + vp.M33 * vp.M43;
float s = dot23 >= 0f ? 1f : -1f;
// row3.w = s*tz => tz = row3.w / s ; row2.w = A*tz + B => B = row2.w - A*tz.
float tz = s != 0f ? vp.M44 / s : 0f;
float b = vp.M34 - a * tz;
// OpenGL perspective: A' = -(f+n)/(f-n), B' = -2fn/(f-n) with the sign carried by s.
// Inverting gives n = B/(A-1) and f = B/(A+1); both are printed raw so an absurd pair
// is visible rather than silently normalised into something plausible.
float aSigned = a * s;
float near = MathF.Abs(aSigned - 1f) > 1e-6f ? b / (aSigned - 1f) : float.NaN;
float far = MathF.Abs(aSigned + 1f) > 1e-6f ? b / (aSigned + 1f) : float.NaN;
// The perspective w at the screen centre for a point on the camera axis: w is row3
// dotted with the world point plus row3.w. Taking the camera itself gives the offset,
// so the magnitude that matters is how w grows per world unit along the view axis --
// with n3 forced to 1 that rate is 1, and w IS the distance in world units.
float wAtCam = vp.M41 * camX + vp.M42 * camY + vp.M43 * camZ + vp.M44;
float camDist = MathF.Sqrt(camX * camX + camY * camY + camZ * camZ);
// Conditioning: unproject the centre of the screen at mid depth, project it back.
float rt = float.NaN;
if (Matrix4x4.Invert(vp, out Matrix4x4 inv))
@@ -142,8 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (MathF.Abs(back.W) > 1e-20f)
{
// Half the render width is the worst case for a normalised device unit,
// 1920 is only a scale for readability - the verdict is "near zero or not".
rt = MathF.Sqrt(back.X / back.W * (back.X / back.W) +
back.Y / back.W * (back.Y / back.W)) * 1920f;
}
@@ -151,7 +72,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else
{
rt = -1f; // not invertible at all
rt = -1f;
}
Logger.Info?.Print(LogClass.Gpu,
@@ -1,5 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
using Ryujinx.Common.Logging;
using System;
@@ -8,80 +6,26 @@ using System.IO;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Render-target snapshot probe (RYUJINX_RT_DUMP=1, OFF by default).
///
/// WHY WE ARE HERE. The Xenoblade 2 artefact - large, SOFT rectangles of real-but-averaged
/// content, when the camera turns, worse indoors - has survived nine eliminations, each one
/// measured with its switch verified live in the log, never assumed:
/// shader cache · DLSS/MV++/FG (mode=0 proven) · runtime mipmaps · recycled device memory
/// (2000+ allocations zeroed) · history-reset storm · the fork's gobBlocksInZ clamp (zero
/// firings) · sampling a bound render target (0 hits in 16731 draws) · the 1080p scene mod
/// (patch proven applied) · dynamic resolution (depth AND colour sizes rock-steady).
///
/// Counters have run out of road, so we stop guessing at mechanisms and LOOK AT THE PIXELS.
///
/// WHAT THE LAST MEASUREMENT REVEALED. XC2 renders through several LOW-RESOLUTION buffers that
/// are enlarged and composited back over the frame - 640x360 R11G11B10Float and 512x288
/// R16G16B16A16Float, ~300 draws each per 5 s window, permanently. That matches the LOOK of the
/// artefact: the blocks are big and soft, i.e. averaged content stretched up, not the crisp
/// blocks a full-resolution buffer would give. It would also explain "worse indoors", where
/// more lights and volumetrics are in play.
///
/// WHAT THIS PROBE DOES. On a trigger it captures ONE image per distinct colour render-target
/// shape (plus the depth), straight off the GPU, to raw files named with their dimensions and
/// format. Then we look at them one by one and SEE which buffer carries the blocks. No
/// hypothesis survives contact with the actual pixels.
///
/// COST. GetData() is a full GPU-to-CPU readback and it stalls the pipeline, so this is
/// strictly one short burst per session, capped, and off by default.
/// </summary>
static class MvppRtDumpProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP") == "1";
/// <summary>
/// Seconds to wait after boot before the FIRST capture (RYUJINX_RT_DUMP_START, default 90).
/// Measured the hard way 21/07: with captures starting 10 s after boot, all four bursts
/// landed during loading and the intro, so the "clean" buffer they showed said nothing
/// about the moment the artefact is on screen. The captures have to happen when the player
/// is IN the scene, looking at the defect.
/// </summary>
private static readonly int _startSeconds =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_START"), out int st) && st > 0 ? st : 90;
/// <summary>Seconds between capture bursts (RYUJINX_RT_DUMP_EVERY, default 10).</summary>
private static readonly int _everySeconds =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_EVERY"), out int s) && s > 0 ? s : 10;
/// <summary>How many bursts before the probe stops for good (RYUJINX_RT_DUMP_MAX, default 4).</summary>
private static readonly int _maxBursts =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_MAX"), out int m) && m > 0 ? m : 4;
/// <summary>RYUJINX_RT_DUMP_ALL=1: capture every bound target, not just the scene buffer.</summary>
private static readonly bool _dumpAll =
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_ALL") == "1";
/// <summary>RYUJINX_RT_DUMP_INPUTS=1: also capture the textures the draws SAMPLE.</summary>
private static readonly bool _dumpInputs =
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_INPUTS") == "1";
/// <summary>
/// RYUJINX_RT_DUMP_FRAME=1 : une rafale == UNE IMAGE EXACTEMENT, bornée par les presents,
/// au lieu d'une fenêtre en millisecondes.
///
/// Pourquoi (mesuré deux fois le 21/07) : avec un découpage temporel, les cibles intermédiaires
/// et l'image présentée tombent sur des images DIFFÉRENTES. Pendant une rotation caméra la vue
/// change complètement en une seconde — un burst montrait une grue pendant que l'image finale
/// montrait des caisses. On comparait deux scènes sans rapport, et aucune conclusion n'était
/// possible sur « à quel étage les premiers pixels faux apparaissent ».
///
/// Ici la rafale est ouverte par un present et fermée par le SUIVANT : tous les draws capturés
/// entre les deux appartiennent à l'image dont on capture ensuite le résultat présenté. Les deux
/// sondes vivent du même côté du code (couche Gpu), donc ce découpage suffit — aucune
/// synchronisation inter-assembly n'est nécessaire.
/// </summary>
private static readonly bool _frameMode =
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_FRAME") == "1";
@@ -95,11 +39,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static bool _frameArmLogged;
private static readonly HashSet<string> _doneThisBurst = new();
/// <summary>
/// Called from Window.Present with the texture that is about to reach the screen. Captured
/// only while a burst is running, so the presented frame lands in the SAME folder as the
/// intermediate buffers of that moment and the two can be compared directly.
/// </summary>
public static void NotePresented(Image.Texture texture)
{
if (!_enabled)
@@ -126,13 +65,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// --- Mode IMAGE : ce present est la frontière entre deux images. ---
try
{
if (_bursting)
{
// Fin de l'image capturée : son résultat présenté ferme le dossier, à côté des
// cibles intermédiaires des draws de CETTE image, et d'aucune autre.
if (texture != null)
{
TryDump(texture, "PRESENTED");
@@ -173,8 +109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Ouverture : les draws de l'image SUIVANTE seront capturés, puis le present suivant
// fermera le dossier avec le résultat de cette même image.
_burstIndex++;
_doneThisBurst.Clear();
_burstDir = Path.Combine("rtdump", $"image{_burstIndex:D2}");
@@ -212,9 +146,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
long now = Environment.TickCount64;
// En mode IMAGE, l'ouverture et la fermeture de la rafale appartiennent à NotePresented :
// ici on ne fait que capturer les cibles des draws de l'image en cours, sans toucher au
// découpage. Toute logique de temps ici casserait l'appariement.
if (_frameMode)
{
if (!_bursting)
@@ -245,8 +176,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// A burst spans a full second: the low-resolution buffers are bound on their own
// draws, so capturing a single draw would only ever catch the main target.
_bursting = true;
_burstEndsMs = now + 1000;
_burstIndex++;
@@ -288,11 +217,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
TryDump(ds, "depth");
}
// Also capture what the draws READ. Measured 21/07: the 640x360 R32G32Uint buffer that
// the glow draws sample never showed up among the render targets, because nothing
// DRAWS into it -- it is written by a COMPUTE shader. That is exactly how tiled/
// clustered lighting builds its per-tile light lists, so the buffer has to be captured
// from the read side or not at all.
if (_dumpInputs)
{
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) => TryDump(tex, $"input{stage}"));
@@ -313,27 +237,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// One capture per distinct SHAPE, not per texture: the same few buffers are rebound
// thousands of times a second and what we need is one picture of each kind.
string name = $"{role}_{tex.Info.Width}x{tex.Info.Height}_{tex.Info.FormatInfo.Format}";
// The artefact is INTERMITTENT and only shows while the camera moves, so a handful of
// snapshots will miss it. We need many captures over a long window -- which means the
// disk budget has to be spent where the answer is. The 21/07 dumps showed the composed
// scene lives in the 1280x720 R11G11B10Float target; that is the one that answers the
// bisection question "do the blocks exist in the game's own image?". Everything else
// is skipped unless RYUJINX_RT_DUMP_ALL=1.
if (role != "PRESENTED" && !_dumpAll && !_dumpInputs &&
!tex.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
{
return;
}
// [28/07] L'ADRESSE GPU DANS LE NOM DU FICHIER. Sans elle, les dumps sont nommes par
// SLOT (color0, color5...) alors que MvppUiProbe raisonne en ADRESSES : impossible de
// croiser les deux, et un meme buffer lie a trois slots ressort en triple exemplaire
// (color0/5/6 du 28/07 avaient un decor identique a +0,957 -- c'etait le meme).
// Deduplication par ADRESSE et plus par nom : un buffer par contenu reel.
ulong gpuAddr = 0;
try
@@ -342,7 +253,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
catch
{
// pas d'adresse exploitable : on garde 0, le nom reste unique par le slot
}
if (!_doneThisBurst.Add(gpuAddr != 0 ? $"@{gpuAddr:X}" : name))
@@ -1,6 +1,3 @@
// 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;
@@ -8,32 +5,6 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ pipeline-shape inventory probe (RYUJINX_MVPP_SCAN=1). Purpose: games whose camera the
/// capture never arms on (XC2: pub 0 across three configs, native and modded alike - the
/// existing counters only tick AFTER a successful publication, so they cannot say WHICH stage
/// of the chain fails).
///
/// v1 (21/07 morning) answered the first question and killed the working hypothesis: XC2 DOES
/// have non-square depth passes (1280x720 D24S8, ~9k draws / 5 s) so the scene-pass filter is
/// not the blocker; the camera IS present (PROJ fx=1.358 aspect=1.778, exact 16:9); and the
/// canonical [view][proj][viewproj] contiguous row-major triplet the capture requires exists
/// NOWHERE (canonical 0 over the whole run) because XC2 stores view and proj in SEPARATE
/// constant buffers, column-major.
///
/// v2 fixes v1's own defect and lifts the VIEW noise:
/// - v1 only paired proj x view WITHIN one buffer, so on a game that splits them across
/// buffers the product search could never fire (the vp-products 0 was the probe's limit,
/// not the game's shape). v2 pairs GLOBALLY, across every bound buffer and stage.
/// - Any proper rotation passes IsOrthonormalView, so bone palettes / normal matrices /
/// identities flooded the candidate list (~400 hits). v2 rejects near-identity matrices
/// and matrices sitting in a stride-64 RUN (the signature of a skinning palette), then
/// ranks what survives by PROOF: a (proj, view) pair whose product is actually STORED in
/// guest memory is the camera - that is evidence, not a heuristic.
/// - Both multiplication conventions are tested (proj x view AND view x proj), and the
/// product is looked for in either storage order.
/// Read-only, completely off without the env var, self-disabling on any error.
/// </summary>
static class MvppScanProbe
{
private static bool _enabled =
@@ -42,9 +13,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private const int PassWindowMs = 5000;
private const int DeepScanIntervalMs = 1000;
private const int DeepScanFallbackMs = 500;
// Sized so one deep scan stays a few milliseconds at 1 Hz: the product search is
// buffers x windows x products, and that product must not become a visible hitch.
// 4 KiB covers everything measured so far (proj at +0x1F0, views at +0x000).
private const int MaxBytesPerBuffer = 4096;
private const int MaxDetailLines = 28;
@@ -54,8 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private const int MaxViewPaired = 10;
private const int MaxProducts = MaxProjPaired * MaxViewPaired * 2;
// ---- Stage A: pass inventory (cheap per-draw counters, 5 s windows) ----
private struct DepthForm
{
public int Width;
@@ -65,12 +31,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
public int Count;
}
// Aspect of the MAIN render target, i.e. the largest NON-SQUARE depth bound during the
// window. Measured 21/07 on XC2: taking the aspect of whatever draw the 1 Hz tick landed
// on made 56 of 117 verdicts empty, because the tick often lands on a shadow pass
// (1024x1024 -> aspect 1.0) and every 16:9 camera was then rejected as mismatched. The
// reference has to be the scene target, not the current draw. Largest-area wins, promoted
// once per window so a resolution change is picked up without flapping.
private static int _liveMainW;
private static int _liveMainH;
private static long _liveMainArea;
@@ -88,8 +48,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _colorFormCount;
private static int _colorFormOverflow;
// ---- Stage B/C: deep scan (1 Hz, prefers a draw with a depth-stencil bound) ----
private static long _lastDeepScanMs;
private static bool _lastScanOnMainPass;
private static bool _wantDeepScan;
@@ -113,9 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// A diagnostic probe must never take the process down: it runs on the GPU thread,
// where an escaped exception is fatal and unlogged (crash of 2026-07-02 08:22,
// WER e0434352 with a silent Ryujinx log). On any error: disable and tell why.
try
{
OnDrawImpl(channel);
@@ -131,8 +86,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
long now = Environment.TickCount64;
// ---- Stage A: per-draw counters (int adds + a tiny linear form table) ----
_draws++;
Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil;
@@ -156,8 +109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_liveMainW = w;
_liveMainH = h;
// Usable straight away: the first deep scans happen before the first
// window flush, and an empty reference is what caused the 56 blanks.
if (_mainAspect <= 0f)
{
_mainAspect = (float)w / h;
@@ -168,11 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
RecordDepthForm(w, h, ds.Info.FormatInfo.Format, channel.TextureManager.RenderTargetScale > 1f);
}
// COLOUR targets too. Measured 21/07 indoors on XC2: the depth stayed a rock-steady
// 1280x720 for 20 straight windows while the artefact was on screen -- but a game can
// perfectly well scale its COLOUR target and leave depth alone, which is a common way
// to do dynamic resolution. Reporting only depth would have let me call dynamic
// resolution "eliminated" while never having looked at the buffer it actually resizes.
Image.Texture c0 = channel.TextureManager.RenderTargetColor0;
if (c0 != null)
@@ -190,22 +136,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_passWindowStartMs = now;
}
// ---- Stage B/C: 1 Hz deep scan. Prefer a draw with ANY depth bound (most likely
// scene-adjacent); fall back to any draw if none showed up for 500 ms. ----
if (!_wantDeepScan && now - _lastDeepScanMs >= DeepScanIntervalMs)
{
_wantDeepScan = true;
_wantDeepScanSinceMs = now;
}
// Scan on a draw of the MAIN scene pass whenever possible: the constant buffers bound
// at that instant are the ones the game is drawing the world with. Measured 21/07 on
// XC2 -- two legitimate 16:9 cameras move at once (same fx, different positions), and
// neither the vote nor the motion can separate them; what separates them is WHICH
// pass is being drawn when they are bound. That is the pipeline SHAPE, which is the
// criterion this whole chantier is built on. Falls back to any depth-bound draw, then
// to any draw, so a game that never matches still gets measured.
bool onMainPass = ds != null && ds.Info.Width != ds.Info.Height &&
(long)ds.Info.Width * ds.Info.Height >= _liveMainArea;
@@ -325,7 +261,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
// Promote the window's main target as the aspect reference for the next window.
if (_liveMainH > 0)
{
_mainAspect = (float)_liveMainW / _liveMainH;
@@ -345,27 +280,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_deepScans = 0;
}
// ---- Deep scan ----
private enum MatClass
{
Proj, // perspective projection, row-major
ProjT, // perspective projection, column-major (transposed storage)
View, // orthonormal view, row-major
ViewT, // orthonormal view, column-major
Proj,
ProjT,
View,
ViewT,
}
private struct Candidate
{
public int Stage;
public int Slot;
public int Offset; // in BYTES from the start of the buffer
public int Offset;
public MatClass Class;
public float A; // proj: m00 (fx) | view: camera pos X
public float B; // proj: m11 (fy) | view: camera pos Y
public float C; // view: camera pos Z (unused for proj)
public bool InRun; // sits in a stride-64 chain = skinning palette signature
public bool Identity; // near-identity = structurally meaningless
public float A;
public float B;
public float C;
public bool InRun;
public bool Identity;
}
private struct BufRef
@@ -380,7 +313,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly BufRef[] _bufs = new BufRef[MaxBuffers];
private static int _bufCount;
// Products to look for, built in phase 2 and searched in phase 3.
private static readonly float[] _products = new float[MaxProducts * 16];
private static readonly int[] _prodProjIdx = new int[MaxProducts];
private static readonly int[] _prodViewIdx = new int[MaxProducts];
@@ -391,19 +323,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly float[] _tmpB = new float[16];
private static readonly float[] _tmpT = new float[16];
// ---- Stage E: consensus verdict ----
// Measured 21/07 on the TOTK control run (ground truth = the triplet's viewproj at
// stage0 cbuf8 +0x080, the address the current capture publishes): a lone structural
// validator is NOT enough - it named 13 candidates, and picking any of them would have
// put the camera on a wrong matrix ~10% of frames = the ghosting family we spent weeks
// killing. What DOES separate them: the game copies its real view-projection into
// SEVERAL constant buffers (one per shader that needs it), so the true camera is the
// position that independent buffers AGREE on. Scored over 52 scans:
// consensus alone ................ 47/52 (5 misses)
// consensus + degenerate guard ... 52/52 (0 miss)
// The 5 misses were a 3-strong cluster at (2, -0, -0): two coordinates EXACTLY zero,
// the signature of a misaligned window landing on padding rather than a camera.
// Still measurement only - nothing here feeds MvppCameraCapture.
private struct VpSolo
{
public int Stage;
@@ -419,15 +338,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
public float Motion;
public float Straightness;
// [DIVERG C 01/08] La matrice elle-meme (transposition resolue), pour mesurer la
// divergence de ROTATION contre la camera publiee — dimension absente de tous les
// journaux jusqu'ici (revue 310). Scratch local au fil GPU, 8 Ko statiques.
public System.Numerics.Matrix4x4 M;
// Physical address of the buffer this was read from. The SAME buffer is routinely
// bound to several shader stages (TOTK: stage0 cbuf8 and stage4 cbuf8 are the same
// bytes), and counting it once per stage would inflate the consensus with copies of
// a single source. Votes are counted per distinct address.
public ulong Address;
}
@@ -435,39 +347,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly VpSolo[] _vpSolos = new VpSolo[MaxVpSolos];
private static int _vpSoloCount;
// Position agreement tolerance: the same camera read from two buffers can differ by a
// float rounding, never by a world unit.
private const float PosAgreeEps = 0.5f;
// Temporal lock on the elected location (rule 4), keyed on the same stable identity as
// the motion history: address + offset, never the cbuf slot.
private static bool _hasLock;
private static ulong _lockAddress;
private static int _lockOffset;
// Per-location motion history (rule 3). Measured 21/07 on XC2: the runner-up was not
// junk at all, it was the CAMERA-RELATIVE view-projection (the one engines build for the
// skybox, with the translation removed by construction). It is structurally a perfect
// view-projection, carries the SAME fx as the real camera, and no static test can tell
// them apart -- which is why every static guard I tried failed on one game or the other.
// What separates them is movement:
// real camera .......... 67 world units, and it TRAVELS
// camera-relative ...... ~0.3, pinned to the origin forever
// placeholder sentinel .. 20000, constant forever
// Comparing positions cannot work (the 20000 sentinel would out-scale the real camera
// and get it rejected). Comparing MOTION separates all three, with no threshold and no
// unit: the camera is the one that moves. Motion is remembered per location for the whole
// session, so a single moment of movement is enough to mark it for good.
private struct Tracked
{
// Identity is (physical ADDRESS, offset), NOT (stage, slot, offset). Measured 21/07
// on XC2 and this settled a whole day of wandering verdicts: stage0 cbuf4 +0x000 was
// bound to 27 DIFFERENT buffers across 113 sightings. A cbuf slot is a drawer the
// game rebinds between draws, so every motion and straightness score computed on a
// slot was averaging unrelated objects -- which is exactly why a real camera looked
// like it "hopped around". Addresses, by contrast, persist (45 distinct in a session,
// the top one seen 91 times), and address+offset separates cleanly: @2297C86100+0x070
// is the travelling camera while @2297C86100+0x180 is the frozen sky matrix.
public ulong Address;
public int Offset;
public float LastX;
@@ -477,7 +364,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
public float MaxStep;
public int Seen;
// Ring of recent positions, used for the straightness ratio (see TrackMotion).
public int RingHead;
public int RingCount;
public float PathLength;
@@ -489,7 +375,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly Tracked[] _tracks = new Tracked[MaxTracks];
private static int _trackCount;
// Positions ring, flattened: track i occupies [i * RingSize, (i + 1) * RingSize).
private static readonly float[] _ringX = new float[MaxTracks * RingSize];
private static readonly float[] _ringY = new float[MaxTracks * RingSize];
private static readonly float[] _ringZ = new float[MaxTracks * RingSize];
@@ -513,8 +398,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
System.Text.StringBuilder detail = null;
int detailLines = 0;
// ---- Phase 1: collect every matrix-shaped candidate across ALL stages/slots ----
for (int stage = 0; stage < Constants.ShaderStages; stage++)
{
uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(stage);
@@ -596,13 +479,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
MarkRuns(firstCand);
// ---- Stage D: STANDALONE view-projection recognition ----
// Measurement only: this is the candidate contract for games that never
// store the [view][proj][viewproj] triplet (XC2 stores the VP alone at
// stage0 cbufN +0x000, row-major, proven 21/07 by cross-buffer pairing).
// Nothing is wired into MvppCameraCapture yet - the point of this pass is
// to show, in a log, that it accepts the RIGHT matrix on XC2 (same campos
// as the pair proof) and does not fire wrongly on TOTK.
for (int i = 0; i + 16 <= data.Length; i += 4)
{
ReadOnlySpan<float> m = data.Slice(i, 16);
@@ -622,8 +498,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ReadOnlySpan<float> vpm = transposed ? _tmpT : m;
// Same rival guard as the capture: a SQUARE projection is the
// environment-cubemap camera, structurally indistinguishable otherwise.
if (MathF.Abs(fy / fx - 1f) < 0.2f)
{
_lastStandaloneSquare++;
@@ -637,8 +511,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ViewProjPos(vpm, fx, fy, out float px, out float py, out float pz);
// Sentinel/degenerate positions (XC2 showed a [20000 20000 20000]
// placeholder camera): not a place anything is ever rendered from.
if (!float.IsFinite(px) || !float.IsFinite(py) || !float.IsFinite(pz) ||
MathF.Abs(px) > 1e6f || MathF.Abs(py) > 1e6f || MathF.Abs(pz) > 1e6f)
{
@@ -669,20 +541,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
};
}
// The physical address goes in the log because a cbuf SLOT is not an
// identity: the game rebinds slot N to a different buffer between draws,
// so tracking (stage, slot, offset) may be following a drawer rather than
// what is inside it. Hypothesis raised 21/07 after the lock wandered over
// five separately-proven addresses; this line is what will settle it.
AppendDetail(ref detail, ref detailLines,
$"VP-SOLO{(transposed ? "^T" : " ")} stage{stage} cbuf{slot} +0x{i * 4:X3} " +
$"@{channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot):X10} " +
$"fx={fx:0.###} fy={fy:0.###} aspect={fy / fx:0.###} campos=[{px:0.#} {py:0.#} {pz:0.#}]");
}
// Canonical triplet the capture expects TODAY ([view][proj][viewproj],
// row-major, contiguous). Kept so the log can still say outright whether the
// CURRENT contract would match anywhere at all.
for (int i = 0; i + 48 <= data.Length; i += 4)
{
if (IsOrthonormalView(data.Slice(i, 16)) &&
@@ -699,8 +563,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// ---- Phase 2: build the products of every (proj, view) pair, GLOBALLY ----
int projCount = 0;
int viewCount = 0;
int viewKept = 0;
@@ -726,8 +588,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
BuildProducts(channel);
// ---- Phase 3: is any of those products actually STORED in guest memory? ----
int vpProducts = SearchProducts(channel, ref detail, ref detailLines, ref foundFp);
if (layoutFp != _lastCbufLayoutFp)
@@ -743,9 +603,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_lastCanonicalCount = canonical;
_lastStandaloneVp = standaloneVp;
// Stage E verdict: emitted on EVERY scan (not gated on the found-set changing) --
// its whole point is to be readable as a time series, so a wrong pick on a single
// scan cannot hide behind an unchanged fingerprint.
System.Text.StringBuilder verdict = null;
int verdictLines = 0;
LogVerdict(ref verdict, ref verdictLines);
@@ -759,7 +616,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
_lastFoundSetFp = foundFp;
// Projections first (the solid signal), then the views that survived the filter.
for (int i = 0; i < _candCount && detailLines < MaxDetailLines; i++)
{
ref Candidate c = ref _cands[i];
@@ -803,23 +659,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Stage E: picks THE camera among the standalone view-projection candidates, by the two
/// rules the 21/07 TOTK control run scored (52/52 together, 47/52 for the vote alone):
/// 1. the projection's aspect must match the render target's (kills the inverse-matrix
/// family at 0.563 and the junk at 0.16 - a ratio, never a resolution, so it stays
/// game-agnostic);
/// 2. among survivors, the winner is the position the MOST INDEPENDENT BUFFERS agree
/// on, after dropping degenerate positions (two coordinates exactly zero = a window
/// straddling two matrices, not a camera).
/// Logged, never published: this is the candidate contract for step 4, not a decision.
/// </summary>
/// <summary>
/// Records this candidate's displacement since the previous scan and returns the largest
/// single-step displacement ever observed at that exact location. Session-wide, so one
/// moment of movement marks a location as mobile for good - the camera does not stop
/// being the camera when the player stands still.
/// </summary>
private static float TrackMotion(in VpSolo c, out float straightness)
{
straightness = 1f;
@@ -850,19 +689,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ref Tracked t = ref _tracks[idx];
t.Seen++;
// RECENT motion, not the session maximum. Measured 21/07 on XC2: with a session-wide
// maximum, stage0 cbuf6 +0x040 held the lock for 72 scans while reporting the exact
// same [67 -7.8 53.9] every single time -- a constant that had changed value once (a
// load, a spawn) and stayed "mobile" for ever after. Decaying the score makes a
// matrix that stopped moving fade out over ~10 scans while a travelling camera keeps
// its score high. When EVERYTHING is still (player not moving) all scores decay
// together, bestMotion goes to zero, rule 3 stops filtering and the lock simply
// holds -- which is the correct behaviour, not a special case.
// 0.5 chosen by simulation on the measured numbers, not by feel: replaying the run's
// own values (a 163-unit load spike then nothing, against a camera stepping ~12/scan)
// a frozen matrix falls under rule 3's bar after 8 scans at 0.5, versus 22 at 0.8 --
// and the travelling camera's score is unchanged either way (~13). Faster decay buys
// responsiveness for free here.
t.MaxStep *= 0.5f;
if (t.HasLast)
@@ -872,28 +698,16 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
float dz = c.Z - t.LastZ;
float step = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
// A teleport-sized jump is a scene cut or a garbage read, not travel: it must not
// crown a constant matrix that flipped value once.
if (float.IsFinite(step) && step < 1e5f && step > t.MaxStep)
{
t.MaxStep = step;
}
}
// STRAIGHTNESS: net displacement over the ring divided by the path actually walked.
// Measured 21/07 on XC2, and this is the criterion that finally separates the last
// two survivors -- both 16:9, both genuinely changing every scan:
// camera (cbuf5 +0x000) .......... 0.57 it goes somewhere
// per-object matrix (cbuf4+0x000) 0.02 it hops around a point
// A camera travels; a per-draw object transform jumps to wherever the next object
// is. Raw motion magnitude cannot tell them apart -- it actively favours the jumper,
// which is exactly how the lock landed on it. A ratio of two lengths: no unit, no
// threshold in world units, nothing tuned to a game.
int slot = idx * RingSize;
if (t.RingCount == RingSize)
{
// Drop the step leaving the window.
t.PathLength -= _ringStep[slot + t.RingHead];
}
@@ -952,8 +766,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
float targetAspect = _mainAspect;
// Rule 1 + motion bookkeeping: keep only the aspect-valid candidates, and update
// each one's motion history BEFORE any decision is taken.
float bestMotion = 0f;
float bestStraight = 0f;
int considered = 0;
@@ -999,20 +811,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
continue;
}
// Rule 3: it has to MOVE like the most mobile candidate does. A location whose
// position never budged while another travelled is the camera-relative matrix
// (or a constant placeholder), not the camera. Relative to the best mover, so
// there is no threshold in world units and nothing tuned to a game. While
// nothing has moved yet (start of session, player standing still) bestMotion is
// 0 and this test lets everything through -- the verdict then says "CHAUFFE".
if (bestMotion > 0f && a.Motion < bestMotion * 0.1f)
{
continue;
}
// Rule 3b: it has to TRAVEL, not hop. A per-draw object transform changes every
// scan (so it sails through rule 3) but wanders around a point instead of going
// anywhere. Relative to the straightest mover, so still no absolute threshold.
if (bestStraight > 0f && a.Straightness < bestStraight * 0.35f)
{
continue;
@@ -1020,8 +823,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
movers++;
// Rule 2: how many DISTINCT buffers report this same world position? Counted per
// physical address, so one buffer bound to several stages is one voice, not two.
int votes = 0;
for (int j = 0; j < _vpSoloCount; j++)
@@ -1072,22 +873,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// The lock must never survive its own location failing a rule: that is exactly how
// the 21/07 run stayed pinned on the sky matrix for 15 scans while the DIVERGENCE
// line showed the consensus was right all along.
if (_hasLock && lockedIdx < 0)
{
_hasLock = false;
}
// Rule 3: TEMPORAL LOCK. Consensus alone still lost on XC2, where a 3-strong cluster
// sitting near the origin outvoted the real 2-strong camera on some scans (its
// coordinates were tiny but NOT exactly zero, so the TOTK-tuned degenerate guard --
// cut against a single game, the very trap the 3-case rule exists to catch -- never
// fired). Once a location has been elected, we keep READING THAT LOCATION as long as
// it still yields a consensus-backed camera, and only re-elect when it stops. Same
// shape as MvppCameraCapture's cached slot + lineage gate, which is proven in the
// field. Divergences are logged, never silently resolved.
int winner;
string mode;
@@ -1100,11 +890,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
winner = elected;
// A lock must never be taken on a decision we could not check. Measured 21/07:
// during warm-up the aspect reference is still unknown, EVERY candidate passes
// rule 1 by default, and a lock taken then (on a 0.563 portrait matrix) survived
// long after the reference became available. Only lock on a scan that was both
// aspect-checked AND taken on the main scene pass.
bool trustworthy = targetAspect > 0f && bestMotion > 0f && _lastScanOnMainPass;
mode = trustworthy ? (_hasLock ? "RE-ELU" : "ELU") : "PROVISOIRE";
@@ -1147,16 +932,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"mouvement={w.Motion:0.##} -- {(lockedIdx >= 0 ? lockedVotes : electedVotes)} sources d'accord, " +
$"{movers} mobiles sur {considered} au bon aspect ({_vpSoloCount} bruts){pass}{divergence}");
// [DIVERG C 01/08] PHASE C SEULE — calibrage du declencheur consensus-divergence
// (journal 308/310/311). LECTURE PURE : une ligne par verdict, aucune decision,
// aucun chemin de comportement. Compare le VAINQUEUR (par VALEUR, jamais par siege)
// a la camera PUBLIEE. dpos = distance des positions ; drot = ecart max des 9
// elements du bloc 3x3 (meme metrique que RotGap cote capture). ⚠️ La publiee est
// POST-DEJITTER : les deltas portent ce terme sous-pixel, negligeable devant les
// tolerances visees (~0,1) — dit pour l'interpretation. bornes : OK si dpos < 150 u
// (borne de concordance, tue l'intruse a 546/626 u) ET |pos| >= 5 u (anti-origine).
// n = divergences CONSECUTIVES en bornes (dpos>3 ou drot>0,1) — la matiere premiere
// des distributions demandees (Δpos, Δrot, persistance, corr. fenetres de gel).
if (MvppSoloCamera.TryGetPublishedForProbe(out System.Numerics.Matrix4x4 pubVp,
out float pubX, out float pubY, out float pubZ))
{
@@ -1189,7 +964,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [DIVERG C 01/08] Divergences consecutives en bornes (voir la ligne DIVERG).
private static int _dgConsecutive;
private static bool TryReadBuffer(GpuChannel channel, int stage, int slot, out ReadOnlySpan<float> data)
@@ -1206,8 +980,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
try
{
// The bound ranges are already TRANSLATED: physical addresses, not GPU VAs
// (SetGraphicsUniformBuffer runs TranslateAndCreateBuffer). Read physical.
data = MemoryMarshal.Cast<byte, float>(channel.MemoryManager.Physical.GetSpan(address, size));
}
catch
@@ -1218,12 +990,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return true;
}
/// <summary>
/// Flags candidates that sit in a stride-64-byte chain of 3+ orthonormal matrices: the
/// signature of a skinning palette (an array of bone matrices), which is what floods the
/// VIEW candidate list on any character-heavy game. The camera's view matrix lives in a
/// small per-frame header, not in such a run.
/// </summary>
private static void MarkRuns(int firstCand)
{
for (int i = firstCand; i < _candCount; i++)
@@ -1300,7 +1066,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
viewUsed++;
// Both conventions: proj x view (row-vector engines) and view x proj.
Multiply(_tmpA, _tmpB, _products.AsSpan(_prodCount * 16, 16));
_prodProjIdx[_prodCount] = pi;
_prodViewIdx[_prodCount] = vi;
@@ -1344,7 +1109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
ReadOnlySpan<float> exp = _products.AsSpan(k * 16, 16);
// Two-element prefilter before the full 16-element compare.
bool direct = Close(w[0], exp[0]) && Close(w[5], exp[5]) && MatchesLoose(w, exp, false);
bool transposed = !direct &&
Close(w[0], exp[0]) && Close(w[5], exp[5]) && MatchesLoose(w, exp, true);
@@ -1409,27 +1173,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return true;
}
/// <summary>
/// Structural recognition of a STANDALONE view-projection matrix, row-major.
///
/// VP = P x V with P perspective (row 3 = [0,0,+/-1,0]) and V rigid ([R|t], R orthonormal)
/// expands to:
/// row0.xyz = fx * R.row0 row1.xyz = fy * R.row1
/// row2.xyz = A * R.row2 row3.xyz = +/- R.row2
/// so the shape is fully constrained without ever seeing P or V:
/// (1) ||row3.xyz|| == 1 (it IS a rotation row)
/// (2) ||row0.xyz|| = fx > 0, ||row1.xyz|| = fy > 0
/// (3) row0.xyz, row1.xyz, row3.xyz mutually orthogonal (scaled rotation rows)
/// (4) row2.xyz parallel to row3.xyz (both along R.row2)
/// A skinning bone matrix fails (1) outright: its row 3 is [0,0,0,1], xyz norm 0.
/// The caller still has to apply the square-projection rival guard (cubemap cameras).
/// </summary>
private static bool IsViewProj(ReadOnlySpan<float> m, out float fx, out float fy)
{
fx = 0f;
fy = 0f;
// (1) the w-carry row must be a unit direction.
float n3 = MathF.Sqrt(m[12] * m[12] + m[13] * m[13] + m[14] * m[14]);
if (MathF.Abs(n3 - 1f) > 0.02f)
@@ -1437,9 +1185,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return false;
}
// (2) focal scales, with sanity bounds. Measured 21/07 on XC2: without an upper
// bound, junk windows sail through (fx=1318930, fy=2.88e18) and poison the position
// maths into NaN. A real focal term lives in a narrow band whatever the game.
float n0 = MathF.Sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
float n1 = MathF.Sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]);
@@ -1449,7 +1194,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return false;
}
// (3) mutual orthogonality of the three rotation-derived rows.
float d01 = (m[0] * m[4] + m[1] * m[5] + m[2] * m[6]) / (n0 * n1);
float d03 = (m[0] * m[12] + m[1] * m[13] + m[2] * m[14]) / n0;
float d13 = (m[4] * m[12] + m[5] * m[13] + m[6] * m[14]) / n1;
@@ -1459,7 +1203,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return false;
}
// (4) row2.xyz must be collinear with row3.xyz (cross product ~ 0).
float n2 = MathF.Sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]);
if (n2 > 1e-4f)
@@ -1474,11 +1217,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// (5) a BARE perspective projection satisfies (1)-(4) exactly, because a projection
// IS a view-projection whose view is the identity. Mathematically right, useless to
// us: it carries no camera. Measured 21/07 on XC2 - the pure projections at cbuf3
// +0x030/+0x180/+0x1F0 all reported campos=[0 0 0]. Reject the identity-view case:
// rotation ~ identity AND translation ~ 0.
bool rotIsIdentity =
MathF.Abs(m[0] / n0 - 1f) < 1e-3f && MathF.Abs(m[1]) < 1e-3f && MathF.Abs(m[2]) < 1e-3f &&
MathF.Abs(m[4]) < 1e-3f && MathF.Abs(m[5] / n1 - 1f) < 1e-3f && MathF.Abs(m[6]) < 1e-3f &&
@@ -1498,13 +1236,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return true;
}
/// <summary>
/// Recovers the camera position from a STANDALONE view-projection matrix, by rebuilding
/// the rigid view rows: R.row0 = row0.xyz / fx, R.row1 = row1.xyz / fy, R.row2 = row3.xyz,
/// and t = (row0.w / fx, row1.w / fy, row3.w). Position = -R^T t, same convention as
/// <see cref="ViewPos"/>, so the two can be compared directly - that comparison is the
/// probe's self-test on XC2 (both must land on the same coordinates).
/// </summary>
private static void ViewProjPos(ReadOnlySpan<float> m, float fx, float fy, out float x, out float y, out float z)
{
float r00 = m[0] / fx, r01 = m[1] / fx, r02 = m[2] / fx;
@@ -1522,7 +1253,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static void ViewPos(ReadOnlySpan<float> m, out float x, out float y, out float z)
{
// view = [R|t] row-major, camera position = -R^T t.
x = -(m[0] * m[3] + m[4] * m[7] + m[8] * m[11]);
y = -(m[1] * m[3] + m[5] * m[7] + m[9] * m[11]);
z = -(m[2] * m[3] + m[6] * m[7] + m[10] * m[11]);
@@ -1657,10 +1387,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return fp;
}
// ---- Structural validators: exact replicas of MvppCameraCapture's (kept private
// there on purpose - the probe must observe the SAME contract it diagnoses, without
// touching the capture code). ----
private static bool IsOrthonormalView(ReadOnlySpan<float> m)
{
if (MathF.Abs(m[12]) > 1e-3f || MathF.Abs(m[13]) > 1e-3f ||
@@ -1,68 +1,9 @@
// 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;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// ⛔⛔ MORT PAR LA MESURE, 29/07 AU SOIR -- NE PAS RE-ARMER, NE PAS RE-REGLER. Sonde `HDRTRY` :
/// la passe de scene HDR existe dans **98,9 %** des images 3D, mais la camera n'y est **lisible
/// que dans 36,5 %** des cas. N'autoriser QUE cette passe ferait donc tomber la publication de
/// 83 % a ~37 % : la camera serait AFFAMEE, et une camera manquee fait ecrire zero sur toute
/// l'image. Ce n'est pas un probleme de reglage du filet -- le filet mal dimensionne (voir plus
/// bas) n'etait que le symptome. Le remplacant est <see cref="MvppLdrSkip"/>, qui ECARTE la pire
/// passe au lieu d'en ELIRE une. Ce fichier est conserve pour la trace, et parce que
/// <see cref="IsSceneColorPass"/> sert encore a la sonde.
///
/// [SCENEPASS 29/07 SOIR] Capturer la camera sur LA PASSE DE LA SCENE, et non sur la premiere
/// passe 3D venue (RYUJINX_MVPP_SCENEPASS=1, coupe par defaut).
///
/// POURQUOI, ET SUR QUELLE MESURE. <see cref="MvppCameraCapture.OnDraw"/> capture au PREMIER
/// dessin de l'image qui passe le pre-filtre, puis se tait. Ce pre-filtre reconnait "une passe
/// 3D", pas "LA passe de la scene". Recensement CTXPROBE du 29/07 au soir, run etalon de 3 min,
/// 3 633 lectures et 177 intruses -- du GROS volume, pas trois evenements tires au sort :
///
/// cible couleur branchee a la lecture lectures intruses taux
/// R8G8B8A8Unorm (8 bits, LDR) 1 340 119 8,9 % <- 67 % des intruses
/// aucune (passe profondeur seule) 2 044 52 2,5 %
/// R11G11B10Float (couleur HDR de scene) 230 3 1,3 % <- la vraie passe
/// R32Float 19 3 15,8 %
///
/// La passe 8 bits donne SEPT FOIS plus d'intruses que la couleur HDR et fournit deux tiers du
/// total. Le discriminant que le dossier croyait inexistant n'est pas dans la MATRICE (fermé le
/// 27/07 : vue-projection structurellement parfaite, rien ne l'identifie) -- il est dans la
/// PASSE. Et il y a de quoi choisir : 125 passes qualifiantes par image en moyenne, dont ~87
/// sur la couleur HDR.
///
/// LE CRITERE EST GENERIQUE -- forme de pipeline, rien d'autre : cible couleur attachee, NON
/// CARREE (une cible carree est un atlas d'ombres), format flottant HDR. C'est mot pour mot
/// celui que <see cref="MvppColorSnapshot"/> utilise deja pour epingler la couleur de scene.
/// Aucune adresse, aucun hash, aucune resolution, aucun nom de jeu.
///
/// ⛔ CE QUE CE FICHIER NE PROMET PAS. Attendu : 4,9 % d'intruses -> 1,3 %, soit environ 73 %
/// de moins. PAS zero : la passe de scene elle-meme en porte 3 sur 230. C'est une attenuation,
/// obtenue a la source au lieu d'un filtre pose apres coup -- ce n'est pas une execution.
///
/// ⚠️ LE RISQUE EST LA PUBLICATION, PAS L'INTRUSE, et c'est la borne INFERIEURE qui protege
/// (lecon du 29/07 : pour un composant dont le role est de refuser, prouver "il ne refuse
/// jamais plus" est la moitie inutile de la question). Une camera manquee fait ecrire ZERO sur
/// toute l'image. Donc la restriction s'auto-desarme, en deux etages :
/// 1. RATTRAPAGE -- deux images d'affilee avec des passes qualifiantes mais AUCUNE capture,
/// et la restriction se leve pour 300 images (comportement d'avant, a l'identique). Perte
/// bornee : 2 images sur 300, soit 0,7 %.
/// 2. DESARMEMENT -- cinq rattrapages, et la restriction se coupe pour la session, avec une
/// ligne de journal explicite. Si la passe HDR n'est pas fiable sur ce jeu, on le DIT au
/// lieu de saigner 2 images sur 3.
/// Le taux de publication se lit dans l'instrument qui existe deja ("MVPP capture window:
/// enq/pub") : mesure de reference du 29/07 au soir = 6 761 / 8 127 = 83,2 %. En dessous, le
/// correctif est mauvais quel que soit son gain sur les intruses.
///
/// Les compteurs sont approximatifs : OnDraw tourne sur le fil GPU, OnFrame sur celui de la
/// presentation. On ne batit aucune conclusion fine dessus -- seulement des ordres de grandeur.
/// </summary>
static class MvppScenePass
{
public static readonly bool Enabled =
@@ -86,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _catchUpFramesSeen;
private static long _lastReportMs;
/// <summary>
/// Appelee sur un dessin qualifiant tant que l'image n'a pas encore sa camera. Rend false
/// pour SAUTER la tentative de capture sur ce dessin -- ce qui laisse la capture se faire
/// plus loin dans la MEME image, sur la passe de la scene.
/// </summary>
public static bool Allows(GpuChannel channel)
{
_qualThisFrame = true;
@@ -110,12 +46,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return false;
}
/// <summary>
/// Reconnait la passe de la scene a la FORME de sa cible couleur : attachee, non carree,
/// format flottant HDR. Meme critere que <see cref="MvppColorSnapshot"/>. Une cible 8 bits
/// (interface, composition, passe auxiliaire) et une passe sans couleur du tout echouent
/// ici -- ce sont exactement les deux contextes qui portaient 96 % des intruses.
/// </summary>
internal static bool IsSceneColorPass(GpuChannel channel)
{
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
@@ -130,17 +60,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return f == GAL.Format.R11G11B10Float || f == GAL.Format.R16G16B16A16Float;
}
/// <summary>
/// Une fois par image presentee, AVANT que le drapeau de capture ne soit re-arme. C'est ici
/// que vit le filet de securite : une image qui avait des passes qualifiantes et n'a rien
/// capture est une image potentiellement perdue, et on ne l'accepte pas deux fois de suite.
/// </summary>
public static void OnFrame(bool captured)
{
if (!_qualThisFrame)
{
// Image sans passe 3D (menu, chargement) : elle n'a jamais eu de camera a prendre,
// la compter comme un manque ferait declencher le rattrapage pour rien.
return;
}
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -12,20 +9,8 @@ using System.Text;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// [MVPP SURVIVE PROBE -- TEMPORARY DIAGNOSTIC, DISPOSABLE] Owns the host buffer backing the
/// fragment survivor-map SSBO (fixed binding MvppSurviveProbe.HostBinding). Lazily creates the
/// buffer on first use, clears it to zero, hands out the range for the per-session graphics
/// bind in StateUpdater.CommitBindings, and (Phase 2) periodically reads the map back and
/// dumps it as a PGM image for offline inspection. Gated by RYUJINX_MVPP_SURVIVE_PROBE
/// (checked by the caller); when OFF nothing here ever runs.
/// </summary>
static class MvppSurviveProbeBuffer
{
// Window count is env-tunable: the timer starts at the first scene bind, and the TotK
// TITLE SCREEN is a real 3D scene (inject runs on it) -- with a slow load (shader cache
// off) the default 3 windows can be fully consumed before gameplay starts. More windows
// keep rolling into gameplay; menu windows are discarded offline.
private static readonly int MaxDumps =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_SURVIVE_DUMPS"), out int maxDumps) && maxDumps > 0
? maxDumps
@@ -37,12 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static long _nextDumpMs = -1;
private static int _dumpCount;
/// <summary>
/// Gets the survivor-map buffer range, creating and zero-clearing the buffer on first call.
/// Must be called from the GPU thread (StateUpdater runs there; GAL calls are queued).
/// </summary>
/// <param name="context">GPU context used to reach the host renderer</param>
/// <returns>Range covering the whole survivor map</returns>
public static BufferRange GetRange(GpuContext context)
{
if (_handle == BufferHandle.Null)
@@ -56,19 +35,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"binding {MvppSurviveProbe.HostBinding}) and cleared to zero.");
}
// write:true -- the fragment epilogue stores into this buffer; the flag keeps the
// backend's modification tracking honest (and the mirror capture carries it too).
return new BufferRange(_handle, 0, MvppSurviveProbe.BufferSize, true);
}
/// <summary>
/// [P3 scene window] Gets the DUMMY buffer range: same size as the real mask (the shader's
/// bounds guard is sized for the full map, so a smaller target would be out-of-bounds),
/// bound for every draw that is NOT on the pinned scene depth. Write-only garbage sink,
/// never read back, never cleared after creation.
/// </summary>
/// <param name="context">GPU context used to reach the host renderer</param>
/// <returns>Range covering the dummy buffer</returns>
public static BufferRange GetDummyRange(GpuContext context)
{
if (_dummyHandle == BufferHandle.Null)
@@ -82,14 +51,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return new BufferRange(_dummyHandle, 0, MvppSurviveProbe.BufferSize, true);
}
/// <summary>
/// [Phase 2] Every DumpIntervalMs (first window starts after the bind), reads the survivor
/// map back to the CPU, logs the non-zero pixel count, writes a binary PGM (P5) next to the
/// executable, then clears the buffer so the next window accumulates fresh. At most
/// MaxDumps dumps per session; a no-op before the buffer exists. The readback is a
/// deliberate one-shot stall (diagnostic only).
/// </summary>
/// <param name="context">GPU context used to reach the host renderer</param>
public static void MaybeDump(GpuContext context)
{
if (_handle == BufferHandle.Null || _dumpCount >= MaxDumps)
@@ -113,7 +74,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_nextDumpMs = now + DumpIntervalMs;
_dumpCount++;
// Make the fragment shader stores visible, then read the whole map back.
context.Renderer.Pipeline.Barrier();
long nonZero = 0;
@@ -130,16 +90,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
for (int i = 0; i < words.Length; i++)
{
// Words now hold the packed viewport size (W<<16|H) of the writing pass; the
// raw .bin dump below carries the full values for the offline histogram.
if (words[i] != 0)
{
nonZero++;
pgmPixels[i] = 255;
// MapStride is 4096 = 2^12, so y = i >> 12 and x = i & 4095. The bounding
// box of written pixels measures the REAL scene write extent (settles the
// dynamic-resolution question from the quadrant observation).
int x = i & (MvppSurviveProbe.MapStride - 1);
int y = i >> 12;
@@ -179,7 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"sceneDepthPin={MvppCameraCapture.SceneDepthHostWidth}x{MvppCameraCapture.SceneDepthHostHeight} " +
$"-> {path} + raw .bin");
// Fresh window for the next dump.
context.Renderer.Pipeline.ClearBuffer(_handle, 0, MvppSurviveProbe.BufferSize, 0);
}
}
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -10,29 +7,14 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// MV++ UI-separability probe (RYUJINX_MVPP_UIPROBE=1). READ-ONLY, default off, nothing in the
/// render path branches on it. Answers ONE question with measured data: are the HUD/UI draws
/// written into a render target whose GPU address DIFFERS from the final composite buffer (=
/// separable = capturable), or are they alpha-blended IN-PLACE onto the same buffer that already
/// holds the game-upscaled scene (= not separable without a diff)?
///
/// Method: at every DrawEnd it groups consecutive draws that share the same colour-RT address into
/// "epochs" and records per epoch { RT address, format, dims, RenderTargetScale, any-depth-test,
/// any-blend, draw count }. A frame boundary is the rising edge of RenderTargetScale (UI/native
/// passes run at 1x, the scaled 3D scene at >1x -- established in MvppP2Probe). At each boundary it
/// classifies the epochs (HDR scene / LDR composite / blended overlay) and logs whether the blended
/// overlay writes the composite address (IN-PLACE) or a different one (SEPARATE). The raw epoch list
/// is logged for the first frames so the verdict can be read from data, not from the classifier.
/// </summary>
static class MvppUiProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE") == "1";
private const int MaxEpochsPerFrame = 64; // hard cap so a pathological frame cannot grow unbounded
private const int FullDetailFrames = 8; // log every epoch for the first frames, then only the verdict
private const int VerdictThrottleMs = 2000; // steady-state: one verdict line per change or per 2 s
private const int MaxEpochsPerFrame = 64;
private const int FullDetailFrames = 8;
private const int VerdictThrottleMs = 2000;
private struct Epoch
{
@@ -44,8 +26,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
public bool AnyDepth;
public bool AnyBlend;
public int Draws;
public int NoDepthDraws; // [HUDSPLIT] dessins sans test de profondeur = candidats HUD
public int FirstNoDepthAt; // rang du premier d'entre eux dans l'epoque (1 = des le debut)
public int NoDepthDraws;
public int FirstNoDepthAt;
}
private static readonly List<Epoch> _epochs = new();
@@ -53,60 +35,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _frameNo;
private static int _framesLoggedFull;
private static long _lastVerdictMs;
private static long _lastDetailMs; // [28/07] cadence du dump detaille en regime de jeu
private static long _lastDetailMs;
// [EPOCHDUMP 28/07] RYUJINX_MVPP_UIPROBE_DUMP=<secondes avant armement>, 0 = OFF.
//
// POURQUOI CE MODE EXISTE. Les captures de render targets photographient soit un instant
// arbitraire, soit la PRESENTATION -- et a ce moment-la le buffer LDR pleine resolution
// qui recoit 95 a 99 dessins par image est deja RECYCLE : mesure du 28/07, il ressort
// VIDE alors que la sonde d'epoques le voit travailler. C'est exactement pour ca qu'on
// n'a jamais pu regarder ce que le jeu y peint.
//
// Ici on capture le render target AU MOMENT OU SON EPOQUE SE FERME, c'est-a-dire juste
// apres son dernier dessin et avant qu'il serve a autre chose. Une seule image capturee,
// toutes ses epoques, puis la sonde se desarme d'elle-meme.
private static readonly int _dumpAfterSeconds =
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE_DUMP"), out int ds) && ds > 0 ? ds : 0;
private static long _dumpArmedAtMs;
private static bool _dumpDone;
private static int _dumpSeq;
private static Image.Texture _epochTex; // le RT de l'epoque en cours, garde pour la capture
private static Image.Texture _epochTex;
// [HUDLESS 28/07] RYUJINX_MVPP_HUDLESS=1 : DETECTION SEULE, ne capture rien, ne change rien.
//
// Reconnait, PAR SA FORME, le render target qui porte l'image tonemappee SANS interface --
// celui que la Frame Generation reclame et qu'on n'a jamais pu lui donner. Signature
// mesuree le 28/07 sur XC2 : format 8 bits par canal (donc MEME espace colorimetrique que
// l'image presentee, ce qui rend la soustraction UI = backbuffer - hudless valide),
// PLEINE largeur de rendu, et une epoque nourrie (~95-99 dessins) -- une passe d'interface
// n'en aurait que quelques-uns, une passe d'effet serait plus petite.
//
// JAMAIS par adresse : 0x2280250000 change d'un lancement a l'autre.
//
// Ce mode existe pour verifier que la regle attrape le bon buffer A TOUS LES COUPS avant
// qu'on branche quoi que ce soit dessus. Le dossier a deja paye une regle non verifiee.
private static readonly bool _hudless =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS") == "1";
private const int HudlessMinDraws = 20;
// [HUDLESSFEED 28/07] RYUJINX_MVPP_HUDLESS_FEED=1 : COPIE le buffer sans interface vers une
// texture persistante, et la publie pour la couche Vulkan. Desarme par defaut.
//
// POURQUOI UNE COPIE ET PAS UNE REFERENCE. Le render target est RECYCLE juste apres son
// epoque -- c'est demontre : toutes les captures faites au present le trouvaient VIDE.
// Publier une simple reference donnerait donc a la Frame Generation un contenu deja
// ecrase. La copie est faite a l'instant exact de la fermeture, seul moment ou le buffer
// contient l'image.
//
// POURQUOI ON COPIE CHAQUE CANDIDAT PLUTOT QUE LE SEUL ELU. L'election ne peut se faire
// qu'a la fin de l'image, quand le buffer n'existe plus. On copie donc tout candidat au
// moins aussi nourri que le meilleur vu dans cette image : le dernier ecrase les
// precedents, et c'est justement lui le bon (29 dessins puis 81 -- mesure du 28/07).
//
// Cout : une copie GPU pleine resolution par candidat, 1 a 2 par image.
private static readonly bool _hudlessFeed =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS_FEED") == "1";
@@ -120,13 +63,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static int _renderWidthSeen;
private static int _hudlessHits;
private static int _hudlessFrames;
private static int _hudlessMultiple; // images ou PLUSIEURS epoques matchent = regle ambigue
private static int _hudlessMultiple;
private static long _hudlessLogMs;
private static int _hudlessInFrame;
private static int _hudlessBestDraws; // [v2] meilleur candidat de l'image en cours
private static int _hudlessBestDraws;
private static ulong _hudlessBestAddr;
private static int _hudlessBestRank;
private static int _hudlessRetenus; // images ou un candidat a bien ete elu
private static int _hudlessRetenus;
private static int _hudlessEchecsLogues;
private static string _lastVerdictSig = "";
@@ -137,8 +80,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// A diagnostic on the GPU thread must never take the process down: an escaped exception is
// fatal and unlogged. On any error: disable and say why (same hardening as the other probes).
try
{
OnDrawImpl(channel, ref state, context);
@@ -150,11 +91,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// Vraie frontière d'image, appelée depuis Gpu/Window.Present. Indépendante de ResScale, donc
/// fonctionne sur les jeux rendus à l'échelle native (XC2) contrairement à la détection par
/// montée du facteur de résolution, qui n'a de sens que si la scène 3D tourne au-dessus de 1x.
/// </summary>
public static void OnPresent()
{
if (!_enabled)
@@ -180,14 +116,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
float scale = channel.TextureManager.RenderTargetScale;
// Frame boundary = rising edge of the render scale (1x UI/native -> >1x scaled 3D scene = a
// new frame started). Flush what we accumulated for the previous frame.
//
// [21/07] /!\ CETTE DETECTION NE MARCHE QUE SI ResScale > 1. Sur XC2 (ResScale = 1) le facteur
// ne bouge jamais, la condition n'est JAMAIS vraie et la sonde reste MUETTE -- un run entier
// perdu à cause de ça. C'est la 3e sonde héritée du chantier TOTK inutilisable telle quelle sur
// XC2 (avec MvppGlowProbe, filtrée R11G11B10, et MvppHdrCensusProbe, filtrée width >= 1500).
// La vraie fin d'image est maintenant donnée par OnPresent(), appelé depuis Gpu/Window.Present.
if (_prevScale == 1f && scale > 1f && _epochs.Count > 0)
{
FlushFrame();
@@ -208,10 +136,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
h = col0.Info.Height;
}
bool depth = state.DepthTestEnable; // Boolean32 -> implicit bool
bool depth = state.DepthTestEnable;
bool blend = state.BlendEnable[0];
// Same-RT run detection: extend the current epoch, or open a new one on an address change.
if (_epochs.Count > 0)
{
ref Epoch last = ref System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_epochs)[_epochs.Count - 1];
@@ -221,11 +148,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
last.AnyDepth |= depth;
last.AnyBlend |= blend;
// [HUDSPLIT 28/07] Granularite PAR DESSIN. AnyDepth agrege toute l'epoque :
// une epoque qui melange scene et interface ressort "depth=True" et cache
// exactement la frontiere qu'on cherche. Le HUD ne teste jamais la profondeur
// -- compter les dessins SANS test, et retenir le rang du PREMIER, donne le
// point ou l'interface commence a etre posee sur l'image.
if (!depth)
{
if (last.NoDepthDraws == 0)
@@ -240,7 +162,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [HUDLESS] L'epoque qui vient de se fermer correspond-elle a la signature ?
if (_hudless && _epochs.Count > 0)
{
Epoch fini = _epochs[^1];
@@ -256,19 +177,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
_hudlessInFrame++;
// [v2 28/07] LE DERNIER, LE PLUS NOURRI. Mesure : dans certaines situations le
// jeu ecrit DEUX FOIS dans ce buffer par image (29 dessins puis 81) -- la v1
// comptait les deux (149 % de detections, 74 images ambigues sur 151). L'image
// se construit progressivement : le bon candidat est le dernier ET le plus
// nourri. On ne tranche donc qu'a la FIN de l'image, jamais a la volee.
if (fini.Draws >= _hudlessBestDraws)
{
_hudlessBestDraws = fini.Draws;
_hudlessBestAddr = fini.Addr;
_hudlessBestRank = _epochs.Count;
// [HUDLESSFEED] C'est ICI, et nulle part ailleurs, que le buffer contient
// encore l'image : son epoque vient de se fermer, il n'a pas encore servi.
if (_hudlessFeed && _epochTex != null && context != null)
{
CopyHudless(context);
@@ -277,8 +191,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [EPOCHDUMP] L'epoque precedente vient de se fermer : son render target contient
// encore ce que le jeu vient d'y peindre. C'est le SEUL instant ou on peut le voir.
TryDumpEpoch();
_epochTex = col0;
@@ -300,12 +212,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// [HUDLESSFEED] Copie le render target sans interface vers une texture persistante, puis
/// la publie pour la couche Vulkan via le pont GAL existant (MvppColorSnapshot). Aucune
/// modification du GAL n'est necessaire -- ce pont date du 12/07 et vit deja dans le
/// binaire deploye, contrairement a DlssCameraState qui, lui, a bouge le 28/07 a 11h46.
/// </summary>
private static void CopyHudless(GpuContext context)
{
try
@@ -322,8 +228,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
int h = src.Info.Height;
Format fmt = src.Info.FormatInfo.Format;
// Recreation seulement si la FORME change (resolution dynamique, changement de
// cible) : une texture par forme, pas une par image.
if (_hudlessCopy == null || _hudlessCopyW != w || _hudlessCopyH != h || _hudlessCopyFmt != fmt)
{
_hudlessCopy?.Release();
@@ -358,10 +262,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
/// <summary>
/// [EPOCHDUMP] Capture le render target de l'epoque qui vient de se fermer. Lecture seule :
/// aucune ecriture GPU, aucun changement de rendu. Une seule image, puis desarmement.
/// </summary>
private static void TryDumpEpoch()
{
if (_dumpAfterSeconds == 0 || _dumpDone || _epochTex == null)
@@ -420,9 +320,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
_frameNo++;
// Classify. HDR (R11G11B10Float / RG16BA16F) = the 3D scene target; 8-bit = candidate final
// composite / UI. Composite = the largest-area LDR epoch (the output-res backbuffer). Overlay
// = LDR epochs that alpha-blend (the HUD draws). The decision is purely an ADDRESS comparison.
int sceneIdx = -1, compIdx = -1;
long sceneArea = -1, compArea = -1;
@@ -446,8 +343,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ulong compAddr = compIdx >= 0 ? _epochs[compIdx].Addr : 0;
bool blendIntoComposite = false; // HUD alpha-blends onto the final buffer itself
ulong separateUiAddr = 0; // a distinct RT that alpha-blends (would be capturable)
bool blendIntoComposite = false;
ulong separateUiAddr = 0;
int separateUiW = 0, separateUiH = 0;
for (int i = 0; i < _epochs.Count; i++)
@@ -456,7 +353,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
bool hdr = e.Fmt == Format.R11G11B10Float || e.Fmt == Format.R16G16B16A16Float;
if (hdr || !e.AnyBlend)
{
continue; // only LDR blended (overlay-like) epochs matter for the UI question
continue;
}
if (e.Addr == compAddr)
@@ -476,7 +373,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
blendIntoComposite ? "INPLACE(NO-GO)" :
"UNCLEAR";
// Per-frame verdict line, throttled to a change of the verdict signature or every 2 s.
string sig = $"{verdict}|comp0x{compAddr:X}|ui0x{separateUiAddr:X}";
long now = Environment.TickCount64;
if (sig != _lastVerdictSig || now - _lastVerdictMs >= VerdictThrottleMs)
@@ -499,13 +395,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"scene={sceneStr} composite={compStr} blendIntoComposite={blendIntoComposite} separateUiRT={uiStr}");
}
// First frames: dump the full epoch list so the verdict is auditable from raw data.
//
// [28/07] ET AUSSI UNE FOIS TOUTES LES 5 s ENSUITE. Motif : les 8 premieres images
// tombent sur le logo/l'ecran de chargement (mesure du 28/07 17h51 : une seule epoque,
// 768x432), donc le detail n'a JAMAIS decrit une image de jeu reelle. Or c'est
// exactement ce detail qu'il faut pour savoir comment le HUD est compose avant de
// pouvoir le sortir de l'image donnee a la Frame Generation.
bool periodic = now - _lastDetailMs >= 5000;
if (_framesLoggedFull < FullDetailFrames || periodic)
@@ -526,9 +415,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [HUDLESS] Bilan par image, puis resume a 1 Hz. Ce qu'on veut lire : UNE seule
// epoque candidate par image (regle non ambigue) et UNE detection sur CHAQUE image
// (regle qui ne decroche pas). Tout ecart se voit dans ces trois chiffres.
if (_hudless)
{
_hudlessFrames++;
@@ -538,7 +424,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_hudlessMultiple++;
}
// [v2] Election de fin d'image : UN candidat retenu, et un seul.
if (_hudlessBestDraws > 0)
{
_hudlessRetenus++;
@@ -552,12 +437,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [v2] ECHEC D'ELECTION : dire CE QU'ON A VU plutot que de le contourner par un
// repli. Mesure du 28/07 : la regle tient a 100 % dans la plupart des situations
// mais tombe a 83,9 % par moments -- 24 images sur 149 sans aucun candidat. Un
// HUD-less manquant sur une image = la FG travaille sur du perime, donc exactement
// l'artefact intermittent qu'on veut supprimer. Il faut savoir ce que sont ces
// images avant de decider quoi faire.
else if (_hudlessEchecsLogues < 8)
{
_hudlessEchecsLogues++;
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,24 +6,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
/// <summary>
/// Viewport-vs-render-target census (RYUJINX_VP_PROBE=1). READ-ONLY, default off.
///
/// XC2 lateral-band artefact, 21/07. What is already MEASURED: the presented guest image is 1280x720
/// and the present blits the FULL rectangle (`src[0,0,1280,720]`, constant over a whole capture run),
/// so the corrupt side bands are already inside the guest image -- it is not a crop or a stretch.
/// What is NOT measured, on this game, ever: the VIEWPORT. Pixels of a render target lying OUTSIDE the
/// current viewport are neither cleared nor defined (the host texture is always allocated at the FULL
/// guest size, and nothing wipes the remainder), so a game that shrinks its viewport -- dynamic
/// resolution, which XC2 is known for -- leaves stale content on the sides of an otherwise fresh frame.
/// The earlier "dynamic resolution eliminated" verdict only ever compared SURFACE sizes, which is
/// precisely what dyn-res keeps constant; it never looked at the viewport.
///
/// This probe answers one question: does XC2's viewport cover its render target, and if not, is it
/// CENTRED (two bands, matching the observed artefact) or anchored to one edge (a single band)?
/// Stats are kept PER RENDER-TARGET SHAPE -- mixing the scene pass with shadow and low-resolution
/// passes is what produced empty verdicts on the earlier probes.
/// </summary>
static class MvppViewportProbe
{
private static bool _enabled =
@@ -38,37 +17,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private sealed class Stats
{
public int Draws;
public int Narrow; // viewport does not cover the target horizontally
public int Short; // ... vertically
public int TransformOff; // ViewportTransformEnable == 0 (screen-scissor path)
public int Narrow;
public int Short;
public int TransformOff;
public float MinX, MaxX, MinW, MaxW, MinY, MaxY, MinH, MaxH;
public bool Seeded;
// [v2, 21/07] The viewport came back covering the target on 91 398 draws, so the OTHER half of
// the same mechanism is what matters now: the SCISSOR, and the SCREEN scissor that feeds the
// texture-width heuristic. Neither has ever been measured on this game's scene passes -- the one
// scissor test on record only ever looked at draws targeting the HDR buffer.
// [v3, 21/07] The three values NOTHING in this emulator ever reads, and that no probe in this
// dossier has ever looked at. All three are structurally symmetric in X, which is the one
// property the artefact demands and that every mechanism eliminated so far fails to provide.
// - ViewportExtents.X/Width : the guest's viewport CLIP rectangle. The struct is documented
// "viewport extents for viewport clipping", but a grep over StateUpdater shows only
// DepthNear/DepthFar are consumed (l.1240-1241, 2117-2119). Vulkan does not clip to the
// viewport -- only scissor and framebuffer do -- so an ignored clip means we rasterise
// fragments the console discards. X=m, Width=W-2m cuts m pixels off BOTH sides.
// - sign of ScaleX : a negative ScaleX is a horizontal MIRROR. StateUpdater.cs:1204 destroys
// it with MathF.Abs, and so did v1/v2 of this very probe -- the measurement copied the bug.
// A mirror leaves the centre invariant and grows outward symmetrically: the exact geography.
// - swizzle X : NegativeX and the X<->Y permutations are never consumed by the Vulkan backend.
public int ClipZero; // register never written (X==0 && Width==0)
public int ClipFull; // clip covers the target
public int ClipCuts; // clip leaves pixels out on the left or the right
public int ClipZero;
public int ClipFull;
public int ClipCuts;
public int ClipMinX, ClipMaxX, ClipMinW, ClipMaxW;
public bool ClipSeeded;
public int A2c; // alpha-to-coverage actif sur ce draw
public int A2cDither; // ... ET dither actif => chemin du discard en damier
public int NegScaleX; // ScaleX < 0 -> horizontal mirror the backend cannot express
public int SwizzleXOdd; // swizzle X is not PositiveX
public int A2c;
public int A2cDither;
public int NegScaleX;
public int SwizzleXOdd;
public void AddClip(int x, int w)
{
@@ -86,8 +49,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (w > ClipMaxW) { ClipMaxW = w; }
}
public int ScissorOn; // scissor enabled at all
public int ScissorCuts; // scissor leaves uncovered pixels left or right of the target
public int ScissorOn;
public int ScissorCuts;
public int ScissorSeen;
public int SciMinX1, SciMaxX1, SciMinX2, SciMaxX2;
public int ScrMinW, ScrMaxW, ScrMinH, ScrMaxH;
@@ -149,16 +112,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private static readonly Dictionary<(int W, int H), Stats> _byShape = new();
// [ADDR, 21/07] Modes d'adressage réellement demandés par le jeu, TOUTES passes confondues.
// Ryujinx substitue quatre modes (EnumConversion.cs:97-110), chacun marqué "TODO: Should be ..." :
// Clamp -> ClampToEdge · MirrorClamp -> ClampToEdge · MirrorClampToBorder -> ClampToBorder
// + tout mode inconnu -> ClampToEdge
// Le vrai Clamp renvoie la COULEUR DE BORDURE hors [0,1] ; ClampToEdge RÉPLIQUE le dernier texel.
// Demander le premier et recevoir le second ne donne pas une bordure : ça donne une TRAÎNÉE --
// exactement l'étirement vertical photographié par Alex quand il monte/descend la caméra.
// L'élimination du journal ("modes d'adressage cohérents") venait de MvppGlowProbe, filtrée sur les
// seules cibles R11G11B10 : elle ne couvrait ni le 512x288, ni le buffer d'ids, ni le compute.
// Ici AUCUN filtre. Échantillonné 1 draw sur 16 pour que l'énumération ne coûte pas la frame.
private static readonly int[] _addrU = new int[16];
private static readonly int[] _addrV = new int[16];
private static int _addrBindings;
@@ -177,7 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
catch (Exception e)
{
// A bench instrument must never take the emulator down with it.
_enabled = false;
Logger.Warning?.Print(LogClass.Gpu, $"MVPP VPPROBE: disabled after unexpected error: {e}");
}
@@ -187,7 +139,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
if (!_announced)
{
// Armed proof before any measurement: a switch you cannot SEE in the log is worth nothing.
_announced = true;
Logger.Info?.Print(LogClass.Gpu,
"MVPP VPPROBE: ON -- viewport vs render target, read-only, one line per shape per second.");
@@ -209,8 +160,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
return;
}
// Same arithmetic as StateUpdater.UpdateViewportTransform (l.1204-1221), minus the render-target
// scale (inert at ResScale=1) and the swizzle handling, which do not move the rectangle.
ref ViewportTransform transform = ref state.ViewportTransform[0];
float scaleX = MathF.Abs(transform.ScaleX);
@@ -236,8 +185,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
st.TransformOff++;
}
// "Narrow" = the viewport leaves uncovered pixels on the left or the right of the target.
// One pixel of slack absorbs the float arithmetic; real dyn-res steps are tens of pixels.
if (vpX > 1f || vpX + vpW < rtW - 1f)
{
st.Narrow++;
@@ -248,9 +195,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
st.Short++;
}
// Scissor: same question as the viewport, other register. A scissor narrower than the target
// leaves its sides unwritten, which is exactly the observed geography. Disabled or maxed out,
// it becomes (0,0,0xffff,0xffff) and protects nothing -- that case counts as "no cut".
ScissorState sc = state.ScissorState[0];
bool scEnabled = sc.Enable;
@@ -265,13 +209,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Screen scissor: it is the size hint that decides the created texture WIDTH
// (TextureCache.GetMinimumWidthInGob), so a moving screen scissor can hand back the same guest
// surface at slightly different widths. Worth a number before anyone theorises about it.
ScreenScissorState scr = state.ScreenScissorState;
st.AddScreen(scr.Width, scr.Height);
// [v3] The clip rectangle the emulator decodes and throws away.
ViewportExtents ext = state.ViewportExtents[0];
int clipX = ext.X;
int clipW = ext.Width;
@@ -294,7 +234,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [v3] Raw sign, NOT MathF.Abs -- this is the whole point of the v3 pass.
if (transform.ScaleX < 0f)
{
st.NegScaleX++;
@@ -305,12 +244,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
st.SwizzleXOdd++;
}
// [A2C, 21/07] LA mesure qui décide du dossier. Ryujinx émule le fondu par transparence en
// REJETANT des pixels selon un damier 2x2 (EmitterContext.GenerateAlphaToCoverageDitherDiscard,
// masque 0xfbb99110, discard si le bit est à 0). Sur la console ce fondu est lissé par le
// multi-échantillonnage ; ici les pixels sont jetés et il reste une grille de trous dans la
// géométrie -- exactement le motif régulier qu'Alex photographie. Ce chemin n'est actif que si
// le jeu pose les DEUX drapeaux. Si a2c=0 sur XC2, l'hypothèse meurt en un run.
if ((state.MultisampleControl & 1) != 0)
{
st.A2c++;
@@ -321,8 +254,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Modes d'adressage demandés, toutes passes. 1 draw sur 16 : l'énumération des bindings est
// le seul point coûteux de cette sonde.
if ((++_addrSampled & 15) == 0)
{
channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) =>
@@ -360,8 +291,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
continue;
}
// Left/right margins of the widest and narrowest viewport seen on this shape. Two roughly
// equal margins = a centred viewport = two lateral bands. One-sided = a single band.
float leftMin = s.MinX;
float rightMin = entry.Key.W - (s.MinX + s.MinW);
@@ -396,8 +325,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_addrBindings > 0)
{
// Les modes SUBSTITUES par EnumConversion sont nommes en clair : ce sont les seuls qui
// peuvent transformer une bordure attendue en replication de texel (= trainee).
Logger.Info?.Print(LogClass.Gpu,
$"MVPP ADDRPROBE: {_addrBindings} bindings echantillonnes | U: {DescribeModes(_addrU)} | V: {DescribeModes(_addrV)}");
@@ -182,18 +182,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
float scale = _channel.TextureManager.RenderTargetScale;
float divisor = scale * scale;
// Fractional-crash probe (quality-mode dossier): sample counts are divided
// by scale^2 before the game reads them -- under a FRACTIONAL scale the
// divisor itself is fractional (1.333^2 = 1.777...), a value regime the
// integer daily never exercises. Sampled 1/32 to name the family without
// flooding; silent at integer scales.
// [31/07] GATE AJOUTE. Elle etait en release SANS aucun interrupteur : la
// condition « echelle fractionnaire » est remplie par TOUT utilisateur en DLSS
// quality ou performance (DlssIntegration pilote GraphicsConfig.ResScale ;
// 4K + quality = 1440/1080 = 1,334). Mesure du 31/07 sur GYLT : 21 471 lignes
// en 3 minutes -- soit ~690 000 requetes de compteur -- chez quelqu'un qui
// n'avait aucun probleme a diagnostiquer. La sonde rend son service UNE fois
// par rapport de bug ; elle ne doit pas ecrire le reste du temps.
if (GAL.MvppDev.Enabled && scale != System.MathF.Floor(scale) && (_fractProbeCount++ & 31) == 0)
{
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
@@ -20,8 +20,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
public const int ShaderStateIndex = 26;
public const int RtColorMaskIndex = 14;
// MV++ (b1) Tier 1 targeted restore: the graft's state groups that lacked a named index
// (positions confirmed against the _updateTracker registration order).
public const int StencilTestStateIndex = 4;
public const int DepthTestStateIndex = 5;
public const int DepthBiasStateIndex = 11;
@@ -39,58 +37,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private readonly GpuContext _context;
private readonly GpuChannel _channel;
// JITTER-UI probe: unique viewport sizes already logged (one line per pass geometry).
private static readonly System.Collections.Generic.HashSet<int> _jitterUiProbeSeen = new();
// [JITTER-PASSPROBE, 07/07] Coverage audit for the "un-jittered pass inside a de-jittered image"
// hypothesis: under CamPassOnly, log every unique world-pass geometry once with its cameraPass
// verdict. A depth-bound pass reported cameraPass=FALSE = geometry rasterized WITHOUT jitter while
// DLSS treats the whole frame as jittered => wobble (the far-foliage shimmer). Gated on
// RYUJINX_DLSS_JITTER_PASSPROBE=1; off => zero cost, no behavior change.
private static readonly bool _camPassProbe =
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_PASSPROBE") == "1";
private static readonly System.Collections.Generic.HashSet<(int, int, bool)> _camPassProbeSeen = new();
// [JITTER-WORLDRES, 07/07] Also jitter world render-resolution passes that do NOT bind depth
// (forward/transparent geometry -- distant tree imposters / alpha foliage). The PASSPROBE showed
// only the single opaque depth-bound pass is jittered while a same-resolution no-depth pass is
// not, which under DLSS wobbles = the far-foliage shimmer. Gated; default OFF, no behavior change.
private static readonly bool _worldResJitter =
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES") == "1";
// [WORLDRES-LOW, 20/07 — natif] At native+4K the July selection window ([In/2, In*0.95],
// designed at 2x) leaves the HALF-render-res world passes (1600x900 for a 3200x1800 render:
// cloth/tents/particles/far LOD) un-jittered inside a de-jittered image => residual world
// trembling in motion (run #7, PASSPROBE proof in docs/JITTER-BANC.md). This knob widens the
// low bound to In/3 for 16:9 passes, depth-bound or not (the camera pass needs >= In/2, so no
// overlap). Same NDC path as WORLDRES. Gated; default OFF => byte-identical.
private static readonly bool _worldResLow =
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES_LOW") == "1";
// [FULLJIT, 09/07 — kill the inject blur] The camera-pass jitter amplitude denominator is the
// present-time InputWidth (published at present, one frame stale = the FINAL DLSS-input width 3200)
// instead of THIS frame's real render viewport (2666). So the geometry only receives
// vpW/InputWidth ~= 2666/3200 ~= 0.833x of the declared sub-pixel jitter -> DLSS gets less
// sub-pixel coverage than it thinks -> soft/blurry. The magnificent reference (mode B) had
// InputWidth == vpW == 2666 -> full jitter -> sharp. When ON, use the real viewport (vpW/vpH) as
// the denominator so the geometry gets the FULL declared jitter; pair with
// RYUJINX_DLSS_INJECT_JIT_SCALE=1.0 so DLSS is told the same full amount. Gated, default OFF =
// current 0.833x behavior BYTE-IDENTICAL. Daily is jitter-OFF entirely, so daily is unaffected.
private static readonly bool _injectFullJit =
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_INJECT_FULLJIT") == "1";
// [ALPHATEST-PROBE, 07/07] Does the game use the FIXED-FUNCTION alpha test (vs an in-shader
// discard)? If enable=True appears during gameplay, the hashed-alpha fix has a clean single
// hook (EmitterContext fragment epilogue). Gated on RYUJINX_DLSS_ALPHATEST_PROBE=1, default off.
private static readonly bool _alphaTestProbe =
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_ALPHATEST_PROBE") == "1";
private static readonly System.Collections.Generic.HashSet<(bool, int, int)> _alphaTestProbeSeen = new();
// [JITTERVAL probe, 07/07] Geometry side of the applied-vs-declared jitter proof. On the camera
// pass, once per frame id, log the EXACT inputs used to jitter gl_Position: the generated offset,
// the InputWidth denominator used, the real viewport, the NDC set, and the resulting applied pixel
// shift (offset*vp/InputWidth). Joined offline by frame id with the DLSS-side line. Gated,
// default OFF; reads only, no behavior change.
private static readonly bool _jitterval =
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTERVAL") == "1";
private static long _jittervalLastFrame = -1;
@@ -98,32 +63,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private readonly DrawState _drawState;
private readonly AdvancedBlendManager _blendManager;
// [JITTERVAL-RT probe] host (scaled) dims of the last-bound color[0] / depth render target,
// published by UpdateRenderTargetState and read by the camera-pass probe for the resource
// dimension proof (is the color texture really 3200, or 2666?). Written only when the probe is on.
private int _jvColorW, _jvColorH, _jvDepthW, _jvDepthH;
private readonly StateUpdateTracker<ThreedClassState> _updateTracker;
private readonly ShaderProgramInfo[] _currentProgramInfo;
// [SCENEPROBE] guest shader VAs of the current draw (read-only, for the scene-pass probe).
private ulong _probeSceneFsAddr;
private ulong _probeSceneVsAddr;
// [MVPP CUTOUT PROBE] Read-only per-draw tally: draws whose active FRAGMENT shader emits Discard
// (alpha cutout), + their host viewport, to PROVE where the cutout foliage draws are. Gated by
// RYUJINX_MVPP_CUTOUT_PROBE; default OFF => zero cost, no state touched, no render/DLSS/jitter change.
private static readonly bool _cutoutDrawProbe =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CUTOUT_PROBE") == "1";
private static int _cpTotalDraws, _cpCutoutDraws;
private static long _cpSummaryMs;
private static readonly System.Collections.Generic.Dictionary<(int, int), int> _cpCutoutViewports = new();
// [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] Gate + bind-swap state for the fragment
// survivor-map SSBO (see MvppSurviveProbe / MvppSurviveProbeBuffer). P3 scene window:
// 0 = nothing bound yet, 1 = real mask bound, 2 = dummy sink bound. Default OFF => no
// buffer, no binding, byte-identical.
private static readonly bool _surviveProbe =
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled;
private static int _surviveProbeTarget;
@@ -132,16 +86,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private ShaderSpecializationState _shaderSpecState;
private readonly SpecializationStateUpdater _currentSpecState;
// MV++ double-transform (b1) sub-step 2: generate+publish the MvppVariant program.
private static readonly bool _mvppVariantEnabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_VARIANT") == "1";
private static bool _variantRestoreLogged;
// (b1) sub-step 5a fix @91: the N-1 palette cbuf lives at the FIXED host binding 91 --
// outside every guest window (1..90), declared by the VARIANT programs only (no global
// reservation, no guest shift; single source of truth =
// MvppVariantInjector.MvppN1HostBinding). The run's BINDING MATCH log (injector vs here)
// catches any drift.
private static readonly bool _mvppN1Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_N1") == "1";
private static bool _n1ResolveLogged;
@@ -429,30 +377,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_prevTfEnable = true;
}
// [MVPP CUTOUT PROBE] Read-only per-draw tally. Runs after the bindings are committed, so
// _currentProgramInfo reflects this draw's shaders. Gated; touches no state.
if (_cutoutDrawProbe)
{
CutoutDrawProbeTick();
}
// [CONSTDIFF] Read-only per-draw: trace the builder's prev-frame matrix (journal 221).
if (Image.MvppConstDiffProbe.Enabled)
{
Image.MvppConstDiffProbe.OnDraw(_channel, _probeSceneFsAddr);
}
// [BUILDERIN] Read-only per-draw: which surfaces are actually bound to the builder's
// samplers and the exact constants it consumes (journal 226). Self-gated; always
// called so its per-draw input list is cleared even on non-builder draws.
Image.MvppBuilderInProbe.OnDraw(_channel, _channel.TextureManager, _probeSceneFsAddr);
// [MVHASH] Read-only: content occupancy of the MV twin at builder-draw time (242).
Image.MvppContentProbe.OnBuilderDraw(_channel.TextureManager, _probeSceneFsAddr);
// [SCENEPROBE] Read-only per-draw: identify the pass writing the 720p R11G11B10 scene.
// Same point as the cutout probe (after CommitBindings): RT, inputs and shader VAs are all
// current for this draw. Touches no state.
if (Image.MvppScenePassProbe.Enabled)
{
Image.MvppScenePassProbe.OnDraw(
@@ -462,8 +400,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Image.MvppFeedbackProbe.Frame);
}
// [MAP64] Read-only per-draw: who writes the small DoF maps, with the draw's viewport
// extents (a partial viewport leaves stale texels = the staleness suspect). Touches no state.
if (Image.MvppMap64Probe.Enabled)
{
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
@@ -478,8 +414,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
(int)(MathF.Abs(vp0.ScaleY) * 2f * vpScale));
}
// [MVBUF] Read-only per-draw: draws writing the object-MV buffer (R10G10B10A2 720p),
// with the guest per-RT blend enable and colour write mask (v3: state census).
if (Image.MvppMvBufProbe.Enabled)
{
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
@@ -511,10 +445,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
probeMask);
}
// [CENSUS] Read-only per-draw: MV-buffer writers vs the fingerprint-armed registry --
// flags the writers every value scrub missed. v2: write masks passed so bound-but-
// masked-off draws are counted apart. v3: classification by PROGRAM identity via
// ShaderProgramInfo (VA census defeated by code dedup). Touches no state.
if (Image.MvppWriterCensusProbe.Enabled)
{
Span<uint> censusMask = stackalloc uint[Constants.TotalRenderTargets];
@@ -544,8 +474,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Image.MvppWriterCensusProbe.OnDraw(_channel.TextureManager, _probeSceneFsAddr, censusMask, censusFragInfo);
}
// [TWINMAP] Read-only per-draw: writers per MV twin (by guest VA). v2: + live GMMU
// re-translation. v5: masks passed, all slots scanned (no early return). Touches no state.
if (Image.MvppTwinMapProbe.Enabled)
{
Span<uint> twinMask = stackalloc uint[Constants.TotalRenderTargets];
@@ -565,8 +493,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// [MVPP CUTOUT PROBE] Count this draw + (if its fragment shader is a cutout shader) tally it and its
// host viewport; log a summary ~every 3 s. Pure read; never modifies state or the render path.
private void CutoutDrawProbeTick()
{
_cpTotalDraws++;
@@ -653,31 +579,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_channel.BufferManager.CommitGraphicsBindings(_drawState.DrawIndexed);
// [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] Bind the survivor-map SSBO at the fixed
// out-of-window binding 80, ONCE per session, right after the guest storage commit.
// Descriptor updates are sparse per-index (DescriptorSetUpdater.SetStorageBuffers), the
// guest formula can never produce binding 80, and the persisted BufferRef survives
// program changes (SetProgram just marks all descriptors dirty and re-applies the refs)
// -- so one bind is enough and adds zero per-draw cost. Unbound-but-in-layout draws
// before this point fall back to the dummy buffer (UpdateBuffer dummyBuffer path).
if (_surviveProbe)
{
// [P3 scene window] Route the epilogue writes per draw: draws whose depth-stencil
// is the PINNED scene depth (MvppCameraCapture, requires RYUJINX_MVPP_CAP=1) hit
// the REAL mask; everything else (UI, shadow, texture-processing passes) hits a
// same-size dummy sink. Compared by GUEST ADDRESS -- host texture references can
// be recreated on rescale, which would silently break a reference compare. The
// descriptor is only rewritten on transitions (sparse per-index update, proven
// no-clobber); the check itself is one ulong compare per commit.
Image.Texture dsCur = _channel.TextureManager.RenderTargetDepthStencil;
ulong sceneDepthAddr = MvppCameraCapture.SceneDepthAddress;
int wantTarget = (dsCur != null && sceneDepthAddr != 0 &&
dsCur.Range.GetSubRange(0).Address == sceneDepthAddr) ? 1 : 2;
// [MIPMARK, 11/07] Preuve pixel : sous RYUJINX_MVPP_MIPMARK=1, seuls les draws de
// scène qui LIENT une texture 2D levels=1 minifiable (>=64px) écrivent le masque
// réel ; les autres vont au dummy. Lecture seule, routage de descriptor seulement.
if (wantTarget == 1 &&
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.MipMarkEnabled)
{
@@ -714,10 +623,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Logger.Info?.Print(LogClass.Gpu,
"MVPP-SURVIVE: scene-window bind-swap ACTIVE (mask<->dummy at binding 80, first scene match).");
// [Cutout bias mask] Also mirror the REAL mask once to the pass-through
// binding 81 (never in any layout, never swapped): the Vulkan backend
// captures the mapped range there so the DLSS inject can convert the mask
// (see MvppSurviveMaskState / DlssUpscaler).
_context.Renderer.Pipeline.SetStorageBuffers(
[new BufferAssignment(
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BackendMirrorBinding,
@@ -725,8 +630,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
}
// Periodic readback + PGM dump (cheap tick check per commit, at most 3 dumps per
// session; see MvppSurviveProbeBuffer.MaybeDump).
MvppSurviveProbeBuffer.MaybeDump(_context);
}
}
@@ -965,8 +868,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
oldScaleX != _channel.TextureManager.RenderTargetScaleX ||
oldScaleY != _channel.TextureManager.RenderTargetScaleY)
{
// The shader gl_FragCoord compensation must see the exact (quantized) ratio,
// not the nominal scale, or it lands short of the real content edge.
_context.SupportBufferUpdater.SetRenderTargetScale(_channel.TextureManager.RenderTargetScaleX);
UpdateViewportTransform();
@@ -1055,7 +956,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
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
Image.MvppSeamProbe.OnScaledCopy(
"SCISSOR",
null, null,
gx, gy, gx + gw, gy + gh,
@@ -1148,18 +1049,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Span<ViewportTransform> viewportTransformSpan = _state.State.ViewportTransform.AsSpan();
Span<ViewportExtents> viewportExtentsSpan = _state.State.ViewportExtents.AsSpan();
// DLSS Mode B: hand the shader this frame's sub-pixel jitter as a clip-space NDC offset, which it
// adds to gl_Position scaled by w (the correct, native-DLSS way to jitter -- not a viewport shift).
// Only the scaled (main) 3D pass; native passes (UI) keep RenderTargetScale 1 and are NOT jittered.
// The offset is 0 unless jitter is enabled, so the default path is unchanged.
float jitterNdcX = 0f, jitterNdcY = 0f;
// [JITTER-CAMPASS, 05/07 — the by-the-book mode] NVIDIA's rule: camera jitter is applied at
// ONE place, the main camera geometry pass. Shadows (light space), post chains (inherit) and
// UI are never jittered by an engine. Our legacy mode injected EVERY pass (the actual hack);
// this mode POSITIVELY identifies the camera pass -- the 16:9 pass with a depth buffer bound --
// and injects only there, with the amplitude expressed in FINAL DLSS-input pixels
// (NDC = offset*2 / inputSize) so applied == declared exactly, whatever the pass resolution.
if (DlssJitterState.Enabled && DlssJitterState.Inject && DlssJitterState.CamPassOnly)
{
ref ViewportTransform vp0 = ref viewportTransformSpan[0];
@@ -1172,8 +1063,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
DlssJitterState.InputWidth > 0 &&
vpW >= DlssJitterState.InputWidth / 2f && vpW <= DlssJitterState.InputWidth + 2f;
// [SUBRECT measurement] Publish the camera pass's host viewport = the game's current
// dynamic-resolution step, for the inject-side dump gate (see DlssJitterState.CamVpW).
if (cameraPass)
{
DlssJitterState.CamVpW = vpW;
@@ -1182,17 +1071,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
bool depthOn = _state.State.RtDepthStencilEnable;
// [JITTER-WORLDRES, 07/07] A world render-resolution pass (16:9, width in the camera-pass
// window) that FAILED cameraPass = a same-res pass without depth bound = forward/transparent
// geometry (distant tree imposters, alpha foliage). Kept strictly BELOW the final input
// width so the final composite / native UI passes stay excluded (they must not be jittered).
bool worldResPass = _worldResJitter && !cameraPass &&
aspect > 1.5f && aspect < 1.95f &&
DlssJitterState.InputWidth > 0 &&
vpW >= DlssJitterState.InputWidth / 2f &&
vpW <= DlssJitterState.InputWidth * 0.95f;
// [WORLDRES-LOW] The half-render-res world passes, below the July window.
bool worldResLowPass = _worldResLow && !cameraPass &&
aspect > 1.5f && aspect < 1.95f &&
DlssJitterState.InputWidth > 0 &&
@@ -1202,17 +1086,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (cameraPass || worldResPass)
{
// Amplitude denominator: default = the final DLSS-input size (present, 1-frame stale).
// FULLJIT uses THIS frame's real viewport so the geometry receives the FULL declared
// sub-pixel jitter (kills the under-coverage blur). OFF => byte-identical.
float denomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth;
float denomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight;
// [WORLDRES-LOW v2, 20/07] Sub-window passes are composited with a STRETCH to the
// final image (run #8 proof: vpW denominator => world tremble x2, matching the 2.4x
// overshoot the stretch model predicts). A stretched pass must receive its jitter as
// a fraction of the FINAL input, whatever FULLJIT says, so the on-screen shift equals
// the declared offset exactly.
if (worldResLowPass)
{
denomW = DlssJitterState.InputWidth;
@@ -1223,8 +1099,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
jitterNdcY = DlssJitterState.OffsetY * 2f / denomH;
}
// [JITTERVAL-GEO, 07/07] Ground truth of what was ACTUALLY applied to the geometry, once
// per frame on the real camera pass. appliedPx = offset * viewport / InputWidth (== ndc/2*vp).
if (_jitterval && cameraPass && DlssJitterState.FrameId != _jittervalLastFrame)
{
_jittervalLastFrame = DlssJitterState.FrameId;
@@ -1235,10 +1109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
float appliedPxX = inW > 0 ? offX * vpW / inW : 0f;
float appliedPxY = inH > 0 ? offY * vpH / inH : 0f;
var sc = _state.State.ScreenScissorState;
// [JITTERVAL v2, 20/07] The 07/07 fields above assume the LEGACY denominator (inW) and
// are blind to FULLJIT. truePx = offset scaled by the denominator ACTUALLY used for the
// NDC this frame (same expression as the inject block above), so the applied-vs-declared
// proof measures the real consumption point whatever the knobs. Log-only, same gate.
float jvDenomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth;
float jvDenomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight;
float truePxX = jvDenomW > 0f ? offX * vpW / jvDenomW : 0f;
@@ -1253,8 +1123,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
$"truePxX={truePxX:0.0000} truePxY={truePxY:0.0000}");
}
// [JITTER-PASSPROBE, 07/07] One line per unique (width, height, depth-bound) pass geometry.
// Watch for depth=False on a 16:9 render-res pass -> world geometry drawn without jitter.
if (_camPassProbe)
{
var probeKey = ((int)vpW, (int)vpH, depthOn);
@@ -1273,9 +1141,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
float vpWidth = MathF.Abs(vp0.ScaleX) * 2f * jScale;
float vpHeight = MathF.Abs(vp0.ScaleY) * 2f * jScale;
// [JITTER-NOSHADOW, 05/07] Camera jitter belongs to CAMERA passes. Square scaled passes
// (TOTK shadow atlas 2048/4096) render in LIGHT space: jittering them makes shadows swim
// -> shimmer on characters/foliage (the user's exact residual symptom with correct signs).
float passAspect = vpHeight > 0f ? vpWidth / vpHeight : 0f;
bool squarePass = passAspect > 0.95f && passAspect < 1.05f;
@@ -1287,13 +1152,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else if (DlssJitterState.Enabled && DlssJitterState.Inject && DlssJitterState.JitterUi)
{
// [JITTER-UI, 05/07] The UI/compositing passes run at scale 1 and were NOT jittered, but
// the WHOLE frame is declared as jittered to DLSS -- so DLSS "de-jitters" the static UI
// and shakes it (measured: world regions at the no-jitter floor in every sign combo while
// UI regions tremble). Jitter the full-screen scale-1 color passes too, with the SAME
// guest-relative shift as the world (NDC = offset*2 / (vpWidth * resScale)), so DLSS
// stabilizes the UI like the world. Square/small targets (shadow maps, bloom mips) are
// excluded by the viewport heuristic.
ref ViewportTransform vp0 = ref viewportTransformSpan[0];
float vpWidth = MathF.Abs(vp0.ScaleX) * 2f;
float vpHeight = MathF.Abs(vp0.ScaleY) * 2f;
@@ -1305,8 +1163,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
jitterNdcY = DlssJitterState.OffsetY * 2f / (vpHeight * GraphicsConfig.ResScale);
}
// One log per unique scale-1 viewport size: which passes exist and which ones the
// heuristic catches (JITTER-UI probe).
int vpKey = ((int)vpWidth << 16) | ((int)vpHeight & 0xFFFF);
if (_jitterUiProbeSeen.Add(vpKey))
{
@@ -1392,11 +1248,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
// Viewport size is only used on the shader when YNegate is enabled,
// and if the fragment shader accesses gl_FragCoord,
// so there's no need to update it in other cases.
// [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] The probe's epilogue store packs
// ViewportSize as the writing-pass identifier, so keep the field current whenever the
// probe is ON (measured: without this the field stays zero on this path and the map
// reads all-zero). SetViewportSize only dirties on actual change; default OFF =
// stock condition, byte-identical.
if ((yNegate && _fsReadsFragCoord) || _surviveProbe)
{
UpdateSupportBufferViewportSize();
@@ -1418,9 +1269,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_context.Renderer.Pipeline.SetDepthMode(mode);
// MV++: the reprojection shader must map window depth back to the NDC z the game's
// matrices produce, and this is where the guest's convention is known. Same thread
// as the draws, so the value read at capture time is the active one.
if (DlssCameraState.Enabled)
{
DlssCameraState.GuestDepthMinusOneToOne = mode == DepthMode.MinusOneToOne;
@@ -2050,9 +1898,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
addressesSpan[index] = baseAddress + shader.Offset;
}
// [SCENEPROBE] stable per-pass identity for the current draw (guest shader VAs).
_probeSceneFsAddr = addressesSpan[5]; // fragment
_probeSceneVsAddr = addressesSpan[1]; // vertex (main)
_probeSceneFsAddr = addressesSpan[5];
_probeSceneVsAddr = addressesSpan[1];
int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex
? _state.State.TexturePoolState.MaximumId
@@ -2067,10 +1914,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ref _currentSpecState.GetGraphicsState(),
addresses);
// MV++ double-transform (b1) sub-step 2: also request/cache the MvppVariant of the
// same shaders (same args + the flag) and publish it for the graft to SetProgram.
// The flag changes NOTHING in the translation yet (sub-step 3 wires the double-skin)
// so the variant is byte-identical to the base -- transparency preserved. Gated OFF.
if (_mvppVariantEnabled)
{
CachedShaderProgram gsVariant = shaderCache.GetGraphicsShader(
@@ -2085,17 +1928,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
MvppVelState.VariantProgram = gsVariant.HostProgram;
// (b1) sub-step 5a: resolve the host binding of the N-1 palette cbuf the injector
// added to the variant VS. READ it from the real BufferDescriptors -- the cbuf Slot
// present in the VARIANT VS but not the BASE VS is ours -- never guessed (loc-31
// lesson). Published for the graft to bind the ring to it; -1 -> graft skips (MV=0).
if (_mvppN1Enabled)
{
// (b1) 5a fix @91: the N-1 cbuf is at the FIXED host binding 91, outside every
// guest window (1..90) and declared by the variant only -- no guest binding can
// ever alias it, so the graft's direct bind can't pollute a non-classified draw,
// and no guest program is shifted (host disk cache valid both ways). Injector
// logs the same binding -> BINDING MATCH catches drift.
MvppVelState.VariantN1Binding = MvppN1ReservedBinding;
MvppVelState.VariantN1Set = 0;
@@ -2154,7 +1988,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_currentProgramInfo[stageIndex] = info;
}
if (gs.Shaders[5]?.Info.UsesFragCoord == true)
{
// Make sure we update the viewport size on the support buffer if it will be consumed on the new shader.
@@ -2186,9 +2019,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_context.Renderer.Pipeline.SetProgram(gs.HostProgram);
// (c) RESTORE proof: this unconditional SetProgram(base) is what re-binds the base
// after the graft bound the variant (graft -> ForceStateDirty -> here). One-shot log
// + per-window counter (vRestore) instead of a line per restore (272k-line spam).
if (MvppVelState.VariantBound)
{
MvppVelState.VariantBound = false;
@@ -1,6 +1,3 @@
// 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;
@@ -8,26 +5,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Engine.Twod
{
/// <summary>
/// Recensement des blits du MOTEUR 2D (RYUJINX_TWOD_PROBE=1). READ-ONLY, off par défaut.
///
/// POURQUOI ICI, ET PAS AILLEURS. Au 21/07, l'artefact XC2 a été poursuivi étage par étage et chacun
/// est tombé sur mesure :
/// - présentation / blit final / HDR / swapchain : hors de cause (paire même-image guest+swapchain) ;
/// - états de rastérisation : viewport, scissor, screen scissor, clip, miroir, swizzle = 6 mesures,
/// 0 anomalie sur ~90 000 draws ;
/// - appels de dessin : TOUS capturés sur des images entières, la scène est propre au dernier draw
/// alors que l'image présentée est détruite ;
/// - compute : 0 dispatch par image, compteur visible dans le log.
/// Il ne reste que les COPIES. Le moteur 2D est un moteur SÉPARÉ du 3D : aucune sonde de ce dossier ne
/// le regarde, puisqu'elles sont toutes accrochées aux draws.
///
/// CE QU'ON MESURE. Les rectangles source et destination de chaque blit, et surtout les cas anormaux :
/// coordonnée négative, région qui dépasse la texture, source et destination de tailles différentes.
/// L'analyse forensique avait désigné TextureCopy.Blit comme le seul mécanisme de copie capable de
/// mordre les DEUX bords à la fois (clamps indépendants sur la source et sur la destination, avec des
/// coordonnées qui peuvent devenir négatives) -- c'est exactement la géométrie de l'artefact.
/// </summary>
static class MvppTwodProbe
{
private static bool _enabled =
@@ -36,15 +13,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
private static bool _announced;
private static long _nextLogMs;
private static int _blits;
private static int _negatives; // une coordonnée source ou destination est négative
private static int _resized; // la région source n'a pas la même taille que la destination
private static int _negatives;
private static int _resized;
private static readonly Dictionary<string, int> _shapes = new();
/// <summary>
/// Battement branché sur la fin d'image, appelé depuis Gpu/Window.Present. Indispensable : la
/// synthèse ci-dessous ne s'écrivait qu'au premier blit, donc "aucune ligne dans le log" ne
/// distinguait pas "aucun blit 2D" de "sonde jamais armée". Ici la ligne sort même à zéro.
/// </summary>
public static void OnPresent()
{
if (!_enabled)
@@ -114,12 +86,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
_resized++;
}
// Une forme = la géométrie complète du blit. C'est elle qui dira si XC2 recopie des bandes
// latérales, et à quelles abscisses exactes.
string shape = $"src[{srcX1},{srcY1}->{srcX2},{srcY2}]/{srcW}x{srcH} dst[{dstX1},{dstY1}->{dstX2},{dstY2}]/{dstW}x{dstH}";
_shapes[shape] = _shapes.TryGetValue(shape, out int n) ? n + 1 : 1;
// Le log periodique vit dans OnPresent : il doit sortir meme quand il n'y a aucun blit.
}
catch (Exception e)
{
@@ -295,7 +295,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
FormatInfo dstCopyTextureFormat = dstCopyTexture.Format.Convert();
// Dernier étage jamais instrumenté du dossier XC2 : le moteur 2D. Lecture seule, gated.
MvppTwodProbe.OnBlit(
srcX1, srcY1, srcX2, srcY2,
dstX1, dstY1, dstX2, dstY2,
@@ -364,15 +363,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
return;
}
MvppBlitProbe.OnBlit(srcTexture, dstTexture, duDx, dvDy); // read-only composite tracer (gated)
MvppBlitProbe.OnBlit(srcTexture, dstTexture, duDx, dvDy);
if (srcTexture.Info.Samples > 1 || dstTexture.Info.Samples > 1)
{
srcTexture.PropagateScale(dstTexture);
}
// Exact (quantized) per-axis ratios so full-surface blits cover the whole
// allocation on both ends; matches ScaleFactor when fractional scaling is off.
float scaleX = srcTexture.EffectiveScaleX;
float scaleY = srcTexture.EffectiveScaleY;
float dstScaleX = dstTexture.EffectiveScaleX;
@@ -392,7 +389,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
bool linearFilter = _state.State.SetPixelsFromMemorySampleModeFilter == SetPixelsFromMemorySampleModeFilter.Bilinear;
Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated)
Image.MvppSeamProbe.OnScaledCopy(
"2D-BLIT",
srcTexture, dstTexture,
srcX1 / srcTexture.Info.SamplesInX, srcY1 / srcTexture.Info.SamplesInY,
@@ -403,7 +400,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
dstRegion.X1, dstRegion.Y1, dstRegion.X2, dstRegion.Y2,
linearFilter ? "linear" : "point");
Image.MvppTwinXferProbe.OnCopy("2D-BLIT", srcTexture, dstTexture); // [TWINXFER] read-only, self-gated
Image.MvppTwinXferProbe.OnCopy("2D-BLIT", srcTexture, dstTexture);
srcTexture.HostTexture.CopyTo(dstTexture.HostTexture, srcRegion, dstRegion, linearFilter);
-4
View File
@@ -47,9 +47,6 @@ namespace Ryujinx.Graphics.Gpu
public Window Window { get; }
/// <summary>
/// Most recently bound depth-stencil render target, captured so the presentation path can hand a
/// real depth buffer to temporal upscalers (DLSS). May be stale or a UI depth, so consumers must
/// validate it (e.g. by dimensions) before use. Written and read on the GPU thread only.
/// </summary>
internal Image.Texture LastPresentDepthStencil { get; set; }
@@ -105,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu
/// </summary>
internal DirtyHacks DirtyHacks { get; }
/// <summary>
/// Host hardware capabilities.
/// </summary>
@@ -119,11 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Image
{
_totalSize += texture.Size;
// Report the cache state on a timer, NOT only when an eviction happens. First version
// of this probe logged from inside RemoveLeastUsedTexture, so a run with zero evictions
// produced zero lines, and "no line" meant either "no eviction" or "probe not running".
// Same trap as the birth-clear switch earlier today: a diagnostic you cannot SEE is
// worth nothing. Now the line always comes, and the eviction count is a field in it.
if (CacheProbeEnabled)
{
long tickMs = System.Environment.TickCount64;
@@ -184,15 +179,6 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary>
/// Removes the least used texture from the cache.
/// </summary>
// [CACHEPROBE, 21/07] Alex's decisive observation on the Xenoblade 2 artefact: "it is not
// there at the start, it ACCUMULATES -- the more I turn the camera, the more it comes".
// A defect that grows with time is a different family from the one-off race I had been
// chasing all day, and the obvious thing that grows while turning the camera is the number
// of loaded textures. This cache is bounded (2048 entries, or a memory budget) and starts
// EVICTING the least recently used once full. Empty cache at first, no eviction, no
// artefact; keep turning, cache fills, evictions begin, and if one of them takes a texture
// that is still needed the game draws something real in the wrong place. Read-only: counts
// evictions and reports the cache state, changes nothing.
internal static readonly bool CacheProbeEnabled =
System.Environment.GetEnvironmentVariable("RYUJINX_CACHE_PROBE") == "1";
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader;
@@ -10,34 +7,11 @@ using System.Text;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [BUILDERIN] (RYUJINX_BUILDERIN=1, inert unless set). Read-only, BACKEND-NEUTRAL.
///
/// Why this exists. The offline bench (journal 226) runs the builder's two translations
/// outside the game on byte-identical inputs and finds them equivalent to well under one
/// 10-bit code on the real target. That contradicts the elimination chain of (222), which
/// concluded the divergence must live in the builder's translation because "inputs equal,
/// constants equal, outputs divergent". One of those premises is therefore wrong -- and the
/// two that were established indirectly are the input identity (measured on dump FILES whose
/// mapping to the shader's samplers was inferred, never read) and the output identity.
///
/// So this probe stops inferring and reads it: for the builder draw only, which surface is
/// actually bound to each sampler (guest VA, format, size, handle -> fp_t_tcb_&lt;handle&gt;),
/// and the exact constant vec4s it consumes. Run it once per backend and the two logs settle,
/// by reading rather than by matching filenames, whether the builder really is fed the same
/// thing on both paths.
///
/// Touches no GPU state and allocates nothing on the hot path when disabled.
/// </summary>
static class MvppBuilderInProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_BUILDERIN") == "1";
// The whole DoF/motion chain, not just the builder: (229) showed the builder writes a
// single 320x180 target and does NOT produce the 64x36 maps, so its consumers are now
// the suspects - and they read their OWN constants, which is why the bench found them
// dead when fed the builder's.
private static readonly (ulong Addr, string Name)[] Passes =
{
(0x1000AD730UL, "builder"),
@@ -51,7 +25,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static readonly HashSet<string> _census = new();
private static int _censusLines;
/// <summary>Cheap identity of the draw's first colour target, for throttling.</summary>
private static int TargetKey(TextureManager texMgr)
{
if (texMgr == null)
@@ -110,10 +83,8 @@ namespace Ryujinx.Graphics.Gpu.Image
}
private static string _shape = "?";
/// <summary>The 1280x720 twin the builder sampled on its last draw (journal 242).</summary>
public static Texture LastTwin { get; private set; }
/// <summary>True when the draw writes one of the small DoF map shapes.</summary>
private static bool MatchesUnknownTarget(TextureManager texMgr, out string shape)
{
shape = null;
@@ -165,12 +136,9 @@ namespace Ryujinx.Graphics.Gpu.Image
return null;
}
private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order)
private const int CbufSlot = 3; // fp_c3
private const int FragmentStageIndex = 4;
private const int CbufSlot = 3;
// The vec4 indices the passes read, from its decompiled source:
// [0] output scale / blend / strength / falloff, [5][6][7][8] the reprojection columns,
// [11] the NDC->pixel scale. Anything else in the buffer is not consumed by this shader.
private static readonly int[] WantedC3 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13 };
private struct Bound
@@ -190,15 +158,8 @@ namespace Ryujinx.Graphics.Gpu.Image
private static bool _armed;
private static bool _failLogged;
// v2. The first cut logged once and fired at ~8 s - the title screen, where the game
// has motion blur off (fp_c3[0].z = 0 kills the whole camera branch). The bench fed
// with those values is structurally dead, so a one-shot log is worthless. Now it
// re-reports whenever the values actually CHANGE, capped so the log stays readable.
private const int MaxReports = 40;
private static int _reports;
// v5. A single global throttle meant one sampled draw every 250 ms for the WHOLE
// chain, so the busiest passes ate every slot and coc64/bokeh/pingpong were never
// sampled at all (23rd trap). The throttle is per pass now.
private static readonly Dictionary<ulong, long> _lastBeat = new();
private static readonly Dictionary<string, string> _lastSig = new();
private static readonly Dictionary<string, int> _perConfig = new();
@@ -239,10 +200,6 @@ namespace Ryujinx.Graphics.Gpu.Image
H = info.Height,
Handle = handle,
Binding = binding,
// down64's second invocation reduces 320 -> 64 horizontally with NO
// horizontal taps: that factor-5 reduction rests entirely on the sampler.
// The address mode was checked long ago and matches the console; the FILTER
// never was, on either backend.
Levels = texture.Info.Levels,
Tex = texture,
Filter = sampler == null
@@ -253,11 +210,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Called once per draw after bindings are committed. Logs the builder's full input
/// identity and constants once, then keeps quiet; always clears the per-draw list so a
/// non-builder draw cannot leak its inputs into the next one.
/// </summary>
public static void OnDraw(GpuChannel channel, TextureManager texMgr, ulong fsAddr)
{
if (!Enabled)
@@ -269,20 +221,12 @@ namespace Ryujinx.Graphics.Gpu.Image
{
try
{
// CENSUS: every (shader, target shape) pair seen, deduplicated. After the
// 2D engine reported ZERO blits, "no draw writes the 64x36" rests entirely
// on my shape detection - so this stops trusting it and enumerates instead.
CensusTick(texMgr, fsAddr);
string pass = PassName(fsAddr);
if (pass == null)
{
// The chain read in (232) has a HOLE: down64 emits 64x180 but coc64 and
// the bokeh consume 64x36, so an unlisted pass performs the vertical
// reduction - and that pass produces the very map whose z-channel
// variation Alex proved to be the trigger. Catch it by its target
// shape rather than by an address we do not have yet.
if (!MatchesUnknownTarget(texMgr, out string shape))
{
return;
@@ -299,9 +243,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_builderDraws++;
// Latch the twin from THIS draw's inputs, and only when this draw is the
// builder - otherwise the last unrelated pass wins and the content probe
// ends up measuring a different texture entirely (29th trap).
if (fsAddr == Passes[0].Addr)
{
foreach (Bound bb in _inputs)
@@ -320,12 +261,6 @@ namespace Ryujinx.Graphics.Gpu.Image
"[BUILDERIN] armed: tracking the DoF/motion chain (builder, down64, coc64, bokeh_fs, pingpong)");
}
// Sample at most ~4x/s: reading guest memory on every builder draw would
// cost far more than the answer is worth.
// The throttle was keyed on the shader alone, so down64's two invocations
// (64x180 then 64x36) shared one 250 ms slot and the vertical one was never
// sampled. Key it on shader + target shape: same shader, two targets, two
// independent slots.
long now = Environment.TickCount64;
ulong beatKey = fsAddr ^ ((ulong)TargetKey(texMgr) << 40);
_lastBeat.TryGetValue(beatKey, out long beat);
@@ -340,10 +275,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_texMgr = texMgr;
string signature = BuildSignature(channel, out string[] lines);
// 24th trap: budgeting per PASS let constant churn eat all eight slots, so a
// second invocation of the same shader with a DIFFERENT target could never be
// reported. The key is the pass plus its I/O shape, so every distinct
// configuration gets its own budget.
string config = pass + "|" + _shape;
_lastSig.TryGetValue(config, out string prev);
@@ -390,10 +321,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Renders the draw's full input state as log lines, and returns a signature that
/// changes exactly when any of those values changes.
/// </summary>
private static string BuildSignature(GpuChannel channel, out string[] lines)
{
List<string> outLines = new();
@@ -425,11 +352,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return sig.ToString();
}
/// <summary>
/// The draw's colour targets. (219) attributed a divergence to "the builder's output"
/// but measured it partly on the 64x36 maps, which two later passes produce. Reading the
/// targets here says, without inference, which surface the builder actually writes.
/// </summary>
private static void AppendTargets(TextureManager texMgr, List<string> lines, StringBuilder sig, StringBuilder shape)
{
if (texMgr == null)
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,42 +6,20 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// Recensement des opérations INTERNES du cache de textures (RYUJINX_CACHEOPS=1). READ-ONLY, off par défaut.
///
/// DERNIER ÉTAGE DEBOUT du dossier XC2 au 21/07. Tous les autres sont tombés sur mesure :
/// - présentation / blit final / HDR / swapchain : paire MÊME-IMAGE guest+swapchain ;
/// - états de rastérisation : 6 mesures, ~90 000 draws, 0 anomalie ;
/// - appels de dessin : images entières capturées draw par draw, scène PROPRE au dernier draw
/// alors que l'image présentée est détruite ;
/// - compute : 0 dispatch par image ;
/// - moteur 2D : 0 blit sur 61 relevés.
/// Le jeu produit donc une image propre et ne demande AUCUNE copie. Ce qui reste ne peut être que ce
/// que l'émulateur fait de lui-même : recopier entre textures qui se chevauchent en mémoire,
/// resynchroniser depuis la mémoire invitée, créer des vues partielles.
///
/// /!\ L'ancien verdict du journal « cache de textures : 363/2048 entrées, 0 éviction » ne mesurait QUE
/// les évictions. Les copies de dépendance et SynchronizeMemory n'ont JAMAIS été comptées.
///
/// Le battement est branché sur la FIN D'IMAGE, jamais sur l'événement mesuré : deux fois ce soir un
/// compteur qui ne s'annonçait qu'en cas de trouvaille a rendu un run inexploitable, parce que
/// « aucune ligne » ne distinguait pas « rien trouvé » de « jamais armé ».
/// </summary>
static class MvppCacheProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_CACHEOPS") == "1";
private static long _nextLogMs;
private static int _copyDeps; // copies de dépendance créées entre textures qui se chevauchent
private static int _syncs; // APPELS à SynchronizeMemory (la plupart ressortent sans rien faire)
private static int _mismatch; // ... dont les deux textures n'ont PAS la même largeur
private static int _realSyncs; // appels qui rechargent VRAIMENT des données (texture sale)
private static int _realNoData; // ... dont la texture n'avait pas encore de données (SynchronizeFull)
private static readonly Dictionary<string, int> _realShapes = new(); // formes qui rechargent, par fréquence
private static int _copyDeps;
private static int _syncs;
private static int _mismatch;
private static int _realSyncs;
private static int _realNoData;
private static readonly Dictionary<string, int> _realShapes = new();
private static readonly Dictionary<string, int> _pairs = new();
/// <summary>Un appel qui recharge RÉELLEMENT des données depuis la mémoire invitée.</summary>
public static void OnRealSync(int w, int h, Format fmt, bool hadData)
{
if (!_enabled)
@@ -63,7 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_realShapes[key] = _realShapes.TryGetValue(key, out int n) ? n + 1 : 1;
}
/// <summary>Une copie de dépendance vient d'être créée entre deux textures qui se chevauchent.</summary>
public static void OnCopyDependency(Texture a, Texture b)
{
if (!_enabled || a == null || b == null)
@@ -83,9 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_mismatch++;
}
// Une largeur ou un format différent entre deux textures qui partagent la MÊME mémoire,
// c'est le mécanisme canonique qui recopie du contenu valide au mauvais endroit : une ligne
// de l'une tombe au milieu d'une ligne de l'autre.
string key = $"{a.Info.Width}x{a.Info.Height} {a.Info.FormatInfo.Format} <-> {b.Info.Width}x{b.Info.Height} {b.Info.FormatInfo.Format}{(diff ? " <<< DIFFERENT" : "")}";
_pairs[key] = _pairs.TryGetValue(key, out int n) ? n + 1 : 1;
}
@@ -96,7 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Une texture vient d'être resynchronisée depuis la mémoire invitée.</summary>
public static void OnSynchronize()
{
if (_enabled)
@@ -105,7 +75,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Battement, branché sur la fin d'image. Sort même quand tous les compteurs sont à zéro.</summary>
public static void OnPresent()
{
if (!_enabled)
@@ -1,32 +1,17 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using System;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [CONSTDIFF] (RYUJINX_CONSTDIFF=1, inert unless set). Journal 221, read-only,
/// BACKEND-NEUTRAL (lives in the Gpu layer: the same probe serves GL and Vulkan).
///
/// 219 proved the builder transforms comparable texture inputs differently per backend;
/// 220-221 proved its MUFU precision is a lever but the calibration sweep dead-ends
/// (only full quantization is clean). The last ingredient never compared: the builder's
/// CONSTANTS at pass time -- fp_c3[5..8] is the PREVIOUS-FRAME reprojection matrix. A
/// stale matrix is invisible at rest (prev==cur) and wrong exactly in motion: the
/// artifact's oldest signature. Logs, once per builder draw, one row of that matrix:
/// a value REPEATING across consecutive frames during motion = staleness caught.
/// </summary>
static class MvppConstDiffProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_CONSTDIFF") == "1";
private const ulong BuilderFsAddress = 0x1000AD730UL;
private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order)
private const int CbufSlot = 3; // fp_c3
private const int FragmentStageIndex = 4;
private const int CbufSlot = 3;
private static bool _armedLogged;
private static bool _failLogged;
@@ -86,8 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Image
continue;
}
// GetGraphicsUniformBufferAddress renvoie une adresse DEJA physique
// (Range traduite) : lecture directe, PAS de Translate.
ulong addr = channel.BufferManager.GetGraphicsUniformBufferAddress(FragmentStageIndex, slot);
if (addr == 0 || addr == ulong.MaxValue)
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
@@ -9,26 +6,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [MVHASH] (RYUJINX_MVHASH=1, inert unless set). Read-only, BACKEND-NEUTRAL.
///
/// Journal 241 closed the shader family: every stage of the DoF chain - builder, down64,
/// pingpong, bokeh fragment AND the bokeh geometry shader - is bit-identical between OpenGL
/// and Vulkan when fed identical inputs, proven on an offline bench. And 229 proved the
/// builder's inputs are identical in IDENTITY: same guest address, same format, same size,
/// same constants. What was never compared is their CONTENT.
///
/// By elimination the content must differ, and it is produced upstream by the ~730 draws
/// that write the motion-vector twin. This measures that content directly, at the moment
/// the builder reads it.
///
/// Comparing two runs pose-for-pose is impossible by hand, so this does NOT rely on a hash
/// matching. It reports pose-robust occupancy statistics - what fraction of the twin carries
/// a written motion vector rather than its clear value - sampled over the whole run. A
/// backend where a class of geometry fails to export its vectors shows a systematically
/// different occupancy, whatever the camera is doing. That is the (217) observation, this
/// time measured over a distribution instead of a single snapshot (the mistake 218 caught).
/// </summary>
static class MvppContentProbe
{
public static readonly bool Enabled =
@@ -92,9 +69,6 @@ namespace Ryujinx.Graphics.Gpu.Image
double sumAbs = 0;
ulong hash = 14695981039346656037UL;
// The twin is A2B10G10R10: R and G carry sqrt-encoded magnitude, the 2-bit alpha the
// signs. A texel still at its clear value has R and G at zero, so "occupancy" is the
// share of texels some draw actually wrote a vector into.
for (int i = 0; i < words; i++)
{
uint w = (uint)(bytes[i * 4] |
+1 -42
View File
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader;
@@ -9,36 +6,19 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [DOFPROBE, READ-ONLY, default OFF] Grounding probe for the XC2 depth-of-field bokeh
/// artifact (a temporal history buffer that diverges under camera motion and accumulates).
/// Nsight located the bokeh pass writing a low-res 512x288 target (event 11872) and reading
/// R8G8B8A8 inputs (candidates Image_4615 / Image_4601). This probe reproduces that mapping
/// emulator-side: when a draw's colour render target is the low-res DoF buffer, it logs each
/// distinct input texture (format, size, guest VA) plus how many DoF draws it has fed. The
/// history/feedback buffer is the input that persists on (nearly) every DoF draw; its
/// format+size+VA signature is what the later 8-bit -&gt; 16f experiment must target.
/// A separate census lists every small render-target size seen, so if the real DoF size
/// differs from 512x288 we still learn it in a single run instead of logging nothing.
/// Enable with RYUJINX_DOF_PROBE=1. Never modifies bindings, textures, samplers or targets.
/// </summary>
static class MvppDofProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_DOF_PROBE") == "1";
// Nsight-confirmed low-res DoF target on XC2 with res-scale 1 (Image_4637, event 11872).
private const int DofWidth = 512;
private const int DofHeight = 288;
// Anything this small is a candidate small pass (DoF / bloom / blur chain). Census only.
private const int SmallRtMax = 640;
private static readonly HashSet<string> _smallRtSizes = new();
private static readonly Dictionary<string, long> _dofInputs = new();
/// <summary>Guest VAs of every texture seen as an input to the low-res DoF pass. Read by
/// <see cref="MvppFeedbackProbe"/> to flag which temporal-history buffers actually feed the DoF.</summary>
public static readonly HashSet<ulong> DofInputVas = new();
private static long _lastSummaryMs;
private static long _lastHeartbeatMs;
@@ -46,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static bool _armed;
private static int _announced;
/// <summary>
/// Logs the gate state exactly once per process, regardless of <see cref="Enabled"/>, so
/// every run makes it visible whether the probe is armed. Removes the "no lines" ambiguity
/// (probe off vs. probe on but never reached the DoF pass). Cheap: a single interlocked
/// exchange after the first call. Thread-safe (this path runs on multiple GPU threads).
/// </summary>
public static void AnnounceOnce()
{
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
@@ -65,13 +39,6 @@ namespace Ryujinx.Graphics.Gpu.Image
(Enabled ? "Probe ACTIVE." : "Probe OFF -- launch via XC2_DOF_PROBE.bat to enable."));
}
/// <summary>
/// Inspects one input texture bound during a draw, with the draw's active colour target
/// (call site is gated on <see cref="Enabled"/>). Purely observational.
/// </summary>
/// <param name="texture">The input texture being bound</param>
/// <param name="renderTarget">The draw's active colour render target (may be null)</param>
/// <param name="stage">The shader stage this texture is bound to (DoF reads are Fragment)</param>
public static void OnInput(Texture texture, Texture renderTarget, ShaderStage stage)
{
if (texture == null || renderTarget == null)
@@ -85,11 +52,6 @@ namespace Ryujinx.Graphics.Gpu.Image
{
_totalBinds++;
// Heartbeat so "no DoF lines" is never ambiguous (a gated switch you cannot SEE
// in the log is worthless -- lesson from this dossier). If OnInput ever runs, we
// log "armed" once; then a periodic "alive" line proves the hook is live and how
// many draws/small-RTs/DoF-inputs it has counted -- distinguishing "probe off"
// from "probe on but never reached the DoF pass (still at a 2D menu/loading)".
if (!_armed)
{
_armed = true;
@@ -105,8 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image
$"DOFPROBE alive: binds={_totalBinds} smallRTsizes={_smallRtSizes.Count} dofInputs={_dofInputs.Count}");
}
// Census: record every distinct small render-target size once. Confirms the real
// DoF resolution without hard-assuming 512x288.
if (rt.Width <= SmallRtMax)
{
string rtSize = $"{rt.Width}x{rt.Height} {rt.FormatInfo.Format}";
@@ -116,7 +76,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
// Grounding gate: only draws that render INTO the low-res DoF buffer.
if (rt.Width != DofWidth || rt.Height != DofHeight)
{
return;
@@ -134,7 +93,7 @@ namespace Ryujinx.Graphics.Gpu.Image
va = 0;
}
DofInputVas.Add(va); // cross-reference key for the feedback probe
DofInputVas.Add(va);
string key =
$"stage={stage} @0x{va:X}/{info.FormatInfo.Format}/{info.Width}x{info.Height}/lv{info.Levels}/{info.Target}";
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using System;
@@ -8,23 +5,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [FEEDBACKPROBE, READ-ONLY, default OFF] Frame-to-frame feedback detector for the XC2
/// temporal artifact. For every texture ever used as a colour render target, it classifies
/// the transition across each guest frame boundary (Window.Present):
///
/// Case A = written in frame N, and its FIRST access in frame N+1 is a READ
/// (read before any write) ==> TRUE temporal history buffer.
/// Case B = written in frame N, and its FIRST access in frame N+1 is a WRITE
/// (write before any read) ==> transient render target reused every frame.
///
/// Writes are observed at SetRenderTargetColor (a texture bound as a colour target =
/// SignalModifying(true)); reads at CommitTextureBindings (a texture bound as a sampled input).
/// All three call sites (write, read, frame boundary) run on the GPU command thread, so the
/// first-access ordering within a frame is the true GPU order. Only render-target textures are
/// tracked: a read of a texture never seen as a target is a single dictionary miss, no insert.
/// Enable with RYUJINX_FEEDBACK_PROBE=1. Never modifies textures, targets or bindings.
/// </summary>
static class MvppFeedbackProbe
{
public static readonly bool Enabled =
@@ -34,8 +14,8 @@ namespace Ryujinx.Graphics.Gpu.Image
{
public long LastWriteFrame = long.MinValue;
public long FirstAccessFrame = long.MinValue;
public long CaseA; // write(N) -> read-first(N+1): temporal history
public long CaseB; // write(N) -> write-first(N+1): transient reused
public long CaseA;
public long CaseB;
public string Format;
public int Width;
public int Height;
@@ -47,7 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static long _lastReportMs;
private static int _announced;
/// <summary>Logs the gate state once per process, even when OFF (removes the "no lines" ambiguity).</summary>
public static void AnnounceOnce()
{
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
@@ -61,10 +40,8 @@ namespace Ryujinx.Graphics.Gpu.Image
(Enabled ? "Probe ACTIVE." : "Probe OFF -- launch via XC2_FEEDBACK_PROBE.bat to enable."));
}
/// <summary>Current guest frame counter (for other probes' log lines).</summary>
public static long Frame => System.Threading.Interlocked.Read(ref _frame);
/// <summary>Read the frame-to-frame classification of a texture VA (for the trace probe).</summary>
public static (long caseA, long caseB) Classify(ulong va)
{
lock (_rts)
@@ -85,7 +62,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>A texture is bound as a colour render target this frame (a write).</summary>
public static void OnWrite(Texture texture)
{
if (texture == null)
@@ -107,8 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Image
{
rec.FirstAccessFrame = _frame;
// First access of this frame is a WRITE. If it was also written the previous
// frame, it is a transient target reused every frame (Case B).
if (rec.LastWriteFrame == _frame - 1)
{
rec.CaseB++;
@@ -125,7 +99,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>A texture is bound as a sampled input this frame (a read). Only render targets are tracked.</summary>
public static void OnRead(Texture texture)
{
if (texture == null)
@@ -139,15 +112,13 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (!_rts.TryGetValue(va, out Rec rec))
{
return; // never a render target -> cannot be a history buffer
return;
}
if (rec.FirstAccessFrame != _frame)
{
rec.FirstAccessFrame = _frame;
// First access of this frame is a READ. If it was written the previous frame,
// it carries content across the frame boundary = TRUE temporal history (Case A).
if (rec.LastWriteFrame == _frame - 1)
{
rec.CaseA++;
@@ -156,7 +127,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Called at the guest frame boundary (Window.Present). Advances the frame and reports periodically.</summary>
public static void OnFrameBoundary()
{
lock (_rts)
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using System;
@@ -8,28 +5,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [FULLSYNC] Read-only probe (RYUJINX_FULLSYNC=1), inert unless set. Changes nothing that is
/// rendered; it only reports which textures get a FULL upload from guest memory, and whether that
/// guest memory is actually empty.
///
/// Why this is the current suspect. Journal (111) localised the XC2 block corruption to the pass
/// filling slot4, found the guest memory of that surface EMPTY, and measured a period of 8 lines =
/// one GOB. Two facts sat badly together: a desktop GPU has no GOB, so an 8-line period means the
/// data went through a guest block-linear layout at some point -- yet the surface was said to be a
/// pure GPU target that never round-trips through guest memory.
///
/// Texture.SynchronizeMemory takes `SynchronizeFull()` whenever `_hasData` is false, which is the
/// state of a freshly created texture -- including a view created over an existing overlap, where
/// TextureCache calls SynchronizeMemory immediately after CreateView. SynchronizeFull reads
/// `_physicalMemory.GetSpan(Range)` and uploads it through the block-linear conversion. Uploading
/// empty or stale guest memory over an already-rendered surface would produce exactly what the
/// captures show: flat colour blocks where the memory is zero, displaced valid content where it is
/// stale, at GOB granularity. Nothing else proposed so far explains the flat blocks.
///
/// This probe does not decide that this is the bug. It answers: does slot4 take this path, and is
/// the data it uploads empty?
/// </summary>
static class MvppFullSyncProbe
{
public static readonly bool Enabled =
@@ -40,10 +15,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static bool _armedLogged;
private static int _count;
/// <summary>
/// Positive control. Without it, "no [FULLSYNC] line" cannot be told apart from "the flag never
/// took", and a silence that cannot be read is not a measurement.
/// </summary>
public static void ReportArmed()
{
if (!Enabled || _armedLogged)
@@ -56,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image
Logger.Warning?.Print(LogClass.Gpu, "[FULLSYNC] ARMED (RYUJINX_FULLSYNC=1). Expect one line per distinct texture shape taking the full-upload path.");
}
// Per-shape counters. The first run showed that BOTH the corrupted surface (R8G8B8A8 720p) and
// the clean one (R11G11B10 720p) take this path with empty guest memory, so "takes the path" is
// not the discriminator. What can still separate them is HOW OFTEN, and whether the texture is
// a VIEW over a parent that already holds rendered pixels -- an empty upload onto a fresh
// texture is harmless, the same upload onto a parent's memory wipes what was drawn.
private static readonly Dictionary<string, int> _shapeCounts = new();
private static int _fullResLogged;
@@ -83,8 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_shapeCounts.TryGetValue(key, out int n);
_shapeCounts[key] = n + 1;
// Full-screen surfaces are the ones journal (111) is about, and they are rare enough to
// log individually. Capped so a runaway loop cannot fill the log and slow the run.
if (info.Width == 1280 && info.Height == 720 && _fullResLogged < FullResLogCap)
{
_fullResLogged++;
@@ -103,9 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
// Sampled rather than exhaustive: these spans reach several megabytes and this runs on the
// render thread. A stride of 4093 (prime) avoids aligning with any power-of-two structure
// in the data, which a round stride could alias with and mistake for "all zero".
int nonZero = 0;
int sampled = 0;
@@ -1,5 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
using Ryujinx.Common.Logging;
using System;
@@ -7,39 +5,11 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// Probe for the fork's gobBlocksInZ clamp (RYUJINX_GOBZ_PROBE=1, off by default).
///
/// WHY. Xenoblade 2 shows rectangles of displaced-but-real content, in rows, appearing as the
/// camera turns and reportedly worse indoors where more objects are on screen. Measured and
/// ELIMINATED so far, each with the switch verified live in the log: shader cache (purged),
/// DLSS/MV++/FG (log proved mode=0, zero "using mode"), runtime mipmaps (never applied when
/// DLSS is off - checked in the code path), recycled device memory (2000+ fresh allocations
/// zeroed, artefact unchanged), history-reset storm (4 resets in a minute).
///
/// What is left that can displace real content in rectangles is the BLOCK-LINEAR layout. Switch
/// textures are tiled, and decoding them needs the exact gob parameters the game declared. The
/// fork overrides one of them: for any non-3D target with more than one layer it forces
/// gobBlocksInZ to 1, a change added for the BOTW/TOTK 4K packs that upstream does not have.
/// A wrong stride there detiles the texture with the wrong pitch, which looks like blocks of
/// content moved around - exactly the reported shape.
///
/// This probe does NOT change behaviour: the clamp still applies. It counts how often it fires
/// and describes what it hit, so the lead is settled by measurement:
/// never fires on XC2 -> dead lead, drop it;
/// fires constantly -> suspect confirmed, and RYUJINX_NO_GOBZ_CLAMP=1 tests it for real.
/// </summary>
static class MvppGobProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_GOBZ_PROBE") == "1";
/// <summary>
/// RYUJINX_NO_GOBZ_CLAMP=1: honour the descriptor instead of forcing gobBlocksInZ to 1.
/// The actual A/B, to be run only once the probe has shown the clamp fires at all.
/// ⚠️ The clamp exists for the BOTW/TOTK 4K packs -- disabling it may bring their original
/// problem back, so this is a diagnostic, never a default.
/// </summary>
public static readonly bool ClampDisabled =
Environment.GetEnvironmentVariable("RYUJINX_NO_GOBZ_CLAMP") == "1";
@@ -54,8 +24,6 @@ namespace Ryujinx.Graphics.Gpu.Image
{
_total++;
// Distinct shapes rather than a line per texture: a game binds the same few
// hundred textures over and over, and what we need is WHICH KINDS get clamped.
string key = $"{width}x{height}x{layers} {target} {format} gobZ={gobZ}";
_shapes.TryGetValue(key, out int n);
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader;
@@ -11,31 +8,6 @@ using System.Text;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [MAP64] Read-only probe (RYUJINX_MAP64_PROBE=1), inert unless set. Changes nothing that is
/// rendered; it reports who PRODUCES the small DoF maps and what the host cache does to them.
///
/// Why. EXP 4 (journal 122) proved the XC2 form-A rectangles require the data of the 64x36 DoF
/// tile map (fp_t_tcb_E): forcing its samples to a constant removes them, and the downstream
/// arithmetic was already exonerated. So the map's CONTENT arrives corrupted -- and every host
/// probe so far (STORAGEID/FULLSYNC/VIEWALIAS) filtered on 1280x720, so this surface was never
/// watched. This probe watches the small shapes (64x36, 320x180, 160x90) on four channels:
///
/// 1. WRITER passes: every draw whose colour target is one of these shapes -- fragment/vertex
/// guest addresses, target VA and instance, and the VIEWPORT extents of the draw. A viewport
/// smaller than the map would leave the rest of the texels STALE, which is invisible while
/// the camera is still and visible in motion -- exactly the artifact's behaviour.
/// 2. Guest uploads onto them (SynchronizeFull), with a FULL emptiness scan (the texture is
/// tiny): an upload of empty guest memory over a rendered map mid-game would inject garbage.
/// 3. Partial group syncs onto them (the branch never instrumented before).
/// 4. Instance churn: distinct object identities seen per shape (STORAGEID's question, but on
/// the right surface this time).
///
/// Reading grid, written before coding: writer draws with FULL 64x36 viewport every frame and
/// zero syncs => corruption comes from the writer's INPUTS, move up the chain. In-game uploads
/// or partial viewports or heavy churn => host-side mechanism found, instrument that path next.
/// No [MAP64] lines at all => instrument mute (VOID), never "nothing happens".
/// </summary>
static class MvppMap64Probe
{
public static readonly bool Enabled =
@@ -43,7 +15,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static bool _armedLogged;
/// <summary>Positive control (TextureCache ctor). Silence without it is unreadable.</summary>
public static void ReportArmed()
{
if (!Enabled || _armedLogged)
@@ -75,7 +46,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static readonly List<In> _inputs = new();
/// <summary>Per-draw sampled input (TextureBindingsManager, both bind paths). Self-gated.</summary>
public static void OnInput(Texture texture, ShaderStage stage, int handle)
{
if (!Enabled || texture == null)
@@ -102,11 +72,8 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
// One detailed line per distinct (fragment shader, target shape, viewport) -- a NEW viewport
// on a known writer is a finding on its own (partial write), so it re-triggers the log.
private static readonly HashSet<(ulong, string, int, int)> _seenWriters = new();
// Instance churn per shape, WRITER side (RuntimeHelpers identity, no field added).
private static readonly Dictionary<string, HashSet<int>> _instances = new();
private static long _totalDraws;
@@ -116,10 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static long _groupSyncs;
private static long _summaryMs;
/// <summary>
/// Called once per draw AFTER bindings are committed (StateUpdater probe block, gated there).
/// Viewport extents are the draw's host viewport 0, already multiplied by the RT scale.
/// </summary>
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, int vpW, int vpH)
{
_totalDraws++;
@@ -211,7 +174,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static long _groupSyncLogged;
private const long PerEventLogCap = 40;
/// <summary>Full guest-memory upload onto a watched shape (Texture.SynchronizeFull). Rare = log each, capped.</summary>
public static void OnFullSync(Texture texture, ReadOnlySpan<byte> data)
{
if (!Enabled || texture == null || !IsWatched(texture.Info))
@@ -221,7 +183,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_fullSyncs++;
// The texture is tiny (64x36x4 = 9 KiB guest range): scan EVERY byte, no sampling caveat.
bool empty = true;
for (int i = 0; i < data.Length; i++)
{
@@ -247,7 +208,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Partial (per-handle) group sync onto a watched shape (Texture.SynchronizeMemory, _hasData branch).</summary>
public static void OnGroupSync(Texture texture)
{
if (!Enabled || texture == null || !IsWatched(texture.Info))
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
@@ -10,30 +7,6 @@ using System.Text;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [MVBUF] Read-only probe (RYUJINX_MVBUF_PROBE=1), inert unless set. Watches the XC2
/// object-motion-vector buffer (R10G10B10A2Unorm 1280x720), the carrier of the form-A/B
/// corruption (EXP 5, journal 126): with its samples nulled, every artifact form disappears.
///
/// Why clears matter. That buffer is written per-frame ONLY by moving/skinned geometry; the
/// rest of the screen keeps whatever the buffer already contains, so the game depends on a
/// RELIABLE per-frame clear to a neutral value. A clear that is skipped, scissored short, or
/// reordered leaves RECTANGLES of stale motion -- which the motion-blur chain then renders as
/// the measured smears. Upstream PR #4596 documents exactly this clear/read hazard family on
/// RTX 3000+ and names Xenoblade explicitly; and a MISSING clear is not a timing bug, which is
/// consistent with forced barriers and DeviceWaitIdle having had no effect (journal 112).
///
/// Reading grid, written before coding:
/// - clears present EVERY frame, full scissor, neutral value -> the clear COMMAND is fine;
/// the failure would be content/decode or host-side execution -> instrument the Vulkan
/// clear path (v2) or the A2 sign-channel decode next;
/// - frames with ZERO clear of this target (and no full-screen writer draw those frames) ->
/// the game relies on a clear path this probe does not see (loadOp/fast clear) or the
/// clear is genuinely skipped -> hook the Vulkan level next;
/// - clears with PARTIAL scissor or component mask != 0xF -> partial-clear family;
/// - no [MVBUF] lines at all with ARMED present -> the buffer is not engine-cleared and not
/// drawn: copy path, v2.
/// </summary>
static class MvppMvBufProbe
{
public static readonly bool Enabled =
@@ -62,7 +35,6 @@ namespace Ryujinx.Graphics.Gpu.Image
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
}
// Frame accounting (advanced from the present boundary, always on GPU thread).
private static long _frames;
private static int _clearsThisFrame;
private static int _writesThisFrame;
@@ -79,21 +51,13 @@ namespace Ryujinx.Graphics.Gpu.Image
private static readonly HashSet<string> _clearSignatures = new();
private static readonly HashSet<(ulong, int, int)> _writers = new();
// v3: per-RT blend/write-mask census on the MV attachment. A transparent/additive draw
// BLENDING into the MV target (instead of masked-off or opaque-write) would accumulate
// garbage magnitudes exactly where transparency-heavy content sits -- the co-location the
// captures show. Console intent for such draws is mask-off or no MV attachment at all.
private static readonly HashSet<string> _writerStates = new();
private static long _drawsBlendOnMv;
private static long _drawsPartialMaskOnMv;
// Host-instance identity split: the object the engine CLEARS vs the object shaders SAMPLE.
// Full overlap = one host texture, identities fine. Disjoint sets = the clear lands on one
// host copy while consumers read another (stale) one -- the desync suspect.
private static readonly HashSet<int> _clearedIds = new();
private static readonly HashSet<int> _sampledIds = new();
/// <summary>Guest frame boundary (Window present). Folds the per-frame counters into histograms.</summary>
public static void OnPresent()
{
if (!Enabled)
@@ -101,8 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return;
}
// Only count frames once the buffer exists (first clear or write seen), so the
// pre-title-screen frames do not drown the histogram.
if (_clears + _writerDraws > 0)
{
_frames++;
@@ -146,7 +108,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Engine clear about to hit a colour target (DrawManager.Clear). Logs each DISTINCT signature.</summary>
public static void OnClear(Texture target, int index, uint componentMask, ColorF color, bool customScissor, int sx, int sy, int sw, int sh)
{
if (!Enabled || !IsMvBuffer(target))
@@ -180,7 +141,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Shader sampling of the MV buffer (TextureBindingsManager, both bind paths). Self-gated.</summary>
public static void OnSampled(Texture texture)
{
if (!Enabled || !IsMvBuffer(texture))
@@ -202,7 +162,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Per-draw (StateUpdater probe block): draws whose colour target is the MV buffer.</summary>
public static void OnDraw(TextureManager texMgr, ulong fsAddr, int vpW, int vpH, ReadOnlySpan<bool> blendEnable, ReadOnlySpan<uint> writeMasks)
{
if (texMgr == null)
@@ -244,8 +203,6 @@ namespace Ryujinx.Graphics.Gpu.Image
if (isNew && (blend || mask != 0xF))
{
// Only the suspicious combinations get their own line; clean opaque writers
// stay in the summary counts (there are hundreds of them).
Logger.Warning?.Print(LogClass.Gpu,
$"[MVBUF/WRITER-STATE] {state}{(blend && mask != 0 ? " *** BLEND INTO MV ***" : "")}");
}
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
@@ -8,28 +5,6 @@ using System;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [MVSYNC] Read-only probe (RYUJINX_MVSYNC_PROBE=1), inert unless set. Journal 158.
///
/// Last family standing: the poison appears IN the XC2 object-MV buffer (R10G10B10A2
/// 1280x720) between the writers' stores and the consumers' reads -- every write-side value
/// scrub was negative (NaN, >=0.999, >=0.5), both read-side kills are positive, and the
/// periscope shows saturated garbage in the buffer at rest. This probe watches the three
/// host paths that can INJECT content into an existing texture behind the game's back,
/// none of which were ever measured for this format:
/// 1. TextureGroup.SynchronizePartial -- partial upload of "CPU-dirty" guest pages;
/// 2. Texture.SynchronizeFull (re-sync branch) -- full guest upload over live content;
/// 3. TextureGroupHandle.Copy -- copy-dependency pull from an overlapping texture.
///
/// Reading grid (written before coding):
/// ARMED + heartbeats all zero -> no injection path fires; family dead -> next = Vulkan
/// clear/loadOp execution level.
/// partial/full syncs > 0 -> stale guest pages uploaded over rendered MV = smoking
/// gun -> instrument WHY dirty (tracking/protection).
/// copy-ins > 0 -> alias family: an overlapping texture overwrites the MV
/// buffer via copy dependency -> identify the source.
/// The 3s heartbeat prints even at zero so a negative is distinguishable from a mute probe.
/// </summary>
static class MvppMvSyncProbe
{
public static readonly bool Enabled =
@@ -51,7 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Image
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
}
/// <summary>Frame boundary (Window present): arming witness + 3s heartbeat, zeros included.</summary>
public static void OnPresent()
{
if (!Enabled)
@@ -75,7 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>SynchronizeMemory decided an upload is needed for this storage (dirty branch taken).</summary>
public static void OnSyncDecision(Texture storage, bool partial, int regionCount, bool anyModified)
{
if (!Enabled || !IsMvBuffer(storage))
@@ -100,7 +73,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>A copy dependency pulled data from an overlapping texture into this storage.</summary>
public static void OnCopyIn(Texture storage)
{
if (!Enabled || !IsMvBuffer(storage))
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,16 +6,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [PAIRPROBE, READ-ONLY, default OFF] Logs each unique pairing of a mip-less colour texture
/// (levels=1, 2D, >=64px, RGBA8) with the sampler it is bound with, and whether that sampler
/// could ever fetch generated mip levels (a mipmap filter mode AND maxLod &gt; 0).
/// Pre-check for the FORCEMIPS dossier: if the shimmering tile pools are only ever sampled
/// MIP-DISABLED, runtime mip generation cannot have any effect and must not be built.
/// Enable with RYUJINX_MVPP_PAIRPROBE=1. Never modifies bindings, textures or samplers.
/// Limitation: sampler-level only — an explicit textureLod(0)/texelFetch in the shader would
/// still bypass mips even on a MIP-CAPABLE pair.
/// </summary>
static class MvppPairProbe
{
public static readonly bool Enabled =
@@ -29,12 +16,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static int _disabled;
private static long _lastSummaryMs;
/// <summary>
/// Inspects one texture+sampler pairing (call sites are gated on <see cref="Enabled"/>).
/// Logs the pair once per unique content combo, plus a periodic summary.
/// </summary>
/// <param name="texture">The texture being bound</param>
/// <param name="sampler">The sampler it is bound with</param>
public static void OnPair(Texture texture, Sampler sampler)
{
if (texture == null || sampler == null)
@@ -44,9 +25,6 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureInfo info = texture.Info;
// v2 (11/07): census widened to EVERY mip-less texture >= 64px — all formats and all
// targets (arrays included) — to inventory what the FORCEMIPS v1 criteria still miss
// at the spots where the shimmer survives.
if (info.Levels != 1 ||
info.Width < 64 ||
info.Height < 64)
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
@@ -11,18 +8,6 @@ using System.Text;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [SCENEPROBE, READ-ONLY, default OFF] Per-draw identifier for the pass that produces the fresh
/// 1280x720 R11G11B10 scene the DoF consumes. Fires only for a draw whose colour target is a
/// 1280x720 R11G11B10Float texture, and only once per distinct fragment-shader guest address, so
/// the output is a short list of the distinct passes writing that format. For each it prints the
/// frame, the fragment + vertex shader guest addresses (stable pass identity, maps to Nsight via
/// the shader), the framebuffer (colour-target) VA, and every sampled texture of that draw with
/// its format/size and whether <see cref="MvppFeedbackProbe"/> classifies it as temporal history.
/// The temporal-reconstruction pass is the one whose inputs include a 720p history [H] of its own
/// format (it reads its previous output). Never modifies any GPU state.
/// Enable with RYUJINX_SCENE_PROBE=1 (run alongside RYUJINX_FEEDBACK_PROBE=1 for the [H] flags).
/// </summary>
static class MvppScenePassProbe
{
public static readonly bool Enabled =
@@ -35,13 +20,10 @@ namespace Ryujinx.Graphics.Gpu.Image
public int W;
public int H;
public ShaderStage Stage;
public int Handle; // [E0] texture-CB handle -> GLSL sampler name fp_t_tcb_<Handle:X>
public int Binding; // [E0] runtime binding slot -> GLSL layout(binding=N)
public AddressMode WrapU; // [CLAMPPROBE] sampler address mode U (edge/border/wrap) of this input
public AddressMode WrapV; // [CLAMPPROBE] sampler address mode V
// [FILTERPROBE] The address mode was checked and matches the console; the FILTER never was.
// It decides how the tiny 64x36 CoC map is interpolated, and that map sizes every bokeh
// sprite -- nearest vs linear there changes sprite sizes in ~20-pixel blocks.
public int Handle;
public int Binding;
public AddressMode WrapU;
public AddressMode WrapV;
public MinFilter MinF;
public MagFilter MagF;
}
@@ -75,7 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Appends one sampled input of the current draw (called from CommitTextureBindings, gated).</summary>
public static void OnInput(Texture texture, ShaderStage stage, int handle, int binding, Sampler sampler)
{
if (texture == null)
@@ -87,7 +68,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (_inputs.Count >= 256)
{
return; // safety cap; a real draw never samples this many
return;
}
TextureInfo info = texture.Info;
@@ -108,11 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Called once per draw AFTER bindings are committed (StateUpdater, gated). If the draw writes a
/// 1280x720 R11G11B10 colour target, logs the pass and its sampled inputs (once per shader).
/// Always clears the per-draw input list.
/// </summary>
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, long frame)
{
lock (_inputs)
@@ -127,8 +103,6 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureInfo rt = colorRt.Info;
bool isScene = rt.Width == 1280 && rt.Height == 720 && rt.FormatInfo.Format == Format.R11G11B10Float;
// [CLAMPPROBE] also catch the DoF bokeh scatter pass by its output signature (512x288 R16G16B16A16F),
// so we can log the address mode of the textures IT gathers (the OOB-gather edge suspect).
bool isDof = rt.Width == 512 && rt.Height == 288 && rt.FormatInfo.Format == Format.R16G16B16A16Float;
if (!isScene && !isDof)
{
@@ -137,7 +111,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (!_seenShaders.Add(fsAddr))
{
return; // this pass already reported
return;
}
ulong fbVa = Va(colorRt);
@@ -146,8 +120,6 @@ namespace Ryujinx.Graphics.Gpu.Image
Logger.Info?.Print(LogClass.Gpu,
$"SCENEPROBE pass writes {kind}: fs=0x{fsAddr:X} vs=0x{vsAddr:X} fb=0x{fbVa:X} frame={frame} inputs={_inputs.Count} =====");
// [E0] Every bound colour render target (out_attr_N) with its runtime format. out_attr1 is
// the resolved-colour / temporal-history feedback target H1 cares about. Read-only.
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
{
Texture ct = texMgr.GetColorTarget(slot);
@@ -1,6 +1,3 @@
// 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;
@@ -8,21 +5,9 @@ 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 const int MaxShapes = 400;
private static readonly int[] _watchX;
private static readonly int[] _watchY;
@@ -81,11 +66,6 @@ namespace Ryujinx.Graphics.Gpu.Image
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)
@@ -98,10 +78,6 @@ namespace Ryujinx.Graphics.Gpu.Image
$"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,
@@ -116,11 +92,8 @@ namespace Ryujinx.Graphics.Gpu.Image
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)
@@ -170,7 +143,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <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)
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader;
@@ -8,32 +5,11 @@ using System;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [TAAPROBE 28/07, LECTURE SEULE, OFF par defaut] Trouve la PASSE DE RESOLUTION TEMPORELLE du
/// jeu : le dessin qui LIT une cible couleur pleine resolution ecrite a l'image PRECEDENTE.
///
/// POURQUOI UNE DEUXIEME SONDE ALORS QUE FEEDBACKPROBE EXISTE. FEEDBACKPROBE prend un verrou
/// sur un dictionnaire a CHAQUE liaison de texture, des milliers de fois par image. Il est
/// "lecture seule" pour les pixels, pas pour le temps : Alex a vu un defaut revenir pendant le
/// run qui l'utilisait. Celle-ci ne prend aucun verrou et ne fait, dans le cas courant, que
/// deux comparaisons d'entiers -- le tableau n'est parcouru que pour les textures qui ont deja
/// la bonne taille. Tous les appels arrivent sur le thread de commandes GPU.
///
/// CE QU'ELLE CHERCHE. FEEDBACKPROBE a montre 33 cibles qui survivent d'une image a l'autre,
/// dont QUATRE en R11G11B10Float a la resolution de rendu pleine -- la forme d'un historique
/// couleur HDR (un bloom serait en resolution reduite, et il y en a justement a cote en
/// 960x540). Reste a savoir quel dessin les consomme : c'est lui, la passe temporelle.
///
/// Elle n'ecrit qu'une ligne par combinaison distincte, une poignee en tout, puis se tait.
/// Aucune texture, aucune cible, aucune liaison n'est modifiee.
/// </summary>
static class MvppTaaProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_TAAPROBE") == "1";
// 24 emplacements : FEEDBACKPROBE en a compte 33 au total, dont une majorite en resolution
// reduite que le pre-filtre elimine avant d'arriver ici.
private const int Slots = 24;
private static readonly ulong[] _va = new ulong[Slots];
@@ -61,7 +37,6 @@ namespace Ryujinx.Graphics.Gpu.Image
$"TAAPROBE gate check: RYUJINX_MVPP_TAAPROBE={(v ?? "<unset>")} -> enabled={Enabled}.");
}
/// <summary>Frontiere d'image invitee.</summary>
public static void OnPresent()
{
if (Enabled)
@@ -85,11 +60,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return -1;
}
/// <summary>
/// Appelee la ou la sonde du flou l'est deja : elle recoit l'entree ECHANTILLONNEE et la
/// cible couleur du dessin en cours. Les deux ensemble suffisent -- pas besoin de se
/// greffer aussi sur la pose des cibles.
/// </summary>
public static void OnInput(Texture input, Texture target, ShaderStage stage)
{
if (!Enabled || input == null || target == null)
@@ -97,9 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return;
}
// Pre-filtre : la resolution principale est la plus grande cible couleur vue. Deux
// comparaisons d'entiers eliminent tout le trafic (masques, bloom, ombres, interface)
// avant le moindre parcours.
long area = (long)target.Info.Width * target.Info.Height;
if (area > _mainArea)
@@ -114,7 +81,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return;
}
// La cible pleine resolution de ce dessin est notee comme "vue a cette image".
ulong tva = Va(target);
int ti = Find(tva);
@@ -129,8 +95,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_count++;
}
// L'entree doit elle aussi etre pleine resolution : un historique temporel a forcement
// la taille de ce qu'il reconstruit.
if (input.Info.Width != _mainW || input.Info.Height != _mainH)
{
return;
@@ -138,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Image
int ii = Find(Va(input));
// Ecrite a une image PRECEDENTE et lue maintenant = historique temporel.
if (ii < 0 || _lastSeen[ii] >= _frame)
{
return;
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using System;
@@ -9,17 +6,6 @@ using System.Text;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [TRACEPROBE, READ-ONLY, default OFF] Rendering dependency-graph tracer for the XC2 temporal
/// artifact. At every texture read it records a directed edge "current render target &lt;- input
/// texture" (both by guest VA). Over many draws this reconstructs the pass dependency graph.
/// Periodically it walks the graph UP from each DoF input (see <see cref="MvppDofProbe"/>),
/// printing the producer chain and flagging every node that <see cref="MvppFeedbackProbe"/>
/// classifies as temporal history (Case A). This answers: which pass writes the fresh DoF scene,
/// what it reads, and which of those reads is a temporal-history buffer -- recursively, until the
/// first temporal producer feeding the DoF scene is found. Never modifies any GPU state.
/// Enable with RYUJINX_TRACE_PROBE=1 (run alongside RYUJINX_DOF_PROBE=1 + RYUJINX_FEEDBACK_PROBE=1).
/// </summary>
static class MvppTraceProbe
{
public static readonly bool Enabled =
@@ -78,7 +64,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return n;
}
/// <summary>Records the edge "renderTarget &lt;- input" for one read (call site gated on Enabled).</summary>
public static void OnEdge(Texture renderTarget, Texture input)
{
if (renderTarget == null || input == null)
@@ -97,7 +82,7 @@ namespace Ryujinx.Graphics.Gpu.Image
lock (_graph)
{
Node rt = GetOrAdd(rtVa, renderTarget.Info);
GetOrAdd(inVa, input.Info); // ensure the input has a node (its format), even as a leaf
GetOrAdd(inVa, input.Info);
rt.Inputs.Add(inVa);
}
}
@@ -133,7 +118,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
// Holds the _graph lock (called from OnFrameBoundary).
private static void Walk(ulong va, int depth, HashSet<ulong> visited)
{
if (depth > MaxDepth || !visited.Add(va))
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader;
@@ -9,25 +6,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [TWINFIX] EXP 21 -- the tiebreaker (RYUJINX_TWINFIX=1 + RYUJINX_TWINMAP=1), journal 178.
///
/// Measured chain (journal 174-177): the resolve and the DoF chain sample twin X, whose only
/// writer is the sky; the 800+ material shaders write twin Y; the guest pool descriptor
/// REALLY points at X (28k raw checks, 0 mismatch); and no mechanism on the emulator ever
/// transfers Y into X. The poison is X's never-written content.
///
/// This experiment redirects, READ SIDE ONLY, every sampled binding of the sky-only twin to
/// the material twin. No feedback loop is possible (Y's writers do not read X). The sky
/// keeps writing X; sky pixels lose their MV during the test (acceptable: Y's sky region
/// holds the neutral clear).
/// flat-block counter ~0 => the whole diagnosis is PROVEN experimentally and this redirect
/// is the shape of the fix (X and Y must be one, as on console);
/// artifact unchanged => a flaw exists in the diagnosis chain -- back to cold analysis
/// with a decisive new fact.
/// Twin classification is automatic and conservative: exactly two twins, one with <= 4
/// writers (sky) and one with >= 50 (materials); no redirect until both are established.
/// </summary>
static class MvppTwinFixProbe
{
public static readonly bool Enabled =
@@ -38,8 +16,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static readonly HashSet<(ShaderStage, int)> _redirectTuples = new();
/// <summary>Fast-path gate: forces one slow-path re-resolution for bindings still cached
/// on the sky twin once the classification (and thus the redirect) is available.</summary>
public static bool NeedsRebind(Texture cached)
{
if (!Enabled || cached == null)
@@ -60,7 +36,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return cached.Range.GetSubRange(0).Address == skyVa;
}
/// <summary>Slow-path substitution: reads of the sky twin are served by the material twin.</summary>
public static Texture MaybeRedirect(Texture texture, ShaderStage stage, int handle)
{
if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture))
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader;
@@ -9,20 +6,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [TWINMAP] Read-only probe (RYUJINX_TWINMAP=1), inert unless set. Journal 173.
///
/// VKIMG found the game double-buffers its MV target (two R10G10B10A2 1280x720 guest
/// textures, both written and read every frame -- a two-stage intra-frame chain, VKPHASE).
/// Before any twin-viewing experiment, MEASURE the roles instead of guessing them:
/// - WRITE side: per twin (guest VA), which fragment shaders draw into it, and how many.
/// Expected asymmetry: the raw twin has ~190 material writers; the processed twin has
/// very few -- and those few NAME the processor.
/// - READ side: per twin, which (stage, guest tcb handle) samples it. Cross-checked with
/// the decompiled binding maps (resolve reads its MV at tcb_10, builder at tcb_8), this
/// names which twin the periscope v3 run already showed poisoned.
/// No swaps, no visual change, zero dataflow interference -- pure cartography.
/// </summary>
static class MvppTwinMapProbe
{
public static readonly bool Enabled =
@@ -37,24 +20,16 @@ namespace Ryujinx.Graphics.Gpu.Image
public long WriteDraws;
public int WritersLogged;
// v2 (journal 176): GMMU aliasing check. On console two GPU VAs can alias the same
// physical pages; a missed remap would leave the cached texture on stale backing.
public ulong GpuVa;
public ulong Backing;
public ulong LastTranslation;
// v4 (journal 178): live Texture reference for TWINFIX's read-side redirect.
public WeakReference<Texture> LastTexture;
// v5 (journal 179): per-twin clear attribution + masked-bind census.
public long Clears;
public int ClearsLogged;
public long MaskedBinds;
// v6 (journal 201): clear-COLOUR census, the classification discriminant that
// survives the v5 writer-count correction (both twins have ~730 writers, so the
// old <=4 / >=50 rule never matches any more). Measured (180): X is cleared
// BLACK (0,0,0,1), Y is cleared NEUTRAL (0.5,0.5,1,1) -- every frame.
public long BlackClears;
public long NeutralClears;
}
@@ -63,10 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static readonly HashSet<(ulong, ShaderStage, int)> _reads = new();
private static WeakReference<Memory.MemoryManager> _mm;
// v3 POOLTRUTH (journal 177): raw guest pool descriptor vs actually-bound texture.
// The fast bind path skips descriptor re-reads unless the pool is flagged modified; if
// that tracking misses the game's per-frame updates, a stale binding survives forever --
// which would pin the resolve on twin X while the game re-points it at Y.
private static long _poolMatches;
private static long _poolMismatches;
private static readonly HashSet<(ulong, ulong, int)> _mismatchTuples = new();
@@ -76,8 +47,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return t.Range.GetSubRange(0).Address;
}
/// <summary>v4 (TWINFIX): conservative twin classification. True only with exactly two twins,
/// one clearly sky-only (&lt;= 4 writers) and one clearly material (&gt;= 50 writers).</summary>
public static bool TryClassify(out ulong skyVa, out Texture materialTwin)
{
skyVa = 0;
@@ -106,10 +75,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
// [v6] (journal 201) fallback: the writer-count rule died with the v5 counting
// correction (both twins have ~730 material writers). The discriminant that
// still holds, measured (180): X is cleared BLACK each frame, Y NEUTRAL.
// Same redirect direction as the (179) worse-mode: reads of X served by Y.
if (sky == null || mat == null)
{
TwinStats black = null, neutral = null;
@@ -152,7 +117,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Snapshot of the discovered twin VAs, for cross-probes (TWINXFER's DMA range check).</summary>
public static ulong[] TwinVas()
{
lock (_twins)
@@ -163,7 +127,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Per-draw (StateUpdater): record writers per MV-shaped colour target.</summary>
public static void OnDraw(TextureManager texMgr, ulong fsAddr, Memory.MemoryManager memoryManager, ReadOnlySpan<uint> writeMasks)
{
if (!Enabled || texMgr == null || fsAddr == 0)
@@ -183,9 +146,6 @@ namespace Ryujinx.Graphics.Gpu.Image
"[TWINMAP] ARMED (RYUJINX_TWINMAP=1). Mapping writers and readers of each MV twin by guest VA.");
}
// v5: NO early return -- a single draw can bind BOTH twins at different slots
// (the early return hid that for a whole evening, journal 179). Masks passed in:
// a bound-but-masked-off slot writes nothing and is counted apart.
bool any = false;
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
@@ -251,7 +211,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return stats;
}
/// <summary>v5: engine clear attribution per twin (DrawManager.Clear).</summary>
public static void OnClear(Texture target, uint componentMask, float r, float g, float b, float a)
{
if (!Enabled || target == null || !MvppMvSyncProbe.IsMvBuffer(target))
@@ -266,7 +225,6 @@ namespace Ryujinx.Graphics.Gpu.Image
TwinStats stats = GetOrAdd(va, target, null);
stats.Clears++;
// [v6] clear-colour census for the fallback classification.
if (r < 0.1f && g < 0.1f && b < 0.1f)
{
stats.BlackClears++;
@@ -285,8 +243,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Per-bind (TextureBindingsManager, both paths): record readers per MV-shaped sampled
/// texture. v3: also read the RAW guest pool descriptor and compare with what got bound.</summary>
public static void OnRead(Texture texture, ShaderStage stage, int handle, TexturePool pool, int textureId, Memory.MemoryManager memoryManager)
{
if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture))
@@ -308,7 +264,6 @@ namespace Ryujinx.Graphics.Gpu.Image
$"[TWINMAP] READER of twin@0x{va:X}: stage={stage} handle=0x{handle:X} (tcb_{handle:X}) | distinct readers: {_reads.Count}");
}
// v3 POOLTRUTH: guest truth vs emulator binding.
if (pool != null && memoryManager != null && textureId >= 0)
{
try
@@ -342,7 +297,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
catch
{
// Raw read outside mapped memory: report once via the heartbeat counters only.
}
}
@@ -367,8 +321,6 @@ namespace Ryujinx.Graphics.Gpu.Image
TwinStats stats = kv.Value;
summary += $" twin@0x{kv.Key:X}: writers={stats.Writers.Count} draws={stats.WriteDraws} maskedBinds={stats.MaskedBinds} CLEARS={stats.Clears} |";
// v2: live GMMU re-translation of the twin's VA. A change or a mismatch
// with the cached backing = the missed-remap smoking gun.
if (mm != null && stats.GpuVa != 0)
{
ulong translated = mm.Translate(stats.GpuVa);
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader;
@@ -9,25 +6,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [TWINXFER] Read-only probe (RYUJINX_TWINXFER=1), inert unless set. Journal 174.
///
/// TWINMAP measured the twins' roles and it is damning: the resolve (tcb_10) and the DoF
/// chain read twin X, whose ONLY fragment writer is the sky (1 draw/frame) -- while the 882
/// material shaders write twin Y. On console this can only work if SOMETHING transfers the
/// geometry MVs into X. Two mechanisms are invisible to every instrument so far:
/// 1. engine copies (2D blit / DMA) -- the game may issue a per-frame Y->X copy;
/// 2. a COMPUTE pass writing X via image store (storage-image bindings bypass both the
/// draw census and the sampled-texture hooks).
/// This probe watches both:
/// - every 2D/DMA texture copy whose source or destination is MV-shaped (src/dst VAs);
/// - every storage-IMAGE binding of an MV-shaped texture (stage, handle, isStore).
/// Reading grid:
/// Y->X copies present -> the transfer exists; next: is it executed correctly?
/// image-store writes to X -> the missing writer is a compute pass; instrument it;
/// NOTHING touches X -> X is genuinely sky-only + stale -> the game expects
/// aliasing/offset semantics the emulator does not reproduce -> address decode next.
/// </summary>
static class MvppTwinXferProbe
{
public static readonly bool Enabled =
@@ -43,8 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static readonly HashSet<(ulong, ShaderStage, int, bool)> _imageTuples = new();
private static readonly HashSet<(ulong, ulong, bool)> _dmaPairs = new();
// Generous per-twin window (real surface = 0x3C0000): a copy landing anywhere inside
// still names the mechanism; sub-range starts (per-line copies) must not be missed.
private const ulong TwinWindow = 0x400000;
private static bool IsMv(Texture t)
@@ -57,7 +33,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return t.Range.GetSubRange(0).Address;
}
/// <summary>Frame boundary (Gpu Window present): arming witness + heartbeat, zeros included.</summary>
public static void OnPresent()
{
if (!Enabled)
@@ -82,7 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Engine texture copy (2D blit or DMA). Logs pairs where either side is MV-shaped.</summary>
public static void OnCopy(string path, Texture src, Texture dst)
{
if (!Enabled || (!IsMv(src) && !IsMv(dst)))
@@ -108,9 +82,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Raw copy-engine launch (DmaClass.DmaCopy, BEFORE any branch): catches the
/// buffer-domain path no texture-level hook sees. Twin VAs come live from TWINMAP
/// (run both probes together).</summary>
public static void OnDma(ulong srcVa, ulong dstVa, int xCount, int yCount, bool copy2D, bool srcLinear, bool dstLinear)
{
if (!Enabled)
@@ -155,7 +126,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Storage-image binding of an MV-shaped texture (the compute-writer blind spot).</summary>
public static void OnImageBind(Texture texture, ShaderStage stage, int handle, bool isStore)
{
if (!Enabled || !IsMv(texture))
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using System;
@@ -8,45 +5,17 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [VIEWALIAS] Read-only probe (RYUJINX_VIEWPROBE=1), inert unless set. Changes nothing that is
/// rendered: it only reports, once per distinct case, when a texture is created as a VIEW over an
/// existing texture whose FORMAT differs.
///
/// Why: journal entry (111) localised the XC2 block corruption to the deferred composition pass
/// that fills slot4 (4 bytes per pixel), measured x65 vs a clean buffer, with the signature
/// "valid content displaced" at GOB granularity -- and the guest memory is empty, so nothing is
/// being detiled. Meanwhile slot6 has identical dimensions AND identical bytes per pixel yet stays
/// clean, which rules out the block-linear size/tiling math (it depends only on those two things).
///
/// What is left is format-specific host behaviour, and TextureCompatibility has exactly one rule
/// wide enough to matter here: for non-sampler textures, two textures are considered FULLY view
/// compatible whenever their bytes-per-pixel and compression status match, regardless of the actual
/// format. 4 bytes per pixel is the most crowded class in the renderer, so slot4 can legitimately
/// be born as a view over an unrelated 4-byte texture and read its memory.
///
/// This probe does NOT decide whether that is the bug -- it answers whether it happens at all, and
/// on which pair of formats. Instruments before levers.
/// </summary>
static class MvppViewAliasProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_VIEWPROBE") == "1";
// One line per distinct (child format, parent format, size) case. Texture creation is frequent,
// so an ungated log would drown the file and slow the run enough to change what we measure.
private static readonly HashSet<string> _reported = new();
private static bool _armedLogged;
private static bool _firstViewLogged;
private static int _viewCount;
/// <summary>
/// Positive control, called once when the texture cache is built. WITHOUT it, "no [VIEWALIAS]
/// line" is ambiguous between "the flag never took", "the view path is never reached" and the
/// only meaningful reading, "views happen but never change format". A probe whose silence
/// cannot be interpreted is not an instrument.
/// </summary>
public static void ReportArmed()
{
if (!Enabled || _armedLogged)
@@ -68,9 +37,6 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureInfo p = parent.Info;
// Second control: proves the path is alive, whatever the formats are. Logged before the
// same-format early-out below, otherwise a run with only ordinary views looks identical
// to a run where this code never executed.
_viewCount++;
if (!_firstViewLogged)
@@ -82,14 +48,11 @@ namespace Ryujinx.Graphics.Gpu.Image
$"over parent {p.FormatInfo.Format} {p.Width}x{p.Height} | compat {compatibility}");
}
// Periodic tally so the end of the log states how many views happened in total, which turns
// "no differing-format line" into a quantified negative instead of a silence.
if ((_viewCount % 500) == 0)
{
Logger.Warning?.Print(LogClass.Gpu, $"[VIEWALIAS] tally: {_viewCount} views created so far, {_reported.Count} distinct cross-format cases.");
}
// Same format is the ordinary, expected case: a plain view. Only report reinterpretation.
if (child.FormatInfo.Format == p.FormatInfo.Format)
{
return;
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Shader.Translation;
@@ -9,26 +6,6 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// [CENSUS] Read-only probe (RYUJINX_MVWRITER_CENSUS=1 + RYUJINX_NANSCRUB=1), journal 159.
///
/// The value scrubs covered shaders selected by FINGERPRINT; the writer set is defined by
/// RENDER TARGET. The intersection was never verified. This probe checks, per draw whose
/// colour target is the XC2 object-MV buffer (R10G10B10A2 1280x720), whether the bound
/// fragment shader was armed by the +0.01 encode fingerprint -- any writer that was NOT is a
/// writer every scrub missed, free to deposit garbage since day one.
///
/// Requirements: NANSCRUB=1 fills the armed registry (its NaN wrap is visually inert, EXP 14),
/// and the shader cache must be OFF -- cache-path translations carry address 0 (journal 148),
/// which would make every writer look unarmed (void). The bat handles both.
///
/// Reading grid (written before coding):
/// UNARMED distinct > 0 -> the escaped writers, by address -> dumpmap + decompile them next;
/// UNARMED = 0, armed > 0 -> writers fully covered by the scrubs -> value family truly closed
/// at the FS level -> next stage is the Vulkan object level
/// (clear/loadOp execution, view identity below the Texture cache);
/// registry = 0 in the heartbeat -> VOID (cache ON or NANSCRUB missing), fix the run.
/// </summary>
static class MvppWriterCensusProbe
{
public static readonly bool Enabled =
@@ -45,7 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private static readonly HashSet<ulong> _unarmedFs = new();
private static readonly HashSet<ulong> _maskedOffFs = new();
/// <summary>Frame boundary (Window present): arming witness + 3s heartbeat, zeros included.</summary>
public static void OnPresent()
{
if (!Enabled)
@@ -74,12 +50,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>Per-draw (StateUpdater probe block): classify draws whose colour target is the MV buffer.
/// v2: a draw with the MV slot bound but its colour write mask OFF writes nothing -- counted
/// apart, or the whole static-geometry MRT set shows up as false-positive "unarmed writers".
/// v3: classification by PROGRAM identity (ShaderProgramInfo.MvppMvEncodeArmed), not by VA --
/// the guest places the same code at many VAs and the cache dedups by code, so only the
/// first VA ever reaches translation and the VA registry misses every mirror (journal 163).</summary>
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ReadOnlySpan<uint> writeMasks, Ryujinx.Graphics.Shader.ShaderProgramInfo fragInfo)
{
if (texMgr == null || fsAddr == 0)
@@ -140,7 +110,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
return; // one classification per draw, even if bound on several slots
return;
}
}
}
+1 -19
View File
@@ -19,8 +19,6 @@ namespace Ryujinx.Graphics.Gpu.Image
public bool IsSrgb { get; }
/// <summary>
/// [PAIRPROBE, read-only diagnostics] Unpacked filter/LOD parameters, kept because the
/// descriptor is discarded after construction. Never read by any rendering path.
/// </summary>
public MinFilter ProbeMinFilter { get; }
public MagFilter ProbeMagFilter { get; }
@@ -29,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image
public float ProbeMipLodBias { get; }
/// <summary>
/// [GLOWPROBE, 21/07, read-only] Texture addressing modes and border colour, kept for the
/// Xenoblade 2 light-halo dossier. The halos are sliced off at the edge of their quad
/// instead of fading out - proven abnormal against a TOTK control where the same buffer
/// shows no hard edge at all - and the addressing mode is what decides what a sample
/// beyond the texture edge returns. Never read by any rendering path.
/// </summary>
public AddressMode ProbeAddressU { get; }
public AddressMode ProbeAddressV { get; }
@@ -49,18 +42,9 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
private readonly ISampler _anisoSampler;
// [FORCEMIPS_TRILINEAR, default OFF] Companion knob of RYUJINX_MVPP_FORCEMIPS: for textures
// carrying a generated mip chain, promote mip-NEAREST samplers to mip-LINEAR so the level
// transitions blend instead of snapping (under jitter the per-frame LOD oscillation at level
// boundaries otherwise reads as flicker). Only ever selected for forced-mips textures.
private static readonly bool _forceMipsTrilinear =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_TRILINEAR") == "1";
// [FORCEMIPS_BIAS, default 0 = OFF] Extra mip LOD bias applied ONLY to the forced-mips
// sampler variant. The DLSS jitter path adds a global -0.5 at the Vulkan level (kept sharp
// for regular mipped art, user-validated); on the generated chains that -0.5 pins the near
// band onto over-sharp level 0 and the shimmer survives there. +0.5 here nets the total to
// 0 for forced-mips textures only — the global look stays untouched.
private static readonly float _forceMipsBias =
float.TryParse(
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_BIAS"),
@@ -69,8 +53,6 @@ namespace Ryujinx.Graphics.Gpu.Image
out float fmBias) ? fmBias : 0f;
/// <summary>
/// Host sampler object with the mip filter promoted from Nearest to Linear, used for
/// forced-mips textures when the knob above is set. Null otherwise.
/// </summary>
private readonly ISampler _trilinearSampler;
@@ -191,7 +173,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (_trilinearSampler != null && texture?.HasForcedMips == true)
{
return _trilinearSampler; // [FORCEMIPS_TRILINEAR] blend between generated levels
return _trilinearSampler;
}
return _anisoSampler != null && texture?.CanForceAnisotropy == true ? _anisoSampler : _hostSampler;
+16 -133
View File
@@ -44,23 +44,12 @@ namespace Ryujinx.Graphics.Gpu.Image
private SizeInfo _sizeInfo;
// [MIPPROBE / Mesure H1, READ-ONLY, default OFF] Measure alpha coverage across mip levels for cutout
// candidates, to test whether the foliage's alpha coverage collapses in the game's mip chain. Decodes
// each mip to a TEMP buffer, reads alpha, logs stats -- NEVER modifies the texture data or the upload
// (rendering / DLSS / colour / depth / MV / jitter all untouched). Zero cost when the flag is off.
private static readonly bool _mipProbe =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPPROBE") == "1";
private static int _mipProbeAstc, _mipProbeUnreadable, _mipProbeCand, _mipProbeLogged;
private static long _mipProbeSummaryMs;
private bool _mipProbed;
// [FORCEMIPS, default OFF] Runtime mip generation for mip-less colour textures (the levels=1
// streamed tile pools): the host texture is allocated with extra mip levels and the chain is
// (re)generated from level 0 by GPU blits, lazily, when the texture is bound for sampling
// after its content changed. PAIRPROBE (11/07) measured that the game binds these pools with
// mip-capable samplers (mip filter set, maxLod=13), so generated levels are consumed by the
// ordinary sampling path. Guest data, guest-visible sizes and flushes are untouched (only the
// HOST allocation gains levels). Byte-identical when the flag is off.
private static readonly bool _forceMips =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS") == "1";
private static readonly int _forceMipsExtraLevels =
@@ -73,33 +62,14 @@ namespace Ryujinx.Graphics.Gpu.Image
private static int _forceMipsLost;
private static long _forceMipsSummaryMs;
// [MIPS-RT fix 19/07] Corruption 4K natif PROUVÉE au banc (sonde du jour : 17 textures
// boostées liées en cible couleur, ~35k liaisons/3,5 min — toutes petites et carrées =
// les textures procédurales du jeu, dont le portail de sanctuaire mauve du rapport) :
// à res_scale=1 plus rien n'est Scaled, donc l'exclusion ScaleMode==Scaled qui
// protégeait silencieusement les buffers d'écran en 2x disparaît — il ne reste que
// l'heuristique 16:9 (±16 px) et les cibles carrées passent au travers → mips générées
// derrière un render target écrit chaque frame. Fix = rétrogradation au premier bind
// cible (voir DemoteForcedMipsOnRenderTargetBind). Compteur = rétrogradations.
private static int _forceMipsRtDemotes;
private bool _forceMipsRtDemoted;
// [FORCEMIPS_REFRESH, default 0 = OFF] Regenerate the forced chain of a sampled texture every
// N ms even without a dirty mark. Diagnostic for the progressive-degradation signature (11/07:
// "parfait au début, revient de plus en plus fort" + regens≈boosted in the logs): if periodic
// refresh stops the degradation, level-0 updates are reaching the pools through a path the
// dirty marks do not cover (stale mips), and that path must be found.
private static readonly int _forceMipsRefreshMs =
int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_REFRESH_MS"), out int fmRefresh)
? Math.Max(0, fmRefresh)
: 0;
// [MIPDUMP, default OFF] Read-only diagnostic for the SMART-downsample dossier (pépite 2):
// on its first regeneration, each of the first 16 boosted textures writes every generated
// level to disk (_MIPDUMP/, PPM colour + PGM alpha + index line) so the exact damage of the
// plain-blit chain (tile-border bleed? odd-size drift? stale content? alpha decay?) can be
// SEEN before designing the replacement shader. GetData is a synchronous readback (stalls) —
// measurement runs only. Rendering, generation and filtering are untouched.
private static readonly bool _mipDump =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPDUMP") == "1";
private const int MipDumpMaxTextures = 16;
@@ -111,10 +81,6 @@ namespace Ryujinx.Graphics.Gpu.Image
private long _forcedMipsLastRegenMs;
/// <summary>
/// [FORCEMIPS] True when this texture's host allocation carries a generated mip chain.
/// Read by <see cref="Sampler.GetHostSampler"/> to pick the trilinear-promoted sampler
/// variant (mip-Nearest sampling snaps between generated levels; under jitter the LOD
/// choice oscillates per frame at level boundaries, which reads as flicker).
/// </summary>
public bool HasForcedMips => _forcedMipLevels > 1;
@@ -154,18 +120,12 @@ namespace Ryujinx.Graphics.Gpu.Image
public float ScaleFactor { get; private set; }
/// <summary>
/// Exact horizontal scale of the host texture (host size / guest size).
/// With quantized fractional scaling the host size is snapped up to the mip granularity,
/// so the real ratio sits a hair above the nominal <see cref="ScaleFactor"/>; consumers
/// that place or sample content (viewport, scissor, shader compensation, present crop)
/// must use this exact value or they under-cover the allocation.
/// </summary>
public float EffectiveScaleX => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null
? HostTexture.Width * Info.SamplesInX / (float)Info.Width
: ScaleFactor;
/// <summary>
/// Exact vertical scale of the host texture (host size / guest size). See <see cref="EffectiveScaleX"/>.
/// </summary>
public float EffectiveScaleY => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null
? HostTexture.Height * Info.SamplesInY / (float)Info.Height
@@ -246,7 +206,6 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public MultiRange Range { get; private set; }
/// <summary>[MVPP] Accès lecture seule à la mémoire physique invitée, pour la sonde PRESYNC GUEST.</summary>
internal PhysicalMemory PhysicalMemory => _physicalMemory;
/// <summary>
@@ -328,9 +287,6 @@ namespace Ryujinx.Graphics.Gpu.Image
InitializeTexture(context, physicalMemory, info, sizeInfo, range);
// [TEXPROBE, 10/07] Read-only: log every guest texture's size/mip-chain/format at creation
// (RYUJINX_MVPP_TEXPROBE=1, default OFF) to test the truncated-mip-chain hypothesis on the
// distant-foliage shimmer (journal entry 52). No behavior change.
if (_texProbe && info.Target == Target.Texture2D && info.Width >= 16)
{
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
@@ -338,7 +294,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
// [TEXPROBE] see the gated log in the constructor.
private static readonly bool _texProbe =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_TEXPROBE") == "1";
@@ -387,16 +342,11 @@ namespace Ryujinx.Graphics.Gpu.Image
Debug.Assert(!isView);
TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor);
createInfo = BoostLevelsForForcedMips(createInfo); // [FORCEMIPS] no-op when off
createInfo = BoostLevelsForForcedMips(createInfo);
HostTexture = _context.Renderer.CreateTexture(createInfo);
SynchronizeMemory(); // Load the data.
// [FORCEMIPS] Generate the chain at birth: freshly allocated levels hold whatever
// the VRAM last contained, and a texture can reach the screen on the frame it is
// created (the ~1s startup ghost image). First tried 11/07 stacked with the 150 ms
// refresh and blamed for the white veil; the veil's real culprit was the 16:9
// screen-buffer boosting (now excluded), so this returns alone. No-op unless boosted.
EnsureForcedMips();
if (ScaleMode == TextureScaleMode.Scaled)
@@ -420,32 +370,20 @@ namespace Ryujinx.Graphics.Gpu.Image
}
TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor);
createInfo = BoostLevelsForForcedMips(createInfo); // [FORCEMIPS] no-op when off
createInfo = BoostLevelsForForcedMips(createInfo);
HostTexture = _context.Renderer.CreateTexture(createInfo);
}
}
}
/// <summary>
/// [FORCEMIPS] If this texture is an eligible mip-less colour texture (2D, levels=1, RGBA8,
/// >= 64px, no array/3D/MSAA), returns a copy of the host creation info with extra mip levels
/// so a generated chain can live in the host allocation. The guest-visible Info is untouched.
/// Returns the input unchanged (and clears the boost state) in every other case.
/// </summary>
/// <param name="createInfo">The computed host texture creation info</param>
/// <returns>The creation info, with boosted levels if eligible</returns>
private TextureCreateInfo BoostLevelsForForcedMips(TextureCreateInfo createInfo)
{
_forcedMipLevels = 0;
// Actively res-scaled textures are excluded (their host size is not the guest size, and
// mips would average across dyn-res sub-rect boundaries). Merely scale-ELIGIBLE textures
// stay boosted: excluding them removed the mips from distant-island art (user regression,
// 11/07), and the control run proved the x2 seam lines exist without this fix — they are
// the old upstream res-scale artifact, a separate dossier. If such a texture is later
// actually scaled, ReplaceStorage drops the boost.
if (!_forceMips ||
_forceMipsRtDemoted || // [MIPS-RT fix] une cible de rendu ne se re-booste jamais
_forceMipsRtDemoted ||
ScaleMode == TextureScaleMode.Scaled ||
Info.Target != Target.Texture2D ||
Info.Levels != 1 ||
@@ -459,12 +397,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return createInfo;
}
// MIPDUMP evidence (11/07): 13/16 boosted textures were ~16:9 screen-fraction buffers
// (333x187 = renderRes/8 G-buffer downsamples, false-colour screen captures in the dump).
// The game's screen-space effects sample them at specific LODs; generated deep mips
// (= an average of the whole screen) bleed into those reads -> the white veil and the
// "broken rocks" at 8 levels. Screen-derived buffers are render-aspect; the streamed
// tile pools are square and the art textures arbitrary -> exclude ~16:9 outright.
int aspectWidth = (int)MathF.Ceiling((createInfo.Height / 9f) * 16f);
if (Math.Abs(createInfo.Width - aspectWidth) <= 16)
@@ -505,13 +437,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [MIPS-RT fix 19/07] Appelé quand cette texture (ou la storage derrière sa vue) est
/// liée comme cible couleur. Une texture boostée qui devient render target = le trou de
/// la config native (voir le commentaire des champs) : le jeu écrit le niveau 0 chaque
/// frame, des mips générées derrière lui sont périmées au mieux, la corruption mauve au
/// pire. Le boost est LÂCHÉ : storage recréée SANS niveaux bonus (contenu copié, même
/// mécanique que le chemin de changement d'échelle), vues recréées dessus, et le flag
/// interdit tout re-boost. Une fois par texture ; no-op des le retour (niveaux <= 1).
/// </summary>
public void DemoteForcedMipsOnRenderTargetBind()
{
@@ -535,7 +460,6 @@ namespace Ryujinx.Graphics.Gpu.Image
ITexture newStorage = storage.GetScaledHostTexture(storage.ScaleFactor, true);
storage.ReplaceStorage(newStorage);
// Les vues doivent revivre sur la nouvelle storage (miroir du chemin SetScale).
foreach (Texture view in storage._views)
{
TextureCreateInfo viewCreateInfo = TextureCache.GetCreateInfo(view.Info, storage._context.Capabilities, view.ScaleFactor);
@@ -547,11 +471,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [FORCEMIPS] Regenerates the forced mip chain from level 0 with GPU blits (linear filter),
/// through transient per-level views (their Release is deferred by the backend until the
/// commands complete). Called from the sampling bind sites and cheap to call: it returns
/// immediately unless this texture was boosted AND its content changed since the last pass,
/// so mips are rebuilt at most once per frame and only for textures actually sampled.
/// </summary>
public void EnsureForcedMips()
{
@@ -597,7 +516,6 @@ namespace Ryujinx.Graphics.Gpu.Image
dstView.Release();
}
// [MIPDUMP] First regeneration of the first N boosted textures: photograph the chain.
if (_mipDump && !_mipDumped && _mipDumpCount < MipDumpMaxTextures)
{
_mipDumped = true;
@@ -624,12 +542,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [MIPDUMP] Writes every level of this texture's generated mip chain to disk: PPM (P6,
/// colour) + PGM (P5, alpha) per level, plus one line in _MIPDUMP/index.txt with the
/// texture's dimensions, format, level count and memory address (identifier). Read-only:
/// synchronous GetData readback of what the blits produced, nothing is modified.
/// </summary>
/// <param name="index">Sequential dump index (for file naming)</param>
private void DumpForcedMipChain(int index)
{
string dir = System.IO.Path.Combine(System.Environment.CurrentDirectory, "_MIPDUMP");
@@ -694,13 +607,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [FORCEMIPS] Builds the creation info for a transient single-level view used by the mip
/// regeneration blits.
/// </summary>
/// <param name="baseInfo">Host creation info of the storage</param>
/// <param name="width">View width (storage width at the target level)</param>
/// <param name="height">View height (storage height at the target level)</param>
/// <returns>The view creation info</returns>
private static TextureCreateInfo ForcedMipViewInfo(TextureCreateInfo baseInfo, int width, int height)
{
return new TextureCreateInfo(
@@ -836,7 +743,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param>
public void CreateCopyDependency(Texture contained, int layer, int level, bool copyTo)
{
MvppCacheProbe.OnCopyDependency(this, contained); // read-only (gated)
MvppCacheProbe.OnCopyDependency(this, contained);
if (contained.Group == Group)
{
@@ -906,12 +813,6 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <returns>A host texture containing a scaled version of this texture</returns>
private ITexture GetScaledHostTexture(float scale, bool copy, ITexture storage = null)
{
// Fractional-crash probe (quality-mode dossier): a descale copy is the moment a
// scaled texture becomes guest-visible data (flush/blacklist). Only fires under a
// FRACTIONAL source scale (integer scales = the proven regime, daily untouched).
// [31/07] GATE AJOUTE, meme raison que sa jumelle dans SemaphoreUpdater : elle etait
// en release sans interrupteur et se declenche chez tout utilisateur en DLSS quality
// ou performance, qui produit une echelle fractionnaire par construction.
if (GAL.MvppDev.Enabled && copy && scale == 1f && ScaleFactor != MathF.Floor(ScaleFactor))
{
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
@@ -1023,7 +924,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public void SynchronizeMemory()
{
MvppCacheProbe.OnSynchronize(); // read-only (gated)
MvppCacheProbe.OnSynchronize();
if (Target == Target.TextureBuffer)
{
@@ -1037,14 +938,11 @@ namespace Ryujinx.Graphics.Gpu.Image
_dirty = false;
// Le VRAI travail : seul un appel qui trouve la texture SALE recharge des données. Les
// ~367 000 "resynchronisations" comptées avant étaient surtout des appels qui ressortent ici
// sans rien faire. C'est ce compteur-ci qui dit si le cache RECHARGE en boucle.
MvppCacheProbe.OnRealSync(Info.Width, Info.Height, Info.FormatInfo.Format, _hasData); // read-only (gated)
MvppCacheProbe.OnRealSync(Info.Width, Info.Height, Info.FormatInfo.Format, _hasData);
if (_hasData)
{
MvppMap64Probe.OnGroupSync(this); // [MAP64] read-only (gated): partial syncs onto the small DoF maps
MvppMap64Probe.OnGroupSync(this);
Group.SynchronizeMemory(this);
}
else
@@ -1078,14 +976,10 @@ namespace Ryujinx.Graphics.Gpu.Image
{
ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Range);
// [FULLSYNC, READ-ONLY] Report which surfaces get a full upload from guest memory, and
// whether that memory is empty. Gated; does nothing to the data or the upload.
MvppFullSyncProbe.OnFullSync(Info, data, IsView);
// [MAP64, READ-ONLY] Same question, filtered on the small DoF maps (gated).
MvppMap64Probe.OnFullSync(this, data);
// [MIPPROBE, READ-ONLY] Measure mip alpha coverage once per texture. Gated; does nothing to data/upload.
if (_mipProbe && !_mipProbed)
{
_mipProbed = true;
@@ -1114,10 +1008,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
// Large 4K texture-pack array textures (e.g. a 2048x2048 ASTC 2D array with 121 layers) decode to
// more than 2 GiB of RGBA8, which overflows Int32 and cannot fit in a single array. Decode and
// upload them one slice at a time instead (each slice is only a few MiB), reusing the same
// single-slice path that partial synchronization already uses.
if (Info.FormatInfo.Format.IsAstc &&
!_context.Capabilities.SupportsAstcCompression &&
_depth == 1 &&
@@ -1158,7 +1048,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_forcedMipLevels > 1)
{
_forcedMipsDirty = true; // [FORCEMIPS] level 0 changed; regenerated lazily at the next sampling bind
_forcedMipsDirty = true;
_forceMipsMarks++;
}
@@ -1166,9 +1056,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [MIPPROBE / Mesure H1, READ-ONLY] Decodes each mip level to a TEMP buffer, measures the alpha
/// coverage (fraction of texels with alpha &gt; 0.5) per level for cutout (binary-alpha) textures, and
/// logs the coverage loss down the chain. Never modifies the texture data or the upload path.
/// </summary>
private void MipCoverageProbe(ReadOnlySpan<byte> data)
{
@@ -1186,7 +1073,6 @@ namespace Ryujinx.Graphics.Gpu.Image
if (!astc && !rgba8)
{
// Mipped texture whose alpha we can't read cheaply (BCn native, etc.) -- log the landscape.
System.Threading.Interlocked.Increment(ref _mipProbeUnreadable);
return;
}
@@ -1203,7 +1089,7 @@ namespace Ryujinx.Graphics.Gpu.Image
for (int level = 0; level < levels; level++)
{
int off = _sizeInfo.AllOffsets[level]; // layer 0
int off = _sizeInfo.AllOffsets[level];
if (off < 0 || off >= data.Length)
{
return;
@@ -1246,7 +1132,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (level == 0 && !binary)
{
return; // not a cutout texture -> skip
return;
}
}
@@ -1299,7 +1185,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_forcedMipLevels > 1)
{
_forcedMipsDirty = true; // [FORCEMIPS]
_forcedMipsDirty = true;
_forceMipsMarks++;
}
@@ -1320,7 +1206,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_forcedMipLevels > 1 && level == 0)
{
_forcedMipsDirty = true; // [FORCEMIPS]
_forcedMipsDirty = true;
_forceMipsMarks++;
}
@@ -1344,7 +1230,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_forcedMipLevels > 1 && level == 0)
{
_forcedMipsDirty = true; // [FORCEMIPS] tile streamed into the pool
_forcedMipsDirty = true;
_forceMipsMarks++;
}
@@ -2086,12 +1972,12 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (MvppDestProbe.Enabled)
{
MvppDestProbe.OnModify(this, viaModifying: false, bound: true); // read-only (gated)
MvppDestProbe.OnModify(this, viaModifying: false, bound: true);
}
if (_forcedMipLevels > 1)
{
_forcedMipsDirty = true; // [FORCEMIPS] GPU-side write (copy/RT); regenerated lazily if sampled
_forcedMipsDirty = true;
_forceMipsMarks++;
}
@@ -2115,7 +2001,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (MvppDestProbe.Enabled)
{
MvppDestProbe.OnModify(this, viaModifying: true, bound: bound); // read-only (gated)
MvppDestProbe.OnModify(this, viaModifying: true, bound: bound);
}
if (bound)
@@ -2149,9 +2035,6 @@ namespace Ryujinx.Graphics.Gpu.Image
{
DisposeTextures();
// [FORCEMIPS] Replacement storages are allocated by other paths (scale change, size
// change) without the level boost; drop the forced-mips state so the regeneration
// never blits into levels the new storage does not have.
if (_forcedMipLevels > 1)
{
_forceMipsLost++;
@@ -898,10 +898,10 @@ namespace Ryujinx.Graphics.Gpu.Image
if (MvppPairProbe.Enabled)
{
MvppPairProbe.OnPair(texture, sampler); // read-only (gated)
MvppPairProbe.OnPair(texture, sampler);
}
texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty
texture?.EnsureForcedMips();
hostSampler = sampler?.GetHostSampler(texture);
}
@@ -111,9 +111,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [MV++ UI audit, read-only] Enumerates the currently-bound SAMPLED input textures across ALL
/// stages (compute has 1 stage, graphics has several). No effect on rendering state. Used by the
/// gated composite-locator probe to test whether a draw samples the captured scene texture.
/// </summary>
public void MvppEnumerateInputs(System.Action<Texture> report)
{
@@ -137,10 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>[MV++ UI audit, read-only] Like MvppEnumerateInputs but also reports the shader stage.</summary>
/// <summary>[GLOWPROBE, read-only] Same as MvppEnumerateInputsStage but also hands over the
/// SAMPLER bound with each texture: the Xenoblade 2 halo dossier needs the addressing mode,
/// which is what decides what a read past the texture edge returns.</summary>
public void MvppEnumerateInputsWithSampler(System.Action<int, Texture, Sampler> report)
{
for (int stage = 0; stage < _textureBindings.Length; stage++)
@@ -186,9 +179,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [MV++ UI audit, read-only] Enumerates the currently-bound STORAGE IMAGES across all stages
/// (image load/store), the blind spot of MvppEnumerateInputs which only sees sampled textures. A
/// fullscreen upscale that reads its source via imageLoad shows up here, not as a sampled input.
/// </summary>
public void MvppEnumerateImages(System.Action<Texture, bool> report)
{
@@ -213,9 +203,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [MV++ UI audit, read-only] Counts the CURRENT texture bindings that are arrays/bindless
/// (ArrayLength > 1), across all stages. Lets the gated probe know whether a scene sample could be
/// hidden in an array binding that MvppEnumerateInputs cannot resolve.
/// </summary>
public int MvppArrayBindingCount()
{
@@ -236,10 +223,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// [MV++ UI audit, read-only] Enumerates the CURRENT compute bindings so a gated diagnostic can
/// see what a DispatchCompute reads and writes. No effect on rendering state. The callback gets
/// (isImage, isStore, texture): isImage=false is a sampled input texture; isImage=true is a
/// storage image, and isStore=true means the shader WRITES it (the output candidate).
/// </summary>
public void MvppEnumerateCompute(System.Action<bool, bool, Texture> report)
{
@@ -364,10 +347,10 @@ namespace Ryujinx.Graphics.Gpu.Image
if (MvppPairProbe.Enabled)
{
MvppPairProbe.OnPair(texture, sampler); // read-only (gated)
MvppPairProbe.OnPair(texture, sampler);
}
texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty
texture?.EnsureForcedMips();
return (texture, sampler);
}
@@ -397,13 +380,6 @@ namespace Ryujinx.Graphics.Gpu.Image
switch (stage)
{
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)
@@ -633,11 +609,11 @@ namespace Ryujinx.Graphics.Gpu.Image
return true;
}
MvppDofProbe.AnnounceOnce(); // [DOFPROBE] one-shot gate-state line on the real draw path, even when OFF
MvppFeedbackProbe.AnnounceOnce(); // [FEEDBACKPROBE] one-shot gate-state line, even when OFF
MvppTraceProbe.AnnounceOnce(); // [TRACEPROBE] one-shot gate-state line, even when OFF
MvppScenePassProbe.AnnounceOnce(); // [SCENEPROBE] one-shot gate-state line, even when OFF
MvppTaaProbe.AnnounceOnce(); // [TAAPROBE] one-shot gate-state line, even when OFF
MvppDofProbe.AnnounceOnce();
MvppFeedbackProbe.AnnounceOnce();
MvppTraceProbe.AnnounceOnce();
MvppScenePassProbe.AnnounceOnce();
MvppTaaProbe.AnnounceOnce();
bool specStateMatches = true;
@@ -683,45 +659,42 @@ namespace Ryujinx.Graphics.Gpu.Image
state.CachedTexture != null &&
state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence &&
state.CachedSampler?.IsDisposed != true &&
!MvppTwinFixProbe.NeedsRebind(state.CachedTexture)) // [TWINFIX] one forced re-resolve once classified
!MvppTwinFixProbe.NeedsRebind(state.CachedTexture))
{
// The texture is already bound.
state.CachedTexture.SynchronizeMemory();
if (MvppDofProbe.Enabled)
{
// [DOFPROBE] cached (fast) path: a stable input re-bound this draw.
MvppDofProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage);
}
if (MvppTaaProbe.Enabled)
{
// [TAAPROBE] meme point d'observation, sans verrou : deux comparaisons
// d'entiers eliminent tout le trafic qui n'est pas pleine resolution.
MvppTaaProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage);
}
if (MvppFeedbackProbe.Enabled)
{
MvppFeedbackProbe.OnRead(state.CachedTexture); // [FEEDBACKPROBE] input read (fast path)
MvppFeedbackProbe.OnRead(state.CachedTexture);
}
if (MvppTraceProbe.Enabled)
{
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), state.CachedTexture); // [TRACEPROBE] dep edge (fast path)
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), state.CachedTexture);
}
if (MvppScenePassProbe.Enabled)
{
MvppScenePassProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); // [SCENEPROBE] per-draw input (fast path)
MvppScenePassProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler);
}
MvppBuilderInProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); // [BUILDERIN] per-draw input (fast path, self-gated)
MvppMap64Probe.OnInput(state.CachedTexture, stage, bindingInfo.Handle); // [MAP64] per-draw input (fast path, self-gated)
MvppMvBufProbe.OnSampled(state.CachedTexture); // [MVBUF] identity of the sampled MV buffer (self-gated)
MvppTwinMapProbe.OnRead(state.CachedTexture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated)
MvppBuilderInProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler);
MvppMap64Probe.OnInput(state.CachedTexture, stage, bindingInfo.Handle);
MvppMvBufProbe.OnSampled(state.CachedTexture);
MvppTwinMapProbe.OnRead(state.CachedTexture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager);
state.CachedTexture.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty
state.CachedTexture.EnsureForcedMips();
if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 &&
UpdateScale(state.CachedTexture, usageFlags, index, stage))
@@ -745,48 +718,42 @@ namespace Ryujinx.Graphics.Gpu.Image
if (MvppPairProbe.Enabled)
{
MvppPairProbe.OnPair(texture, sampler); // read-only (gated)
MvppPairProbe.OnPair(texture, sampler);
}
if (MvppDofProbe.Enabled)
{
// [DOFPROBE] resolve (cache-miss) path: an input re-fetched this draw. The DoF
// feedback/history buffer is re-invalidated every frame, so it lands here often.
MvppDofProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage);
}
if (MvppTaaProbe.Enabled)
{
// [TAAPROBE] chemin de resolution (defaut de cache) : c'est celui ou un
// historique temporel, re-invalide a chaque image, atterrit le plus souvent.
MvppTaaProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage);
}
if (MvppFeedbackProbe.Enabled)
{
MvppFeedbackProbe.OnRead(texture); // [FEEDBACKPROBE] input read (resolve path)
MvppFeedbackProbe.OnRead(texture);
}
if (MvppTraceProbe.Enabled)
{
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), texture); // [TRACEPROBE] dep edge (resolve path)
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), texture);
}
if (MvppScenePassProbe.Enabled)
{
MvppScenePassProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); // [SCENEPROBE] per-draw input (resolve path)
MvppScenePassProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler);
}
MvppBuilderInProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); // [BUILDERIN] per-draw input (resolve path, self-gated)
MvppMap64Probe.OnInput(texture, stage, bindingInfo.Handle); // [MAP64] per-draw input (resolve path, self-gated)
MvppMvBufProbe.OnSampled(texture); // [MVBUF] identity of the sampled MV buffer (self-gated)
MvppTwinMapProbe.OnRead(texture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated)
MvppBuilderInProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler);
MvppMap64Probe.OnInput(texture, stage, bindingInfo.Handle);
MvppMvBufProbe.OnSampled(texture);
MvppTwinMapProbe.OnRead(texture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager);
// [TWINFIX] (EXP 21) read-side redirect: reads of the sky-only twin are served by
// the material twin. Self-gated; returns the input texture unless armed+matched.
texture = MvppTwinFixProbe.MaybeRedirect(texture, stage, bindingInfo.Handle);
texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty
texture?.EnsureForcedMips();
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
@@ -901,7 +868,7 @@ namespace Ryujinx.Graphics.Gpu.Image
// The texture is already bound.
cachedTexture.SynchronizeMemory();
MvppTwinXferProbe.OnImageBind(cachedTexture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated
MvppTwinXferProbe.OnImageBind(cachedTexture, stage, bindingInfo.Handle, isStore);
if (isStore)
{
@@ -942,7 +909,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
else
{
MvppTwinXferProbe.OnImageBind(texture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated
MvppTwinXferProbe.OnImageBind(texture, stage, bindingInfo.Handle, isStore);
if (isStore)
{
@@ -66,7 +66,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
_overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
// [VIEWALIAS] / [FULLSYNC] / [MAP64] Positive controls: make the probes' silence interpretable.
MvppViewAliasProbe.ReportArmed();
MvppFullSyncProbe.ReportArmed();
MvppMap64Probe.ReportArmed();
@@ -878,7 +877,6 @@ namespace Ryujinx.Graphics.Gpu.Image
info = info.CreateInfoWithFormat(overlap.Info.FormatInfo);
}
// [VIEWALIAS] Read-only: report views created over a parent of a DIFFERENT format.
MvppViewAliasProbe.OnViewCreated(info, overlap, oInfo.FirstLayer, oInfo.FirstLevel, oInfo.Compatibility.ToString());
texture = overlap.CreateView(info, sizeInfo, range.Value, oInfo.FirstLayer, oInfo.FirstLevel);
@@ -1287,9 +1285,6 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="caps">GPU capabilities</param>
/// <param name="scale">Texture scale factor, to be applied to the texture size</param>
/// <returns>The texture creation information</returns>
// E3 (DLSS-SR) gate: fractional resolution scale support. Off by default; the daily
// driver is never affected. Shared by every site that must agree on the quantized
// per-texture effective scale (texture sizes, viewport/scissor, shader compensation).
internal static readonly bool DlssSrSpike =
Environment.GetEnvironmentVariable("RYUJINX_DLSS_SR") == "1";
@@ -1336,13 +1331,6 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (DlssSrSpike)
{
// Fractional scale, NO padding: alloc == active == ceil(size * scale).
// Any padding (mip alignment) leaves a dead border that later passes sample
// (the BOTW terrain hole), and it CANNOT be covered by stretching the
// viewport: the framebuffer is bounded by the SMALLEST attachment (a
// single-mip depth bound with a multi-mip color kills the stretch). Mip
// consistency (VUID-04533: fb larger than a mip attachment) is instead
// guaranteed by ClampViewToStorage at view-creation time.
width = (int)MathF.Ceiling(width * scale);
height = (int)MathF.Ceiling(height * scale);
activeWidth = width;
@@ -1376,18 +1364,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Clamps a view's host size to the physical dimensions of the parent storage at the
/// level the view starts on. With fractional scaling, host storage mips halve by floor
/// while scaled guest mip sizes round up (ceil), so a view's computed size can exceed
/// the storage mip by a pixel; a framebuffer sized from that view is then larger than
/// the attachment (VUID-04533, device lost when rendering to low mips). This clamp
/// replaces the old mip-alignment padding, which created the dead borders instead.
/// </summary>
/// <param name="createInfo">The computed view creation info</param>
/// <param name="storage">The parent host storage the view lives in</param>
/// <param name="firstLevel">First mip level of the view on the parent storage</param>
/// <param name="scale">Scale factor of the texture (no-op at 1.0)</param>
/// <returns>The creation info, size-clamped to the storage if needed</returns>
public static TextureCreateInfo ClampViewToStorage(TextureCreateInfo createInfo, ITexture storage, int firstLevel, float scale)
{
if (!DlssSrSpike || scale == 1f || storage == null || firstLevel <= 0)
@@ -67,22 +67,12 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="info">Texture information</param>
/// <param name="caps">Host GPU capabilities</param>
/// <returns>A host compatible format</returns>
// [FORCEMIPS_MASKS, default OFF] Companion knob of RYUJINX_MVPP_FORCEMIPS: cover the
// single-channel MASK textures (PAIRPROBE band census 11/07: BC4 = 20 mip-capable pairs =
// the biggest mip-less population left, classic foliage-alpha/detail masks; R8 = 9 more).
// Eligible mip-less BC4 textures are routed through the STOCK unsupported-BC path
// (decode to R8 host at upload) so the generated-mips blit chain can run on them.
internal static readonly bool ForceMipsMasks =
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_MASKS") == "1";
/// <summary>
/// [FORCEMIPS_MASKS] True when this texture should take the software BC-decode path even
/// though the host supports BC natively: mip-less 2D BC4 mask of forced-mips size, while
/// the masks knob is set. Mirrors the forced-mips eligibility so host format and level
/// boost always agree.
/// </summary>
/// <param name="info">Texture information</param>
/// <returns>True if the texture must be decoded to an uncompressed host format</returns>
public static bool ForceMipsWantsUncompressed(in TextureInfo info)
{
return ForceMipsMasks &&
@@ -363,7 +363,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (group.NeedsCopy && group.Copy(_context))
{
MvppMvSyncProbe.OnCopyIn(Storage); // [MVSYNC] read-only, self-gated
MvppMvSyncProbe.OnCopyIn(Storage);
anyModified |= true; // The copy target has been modified.
handleDirty = false;
}
@@ -389,7 +389,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
bool partialPath = anyNotDirty || (_handles.Length > 1 && (anyModified || split));
MvppMvSyncProbe.OnSyncDecision(Storage, partialPath, regionCount, anyModified); // [MVSYNC] read-only, self-gated
MvppMvSyncProbe.OnSyncDecision(Storage, partialPath, regionCount, anyModified);
if (partialPath)
{
@@ -1743,4 +1743,3 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
}
@@ -35,18 +35,13 @@ namespace Ryujinx.Graphics.Gpu.Image
public float RenderTargetScale { get; private set; } = 1f;
/// <summary>
/// Exact horizontal scale (host/guest ratio) of the currently bound render targets.
/// Matches <see cref="RenderTargetScale"/> unless quantized fractional scaling snapped
/// the target sizes up to the mip granularity.
/// </summary>
public float RenderTargetScaleX { get; private set; } = 1f;
/// <summary>
/// Exact vertical scale (host/guest ratio) of the currently bound render targets.
/// </summary>
public float RenderTargetScaleY { get; private set; } = 1f;
// [COUTURE PROBE -- TEMPORARY, DISPOSABLE] throttle for the edge-seam hunt log.
private long _coutureLogMs;
/// <summary>
@@ -72,7 +67,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
_rtColorsBound = new bool[Constants.TotalRenderTargets];
MvppSeamProbe.Announce(); // one status line per boot, even if no call site ever fires
MvppSeamProbe.Announce();
}
/// <summary>
@@ -84,43 +79,36 @@ namespace Ryujinx.Graphics.Gpu.Image
_cpBindingsManager.SetBindings(bindings);
}
/// <summary>[MV++ UI audit, read-only] Enumerates the current compute bindings for the gated probe.</summary>
public void MvppEnumerateComputeBindings(System.Action<bool, bool, Texture> report)
{
_cpBindingsManager.MvppEnumerateCompute(report);
}
/// <summary>[MV++ UI audit, read-only] Enumerates the graphics draw's sampled input textures.</summary>
public void MvppEnumerateGraphicsInputs(System.Action<Texture> report)
{
_gpBindingsManager.MvppEnumerateInputs(report);
}
/// <summary>[MV++ UI audit, read-only] Count of array/bindless texture bindings in the current graphics draw.</summary>
public int MvppGraphicsArrayBindingCount()
{
return _gpBindingsManager.MvppArrayBindingCount();
}
/// <summary>[MV++ UI audit, read-only] Enumerates the graphics draw's bound storage images (imageLoad/Store).</summary>
public void MvppEnumerateGraphicsImages(System.Action<Texture, bool> report)
{
_gpBindingsManager.MvppEnumerateImages(report);
}
/// <summary>[MV++ UI audit, read-only] Enumerates the graphics draw's sampled inputs with shader stage.</summary>
public void MvppEnumerateGraphicsInputsStage(System.Action<int, Texture> report)
{
_gpBindingsManager.MvppEnumerateInputsStage(report);
}
/// <summary>[GLOWPROBE, read-only] Bound textures WITH their sampler.</summary>
public void MvppEnumerateGraphicsInputsWithSampler(System.Action<int, Texture, Sampler> report)
{
_gpBindingsManager.MvppEnumerateInputsWithSampler(report);
}
/// <summary>[MV++ UI audit, read-only] Enumerates the currently-bound colour render targets (all 8 slots).</summary>
public void MvppEnumerateRenderTargets(System.Action<int, Texture> report)
{
for (int i = 0; i < _rtColors.Length; i++)
@@ -212,14 +200,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <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 &&
@@ -236,9 +217,6 @@ namespace Ryujinx.Graphics.Gpu.Image
public bool SetRenderTargetColor(int index, Texture color)
{
bool hasValue = color != null;
// [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)
@@ -256,12 +234,12 @@ namespace Ryujinx.Graphics.Gpu.Image
{
color.SynchronizeMemory();
color.SignalModifying(true);
color.DemoteForcedMipsOnRenderTargetBind(); // [MIPS-RT fix] no-op hors boost (2 tests de champ)
color.DemoteForcedMipsOnRenderTargetBind();
}
if (MvppFeedbackProbe.Enabled && color != null)
{
MvppFeedbackProbe.OnWrite(color); // [FEEDBACKPROBE] colour target bound = write (gated)
MvppFeedbackProbe.OnWrite(color);
}
_rtColors[index] = color;
@@ -278,7 +256,7 @@ namespace Ryujinx.Graphics.Gpu.Image
public bool SetRenderTargetDepthStencil(Texture depthStencil)
{
bool hasValue = depthStencil != null;
bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && (RenderTargetScale != depthStencil.ScaleFactor || EffectiveScaleMismatch(depthStencil))); // [P1.5 RE-APPLIED, A/B user] see SetRenderTargetColor
bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && (RenderTargetScale != depthStencil.ScaleFactor || EffectiveScaleMismatch(depthStencil)));
if (_rtDepthStencil != depthStencil)
{
@@ -300,8 +278,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_rtDepthStencil = depthStencil;
}
// Publish the last bound depth so the presentation path can forward a real depth buffer to
// DLSS. Only non-null binds (a real pass) update it; consumers validate dimensions.
if (depthStencil != null)
{
_context.LastPresentDepthStencil = depthStencil;
@@ -311,26 +287,18 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Currently bound depth-stencil render target (if any).
/// </summary>
public Texture RenderTargetDepthStencil => _rtDepthStencil;
/// <summary>
/// [COLORCAP probe] Currently bound color render target 0 (if any). Read-only accessor added
/// to let the native-color capture prototype read the scene-pass color attachment the same way
/// the depth capture reads <see cref="RenderTargetDepthStencil"/>. No behavior change.
/// </summary>
public Texture RenderTargetColor0 => _rtColors[0];
/// <summary>
/// [E0/SCENEPROBE read-only] Number of colour render-target (MRT) slots. Lets the scene probe
/// enumerate every out_attr_N attachment. No behaviour change.
/// </summary>
public int ColorTargetsLength => _rtColors.Length;
/// <summary>
/// [E0/SCENEPROBE read-only] Bound colour render target at the given MRT slot, or null. Lets the
/// scene probe log every out_attr_N attachment format (esp. out_attr1, the feedback target).
/// </summary>
public Texture GetColorTarget(int index) => (uint)index < (uint)_rtColors.Length ? _rtColors[index] : null;
@@ -446,10 +414,6 @@ namespace Ryujinx.Graphics.Gpu.Image
RenderTargetScale = targetScale;
// With quantized fractional scaling each target's real (host/guest) ratio can sit a
// hair above the nominal scale. Viewport/scissor must cover the full allocation, so
// expose the exact group ratio; the smallest one wins if targets disagree (the
// framebuffer is sized by the smallest attachment).
float scaleX = targetScale;
float scaleY = targetScale;
@@ -498,11 +462,6 @@ 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)
{
@@ -705,12 +664,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// MV++ velocity pass: the graft binds its own framebuffer DIRECTLY at the GAL level,
/// bypassing this class's host-binding cache -- without this invalidation, the next
/// UpdateRenderTargets() compares against stale 'still bound' entries, skips the
/// re-issue, and the game keeps rendering into the graft's framebuffer (the measured
/// black-screen bug of 2026-07-02). Clearing the cache forces the next commit to
/// re-send the guest bindings to the backend.
/// </summary>
public void MvppInvalidateHostRenderTargets()
{
@@ -598,16 +598,6 @@ namespace Ryujinx.Graphics.Gpu.Image
int gobBlocksInZ = descriptor.UnpackGobBlocksInZ();
if (target != Target.Texture3D && gobBlocksInZ > 1 && depthOrLayers > 1)
{
// [GOBPROBE, 21/07] This clamp is a FORK addition (it was put in for the BOTW/TOTK
// 4K packs); upstream honours the descriptor. It overrides the block-linear stride
// the game declared, and getting that stride wrong is precisely what produces
// RECTANGLES OF DISPLACED-BUT-REAL CONTENT -- the shape of the Xenoblade 2 artefact
// (blocky rows, worse indoors where more array textures are in flight, appearing as
// the camera turns and brings new ones in).
//
// Read-only for now: the clamp still applies exactly as before, we only COUNT it and
// describe what it hit. If it never fires on XC2 the lead dies here; if it fires
// constantly we have the suspect, and RYUJINX_NO_GOBZ_CLAMP=1 then tests it for real.
if (MvppGobProbe.Enabled)
{
MvppGobProbe.NoteClamp(width, height, depthOrLayers, gobBlocksInZ, target.ToString(), formatInfo.Format.ToString());
+2 -28
View File
@@ -283,14 +283,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </remarks>
/// <param name="address">Start address of the range to synchronize</param>
/// <param name="size">Size in bytes of the range to synchronize</param>
// [Beast Roofer diag] RYUJINX_BUFSEQ=1 (EXP 13, gated OFF by default): defeat the
// sequence-number short-circuit of buffer synchronization. Within one sequence, a buffer
// region is checked for CPU writes at most once -- so a game writing constants MID-frame
// (CPU running ahead, standard) has those writes served only NEXT sequence: late-frame
// draws read one-generation-stale data. That is the LAST standing family for the XC2
// stale-motion artifact (journal 145): patchy per draw, motion-only, barrier-insensitive.
// With the flag on, every query runs with a fresh sequence number (full recheck; slower,
// cannot be wrong). Artifact gone => root found; see the [BUFSEQ] witness counter.
private static readonly bool _bufSeqForce =
Environment.GetEnvironmentVariable("RYUJINX_BUFSEQ") == "1";
@@ -298,17 +290,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
private static long _bufSeqCalls;
private static long _bufSeqLogMs;
// [BUFCOAL 02/08, journal (364)-(365)] Coalescence a trou tolere des uploads invite->hote
// (RYUJINX_BUFCOAL=1, OFF par defaut = chemin stock a l'octet pres). Mesure (362)-(363) :
// en rotation BOTW le fil GPU emet ~17 000 SetBufferData par vraie image (miettes de
// 160 o a 4 Ko), et A ~50 000 appels/s la machinerie de la file threadee par commande
// coute autant que les octets. Ce gate fusionne les regions sales d'un MEME buffer
// distantes de moins de RYUJINX_BUFCOAL_GAP octets (defaut 4096, borne 64 Ko) en UN
// LoadRegion, a l'interieur d'UN SEUL SynchronizeMemory (jamais de report inter-appel).
// ⚠️ SURETE : re-uploader des octets PROPRES n'est correct que si l'hote n'a pas de
// donnees que l'invite n'a pas — donc coalescence UNIQUEMENT quand _modifiedRanges est
// null (aucune plage ecrite par le GPU) ; sinon repli immediat sur le chemin stock
// (ExcludeModifiedRegions doit continuer de decouper autour des plages GPU).
private static readonly bool _bufCoalEnabled =
Environment.GetEnvironmentVariable("RYUJINX_BUFCOAL") == "1";
@@ -323,10 +304,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
private ulong _coalEnd;
/// <summary>
/// Gated replacement for the load delegate: merges dirty regions of this buffer that are
/// within the tolerated gap into one pending window, flushed by <see cref="FlushCoalesced"/>
/// at the end of the synchronization. Falls back to the stock path the moment the buffer
/// has GPU-modified ranges (see the [BUFCOAL] safety note above).
/// </summary>
private void LoadRegionCoalesced(ulong mAddress, ulong mSize)
{
@@ -365,8 +342,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>Uploads the pending coalesced window, if any. Must run before leaving the
/// synchronization that produced it (the window never survives across calls).</summary>
private void FlushCoalesced()
{
if (_coalStart != ulong.MaxValue)
@@ -416,7 +391,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
BackingState.RecordSet();
_context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size));
MvppPalProbe.OnUpload(Address, Size, site: 0, _physicalMemory); // [PAL/UPVOL] read-only (gated): full guest->host upload
MvppPalProbe.OnUpload(Address, Size, site: 0, _physicalMemory);
CopyToDependantVirtualBuffers();
}
@@ -446,7 +421,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (_bufCoalEnabled)
{
// [BUFCOAL] la fenetre en attente ne survit jamais a la synchro qui l'a produite.
FlushCoalesced();
}
}
@@ -710,7 +684,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
_context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize));
MvppPalProbe.OnUpload(mAddress, mSize, site: 1, _physicalMemory); // [PAL/UPVOL] read-only (gated): dirty-region upload (LoadRegion)
MvppPalProbe.OnUpload(mAddress, mSize, site: 1, _physicalMemory);
CopyToDependantVirtualBuffers(mAddress, mSize);
}
@@ -690,7 +690,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy);
MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size, BufferStage.Copy);
// [PAL] Read-only (gated): does a GPU copy write into the tracked palettes?
MvppPalProbe.OnGpuCopy(dstRange.GetSubRange(0).Address, size);
if (srcRange.Count == 1 && dstRange.Count == 1)
@@ -215,7 +215,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStage.TransformFeedback);
// [PAL] Read-only (gated): does transform feedback write into the tracked palettes?
MvppPalProbe.OnXfbBind(index, range);
_transformFeedbackBuffers[index] = new BufferBounds(range);
@@ -265,7 +264,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags));
// [PAL] Read-only (gated): which compute dispatches touch the tracked palettes.
MvppPalProbe.OnComputeBind(index, range, (int)flags);
_cpStorageBuffers.SetBounds(index, range, flags);
@@ -292,7 +290,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags));
// [PAL] Read-only (gated): guest-hash the XC2 velocity palettes (vertex SSBO slots 0/1).
MvppPalProbe.OnBind(stage, index, range, _channel.MemoryManager.Physical);
if (!buffers.Buffers[index].Range.Equals(range))
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using Ryujinx.Memory.Range;
@@ -9,32 +6,12 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Memory
{
/// <summary>
/// [PAL] Read-only probe (RYUJINX_PAL_PROBE=1), inert unless set. v2.
/// Watches the XC2 velocity matrix palettes (vertex-stage SSBOs, slots 0/1).
///
/// v1 lessons (journal 137/138): slot 0 is shared by 7+ different SSBOs (first-bind-per-frame
/// sampling compared DIFFERENT buffers across frames), and a 256-byte prefix hash misses
/// palette changes past the first matrix. The v1 run still nailed the architecture: the two
/// big palettes (0x5DC00 bytes) PING-PONG between slots 0 and 1 every frame.
///
/// v2 therefore tracks buffers BY ADDRESS, not by slot: every distinct (address, size) bound
/// at vertex slots 0/1 with size >= 64 KiB (the big palettes only), with a STRIDED hash over
/// the WHOLE range (1 byte every 4093 -- prime stride, ~94 samples for 384 KiB), once per
/// frame per buffer. Uploads (Buffer.LoadRegion / full SetBufferData) are attributed per
/// tracked buffer. Reading grid:
/// - a tracked palette shows guest CHANGES with ZERO intersecting upload that frame, and
/// the artifact correlates with motion of skinned objects => STALE DATA SERVED = root;
/// - every guest change is matched by an upload => buffer sync innocent for these ranges;
/// - palettes never change even with NPCs moving on screen => palettes are not per-frame
/// data as assumed: revisit the VS reading.
/// </summary>
static class MvppPalProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("RYUJINX_PAL_PROBE") == "1";
private const ulong MinTrackedSize = 0x1000; // v5: >=4KiB -- catch skinned-character palettes too
private const ulong MinTrackedSize = 0x1000;
private const int MaxTracked = 32;
private const int HashStride = 4093;
private const int AnomalyLogCap = 24;
@@ -62,10 +39,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
private static long _anomaliesLogged;
private static long _summaryMs;
/// <summary>Vertex-stage storage buffer bound (BufferManager.SetGraphicsStorageBuffer). Self-gated.</summary>
public static void OnBind(int stage, int index, MultiRange range, PhysicalMemory pm)
{
if (!Enabled || stage != 0 || index > 5 || pm == null) // v5: slots 0..5
if (!Enabled || stage != 0 || index > 5 || pm == null)
{
return;
}
@@ -100,7 +76,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (rec.SeenThisFrame)
{
return; // one hash per buffer per frame
return;
}
rec.SeenThisFrame = true;
@@ -120,13 +96,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
// v3: identify the PRODUCER. v2 measured that no tracked palette is ever CPU-written
// (guest hash frozen over 3600 frames with NPCs moving) => they are GPU-written. This
// hook logs which COMPUTE dispatches bind a tracked range as storage, with the usage
// flags -- write-usage binds name the producer pass.
private static readonly HashSet<(ulong, int, int)> _computeBinds = new();
/// <summary>Compute storage buffer bound (BufferManager.SetComputeStorageBuffer). Self-gated.</summary>
public static void OnComputeBind(int index, MultiRange range, int flags)
{
if (!Enabled)
@@ -153,13 +124,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
// v4: v3 measured ZERO compute binds on the tracked palettes -- the producer is not a
// compute SSBO write. Remaining GPU write paths: TRANSFORM FEEDBACK (vertex-shader
// skinning into buffers, classic for this engine generation) and DMA buffer copies.
private static readonly HashSet<(ulong, int)> _xfbBinds = new();
private static readonly HashSet<(ulong, ulong)> _copyHits = new();
/// <summary>Transform feedback buffer bound (BufferManager.SetTransformFeedbackBuffer). Self-gated.</summary>
public static void OnXfbBind(int index, MultiRange range)
{
if (!Enabled)
@@ -186,7 +153,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>GPU buffer copy (BufferCache.CopyBuffer). Self-gated; physical addresses.</summary>
public static void OnGpuCopy(ulong dstAddress, ulong size)
{
if (!Enabled)
@@ -211,13 +177,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>Guest-to-host buffer upload (Buffer.LoadRegion / full SetBufferData). Self-gated.
/// site : 0 = SetBufferData COMPLET (creation/reset, Buffer.cs:~338), 1 = LoadRegion
/// (region sale d'un buffer existant, Buffer.cs:~626). Sert au split UPVOL v2 (362).</summary>
public static void OnUpload(ulong address, ulong size, int site = 0, PhysicalMemory pm = null)
{
// [UPVOL] compteur de volume independant (gate RYUJINX_UPVOL, inerte sinon) --
// partage ces sites d'appel pour ne pas toucher une 2e fois au chemin chaud.
MvppUpVolProbe.OnUpload(address, size, site, pm);
if (!Enabled)
@@ -239,7 +200,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>Guest frame boundary (Window present). Folds per-frame flags into the verdict counters.</summary>
public static void OnPresent()
{
if (!Enabled)
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT).
using Ryujinx.Common.Logging;
using System;
@@ -8,18 +5,6 @@ using System.Threading;
namespace Ryujinx.Graphics.Gpu.Memory
{
/// <summary>
/// [UPVOL v2] Compteur de VOLUME des uploads invite->hote (RYUJINX_UPVOL=1, inerte sinon).
/// Journal (361)-(362). v1 a mesure : rotation = ~45 000 uploads/s, 112-122 Mo/s, tout en
/// 4-64 Ko (immobile 3 500/s, 8 Mo/s) => RE-UPLOADS, pas des donnees neuves.
/// v2 SPLIT PAR PORTE pour viser le fix :
/// site 0 = SetBufferData COMPLET (creation/reset de buffer, Buffer.cs:~338)
/// site 1 = LoadRegion (region sale d'un buffer existant, Buffer.cs:~626)
/// Rafale au site 1 => fix = politique de synchro (dedup/fusion/dissociation) ;
/// rafale au site 0 => fix = politique du cache (retention, tempete de recreation).
/// v2 corrige aussi le caveat (362) : DEUX fils appellent (GPU + present) => Interlocked.
/// Fenetre ~5 s, histogramme de tailles, log Info une ligne.
/// </summary>
static class MvppUpVolProbe
{
public static readonly bool Enabled =
@@ -34,15 +19,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
private static long _site0Bytes;
private static long _site1Calls;
private static long _site1Bytes;
// Buckets: <4K, 4K-64K, 64K-1M, >=1M (nombre, octets)
private static readonly long[] _bucketCalls = new long[4];
private static readonly long[] _bucketBytes = new long[4];
// [MIRBENCH v3] micro-banc de vitesse de lecture du MIROIR invite (l'hypothese (366) :
// la copie productrice est ~35x plus lente PAR OCTET que la meme copie depuis le pool).
// A chaque fenetre : retenir le PLUS GROS upload ; au flush, relire CE range depuis le
// miroir (pm.GetSpan -> copie vers un scratch) et copier la MEME taille depuis un tableau
// gere -> deux debits Mo/s compares, meme taille, meme scratch. Lecture seule.
private static ulong _benchAddr;
private static long _benchSize;
private static PhysicalMemory _benchPm;
@@ -129,8 +108,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>[MIRBENCH] relit le plus gros upload de la fenetre depuis le miroir et copie
/// la meme taille depuis un tableau gere : deux debits compares, une ligne de log.</summary>
private static void RunMirrorBench()
{
long benchSize = Interlocked.Exchange(ref _benchSize, 0);
@@ -139,7 +116,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (pm == null || benchSize < 0x4000)
{
return; // rien d'assez gros dans la fenetre pour un chrono fiable
return;
}
int sz = (int)Math.Min(benchSize, 0x40000);
@@ -152,10 +129,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
try
{
// Lecture MIROIR, DEUX passes chronometrees : la 1re est FROIDE (TLB/defauts de
// premiere lecture = exactement le cout que subit la copie de production, qui ne
// lit chaque region qu'une fois), la 2e est chaude (reference haute). Le scratch
// est reutilise entre fenetres => sa premiere-touche ne pollue que la fenetre 1.
ReadOnlySpan<byte> src = pm.GetSpan(benchAddr, sz);
long t0 = System.Diagnostics.Stopwatch.GetTimestamp();
src.CopyTo(_benchScratch);
@@ -163,7 +136,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
src.CopyTo(_benchScratch);
long t2 = System.Diagnostics.Stopwatch.GetTimestamp();
// Meme taille, source = tableau gere ordinaire (chaud)
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
long t3 = System.Diagnostics.Stopwatch.GetTimestamp();
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
@@ -23,12 +23,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
_data.RenderScale.AsSpan().Fill(defaultScale);
DirtyRenderScale(0, SupportBuffer.RenderScaleMaxCount);
// [JITTERINIT 02/08, journal (380)] Le bloc jitter est emis dans CHAQUE vertex shader
// et LIT JitterOffset a chaque image — mais le champ n'etait marque dirty que quand sa
// valeur CHANGEAIT : jitter jamais arme ⇒ zone jamais televersee ⇒ le shader lisait de
// la memoire GPU non initialisee (suspect n°1 du dossier ecran-vert, 4 jeux). Marquer
// le zero initial dirty garantit un (0,0) reellement present sur le GPU des la
// premiere image, exactement comme RenderScale ci-dessus.
_data.JitterOffset.X = 0f;
_data.JitterOffset.Y = 0f;
MarkDirty(SupportBuffer.JitterOffsetOffset, SupportBuffer.FieldSize);
@@ -99,11 +93,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
/// <summary>
/// Sets the DLSS Mode B clip-space jitter offset (in NDC) the shader adds to vertex positions
/// (scaled by w). Zero unless jitter is enabled, so the default path is unaffected.
/// </summary>
/// <param name="x">Jitter X in normalized device coordinates</param>
/// <param name="y">Jitter Y in normalized device coordinates</param>
public void SetJitter(float x, float y)
{
if (_data.JitterOffset.X != x || _data.JitterOffset.Y != y)
+2 -29
View File
@@ -1,6 +1,3 @@
// 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 Ryujinx.Graphics.GAL;
@@ -9,24 +6,13 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu
{
/// <summary>
/// MV++ destination-anchored framebuffer-writer probe (RYUJINX_MVPP_DESTPROBE=1). READ-ONLY, default
/// off, nothing in the render path branches on it. Instead of eliminating engines one by one (each
/// un-hooked engine = a blind spot), it anchors on the OUTPUT: it records the GPU address of the
/// surface handed to Present, then logs every texture-modification event that hits that address. The
/// two modification choke points every writer passes through are Texture.SignalModifying (render-target
/// path = Maxwell3D) and Texture.SignalModified (explicit copy/store path = DMA / Fermi2D / compute
/// image store / inline-to-memory). The Modifying-vs-Modified bit alone classifies the final writer as
/// a 3D draw vs an explicit copy, with no per-engine or array/bindless blind spot. Per frame the ordered
/// write chain shows the composite first and the HUD writes after.
/// </summary>
static class MvppDestProbe
{
private static bool _enabled =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DESTPROBE") == "1";
private const int MaxWritesLogged = 400; // hard cap on DEST-WRITE lines
private const int StackSampleWrites = 60; // capture a caller stack only for the first N matches (cost control)
private const int MaxWritesLogged = 400;
private const int StackSampleWrites = 60;
private static readonly HashSet<ulong> _presentedAddrs = new();
private static int _frame;
@@ -34,18 +20,14 @@ namespace Ryujinx.Graphics.Gpu
private static int _writesLogged;
private static int _stackSamples;
// [chain trace] extra intermediate addresses to trace ALL writers of (compute/DMA/2D/3D), fed by
// the final-draw probe when it finds an HDR intermediate whose producer isn't a sampled-texture draw.
private static readonly HashSet<ulong> _trackedAddrs = new();
private static int _trackedLogged;
private const int MaxTrackedLogged = 200;
public static bool Enabled => _enabled;
/// <summary>[read-only] True if the address is a known presented-framebuffer backbuffer.</summary>
public static bool IsPresentedAddress(ulong addr) => _presentedAddrs.Count != 0 && _presentedAddrs.Contains(addr);
/// <summary>[chain trace] Add an intermediate buffer address to trace every writer of.</summary>
public static void AddTracked(ulong addr)
{
if (_enabled && addr != 0 && _trackedAddrs.Count < 32)
@@ -54,10 +36,8 @@ namespace Ryujinx.Graphics.Gpu
}
}
/// <summary>[read-only] Monotonic present frame counter (for the final-draw probe's frame boundary).</summary>
public static int Frame => _frame;
/// <summary>Called at Present with the resolved presented surface; records its address and starts a frame.</summary>
public static void RegisterPresented(Image.Texture tex)
{
if (!_enabled || tex == null)
@@ -88,7 +68,6 @@ namespace Ryujinx.Graphics.Gpu
}
}
/// <summary>Called from Texture.SignalModified (viaModifying=false) and SignalModifying (viaModifying=true).</summary>
public static void OnModify(Image.Texture tex, bool viaModifying, bool bound)
{
if (!_enabled || tex == null || (_presentedAddrs.Count == 0 && _trackedAddrs.Count == 0))
@@ -96,7 +75,6 @@ namespace Ryujinx.Graphics.Gpu
return;
}
// SignalModifying(false) is an UNBIND, not a write -- ignore it.
if (viaModifying && !bound)
{
return;
@@ -112,8 +90,6 @@ namespace Ryujinx.Graphics.Gpu
return;
}
// Intermediate (tracked) writers get their own budget; this is the key question -- who
// writes the HDR intermediate, and via which engine (Modifying=3D RT vs Modified=copy/store).
if (isTracked && !isPres)
{
if (_trackedLogged >= MaxTrackedLogged)
@@ -159,9 +135,6 @@ namespace Ryujinx.Graphics.Gpu
}
}
// Best-effort engine identification: on a rare match, walk the stack past the Texture/Group frames to
// the first engine method (e.g. DmaClass.*, TwodClass.*, TextureBindingsManager.*, TextureManager.*).
// May collapse under Release inlining; the Modifying/Modified bit above is the robust discriminator.
private static string Caller()
{
try
@@ -22,10 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
// [JITTEREMIT 03/08] Le bloc de jitter clip-space n'est plus emis dans les vertex shaders
// quand le jitter est eteint (il lisait Position en sortie avant de l'ecrire = indefini).
// La TRADUCTION change => les caches de shaders existants doivent etre invalides, sinon
// les utilisateurs continuent d'executer l'ancien code et le correctif ne les atteint pas.
private const uint CodeGenVersion = 7355;
private const string SharedTocFileName = "shared.toc";
@@ -326,11 +322,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
throw new DiskCacheLoadException(DiskCacheLoadResult.IncompatibleVersion);
}
// 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;
@@ -566,9 +557,6 @@ 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 ||
@@ -596,7 +584,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
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;
@@ -849,9 +836,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
dataInfo.UsesInstanceId,
dataInfo.UsesDrawParameters,
dataInfo.UsesRtLayer,
// [MVPP CUTOUT PROBE] UsesDiscard is NOT serialized (kept out of the cache format on purpose:
// no version bump, no forced recompile). Disk-cache-loaded shaders read false here; the probe
// is meant to run with the shader cache disabled so every shader is freshly translated.
false,
dataInfo.ClipDistancesWritten,
dataInfo.FragmentOutputMap);
@@ -59,20 +59,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly Dictionary<ulong, CachedShaderProgram> _cpPrograms;
private readonly Dictionary<ShaderAddresses, CachedShaderProgram> _gpPrograms;
// MV++ double-transform (b1): separate cache for the MvppVariant program (same guest
// shaders re-translated with TranslationFlags.MvppVariant). Kept apart from _gpPrograms
// and NOT persisted to the host disk cache, so the base program is never confused with
// the variant (invariant: the flag rides the translation, not just the storage).
private readonly Dictionary<ShaderAddresses, CachedShaderProgram> _gpVariantPrograms;
// (b1) 5B3 diag: variant SPIR-V dump dir (RYUJINX_MVPP_VEL_DUMPSPV=<dir>), write-only.
private static readonly string _mvppDumpDir =
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DUMPSPV");
private static int _mvppDumpCount;
// [DUMPMAP] Read-only bridge: when RYUJINX_DUMP_MAP=1, log each dumped shader's guest
// address <-> ShaderDumper file paths, so a guest VA (e.g. fs=0x100082C30) maps to its
// ShaderNNNN.bin. Gated OFF by default; does not change dumping or translation.
private static readonly bool _dumpMap =
Environment.GetEnvironmentVariable("RYUJINX_DUMP_MAP") == "1";
@@ -331,9 +323,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
ShaderAddresses addresses,
TranslationFlags extraFlags = TranslationFlags.None)
{
// MV++ (b1): the MvppVariant program lives in its own cache and skips the host disk
// cache (which is keyed on guest code, not the flag) so the base is never returned
// for a variant request. Everything else is the identical translation path.
bool isVariant = extraFlags != TranslationFlags.None;
Dictionary<ShaderAddresses, CachedShaderProgram> gpCache = isVariant ? _gpVariantPrograms : _gpPrograms;
@@ -342,9 +331,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
return gpShaders;
}
// TryFind reads the guest bytecode into cachedGuestCode on hit AND miss (that's why
// a cold base translates below). A base HIT returns the disk-cached program; a
// VARIANT ignores the hit and always re-translates with MvppVariant flag.
bool diskHit = _graphicsShaderCache.TryFind(channel, ref poolState, ref graphicsState, addresses, out gpShaders, out CachedGraphicsGuestCode cachedGuestCode);
if (diskHit && !isVariant)
@@ -497,9 +483,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
if (isVariant)
{
// (b1) 5B3 diag (RYUJINX_MVPP_VEL_DUMPSPV=<dir>): dump the variant host SPIR-V
// at generation -- every survival "proof" so far lived at INJECTION time (IR);
// nobody ever read the COMPILED module. Write-only diagnostic, capped.
if (_mvppDumpDir != null && _mvppDumpCount < 2048)
{
try
@@ -521,8 +504,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
}
}
// The MvppVariant lives ONLY in its runtime cache -- never added to the host
// hash table nor persisted to disk (both are keyed on guest code, not the flag).
_gpVariantPrograms[addresses] = gpShaders;
Logger.Info?.Print(LogClass.Gpu,
@@ -854,8 +835,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
ShaderDumpPaths paths = dumper?.Dump(code, context.Stage == ShaderStage.Compute) ?? default;
// [DUMPMAP] Read-only VA<->file bridge (gated RYUJINX_DUMP_MAP=1). Only fires when a dump
// actually happened (paths.HasPath). Does not alter dumping or translation.
if (_dumpMap && paths.HasPath)
{
Logger.Info?.Print(LogClass.Gpu,
@@ -150,11 +150,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, textureSetIndex, textureBinding, texturesPerStage);
AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, imageSetIndex, imageBinding, imagesPerStage);
// MV++ (b1) sub-step 5a fix @91: the MVPP variant VS declares its N-1 palette cbuf at
// the fixed binding 91 -- OUTSIDE the per-stage windows above, so no window covers it.
// Add the one descriptor when (and only when) this stage's program carries it; the
// usage is added by the AddUsage(info.CBuffers) call below like any other cbuf. Base
// programs never carry binding 91 -> their layout is byte-identical to stock.
foreach (BufferDescriptor cb in info.CBuffers)
{
if (cb.Binding == Ryujinx.Graphics.Shader.Translation.MvppVariantInjector.MvppN1HostBinding)
@@ -164,12 +159,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
}
}
// [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] The survivor-map SSBO lives at the fixed
// out-of-window binding 80 (declared by every fragment shader when the probe is ON, see
// TranslatorContext.CreateResourceManager). Reserved-style buffers are NOT part of
// info.SBuffers, so add its descriptor + write usage here explicitly, Fragment stage only
// -- same special-case pattern as the N-1 cbuf above. Default OFF => layout is
// byte-identical to stock.
if (Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled &&
info.Stage == ShaderStage.Fragment)
{
+11 -101
View File
@@ -58,19 +58,13 @@ namespace Ryujinx.Graphics.Gpu
public object UserObj { get; }
/// <summary>
/// DLSS Mode B sub-pixel jitter offset this frame was rendered with, snapshotted at enqueue and
/// carried through the queue so the present can hand the matching offset to DLSS.
/// </summary>
public float JitterX { get; }
public float JitterY { get; }
/// <summary>[JITTERVAL probe] The DlssJitterState.FrameId this frame was tagged with at enqueue,
/// carried through the queue so the DLSS-side probe logs the same id the geometry-side probe did.</summary>
public long JitterFrameId { get; }
/// <summary>
/// MV++ camera view-projection this frame was rendered with, snapshotted at enqueue
/// like the jitter so the present hands DLSS the matrix matching the frame on screen.
/// </summary>
public System.Numerics.Matrix4x4 CameraVp { get; }
public bool CameraVpValid { get; }
@@ -119,12 +113,12 @@ namespace Ryujinx.Graphics.Gpu
private int _framesAvailable;
private long _mvppDepthLogMs;
private long _mvppFifoLogMs;
private long _jitTraceMs; // [JITTRACE] cadence de l'etape B
private long _jitTraceMs;
private int _mvppTakes;
private int _mvppTakeHits;
private Image.Texture _mvppHeldSceneDepth;
private int _mvppHeldSceneDepthSeq;
private long _colorPubDiagMs; // [COLORCAP snapshot] throttled publish diagnostic
private long _colorPubDiagMs;
public bool IsFrameAvailable => _framesAvailable != 0;
@@ -213,16 +207,10 @@ namespace Ryujinx.Graphics.Gpu
MultiRange range = new(address, (ulong)size);
// DLSS Mode B: snapshot the jitter offset this frame was rendered with (the value StateUpdater
// last applied to the scaled viewport) so it rides through the queue with the frame and reaches
// DLSS at present, instead of being looked up by a texture reference that does not match.
float jitterX = DlssJitterState.Enabled ? DlssJitterState.OffsetX : 0f;
float jitterY = DlssJitterState.Enabled ? DlssJitterState.OffsetY : 0f;
// [JITTERVAL probe] tag this frame with the current jitter frame id, carried to the present.
long jitterFrameId = DlssJitterState.Enabled ? DlssJitterState.FrameId : 0L;
// MV++: snapshot the camera view-projection this frame was rendered with, and re-arm
// the once-per-frame capture for the next one.
System.Numerics.Matrix4x4 cameraVp = default;
bool cameraVpValid = DlssCameraState.Enabled && DlssCameraState.SnapshotCurrent(out cameraVp);
if (DlssCameraState.Enabled)
@@ -230,10 +218,8 @@ namespace Ryujinx.Graphics.Gpu
Engine.Threed.MvppCameraCapture.OnFrameEnqueued();
}
// MV++ Phase 2.0: frame boundary for the draw-identity matching probe (no-op flag off).
Engine.Threed.MvppP2Probe.OnFrameEnqueued();
// MV++ Phase 2.2: frame boundary for the velocity pass (coverage clear).
Engine.Threed.MvppVelocityPass.OnFrameEnqueued();
_frameQueue.Enqueue(new PresentationTexture(
@@ -270,55 +256,38 @@ namespace Ryujinx.Graphics.Gpu
pt.Cache.Tick();
// [PRESYNC, 21/07] LE test décisif du dossier XC2. Tous les étages de rendu sont propres au
// dernier draw, l'image affichée est détruite, et rien entre les deux ne montrait d'anomalie
// -- SAUF cette resynchronisation, jamais instrumentée. Si le render target composé sur GPU
// est aussi suivi comme mémoire invitée et marqué sale, SynchronizeMemory recharge la mémoire
// invitée (périmée, car composée côté GPU) PAR-DESSUS le rendu propre. On capture avant/après.
Engine.Threed.MvppPreSyncProbe.Before(texture);
texture.SynchronizeMemory();
Engine.Threed.MvppPreSyncProbe.After(texture);
// [LAYOUT, 21/07] La texture présentée arrive DÉJÀ corrompue (mosaïque en blocs = détuilage).
// On compare ce que le JEU a demandé (pt.Info, issu de EnqueueFrameThreadSafe) à ce que le
// cache a réellement fourni (texture.Info). Un désaccord de tuilage (isLinear / gobBlocksInY /
// stride) est le suspect exact.
Engine.Threed.MvppLayoutProbe.Compare(pt.Info, texture);
Engine.Threed.MvppPreSyncProbe.DumpGuest(texture);
MvppDestProbe.RegisterPresented(texture); // read-only destination-anchored probe (gated)
MvppDestProbe.RegisterPresented(texture);
// [RTDUMP, 21/07] The frame as it actually reaches the screen. Everything captured
// so far was an INTERMEDIATE buffer, so "this one looks odd" could never be tied to
// "that block, right there, is what I see". Capturing the presented image in the
// same burst as the intermediates turns the whole investigation around: point at
// the defect, then find which buffer already carries it.
Engine.Threed.MvppRtDumpProbe.NotePresented(texture);
// Vraie frontière d'image pour la sonde UI : sa détection d'origine (montée du facteur de
// résolution) est muette sur tout jeu rendu à l'échelle native, XC2 compris.
Engine.Threed.MvppUiProbe.OnPresent();
Engine.Threed.MvppDrawStepProbe.OnPresent(texture);
Engine.Twod.MvppTwodProbe.OnPresent();
Image.MvppCacheProbe.OnPresent();
Image.MvppMvBufProbe.OnPresent(); // [MVBUF] frame boundary (self-gated)
Image.MvppMvSyncProbe.OnPresent(); // [MVSYNC] arming witness + heartbeat (self-gated)
Image.MvppWriterCensusProbe.OnPresent(); // [CENSUS] arming witness + heartbeat (self-gated)
Image.MvppTwinXferProbe.OnPresent(); // [TWINXFER] arming witness + heartbeat (self-gated)
Memory.MvppPalProbe.OnPresent(); // [PAL] frame boundary (self-gated)
Image.MvppTaaProbe.OnPresent(); // [TAAPROBE] frontiere d'image (auto-gardee)
// [JITTRACE] cadence de l'etape B, voir plus bas.
Image.MvppMvBufProbe.OnPresent();
Image.MvppMvSyncProbe.OnPresent();
Image.MvppWriterCensusProbe.OnPresent();
Image.MvppTwinXferProbe.OnPresent();
Memory.MvppPalProbe.OnPresent();
Image.MvppTaaProbe.OnPresent();
if (Image.MvppFeedbackProbe.Enabled)
{
Image.MvppFeedbackProbe.OnFrameBoundary(); // [FEEDBACKPROBE] guest frame boundary (gated)
Image.MvppFeedbackProbe.OnFrameBoundary();
}
if (Image.MvppTraceProbe.Enabled)
{
Image.MvppTraceProbe.OnFrameBoundary(); // [TRACEPROBE] dependency-graph report (gated)
Image.MvppTraceProbe.OnFrameBoundary();
}
float cropScaleX = texture.EffectiveScaleX;
@@ -355,76 +324,32 @@ namespace Ryujinx.Graphics.Gpu
crop = new ImageCrop(left, right, top, bottom, crop.FlipX, crop.FlipY, crop.IsStretched, crop.AspectRatioX, crop.AspectRatioY);
}
// DLSS Mode B: publish the jitter offset THIS exact frame was rendered with (carried through
// the queue), so the DLSS backend applies the matching sub-pixel offset. This is the carrier
// that replaces the old texture-reference lookup, which missed because the presented texture
// is not the jittered render target.
DlssJitterState.PresentX = pt.JitterX;
DlssJitterState.PresentY = pt.JitterY;
DlssJitterState.PresentFrameId = pt.JitterFrameId;
// MV++: republish the camera matrix for this exact frame. Ordered mode consumes
// the oldest unconsumed camera the GPU thread saw (order does the pairing --
// immune to the enqueue-snapshot race); snapshot mode keeps the value that rode
// the queue with the frame.
if (DlssCameraState.FifoEnabled)
{
// [GAMEJITTER 28/07] Le decalage sous-pixel du jeu sort de la file AVEC sa
// matrice et entre dans l'anneau de presentation AVEC elle : apparie a la meme
// image d'un bout a l'autre, sans champ statique qui pourrait deriver d'une
// image. Sur un cycle de 8 phases, declarer la mauvaise phase serait aussi faux
// que declarer zero.
bool orderedValid = DlssCameraState.TryConsumeOrdered(out System.Numerics.Matrix4x4 orderedVp);
// [GAMEJITTER 28/07, v2] Le decalage ne passe PLUS par la file ordonnee -- y
// ajouter deux flottants la faisait rendre du vide (voir le commentaire de
// _fifo). Il entre ici, dans l'anneau de presentation, qui est une file
// concurrente donc sure. La valeur est celle de la derniere lecture ACCEPTEE :
// elle a ete prise pendant les dessins de cette image, c'est donc la bonne
// phase pour l'image qu'on presente.
float orderedJx = Engine.Threed.MvppSoloCamera.LastJitterX;
float orderedJy = Engine.Threed.MvppSoloCamera.LastJitterY;
// ⛔ [28/07 17h35] APPEL A 4 PARAMETRES NEUTRALISE — DETTE DE GAMEJITTER.
// La surcharge (vp, valid, jx, jy) a ete ajoutee a DlssCameraState le 28/07 a
// 11:46, mais le GAL.dll de l'installation date de 10:07 et ne la contient pas.
// Tant que le projet Gpu n'est pas recompile, personne ne s'en apercoit ; des
// qu'il l'est, l'appel part dans le vide :
// MissingMethodException: Method not found: 'Void
// DlssCameraState.PublishPresent(Matrix4x4 ByRef, Boolean, Single, Single)'
// (plantage constate 28/07 17h30 en deployant MULTIROT ; binaire restaure).
//
// Reconstruire le GAL reglerait la signature mais REINJECTERAIT GAMEJITTER et
// 18 jours de changements dans l'etat valide par Alex -- c'est exactement la
// casse du matin. Or GAMEJITTER est MORT : mesure du 28/07, 8 valeurs cote
// camera et `brut (0,000;0,000)` cote DLSS, le canal ne transporte rien. On
// perd donc zero fonctionnalite en appelant la surcharge a 2 parametres.
//
// A RETABLIR le jour ou le GAL sera reconstruit volontairement (une seule
// ligne, les deux valeurs sont deja calculees juste au-dessus).
_ = orderedJx;
_ = orderedJy;
DlssCameraState.PublishPresent(in orderedVp, orderedValid);
// [FAMHOLD 01/08] Sonde R1/R2 du mecanisme (290), lecture pure : le hold de
// la file ordonnee (LastConsumeFresh == false) EST l'evenement mesure, et il
// vient d'etre etabli trois lignes plus haut. Voir MvppFamHold.
if (Engine.Threed.MvppFamHold.Enabled)
{
Engine.Threed.MvppFamHold.OnPresent(DlssCameraState.LastConsumeFresh, in orderedVp);
}
// [EXTPROBE 01/08] Phase T du design E : prediction en memoire + comparaison
// au reel suivant, lecture pure. Voir MvppExtProbe.
if (Engine.Threed.MvppExtProbe.Enabled)
{
Engine.Threed.MvppExtProbe.OnPresent(
DlssCameraState.LastConsumeFresh, in orderedVp, DlssCameraState.TeleportSeq);
}
// [JITTRACE 28/07] ETAPE B : ce qui SORT de la file et entre dans l'anneau.
// Si A est non nul et B vaut zero, la perte est dans la file ; si B est non nul
// et C vaut zero, elle est dans l'anneau ou apres.
if (Engine.Threed.MvppCameraCapture.JitTrace &&
Environment.TickCount64 - _jitTraceMs >= 1000)
{
@@ -452,19 +377,11 @@ namespace Ryujinx.Graphics.Gpu
DlssCameraState.PublishPresent(pt.CameraVp, pt.CameraVpValid);
}
// MV++: prefer the depth pinned at the scaled main pass (deterministic) over the
// "last depth bound by any pass" publication (a timing roulette that flapped
// between scene depth, shadow maps and bloom buffers -- the mode 2 tremble).
Image.Texture presentDepth = null;
if (DlssCameraState.Enabled)
{
Image.Texture scene = Engine.Threed.MvppCameraCapture.TakeSceneDepth();
// ~1-2% of presents have no scene draw in their interval (double-present,
// pacing hiccup). Falling back to the last-bind roulette there re-injects
// shadow maps / bloom buffers 1-2x per second -- enough to keep the MV
// source flapping. Hold the last pinned scene depth instead; the texture
// object persists across frames and holds the latest rendered content.
if (scene != null)
{
_mvppHeldSceneDepth = scene;
@@ -472,9 +389,6 @@ namespace Ryujinx.Graphics.Gpu
}
else if (_mvppHeldSceneDepth != null && _mvppHeldSceneDepth.InvalidatedSequence != _mvppHeldSceneDepthSeq)
{
// 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;
}
@@ -489,7 +403,6 @@ namespace Ryujinx.Graphics.Gpu
_mvppTakeHits++;
}
// MV++ diag: what depth is published at present time? (heartbeat, capture runs only)
if (MvppDev.Enabled && DlssCameraState.Enabled && Environment.TickCount64 - _mvppDepthLogMs >= 5000)
{
_mvppDepthLogMs = Environment.TickCount64;
@@ -506,9 +419,6 @@ namespace Ryujinx.Graphics.Gpu
Engine.Threed.MvppCameraCapture.StatSquareRejects = 0;
}
// [COLORCAP snapshot] publish this frame's pinned HDR scene color host to the cross-layer
// bridge so the Vulkan present can snapshot it (parallel to the depth held path). Gap
// frames (no scene color pinned) keep the last good value. Gated; nothing wired to DLSS.
if (GAL.MvppColorSnapshot.Enabled)
{
Image.Texture sceneColor = Engine.Threed.MvppCameraCapture.TakeSceneColor();
@@ -116,6 +116,44 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg
}
}
private static readonly bool _vidProbe =
Environment.GetEnvironmentVariable("RYUJINX_VIDPROBE") == "1";
private static long _vpWindowMs;
private static int _vpSubmitted, _vpDelivered, _vpDelayed, _vpFailed, _vpLastErr;
private static long _vpTotalSubmitted, _vpTotalDelivered;
private static void VidProbeTick(int submitted, int delivered, int delayed, int failed, int lastErr)
{
_vpSubmitted += submitted;
_vpDelivered += delivered;
_vpDelayed += delayed;
_vpFailed += failed;
_vpTotalSubmitted += submitted;
_vpTotalDelivered += delivered;
if (lastErr != 0)
{
_vpLastErr = lastErr;
}
long now = Environment.TickCount64;
if (now - _vpWindowMs < 1000)
{
return;
}
if (_vpWindowMs != 0)
{
Logger.Info?.Print(LogClass.FFmpeg,
$"VIDPROBE 1s : soumis={_vpSubmitted} rendus={_vpDelivered} retardes={_vpDelayed} " +
$"echecs={_vpFailed} dernier-code={_vpLastErr} | cumul soumis={_vpTotalSubmitted} rendus={_vpTotalDelivered}");
}
_vpWindowMs = now;
_vpSubmitted = _vpDelivered = _vpDelayed = _vpFailed = 0;
}
public int DecodeFrame(Surface output, ReadOnlySpan<byte> bitstream)
{
FFmpegApi.av_frame_unref(output.Frame);
@@ -130,8 +168,11 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg
result = _decodeFrame(_context, output.Frame, &gotFrame, _packet);
}
bool delayedTried = false;
if (gotFrame == 0)
{
delayedTried = true;
FFmpegApi.av_frame_unref(output.Frame);
// If the frame was not delivered, it was probably delayed.
@@ -151,9 +192,19 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg
{
FFmpegApi.av_frame_unref(output.Frame);
if (_vidProbe)
{
VidProbeTick(1, 0, delayedTried ? 1 : 0, 1, result);
}
return -1;
}
if (_vidProbe)
{
VidProbeTick(1, 1, delayedTried ? 1 : 0, result < 0 ? 1 : 0, result < 0 ? result : 0);
}
return result < 0 ? result : 0;
}

Some files were not shown because too many files have changed in this diff Show More