The Beast Roofer Edition v1.2.5
This commit is contained in:
@@ -3,3 +3,6 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
*.json text 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
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
One correction while I am at it: the black block artifacts some of you saw in
|
||||||
ones I had described as a long-standing emulation defect and worked around by skipping the
|
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
|
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.
|
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
|
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
|
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.
|
other DLSS settings.
|
||||||
|
|
||||||
An experimental `Advanced motion stack` checkbox is also present, **off by default**. It
|
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.
|
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
|
### Known issue: Frame Generation still produces ghost images
|
||||||
|
|
||||||
With Frame Generation enabled, ghost or "temporal" images can still appear — doubled
|
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,
|
individual moving objects. If ghosting bothers you more than the extra smoothness helps,
|
||||||
set Frame Generation to Off in the graphics settings.
|
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
|
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
|
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
|
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
|
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.
|
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
|
Une correction au passage : les blocs noirs que certains ont vus dans les scènes avec
|
||||||
décrits comme un vieux défaut d'émulation et que je contournais en désactivant la passe de
|
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
|
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.
|
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
|
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.
|
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.
|
réglages DLSS.
|
||||||
|
|
||||||
Une case `Advanced motion stack` expérimentale est également présente, **désactivée par
|
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
|
défaut**. Elle améliore le suivi de la caméra : elle lit la caméra même quand un jeu la range
|
||||||
tremblement en mouvement ailleurs — laissez-la éteinte sauf si vous voulez expérimenter.
|
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
|
### 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
|
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.
|
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
|
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
|
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
|
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.Switch => VSyncMode.Unbounded,
|
||||||
VSyncMode.Unbounded => VSyncMode.Vrr,
|
VSyncMode.Unbounded => VSyncMode.Vrr,
|
||||||
VSyncMode.Vrr => VSyncMode.Switch,
|
VSyncMode.Vrr => VSyncMode.Switch,
|
||||||
VSyncMode.Custom => VSyncMode.Switch, // legacy mode, no longer in the cycle
|
VSyncMode.Custom => VSyncMode.Switch,
|
||||||
_ => VSyncMode.Switch
|
_ => VSyncMode.Switch
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,6 @@ namespace Ryujinx.Common.Utilities
|
|||||||
|
|
||||||
public static void SerializeToFile<T>(string filePath, T value, JsonTypeInfo<T> typeInfo)
|
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";
|
string tmpPath = filePath + ".tmp";
|
||||||
|
|
||||||
using (FileStream file = File.Create(tmpPath, DefaultFileWriteBufferSize, FileOptions.WriteThrough))
|
using (FileStream file = File.Create(tmpPath, DefaultFileWriteBufferSize, FileOptions.WriteThrough))
|
||||||
|
|||||||
@@ -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.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -8,20 +5,6 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.GAL
|
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 class DlssCameraState
|
||||||
{
|
{
|
||||||
public static bool Enabled;
|
public static bool Enabled;
|
||||||
@@ -30,58 +13,26 @@ namespace Ryujinx.Graphics.GAL
|
|||||||
private static Matrix4x4 _currentVp;
|
private static Matrix4x4 _currentVp;
|
||||||
private static bool _currentValid;
|
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 Matrix4x4 PresentVp;
|
||||||
public static bool PresentVpValid;
|
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 PresentJitterX;
|
||||||
public static float PresentJitterY;
|
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();
|
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);
|
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)
|
public static void PublishPresent(in Matrix4x4 vp, bool valid, float jx, float jy)
|
||||||
{
|
{
|
||||||
_presentRing.Enqueue((vp, valid, jx, 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)
|
while (_presentRing.Count > 4)
|
||||||
{
|
{
|
||||||
_presentRing.TryDequeue(out _);
|
_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()
|
public static void ConsumePresent()
|
||||||
{
|
{
|
||||||
if (_presentRing.TryDequeue(out (Matrix4x4 Vp, bool Valid, float Jx, float Jy) entry))
|
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)
|
public static void PublishCurrent(in Matrix4x4 vp)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
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)
|
public static bool SnapshotCurrent(out Matrix4x4 vp)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
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;
|
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;
|
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;
|
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;
|
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 readonly Queue<Matrix4x4> _fifo = new();
|
||||||
private static Matrix4x4 _fifoLast;
|
private static Matrix4x4 _fifoLast;
|
||||||
private static bool _fifoHasLast;
|
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 StatPushes;
|
||||||
public static int StatFresh;
|
public static int StatFresh;
|
||||||
public static int StatHolds;
|
public static int StatHolds;
|
||||||
public static int StatDrops;
|
public static int StatDrops;
|
||||||
public static int StatMaxDepth;
|
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;
|
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, float jx, float jy) => PushOrdered(in vp);
|
||||||
|
|
||||||
public static void PushOrdered(in Matrix4x4 vp)
|
public static void PushOrdered(in Matrix4x4 vp)
|
||||||
@@ -215,8 +95,6 @@ namespace Ryujinx.Graphics.GAL
|
|||||||
StatMaxDepth = _fifo.Count;
|
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)
|
while (_fifo.Count > 4)
|
||||||
{
|
{
|
||||||
_fifo.Dequeue();
|
_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)
|
public static bool TryConsumeOrdered(out Matrix4x4 vp)
|
||||||
{
|
{
|
||||||
if (_fifo.TryDequeue(out vp))
|
if (_fifo.TryDequeue(out vp))
|
||||||
|
|||||||
@@ -1,79 +1,34 @@
|
|||||||
namespace Ryujinx.Graphics.GAL
|
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 class DlssJitterState
|
||||||
{
|
{
|
||||||
public static bool Enabled;
|
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;
|
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;
|
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;
|
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;
|
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 InputWidth;
|
||||||
public static int InputHeight;
|
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 CamVpW;
|
||||||
public static float CamVpH;
|
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 OffsetX;
|
||||||
public static float OffsetY;
|
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 PresentX;
|
||||||
public static float PresentY;
|
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 FrameId;
|
||||||
public static long PresentFrameId;
|
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;
|
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;
|
public static long SceneColorFrameId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ namespace Ryujinx.Graphics.GAL
|
|||||||
{
|
{
|
||||||
void Barrier();
|
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 MvppInjectDlssUpscale(ITexture src, ITexture dst, ITexture depth) { }
|
||||||
|
|
||||||
void BeginTransformFeedback(PrimitiveTopology topology);
|
void BeginTransformFeedback(PrimitiveTopology topology);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||||||
Register<BufferDisposeCommand>(CommandType.BufferDispose);
|
Register<BufferDisposeCommand>(CommandType.BufferDispose);
|
||||||
Register<BufferGetDataCommand>(CommandType.BufferGetData);
|
Register<BufferGetDataCommand>(CommandType.BufferGetData);
|
||||||
Register<BufferSetDataCommand>(CommandType.BufferSetData);
|
Register<BufferSetDataCommand>(CommandType.BufferSetData);
|
||||||
Register<BufferSetDataBatchCommand>(CommandType.BufferSetDataBatch); // [BUFBATCH]
|
Register<BufferSetDataBatchCommand>(CommandType.BufferSetDataBatch);
|
||||||
|
|
||||||
Register<CounterEventDisposeCommand>(CommandType.CounterEventDispose);
|
Register<CounterEventDisposeCommand>(CommandType.CounterEventDispose);
|
||||||
Register<CounterEventFlushCommand>(CommandType.CounterEventFlush);
|
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 Ryujinx.Graphics.GAL.Multithreading.Model;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer
|
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
|
struct BufferSetDataBatchEntry
|
||||||
{
|
{
|
||||||
public BufferHandle Buffer;
|
public BufferHandle Buffer;
|
||||||
@@ -18,13 +11,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer
|
|||||||
public int Size;
|
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>
|
struct BufferSetDataBatchCommand : IGALCommand, IGALCommand<BufferSetDataBatchCommand>
|
||||||
{
|
{
|
||||||
public readonly CommandType CommandType => CommandType.BufferSetDataBatch;
|
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)
|
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
|
Span<BufferSetDataBatchEntry> entries = command._count <= 64
|
||||||
? stackalloc BufferSetDataBatchEntry[64]
|
? stackalloc BufferSetDataBatchEntry[64]
|
||||||
: new BufferSetDataBatchEntry[command._count];
|
: 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.Model;
|
||||||
using Ryujinx.Graphics.GAL.Multithreading.Resources;
|
using Ryujinx.Graphics.GAL.Multithreading.Resources;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
|
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>
|
struct MvppInjectDlssCommand : IGALCommand, IGALCommand<MvppInjectDlssCommand>
|
||||||
{
|
{
|
||||||
public readonly CommandType CommandType => CommandType.MvppInjectDlss;
|
public readonly CommandType CommandType => CommandType.MvppInjectDlss;
|
||||||
|
|||||||
@@ -162,16 +162,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||||||
return _spanPool.Insert(data);
|
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 =
|
private static readonly bool _bufBatchEnabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_BUFBATCH") == "1";
|
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).");
|
$"[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)
|
if (data.Length > BufBatchArenaBytes)
|
||||||
{
|
{
|
||||||
FlushBufferBatch();
|
FlushBufferBatch();
|
||||||
@@ -220,8 +209,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||||||
_bufBatchArenaUsed += data.Length;
|
_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()
|
private unsafe void FlushBufferBatch()
|
||||||
{
|
{
|
||||||
if (_bufBatchCount == 0)
|
if (_bufBatchCount == 0)
|
||||||
@@ -248,9 +235,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||||||
|
|
||||||
internal unsafe T* New<T>() where T : unmanaged, IGALCommand
|
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))
|
if (_bufBatchEnabled && _bufBatchCount != 0 && typeof(T) != typeof(BufferSetDataBatchCommand))
|
||||||
{
|
{
|
||||||
FlushBufferBatch();
|
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
|
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 class MvppColorSnapshot
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORCAP_SNAP") == "1";
|
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 ITexture SceneColorHost;
|
||||||
public static long FrameId;
|
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;
|
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;
|
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 =>
|
public static ITexture SceneColorBackend =>
|
||||||
SceneColorHost is Multithreading.Resources.ThreadedTexture t ? t.Base : SceneColorHost;
|
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;
|
public static ITexture SceneColorHeld;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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 class MvppDev
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
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
|
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 class MvppInjectState
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
|
|||||||
@@ -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
|
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
|
public static class MvppVelState
|
||||||
{
|
{
|
||||||
/// <summary>Coverage render target (R8, scene-depth-sized), owned by the graft.</summary>
|
|
||||||
public static ITexture Coverage;
|
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 =>
|
public static ITexture CoverageBackend =>
|
||||||
Coverage is Multithreading.Resources.ThreadedTexture t ? t.Base : Coverage;
|
Coverage is Multithreading.Resources.ThreadedTexture t ? t.Base : Coverage;
|
||||||
|
|
||||||
public static int Width;
|
public static int Width;
|
||||||
public static int Height;
|
public static int Height;
|
||||||
|
|
||||||
/// <summary>Set at frame boundary; the first graft of the frame clears the coverage.</summary>
|
|
||||||
public static bool FrameClearPending;
|
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;
|
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;
|
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;
|
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;
|
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;
|
public static int VariantN1Binding = -1;
|
||||||
|
|
||||||
/// <summary>Descriptor set of the N-1 palette cbuf (paired with VariantN1Binding).</summary>
|
|
||||||
public static int VariantN1Set;
|
public static int VariantN1Set;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ namespace Ryujinx.Graphics.GAL
|
|||||||
public int Width { get; }
|
public int Width { get; }
|
||||||
public int Height { 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 ActiveWidth { get; }
|
||||||
public int ActiveHeight { 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);
|
_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)
|
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); // read-only (gated)
|
Threed.MvppDrawStepProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa);
|
||||||
|
|
||||||
_3dEngine.ForceShaderUpdate();
|
_3dEngine.ForceShaderUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
|
|||||||
_3dEngine.CreatePendingSyncs();
|
_3dEngine.CreatePendingSyncs();
|
||||||
_3dEngine.FlushUboDirty();
|
_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);
|
Image.MvppTwinXferProbe.OnDma(srcGpuVa, dstGpuVa, xCount, yCount, copy2D, srcLinear, dstLinear);
|
||||||
|
|
||||||
if (copy2D)
|
if (copy2D)
|
||||||
@@ -328,7 +327,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
|
|||||||
target.PropagateScale(source);
|
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);
|
source.HostTexture.CopyTo(target.HostTexture, 0, 0);
|
||||||
target.SignalModified();
|
target.SignalModified();
|
||||||
return;
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,32 +6,18 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine
|
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
|
static class MvppBlitProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_BLITPROBE") == "1";
|
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> _seenBlitSigs = new();
|
||||||
private static readonly HashSet<long> _seenDispatchSigs = new();
|
private static readonly HashSet<long> _seenDispatchSigs = new();
|
||||||
private static int _op;
|
private static int _op;
|
||||||
private static int _dispatchCount;
|
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 int _finalW, _finalH;
|
||||||
private static readonly HashSet<ulong> _finalSrcs = new();
|
private static readonly HashSet<ulong> _finalSrcs = new();
|
||||||
private static readonly HashSet<ulong> _finalDsts = new();
|
private static readonly HashSet<ulong> _finalDsts = new();
|
||||||
@@ -47,7 +30,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A diagnostic on the GPU thread must never take the process down.
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
OnBlitImpl(src, dst, duDx, dvDy);
|
OnBlitImpl(src, dst, duDx, dvDy);
|
||||||
@@ -70,13 +52,9 @@ namespace Ryujinx.Graphics.Gpu.Engine
|
|||||||
int sW = src.Info.Width, sH = src.Info.Height;
|
int sW = src.Info.Width, sH = src.Info.Height;
|
||||||
int dW = dst.Info.Width, dH = dst.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 sx = duDx != 0 ? (float)(4294967296.0 / duDx) : 0f;
|
||||||
float sy = dvDy != 0 ? (float)(4294967296.0 / dvDy) : 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);
|
long sig = Fnv(sAddr, dAddr, (int)sFmt, (int)dFmt, sW, sH, dW, dH);
|
||||||
if (_seenBlitSigs.Count < MaxSigs && _seenBlitSigs.Add(sig))
|
if (_seenBlitSigs.Count < MaxSigs && _seenBlitSigs.Add(sig))
|
||||||
{
|
{
|
||||||
@@ -93,7 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A strictly larger LDR destination resets the size class (first real frame settles it).
|
|
||||||
if ((long)dW * dH > (long)_finalW * _finalH)
|
if ((long)dW * dH > (long)_finalW * _finalH)
|
||||||
{
|
{
|
||||||
_finalW = dW;
|
_finalW = dW;
|
||||||
@@ -129,10 +106,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
|
|||||||
{
|
{
|
||||||
_dispatchCount++;
|
_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);
|
long sig = ((long)(gridX & 0xFFFF) << 32) ^ ((long)(gridY & 0xFFFF) << 16) ^ (long)(shaderVa & 0xFFFFFFF);
|
||||||
if (_seenDispatchSigs.Count >= MaxSigs || !_seenDispatchSigs.Add(sig))
|
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}");
|
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)
|
foreach ((ulong oa, Format of, int ow, int oh) in outs)
|
||||||
{
|
{
|
||||||
if (!IsLdr(of) || (long)ow * oh < 2_000_000L)
|
if (!IsLdr(of) || (long)ow * oh < 2_000_000L)
|
||||||
{
|
{
|
||||||
continue; // only the ~full-res LDR output(s) matter
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ldrInputs = 0;
|
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 =
|
private static readonly bool _uboForceDirty =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_UBO_FORCEDIRTY") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_UBO_FORCEDIRTY") == "1";
|
||||||
|
|
||||||
@@ -138,8 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (!changed)
|
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++;
|
_uboForceDirtyHits++;
|
||||||
long now = Environment.TickCount64;
|
long now = Environment.TickCount64;
|
||||||
if (now - _uboForceDirtyLogMs >= 3000)
|
if (now - _uboForceDirtyLogMs >= 3000)
|
||||||
|
|||||||
@@ -138,8 +138,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
MvppUiProbe.OnDraw(_channel, ref _state.State, _context);
|
MvppUiProbe.OnDraw(_channel, ref _state.State, _context);
|
||||||
MvppCompositeProbe.OnDraw(_channel, ref _state.State);
|
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))
|
if (MvppDofSkip.ShouldSkip(_channel))
|
||||||
{
|
{
|
||||||
_drawState.DrawIndexed = false;
|
_drawState.DrawIndexed = false;
|
||||||
@@ -241,9 +239,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_context.Renderer.Pipeline.EndHostConditionalRendering();
|
_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);
|
MvppDrawStepProbe.OnDrawPost(_channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +497,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
(Image.Texture texture, Sampler sampler) = _channel.TextureManager.GetGraphicsTextureAndSampler(textureId, samplerId);
|
(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 gSrcX0 = srcX0, gSrcY0 = srcY0, gSrcX1 = srcX1, gSrcY1 = srcY1;
|
||||||
float gDstX0 = dstX0, gDstY0 = dstY0, gDstX1 = dstX1, gDstY1 = dstY1;
|
float gDstX0 = dstX0, gDstY0 = dstY0, gDstX1 = dstX1, gDstY1 = dstY1;
|
||||||
@@ -525,7 +520,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (anyScaled)
|
if (anyScaled)
|
||||||
{
|
{
|
||||||
Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated); the bound RT is not resolvable here
|
Image.MvppSeamProbe.OnScaledCopy(
|
||||||
"DRAWTEX",
|
"DRAWTEX",
|
||||||
texture, null,
|
texture, null,
|
||||||
gSrcX0, gSrcY0, gSrcX1, gSrcY1,
|
gSrcX0, gSrcY0, gSrcX1, gSrcY1,
|
||||||
@@ -635,9 +630,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
int firstInstance,
|
int firstInstance,
|
||||||
bool indexed)
|
bool indexed)
|
||||||
{
|
{
|
||||||
MvppFinalDrawProbe.OnDraw(_channel, ref _state.State); // universal draw choke point (gated, read-only)
|
MvppFinalDrawProbe.OnDraw(_channel, ref _state.State);
|
||||||
MvppHdrCensusProbe.OnDraw(_channel, ref _state.State, count, indexed); // HDR-1600 producer census (gated, read-only)
|
MvppHdrCensusProbe.OnDraw(_channel, ref _state.State, count, indexed);
|
||||||
MvppInjectGate.OnDraw(_channel, ref _state.State, count, indexed); // DLSS-injection gate validation, STEP A (gated, read-only)
|
MvppInjectGate.OnDraw(_channel, ref _state.State, count, indexed);
|
||||||
|
|
||||||
if (instanceCount > 1)
|
if (instanceCount > 1)
|
||||||
{
|
{
|
||||||
@@ -668,16 +663,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
ForceStateDirty();
|
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
|
else
|
||||||
{
|
{
|
||||||
if (MvppDlssUpscale.Decide(_channel, ref _state.State, count, indexed, out var injectSrc, out var injectDst, out var injectDepth))
|
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);
|
_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);
|
_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)
|
if (MvppVelocityPass.Enabled)
|
||||||
{
|
{
|
||||||
MvppVelocityPass.AfterDraw(this, engine, _context, _channel, ref _state.State,
|
MvppVelocityPass.AfterDraw(this, engine, _context, _channel, ref _state.State,
|
||||||
@@ -894,7 +881,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
// on the screen scissor state, then we need to force only one texture to be bound to avoid
|
// on the screen scissor state, then we need to force only one texture to be bound to avoid
|
||||||
// host clipping.
|
// host clipping.
|
||||||
ScreenScissorState screenScissorState = _state.State.ScreenScissorState;
|
ScreenScissorState screenScissorState = _state.State.ScreenScissorState;
|
||||||
|
|
||||||
Span<ScissorState> scissorStateSpan = _state.State.ScissorState.AsSpan();
|
Span<ScissorState> scissorStateSpan = _state.State.ScissorState.AsSpan();
|
||||||
|
|
||||||
bool clearAffectedByStencilMask = (_state.State.ClearFlags & 1) != 0;
|
bool clearAffectedByStencilMask = (_state.State.ClearFlags & 1) != 0;
|
||||||
@@ -960,7 +947,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
engine.UpdateState(updateMask);
|
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)
|
if (needsCustomScissor)
|
||||||
{
|
{
|
||||||
@@ -994,7 +981,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
new(scissorX, scissorY, scissorW, scissorH)
|
new(scissorX, scissorY, scissorW, scissorH)
|
||||||
];
|
];
|
||||||
|
|
||||||
probeScX = scissorX; // [MVBUF]
|
probeScX = scissorX;
|
||||||
probeScY = scissorY;
|
probeScY = scissorY;
|
||||||
probeScW = scissorW;
|
probeScW = scissorW;
|
||||||
probeScH = scissorH;
|
probeScH = scissorH;
|
||||||
@@ -1010,7 +997,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
ColorF color = new(clearColor.Red, clearColor.Green, clearColor.Blue, clearColor.Alpha);
|
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(
|
Image.MvppMvBufProbe.OnClear(
|
||||||
_channel.TextureManager.GetColorTarget(index),
|
_channel.TextureManager.GetColorTarget(index),
|
||||||
index,
|
index,
|
||||||
@@ -1022,7 +1008,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
probeScW,
|
probeScW,
|
||||||
probeScH);
|
probeScH);
|
||||||
|
|
||||||
// [TWINMAP v5] per-twin clear attribution (self-gated).
|
|
||||||
Image.MvppTwinMapProbe.OnClear(
|
Image.MvppTwinMapProbe.OnClear(
|
||||||
_channel.TextureManager.GetColorTarget(index),
|
_channel.TextureManager.GetColorTarget(index),
|
||||||
componentMask,
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppCamTrace
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAMTRACE") == "1";
|
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;
|
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 const float JumpThreshold = 50f;
|
||||||
|
|
||||||
private static int _lines;
|
private static int _lines;
|
||||||
@@ -61,10 +19,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static float _prevX, _prevY, _prevZ;
|
private static float _prevX, _prevY, _prevZ;
|
||||||
private static bool _hasPrev;
|
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)
|
public static void Note(ulong address, float px, float py, float pz, float jx, float jy, bool deJittered)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
if (!Enabled)
|
||||||
@@ -72,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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;
|
bool opening = _lines < MaxLines;
|
||||||
|
|
||||||
if (!opening)
|
if (!opening)
|
||||||
@@ -106,8 +54,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_lines++;
|
_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;
|
float step = 0f;
|
||||||
bool addrChanged = false;
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -12,67 +9,38 @@ using System.Threading;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppCameraCapture
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAP") == "1";
|
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 =
|
private static readonly bool _capWhy =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPWHY") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPWHY") == "1";
|
||||||
|
|
||||||
// [DUPSKIP 02/08] Producteur du signal anti-doublons : a chaque present invite portant au
|
internal static readonly bool Verbose =
|
||||||
// moins un dessin, incremente GAL.DlssCameraState.RenderedFrameSeq (voir sa doc). Reutilise
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VERBOSE") == "1";
|
||||||
// le verrou par image de CAPWHY v2 ; ferme = aucun comportement.
|
|
||||||
private static readonly bool _dupSkip =
|
private static readonly bool _dupSkip =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_DLSS_DUPSKIP") == "1";
|
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 =
|
private static readonly bool _fgFeed =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_DLSS_FGFEED") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_DLSS_FGFEED") == "1";
|
||||||
|
|
||||||
private static int _cwQual; // dessins ayant passe le prefiltre (OnDraw)
|
private static int _cwQual;
|
||||||
private static int _cwNeed; // ... dont l'image n'avait pas encore capture
|
private static int _cwNeed;
|
||||||
private static int _cwImpl; // entrees dans OnDrawImpl
|
private static int _cwImpl;
|
||||||
private static int _cwCachedOk; // lecture au cache reussie (publie)
|
private static int _cwCachedOk;
|
||||||
private static int _cwHeld; // quarantaine gate (slot tenu ouvert)
|
private static int _cwHeld;
|
||||||
private static int _cwRescanOk; // relocalisation reussie (publie)
|
private static int _cwRescanOk;
|
||||||
private static int _cwSoloCall; // repli solo atteint
|
private static int _cwSoloCall;
|
||||||
private static int _cwSoloOk; // repli solo a publie
|
private static int _cwSoloOk;
|
||||||
// "rien" = impl - cachedOk - held - rescanOk - soloOk, derivable, pas de compteur dedie.
|
|
||||||
|
|
||||||
// [CAPWHY v2 02/08] Discriminant demande par la correction d'Alex (« les FPS ne descendent
|
private static int _cwRaw;
|
||||||
// pas ») : par PRESENTATION, l'image avait-elle des dessins DU TOUT (avant prefiltre), et
|
private static int _cwFrameHadRaw;
|
||||||
// en avait-elle apres ? Separe « le jeu n'a pas rendu » (monde 1) de « tout est mort au
|
private static int _cwFrameHadQual;
|
||||||
// prefiltre » (monde 2). Verrous par image poses sur le fil GPU, releves au present.
|
private static int _cwFramesNoDraw;
|
||||||
private static int _cwRaw; // dessins vus a l'entree de OnDraw (avant prefiltre)
|
private static int _cwFramesPrefiltred;
|
||||||
private static int _cwFrameHadRaw; // verrou : cette image a vu >= 1 dessin brut
|
private static int _cwFramesOk;
|
||||||
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 _capturedThisFrame;
|
private static int _capturedThisFrame;
|
||||||
private static int _cachedSlot = -1;
|
private static int _cachedSlot = -1;
|
||||||
@@ -81,13 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static ulong _captures;
|
private static ulong _captures;
|
||||||
private static ulong _rescans;
|
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 long _pubWindowMs;
|
||||||
private static int _statEnqueues;
|
private static int _statEnqueues;
|
||||||
private static int _statPubFrames;
|
private static int _statPubFrames;
|
||||||
@@ -99,36 +60,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _vpTransitionLogs;
|
private static int _vpTransitionLogs;
|
||||||
private const int VpTransitionLogCap = 200;
|
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 float _lastR3X, _lastR3Y, _lastR3Z, _lastR3W;
|
||||||
private static bool _hasR3;
|
private static bool _hasR3;
|
||||||
private static int _statVpJumps;
|
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 =
|
private static readonly bool _gateEnabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAP_GATE") == "1";
|
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 GateDirEps = 0.35f;
|
||||||
private const float GatePosEps = 30f;
|
private const float GatePosEps = 30f;
|
||||||
private const int GateResyncFrames = 30;
|
private const int GateResyncFrames = 30;
|
||||||
@@ -144,25 +82,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _statLineageLate;
|
private static int _statLineageLate;
|
||||||
private static int _statResyncs;
|
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;
|
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 =
|
private static readonly bool _pairProbe =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_PAIRPROBE") == "1";
|
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 readonly byte[] _probeLastBlock = new byte[0xC0];
|
||||||
private static bool _probeHasFp;
|
private static bool _probeHasFp;
|
||||||
private static int _probeChanges;
|
private static int _probeChanges;
|
||||||
@@ -170,17 +94,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _probeFrames;
|
private static int _probeFrames;
|
||||||
private static long _probeLogMs;
|
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 =
|
private static readonly bool _fifoMode =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VPFIFO") == "1";
|
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 bool _hasSoloPushed;
|
||||||
private static Matrix4x4 _lastSoloPushed;
|
private static Matrix4x4 _lastSoloPushed;
|
||||||
|
|
||||||
@@ -207,10 +123,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_cwFrameHadRaw = 1;
|
_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)
|
if (MvppSoloCamera.Enabled)
|
||||||
{
|
{
|
||||||
Image.Texture aspectDs = channel.TextureManager.RenderTargetDepthStencil;
|
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 (channel.TextureManager.RenderTargetScale == 1f)
|
||||||
{
|
{
|
||||||
if (GraphicsConfig.ResScale != 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)
|
if (MvppCtxProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppCtxProbe.NoteQualifyingDraw(channel);
|
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))
|
if (needCapture && MvppScenePass.Enabled && !MvppScenePass.Allows(channel))
|
||||||
{
|
{
|
||||||
needCapture = false;
|
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))
|
if (needCapture && MvppLdrSkip.Active && !MvppLdrSkip.Allows(channel))
|
||||||
{
|
{
|
||||||
needCapture = false;
|
needCapture = false;
|
||||||
@@ -299,12 +187,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
StashSceneDepth(channel);
|
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))
|
if (needCapture && MvppSoloCamera.StaleAtElectedLocation(channel))
|
||||||
{
|
{
|
||||||
needCapture = false;
|
needCapture = false;
|
||||||
@@ -316,10 +198,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
else if (MvppSoloCamera.CapTime && Volatile.Read(ref _capturedThisFrame) == 1)
|
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);
|
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;
|
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 StatScaledDraws;
|
||||||
public static int StatDepthBound;
|
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 SceneDepthHostWidth;
|
||||||
public static int SceneDepthHostHeight;
|
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 int _guardRejects;
|
||||||
private static long _guardLogMs;
|
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;
|
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 =
|
private static readonly bool _depthProbe =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHPROBE") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHPROBE") == "1";
|
||||||
private static bool _depthProbeFaulted;
|
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 =
|
private static readonly bool _depthFix =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHFIX") != "0";
|
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 =
|
private static readonly bool _colorCapProbe =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORCAP_PROBE") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORCAP_PROBE") == "1";
|
||||||
private static readonly System.Collections.Generic.HashSet<long> _colorCapSeen = new();
|
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[] _censusH = new int[DepthCensusMax];
|
||||||
private static readonly int[] _censusDraws = new int[DepthCensusMax];
|
private static readonly int[] _censusDraws = new int[DepthCensusMax];
|
||||||
private static readonly bool[] _censusSq = new bool[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 ulong[] _censusIdent = new ulong[DepthCensusMax];
|
||||||
private static readonly int[] _censusIdentN = new int[DepthCensusMax];
|
private static readonly int[] _censusIdentN = new int[DepthCensusMax];
|
||||||
private static int _censusCount;
|
private static int _censusCount;
|
||||||
private static int _censusOverflow;
|
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 ulong _depthSigLast;
|
||||||
private static bool _hasDepthSig;
|
private static bool _hasDepthSig;
|
||||||
private static int _depthTransitionLogs;
|
private static int _depthTransitionLogs;
|
||||||
@@ -420,11 +258,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _statDepthPresents;
|
private static int _statDepthPresents;
|
||||||
private static int _statDepthCensusChanges;
|
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;
|
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()
|
public static Image.Texture TakeSceneColor()
|
||||||
{
|
{
|
||||||
Image.Texture c = _sceneColor;
|
Image.Texture c = _sceneColor;
|
||||||
@@ -432,14 +267,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>[COLORCAP probe] Monotonic present frame id, exposed for the snapshot log.</summary>
|
|
||||||
public static long ColorCapFrameId => _colorCapFrameId;
|
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;
|
public static ulong SceneColorAddress => _sceneColor != null ? _sceneColor.Range.GetSubRange(0).Address : 0UL;
|
||||||
|
|
||||||
private static void StashSceneDepth(GpuChannel channel)
|
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);
|
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)
|
if (ds.Info.Width == ds.Info.Height)
|
||||||
{
|
{
|
||||||
_guardRejects++;
|
_guardRejects++;
|
||||||
@@ -482,9 +305,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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)
|
if (_colorCapProbe)
|
||||||
{
|
{
|
||||||
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
|
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)
|
if (MvppColorSnapshot.Enabled)
|
||||||
{
|
{
|
||||||
Image.Texture c0 = channel.TextureManager.RenderTargetColor0;
|
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))
|
(long)c0.Info.Width * c0.Info.Height > (long)_sceneColor.Info.Width * _sceneColor.Info.Height))
|
||||||
{
|
{
|
||||||
_sceneColor = c0;
|
_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;
|
DlssJitterState.SceneColorFrameId = DlssJitterState.FrameId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,19 +345,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (_sceneDepth == null ||
|
if (_sceneDepth == null ||
|
||||||
(long)ds.Info.Width * ds.Info.Height > (long)_sceneDepth.Info.Width * _sceneDepth.Info.Height ||
|
(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 &&
|
(_depthFix &&
|
||||||
(long)ds.Info.Width * ds.Info.Height == (long)_sceneDepth.Info.Width * _sceneDepth.Info.Height &&
|
(long)ds.Info.Width * ds.Info.Height == (long)_sceneDepth.Info.Width * _sceneDepth.Info.Height &&
|
||||||
DepthRank(ds.Info.FormatInfo.Format) > DepthRank(_sceneDepth.Info.FormatInfo.Format)))
|
DepthRank(ds.Info.FormatInfo.Format) > DepthRank(_sceneDepth.Info.FormatInfo.Format)))
|
||||||
{
|
{
|
||||||
_sceneDepth = ds;
|
_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;
|
DlssJitterState.SceneDepthFrameId = DlssJitterState.FrameId;
|
||||||
|
|
||||||
GAL.ITexture host = ds.HostTexture;
|
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
|
private static int DepthRank(Format fmt) => fmt switch
|
||||||
{
|
{
|
||||||
Format.D32Float or Format.D32FloatS8Uint => 32,
|
Format.D32Float or Format.D32FloatS8Uint => 32,
|
||||||
@@ -566,21 +370,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_ => 0,
|
_ => 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;
|
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 =>
|
public static Ryujinx.Graphics.GAL.Format SceneDepthFormat =>
|
||||||
_sceneDepth != null ? _sceneDepth.Info.FormatInfo.Format : default;
|
_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 Ryujinx.Graphics.GAL.ITexture SceneDepthHost => _sceneDepth?.HostTexture;
|
||||||
|
|
||||||
public static Image.Texture TakeSceneDepth()
|
public static Image.Texture TakeSceneDepth()
|
||||||
@@ -590,9 +384,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (_depthProbe && !_depthProbeFaulted)
|
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
|
try
|
||||||
{
|
{
|
||||||
EmitDepthCensus(d);
|
EmitDepthCensus(d);
|
||||||
@@ -610,10 +401,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return d;
|
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)
|
private static void RecordDepthCensus(Format fmt, int w, int h, bool sq, ulong ident)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _censusCount; i++)
|
for (int i = 0; i < _censusCount; i++)
|
||||||
@@ -649,9 +436,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_censusIdentN[n] = 1;
|
_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)
|
private static void EmitDepthCensus(Image.Texture winner)
|
||||||
{
|
{
|
||||||
bool hasWinner = winner != null;
|
bool hasWinner = winner != null;
|
||||||
@@ -665,7 +449,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
wH = winner.Info.Height;
|
wH = winner.Info.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature over ordered buckets (+ whether each alternates) + the winner + overflow.
|
|
||||||
ulong sig = 14695981039346656037UL;
|
ulong sig = 14695981039346656037UL;
|
||||||
for (int i = 0; i < _censusCount; i++)
|
for (int i = 0; i < _censusCount; i++)
|
||||||
{
|
{
|
||||||
@@ -780,8 +563,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (_fifoMode)
|
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> f = MemoryMarshal.Cast<byte, float>(block);
|
||||||
ReadOnlySpan<float> view = f.Slice(0, 16);
|
ReadOnlySpan<float> view = f.Slice(0, 16);
|
||||||
ReadOnlySpan<float> proj = f.Slice(16, 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()
|
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)
|
if (MvppHangWatch.Enabled)
|
||||||
{
|
{
|
||||||
MvppHangWatch.Ping();
|
MvppHangWatch.Ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
// [CTXPROBE 29/07 SOIR] Re-arme le rang de dessin, au meme endroit que la capture elle-meme.
|
|
||||||
if (MvppCtxProbe.Enabled)
|
if (MvppCtxProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppCtxProbe.OnFrame();
|
MvppCtxProbe.OnFrame();
|
||||||
@@ -831,22 +606,17 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
if (Volatile.Read(ref _capturedThisFrame) == 1)
|
if (Volatile.Read(ref _capturedThisFrame) == 1)
|
||||||
{
|
{
|
||||||
// Lineage published this frame: rivals evaporate.
|
|
||||||
_rivalStreak = 0;
|
_rivalStreak = 0;
|
||||||
_hasRival = false;
|
_hasRival = false;
|
||||||
}
|
}
|
||||||
else if (_rivalSeenThisFrame)
|
else if (_rivalSeenThisFrame)
|
||||||
{
|
{
|
||||||
// Rival-only frame: the persistence clock ticks.
|
|
||||||
_rivalStreak++;
|
_rivalStreak++;
|
||||||
}
|
}
|
||||||
|
|
||||||
_rivalSeenThisFrame = false;
|
_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)
|
if (MvppScenePass.Enabled)
|
||||||
{
|
{
|
||||||
MvppScenePass.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1);
|
MvppScenePass.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1);
|
||||||
@@ -857,16 +627,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
MvppLdrSkip.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1);
|
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.OnPresentBoundary(Volatile.Read(ref _capturedThisFrame) == 1);
|
||||||
MvppSoloCamera.LogWhy();
|
MvppSoloCamera.LogWhy();
|
||||||
|
|
||||||
Volatile.Write(ref _capturedThisFrame, 0);
|
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 (_capWhy || _dupSkip || _fgFeed)
|
||||||
{
|
{
|
||||||
if ((_dupSkip || _fgFeed) && _cwFrameHadRaw == 1)
|
if ((_dupSkip || _fgFeed) && _cwFrameHadRaw == 1)
|
||||||
@@ -894,14 +659,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_cwFrameHadQual = 0;
|
_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++;
|
_statEnqueues++;
|
||||||
|
|
||||||
long nowPub = Environment.TickCount64;
|
long nowPub = Environment.TickCount64;
|
||||||
if (nowPub - _pubWindowMs >= 5000)
|
if (nowPub - _pubWindowMs >= 5000)
|
||||||
{
|
{
|
||||||
if (_pubWindowMs != 0 && _enabled)
|
if (_pubWindowMs != 0 && _enabled && Verbose)
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Gpu,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
$"MVPP capture window: enq {_statEnqueues}, pub {_statPubFrames} " +
|
$"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 =
|
private static readonly bool _dupPair =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUPPAIR") == "1";
|
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 =
|
internal static readonly bool JitTrace =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_JITTRACE") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_JITTRACE") == "1";
|
||||||
|
|
||||||
@@ -1045,7 +801,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_gateHeldSlot = false;
|
_gateHeldSlot = false;
|
||||||
|
|
||||||
// Cached location first: one read + three cheap checks per frame in steady state.
|
|
||||||
if (_cachedSlot >= 0 && TryCapture(channel, _cachedSlot, _cachedOffset))
|
if (_cachedSlot >= 0 && TryCapture(channel, _cachedSlot, _cachedOffset))
|
||||||
{
|
{
|
||||||
if (_capWhy)
|
if (_capWhy)
|
||||||
@@ -1058,9 +813,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (_gateHeldSlot)
|
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)
|
if (_capWhy)
|
||||||
{
|
{
|
||||||
_cwHeld++;
|
_cwHeld++;
|
||||||
@@ -1069,7 +821,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full rescan of the vertex-stage cbufs (first frame, or the game moved its block).
|
|
||||||
uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(0);
|
uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(0);
|
||||||
|
|
||||||
for (int slot = 0; mask != 0; slot++, mask >>= 1)
|
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)
|
if (_capWhy && MvppSoloCamera.Enabled)
|
||||||
{
|
{
|
||||||
_cwSoloCall++;
|
_cwSoloCall++;
|
||||||
@@ -1127,40 +870,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_captures++;
|
_captures++;
|
||||||
_statPubFrames++;
|
_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))
|
if (_fifoMode && (!_hasSoloPushed || soloVp != _lastSoloPushed))
|
||||||
{
|
{
|
||||||
_hasSoloPushed = true;
|
_hasSoloPushed = true;
|
||||||
_lastSoloPushed = soloVp;
|
_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 pushJx = MvppSoloCamera.LastJitterX;
|
||||||
float pushJy = MvppSoloCamera.LastJitterY;
|
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;
|
_ = pushJx;
|
||||||
_ = pushJy;
|
_ = pushJy;
|
||||||
DlssCameraState.PushOrdered(in soloVp);
|
DlssCameraState.PushOrdered(in soloVp);
|
||||||
MvppFamHold.NotePush();
|
MvppFamHold.NotePush();
|
||||||
_soloDistinct++;
|
_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)
|
if (_jitTrace && Environment.TickCount64 - _jitTraceMs >= 1000)
|
||||||
{
|
{
|
||||||
_jitTraceMs = Environment.TickCount64;
|
_jitTraceMs = Environment.TickCount64;
|
||||||
@@ -1171,16 +894,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
else if (_fifoMode)
|
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++;
|
_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;
|
GAL.ITexture soloDs = channel.TextureManager.RenderTargetDepthStencil?.HostTexture;
|
||||||
|
|
||||||
if (soloDs != null && soloDs.Width == SceneDepthHostWidth && soloDs.Height == SceneDepthHostHeight)
|
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;
|
public static int StatSquareRejects;
|
||||||
|
|
||||||
private static bool IsSquareProjection(ReadOnlySpan<float> proj)
|
private static bool IsSquareProjection(ReadOnlySpan<float> proj)
|
||||||
@@ -1250,7 +959,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return false;
|
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 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 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]);
|
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)
|
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;
|
_hasRival = true;
|
||||||
_rivalStreak = 0;
|
_rivalStreak = 0;
|
||||||
}
|
}
|
||||||
@@ -1295,15 +1001,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (_rivalStreak < GateResyncFrames)
|
if (_rivalStreak < GateResyncFrames)
|
||||||
{
|
{
|
||||||
// Quarantined: not published, and the slot stays OPEN -- the
|
|
||||||
// lineage may still draw later in this frame.
|
|
||||||
_gateHeldSlot = true;
|
_gateHeldSlot = true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A rival that persisted ALONE for the full window is the camera now
|
|
||||||
// (real cut): adopt it as the new lineage.
|
|
||||||
_statResyncs++;
|
_statResyncs++;
|
||||||
_hasRival = false;
|
_hasRival = false;
|
||||||
_rivalStreak = 0;
|
_rivalStreak = 0;
|
||||||
@@ -1314,8 +1016,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
else if (_rivalSeenThisFrame)
|
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++;
|
_statLineageLate++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1331,8 +1031,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_captures++;
|
_captures++;
|
||||||
_statPubFrames++;
|
_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;
|
Image.Texture winnerDs = channel.TextureManager.RenderTargetDepthStencil;
|
||||||
GAL.ITexture winnerDsHost = winnerDs?.HostTexture;
|
GAL.ITexture winnerDsHost = winnerDs?.HostTexture;
|
||||||
bool sceneWinner = winnerDsHost != null &&
|
bool sceneWinner = winnerDsHost != null &&
|
||||||
@@ -1348,7 +1046,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_statCapAux++;
|
_statCapAux++;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong pubFp = 14695981039346656037UL; // FNV-1a over the published VP block
|
ulong pubFp = 14695981039346656037UL;
|
||||||
foreach (byte b in MemoryMarshal.AsBytes(vp))
|
foreach (byte b in MemoryMarshal.AsBytes(vp))
|
||||||
{
|
{
|
||||||
pubFp = (pubFp ^ b) * 1099511628211UL;
|
pubFp = (pubFp ^ b) * 1099511628211UL;
|
||||||
@@ -1361,9 +1059,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_statVpChanges++;
|
_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 &&
|
bool teleport = _hasR3 &&
|
||||||
(MathF.Abs(camPosX - _lastPosX) > GatePosEps ||
|
(MathF.Abs(camPosX - _lastPosX) > GatePosEps ||
|
||||||
MathF.Abs(camPosY - _lastPosY) > GatePosEps ||
|
MathF.Abs(camPosY - _lastPosY) > GatePosEps ||
|
||||||
@@ -1413,10 +1108,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return true;
|
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)
|
private static bool IsOrthonormalView(ReadOnlySpan<float> m)
|
||||||
{
|
{
|
||||||
if (MathF.Abs(m[12]) > 1e-3f || MathF.Abs(m[13]) > 1e-3f ||
|
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;
|
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)
|
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 &&
|
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;
|
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)
|
private static bool ProductMatches(ReadOnlySpan<float> p, ReadOnlySpan<float> v, ReadOnlySpan<float> vp)
|
||||||
{
|
{
|
||||||
for (int r = 0; r < 4; r++)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,16 +5,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppCompInputsProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -53,8 +40,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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}";
|
string sig = $"{role} {i.Width}x{i.Height} {i.FormatInfo.Format} gobY={i.GobBlocksInY} lin={i.IsLinear} stride={i.Stride}";
|
||||||
|
|
||||||
if (_seen.Add(sig))
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,25 +6,16 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppCompositeProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_COMPOSITEPROBE") == "1";
|
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 _drawNo;
|
||||||
private static int _fullDumps;
|
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 long _lastSummaryMs;
|
||||||
private static int _sceneReadsSinceSummary;
|
private static int _sceneReadsSinceSummary;
|
||||||
|
|
||||||
@@ -55,7 +43,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
|
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
|
||||||
|
|
||||||
// Does THIS draw sample the scene texture? (address match against any bound input)
|
|
||||||
bool samplesScene = false;
|
bool samplesScene = false;
|
||||||
if (sceneAddr != 0)
|
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}");
|
$"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)
|
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)
|
private static string ClassifyProj(GpuChannel channel)
|
||||||
{
|
{
|
||||||
try
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppCtxProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CTXPROBE") == "1";
|
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 =
|
public static readonly bool TrialRead =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HDRTRY") == "1";
|
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;
|
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;
|
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 MaxCtx = 32;
|
||||||
private const int ReportMs = 5000;
|
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[] _readCtx = new Ctx[MaxCtx];
|
||||||
private static readonly Ctx[] _qualCtx = 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 _rank;
|
||||||
private static int _frameMark;
|
private static int _frameMark;
|
||||||
private static int _seenMark;
|
private static int _seenMark;
|
||||||
@@ -122,17 +52,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _intruders;
|
private static int _intruders;
|
||||||
private static int _ctxOverflow;
|
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 const int MaxIntr = 16;
|
||||||
private static readonly float[] _intrDist = new float[MaxIntr];
|
private static readonly float[] _intrDist = new float[MaxIntr];
|
||||||
private static readonly int[] _intrCount = new int[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 _intrN;
|
||||||
private static int _intrOverflow;
|
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 long _rankSumClean;
|
||||||
private static int _rankNClean;
|
private static int _rankNClean;
|
||||||
private static long _rankSumIntr;
|
private static long _rankSumIntr;
|
||||||
@@ -170,11 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
private static long _lastReportMs;
|
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)
|
public static void NoteQualifyingDraw(GpuChannel channel)
|
||||||
{
|
{
|
||||||
int mark = _frameMark;
|
int mark = _frameMark;
|
||||||
@@ -187,10 +95,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_rank++;
|
_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))
|
if (TrialRead && !_probedThisFrame && MvppScenePass.IsSceneColorPass(channel))
|
||||||
{
|
{
|
||||||
_probedThisFrame = true;
|
_probedThisFrame = true;
|
||||||
@@ -217,30 +121,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
Record(_qualCtx, fmt, cw, ch, dw, dh, cAddr, dAddr, _rank, 0f, false);
|
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)
|
public static void NoteRead(GpuChannel channel, float dist, float px, float py, float pz, long ageMs)
|
||||||
{
|
{
|
||||||
_reads++;
|
_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 freshRef = ageMs <= FreshRefMs;
|
||||||
bool bigJump = dist >= IntruderDist;
|
bool bigJump = dist >= IntruderDist;
|
||||||
bool intruder = bigJump && freshRef;
|
bool intruder = bigJump && freshRef;
|
||||||
@@ -250,15 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_staleJumps++;
|
_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)
|
if (intruder)
|
||||||
{
|
{
|
||||||
_ageSumIntr += ageMs;
|
_ageSumIntr += ageMs;
|
||||||
@@ -296,10 +171,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_rankHistClean[bucket]++;
|
_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);
|
MvppLdrSkip.NoteRead(intruder);
|
||||||
|
|
||||||
Describe(channel, out int fmt, out int cw, out int ch, out int dw, out int dh,
|
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()
|
public static void OnFrame()
|
||||||
{
|
{
|
||||||
if (_rank > 0)
|
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++;
|
_frameMark++;
|
||||||
_probedThisFrame = false;
|
_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)
|
private static void NoteIntruder(float dist, float px, float py, float pz)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _intrN; i++)
|
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 col0 = channel.TextureManager.RenderTargetColor0;
|
||||||
Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil;
|
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 colHost = col0?.HostTexture;
|
||||||
GAL.ITexture dsHost = ds?.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;
|
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(
|
private static void Record(
|
||||||
Ctx[] table,
|
Ctx[] table,
|
||||||
int fmt,
|
int fmt,
|
||||||
@@ -492,8 +324,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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++;
|
_ctxOverflow++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,10 +337,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
$"max {_qualMax} / moyenne {(_frames > 0 ? (float)_qualSum / _frames : 0f):0.##} sur {_frames} images" +
|
$"max {_qualMax} / moyenne {(_frames > 0 ? (float)_qualSum / _frames : 0f):0.##} sur {_frames} images" +
|
||||||
$"{(_ctxOverflow > 0 ? $" · TABLE PLEINE, {_ctxOverflow} contextes non comptes" : "")}");
|
$"{(_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,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
$"MVPP CTXPROBE VERDICT PASSE HDR : {_frames} images 3D · " +
|
$"MVPP CTXPROBE VERDICT PASSE HDR : {_frames} images 3D · " +
|
||||||
$"{_hdrFrames} avec une passe HDR ({(_frames > 0 ? 100f * _hdrFrames / _frames : 0f):0.#} %) · " +
|
$"{_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} " +
|
$"intruses parmi les lisibles : {_hdrIntruders} " +
|
||||||
$"({(_hdrReadable > 0 ? 100f * _hdrIntruders / _hdrReadable : 0f):0.##} %)");
|
$"({(_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,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
$"MVPP CTXPROBE AGE DE LA REFERENCE · saines : " +
|
$"MVPP CTXPROBE AGE DE LA REFERENCE · saines : " +
|
||||||
$"{(_ageNClean > 0 ? (float)_ageSumClean / _ageNClean : 0f):0.#} ms de moyenne sur {_ageNClean} · " +
|
$"{(_ageNClean > 0 ? (float)_ageSumClean / _ageNClean : 0f):0.#} ms de moyenne sur {_ageNClean} · " +
|
||||||
$"intruses : {(_ageNIntr > 0 ? (float)_ageSumIntr / _ageNIntr : 0f):0.#} ms sur {_ageNIntr} " +
|
$"intruses : {(_ageNIntr > 0 ? (float)_ageSumIntr / _ageNIntr : 0f):0.#} ms sur {_ageNIntr} " +
|
||||||
$"(max {_ageMaxIntr} ms)");
|
$"(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,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
$"MVPP CTXPROBE RANG · saines : moyenne " +
|
$"MVPP CTXPROBE RANG · saines : moyenne " +
|
||||||
$"{(_rankNClean > 0 ? (float)_rankSumClean / _rankNClean : 0f):0.#} sur {_rankNClean} " +
|
$"{(_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)} · " +
|
$"4-10 {Pct(_rankHistIntr[2], _rankNIntr)} · 11-50 {Pct(_rankHistIntr[3], _rankNIntr)} · " +
|
||||||
$"51+ {Pct(_rankHistIntr[4], _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++)
|
for (int i = 0; i < _intrN; i++)
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Gpu,
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -8,58 +5,23 @@ using System;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
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;
|
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 readonly bool _quiet = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_QUIET") == "1";
|
||||||
|
|
||||||
private static long _statusMs;
|
private static long _statusMs;
|
||||||
private static long _statusSig = -1;
|
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) =>
|
private static bool IsHdrColor(Format f) =>
|
||||||
f == Format.R11G11B10Float ||
|
f == Format.R11G11B10Float ||
|
||||||
f == Format.R16G16B16A16Float;
|
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,
|
public static bool Decide(GpuChannel channel, ref ThreedClassState state, int count, bool indexed,
|
||||||
out ITexture src, out ITexture dst, out ITexture depth)
|
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;
|
src = null;
|
||||||
dst = null;
|
dst = null;
|
||||||
depth = null;
|
depth = null;
|
||||||
@@ -69,14 +31,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cheap pipeline-shape gate first (skips the vast majority of draws before any enumeration).
|
bool fullscreen = indexed && (count == 3 || count == 6);
|
||||||
bool fullscreen = indexed && (count == 3 || count == 6); // fullscreen triangle or quad
|
|
||||||
if (!fullscreen || state.DepthTestEnable || state.BlendEnable[0])
|
if (!fullscreen || state.DepthTestEnable || state.BlendEnable[0])
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destination = render target slot 0.
|
|
||||||
ulong dstAddr = 0;
|
ulong dstAddr = 0;
|
||||||
Format dstFmt = default;
|
Format dstFmt = default;
|
||||||
int dw = 0, dh = 0;
|
int dw = 0, dh = 0;
|
||||||
@@ -95,7 +55,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Source = the single sampled input.
|
|
||||||
int texCount = 0;
|
int texCount = 0;
|
||||||
ulong srcAddr = 0;
|
ulong srcAddr = 0;
|
||||||
Format srcFmt = default;
|
Format srcFmt = default;
|
||||||
@@ -116,36 +75,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
});
|
});
|
||||||
|
|
||||||
bool detected =
|
bool detected =
|
||||||
texCount == 1 && // exactly one sampled texture
|
texCount == 1 &&
|
||||||
haveDst && srcHost != null && dstHost != null &&
|
haveDst && srcHost != null && dstHost != null &&
|
||||||
IsHdrColor(srcFmt) && IsHdrColor(dstFmt) &&
|
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)
|
if (!detected)
|
||||||
{
|
{
|
||||||
return false;
|
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;
|
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
|
||||||
bool inject = sceneAddr != 0 && srcAddr == sceneAddr;
|
bool inject = sceneAddr != 0 && srcAddr == sceneAddr;
|
||||||
|
|
||||||
if (inject)
|
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;
|
src = srcHost;
|
||||||
dst = dstHost;
|
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)
|
if (_diagCount < 4)
|
||||||
{
|
{
|
||||||
_diagCount++;
|
_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} " +
|
$"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}");
|
$"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,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
$"MVPP-INJECT-DEPTH candidate: depthAddr=0x{MvppCameraCapture.SceneDepthAddress:X} " +
|
$"MVPP-INJECT-DEPTH candidate: depthAddr=0x{MvppCameraCapture.SceneDepthAddress:X} " +
|
||||||
$"depthFmt={MvppCameraCapture.SceneDepthFormat} " +
|
$"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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Gpu.Image;
|
using Ryujinx.Graphics.Gpu.Image;
|
||||||
@@ -8,44 +5,8 @@ using System;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
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 readonly int Mode = ParseMode();
|
||||||
|
|
||||||
public static bool Enabled => Mode != 0;
|
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
|
private const float WidthRatio = 0.4f;
|
||||||
// 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 AspectRatio = 16f / 9f;
|
private const float AspectRatio = 16f / 9f;
|
||||||
private const float Tolerance = 0.02f;
|
private const float Tolerance = 0.02f;
|
||||||
|
|
||||||
@@ -75,19 +30,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static long _lastLogMs;
|
private static long _lastLogMs;
|
||||||
private static int _renderWidth;
|
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 const int CensusCap = 16;
|
||||||
private static readonly long[] _shapes = new long[CensusCap];
|
private static readonly long[] _shapes = new long[CensusCap];
|
||||||
private static int _shapeCount;
|
private static int _shapeCount;
|
||||||
private static readonly object _censusLock = new();
|
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)
|
public static bool ShouldSkip(GpuChannel channel)
|
||||||
{
|
{
|
||||||
if (!Enabled || channel == null)
|
if (!Enabled || channel == null)
|
||||||
@@ -114,15 +61,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
int rawW = target.Info.Width;
|
int rawW = target.Info.Width;
|
||||||
int rawH = target.Info.Height;
|
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)
|
if (rawW > _renderWidth)
|
||||||
{
|
{
|
||||||
int previous = _renderWidth;
|
int previous = _renderWidth;
|
||||||
_renderWidth = rawW;
|
_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)
|
if (Mode == 2 && previous != 0)
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Gpu,
|
Logger.Warning?.Print(LogClass.Gpu,
|
||||||
@@ -147,9 +90,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_skipped++;
|
_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);
|
ReportShape(rawW, rawH, widthRatio);
|
||||||
|
|
||||||
if (!_announced)
|
if (!_announced)
|
||||||
@@ -170,15 +110,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
$"[DOFSKIP{(Mode == 2 ? "/DRY" : "")}] draws matched so far: {_skipped}");
|
$"[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;
|
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)
|
private static void ReportShape(int width, int height, float widthRatio)
|
||||||
{
|
{
|
||||||
long key = ((long)width << 32) | (uint)height;
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,70 +5,29 @@ using System.IO;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppDrawStepProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP") == "1";
|
||||||
|
|
||||||
/// <summary>Secondes avant de capturer l'image (RYUJINX_DRAWSTEP_START, défaut 45).</summary>
|
|
||||||
private static readonly int _startSeconds =
|
private static readonly int _startSeconds =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_START"), out int st) && st > 0 ? st : 45;
|
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 =
|
private static readonly int _everyDraws =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_EVERY"), out int ev) && ev > 0 ? ev : 50;
|
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 =
|
private static readonly int _fromDraw =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FROM"), out int fdr) && fdr > 0 ? fdr : 0;
|
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 =
|
private static readonly int _maxSteps =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_MAX"), out int mx) && mx > 0 ? mx : 25;
|
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 =
|
private static readonly int _frames =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FRAMES"), out int fr) && fr > 0 ? fr : 5;
|
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 =
|
private static readonly int _gapMs =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_GAP"), out int gp) && gp > 0 ? gp : 3000;
|
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 =
|
private static readonly int _vkey =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_VKEY"), out int vk) && vk > 0 ? vk : 0x79;
|
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 int _stepNo;
|
||||||
private static string _dir;
|
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)
|
public static void OnPresent(Image.Texture presented)
|
||||||
{
|
{
|
||||||
if (!_enabled || _done)
|
if (!_enabled || _done)
|
||||||
@@ -137,9 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (_capturing)
|
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)
|
if (presented?.HostTexture != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -173,7 +122,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anti-rebond seulement : la décision appartient entièrement au joueur.
|
|
||||||
if (now < _nextMs || !TriggerHeld())
|
if (now < _nextMs || !TriggerHeld())
|
||||||
{
|
{
|
||||||
return;
|
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)
|
public static void OnDispatch(Image.TextureManager tm, int gridX, int gridY, int gridZ, ulong shaderVa)
|
||||||
{
|
{
|
||||||
if (!_enabled || !_capturing || _stepNo >= _maxSteps || tm == null)
|
if (!_enabled || !_capturing || _stepNo >= _maxSteps || tm == null)
|
||||||
@@ -217,8 +155,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
int idx = 0;
|
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++;
|
_csDispatches++;
|
||||||
|
|
||||||
tm.MvppEnumerateComputeBindings((isImage, isStore, tex) =>
|
tm.MvppEnumerateComputeBindings((isImage, isStore, tex) =>
|
||||||
@@ -278,9 +214,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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;
|
int localDraw = _drawNo;
|
||||||
|
|
||||||
channel.TextureManager.MvppEnumerateRenderTargets((slot, rt) =>
|
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).");
|
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) =>
|
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) =>
|
||||||
{
|
{
|
||||||
if (tex?.HostTexture == null || _stepNo >= _maxSteps)
|
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)
|
public static void OnDrawPost(GpuChannel channel)
|
||||||
{
|
{
|
||||||
if (!_enabled || !_capturing || _stepNo >= _maxSteps)
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,28 +6,17 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppFinalDrawProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_FINALPROBE") == "1";
|
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 MaxPerRt = 40;
|
||||||
private const int MaxTargets = 24; // bound the climb
|
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();
|
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 static int _texCount;
|
||||||
private const int MaxTexLogged = 40;
|
private const int MaxTexLogged = 40;
|
||||||
|
|
||||||
@@ -96,7 +82,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state)
|
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;
|
ulong rtAddr = 0;
|
||||||
int rtSlot = -1;
|
int rtSlot = -1;
|
||||||
Format rtFmt = default;
|
Format rtFmt = default;
|
||||||
@@ -130,7 +115,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-RT budget so each stage of the chain logs a few times, no flood.
|
|
||||||
_loggedPerRt.TryGetValue(rtAddr, out int n);
|
_loggedPerRt.TryGetValue(rtAddr, out int n);
|
||||||
if (n >= MaxPerRt)
|
if (n >= MaxPerRt)
|
||||||
{
|
{
|
||||||
@@ -163,8 +147,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
Logger.Info?.Print(LogClass.Gpu,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
$"MVPP-CHAIN IN @0x{a:X} {f} {w}x{h}{(hdr ? " [HDR]" : "")}{(isScene ? " [=SCENE-PIN]" : "")}");
|
$"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 (hdr && !isScene)
|
||||||
{
|
{
|
||||||
if (_targets.Count < MaxTargets)
|
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) =>
|
channel.TextureManager.MvppEnumerateGraphicsImages((tex, isStore) =>
|
||||||
{
|
{
|
||||||
ulong a = tex.Range.GetSubRange(0).Address;
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -7,28 +5,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppGlowProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -60,8 +36,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state)
|
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;
|
Image.Texture c0 = channel.TextureManager.RenderTargetColor0;
|
||||||
|
|
||||||
if (c0 == null || !c0.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
|
if (c0 == null || !c0.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
|
||||||
@@ -83,8 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
_scissored++;
|
_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} " +
|
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})";
|
$"({(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;
|
_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) =>
|
channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) =>
|
||||||
{
|
{
|
||||||
if (tex == null || smp == null || _inputs.Count >= 24)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,52 +5,13 @@ using System.Threading;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppHangWatch
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HANGWATCH") == "1";
|
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;
|
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 const long RepeatMs = 5000;
|
||||||
|
|
||||||
private static long _lastPresentMs;
|
private static long _lastPresentMs;
|
||||||
@@ -63,10 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _drawsAtStall;
|
private static int _drawsAtStall;
|
||||||
private static bool _inStall;
|
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()
|
public static void Ping()
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _lastPresentMs, Environment.TickCount64);
|
Volatile.Write(ref _lastPresentMs, Environment.TickCount64);
|
||||||
@@ -111,7 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le chiffre qui tranche : les dessins avancent-ils PENDANT le gel ?
|
|
||||||
int draws = MvppCameraCapture.StatScaledDraws;
|
int draws = MvppCameraCapture.StatScaledDraws;
|
||||||
|
|
||||||
if (!_inStall)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -7,31 +5,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppHazardProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -55,7 +28,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Never take the process down from a diagnostic on the GPU thread.
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
OnDrawImpl(channel);
|
OnDrawImpl(channel);
|
||||||
@@ -107,8 +79,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
hitThisDraw = true;
|
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 =
|
string key =
|
||||||
$"{tex.Info.Width}x{tex.Info.Height} {tex.Info.FormatInfo.Format} " +
|
$"{tex.Info.Width}x{tex.Info.Height} {tex.Info.FormatInfo.Format} " +
|
||||||
$"{tex.Info.Target} @{addr:X10} (rt{i}, stage{stage})";
|
$"{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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -10,15 +7,6 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppHdrCensusProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
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)
|
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;
|
ulong rtAddr = 0;
|
||||||
int rtW = 0, rtH = 0;
|
int rtW = 0, rtH = 0;
|
||||||
bool isHdrTarget = false;
|
bool isHdrTarget = false;
|
||||||
@@ -90,17 +77,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
_pairLogged.Add(key);
|
_pairLogged.Add(key);
|
||||||
|
|
||||||
// Geometry
|
|
||||||
int vtx = indexed ? -1 : count;
|
int vtx = indexed ? -1 : count;
|
||||||
int idx = indexed ? count : 0;
|
int idx = indexed ? count : 0;
|
||||||
|
|
||||||
// Pipeline
|
|
||||||
bool depthT = state.DepthTestEnable;
|
bool depthT = state.DepthTestEnable;
|
||||||
bool depthW = state.DepthWriteEnable;
|
bool depthW = state.DepthWriteEnable;
|
||||||
bool blend = state.BlendEnable[0];
|
bool blend = state.BlendEnable[0];
|
||||||
int vpW = (int)MathF.Round(MathF.Abs(state.ViewportTransform[0].ScaleX) * 2f);
|
int vpW = (int)MathF.Round(MathF.Abs(state.ViewportTransform[0].ScaleX) * 2f);
|
||||||
|
|
||||||
// Vertex buffers
|
|
||||||
int vbCount = 0;
|
int vbCount = 0;
|
||||||
for (int i = 0; i < 16; i++)
|
for (int i = 0; i < 16; i++)
|
||||||
{
|
{
|
||||||
@@ -110,7 +94,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data sources
|
|
||||||
int texCount = 0;
|
int texCount = 0;
|
||||||
channel.TextureManager.MvppEnumerateGraphicsInputs(_ => texCount++);
|
channel.TextureManager.MvppEnumerateGraphicsInputs(_ => texCount++);
|
||||||
int imgCount = 0;
|
int imgCount = 0;
|
||||||
@@ -131,7 +114,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classification
|
|
||||||
string cls;
|
string cls;
|
||||||
bool geomLike = depthW && (idx > 100 || vtx > 100) && vbCount > 0;
|
bool geomLike = depthW && (idx > 100 || vtx > 100) && vbCount > 0;
|
||||||
bool fullscreen = (vtx >= 3 && vtx <= 6) || (idx >= 3 && idx <= 6);
|
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} | " +
|
$"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}");
|
$"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;
|
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
|
||||||
bool readsLowerResHdr = false;
|
bool readsLowerResHdr = false;
|
||||||
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) =>
|
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) =>
|
||||||
@@ -166,7 +146,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
int w = tex.Info.Width, h = tex.Info.Height;
|
int w = tex.Info.Width, h = tex.Info.Height;
|
||||||
bool hdr = f == Format.R11G11B10Float || f == Format.R16G16B16A16Float;
|
bool hdr = f == Format.R11G11B10Float || f == Format.R16G16B16A16Float;
|
||||||
bool isScenePin = sceneAddr != 0 && a == sceneAddr;
|
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;
|
readsLowerResHdr |= lowerResHdr || isScenePin;
|
||||||
|
|
||||||
Logger.Info?.Print(LogClass.Gpu,
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,25 +6,13 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
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 =
|
private static readonly bool _verbose =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECTGATE") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECTGATE") == "1";
|
||||||
private static bool _enabled = _verbose || MvppInjectState.Enabled;
|
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 const int MaxFullLogged = 40;
|
||||||
|
|
||||||
private static long _total;
|
private static long _total;
|
||||||
@@ -37,9 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static long _summaryMs;
|
private static long _summaryMs;
|
||||||
private static readonly HashSet<(ulong, ulong)> _candidatePairs = new();
|
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 long _relaxedCandidates;
|
||||||
private static readonly HashSet<(ulong, ulong)> _relaxedPairs = new();
|
private static readonly HashSet<(ulong, ulong)> _relaxedPairs = new();
|
||||||
|
|
||||||
@@ -65,12 +47,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
_total++;
|
_total++;
|
||||||
|
|
||||||
// Cheap pipeline-signature criteria first.
|
|
||||||
bool idx3 = indexed && count == 3;
|
bool idx3 = indexed && count == 3;
|
||||||
bool depthOff = !state.DepthTestEnable;
|
bool depthOff = !state.DepthTestEnable;
|
||||||
bool blendOff = !state.BlendEnable[0];
|
bool blendOff = !state.BlendEnable[0];
|
||||||
|
|
||||||
// Destination RT slot 0.
|
|
||||||
ulong dstAddr = 0;
|
ulong dstAddr = 0;
|
||||||
Format dstFmt = default;
|
Format dstFmt = default;
|
||||||
int dw = 0, dh = 0;
|
int dw = 0, dh = 0;
|
||||||
@@ -87,7 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sampled input textures (count + first).
|
|
||||||
int texCount = 0;
|
int texCount = 0;
|
||||||
ulong srcAddr = 0;
|
ulong srcAddr = 0;
|
||||||
Format srcFmt = default;
|
Format srcFmt = default;
|
||||||
@@ -106,16 +85,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
|
ulong sceneAddr = MvppCameraCapture.SceneColorAddress;
|
||||||
|
|
||||||
// The six gate criteria.
|
bool c1 = texCount == 1;
|
||||||
bool c1 = texCount == 1; // exactly one sampled texture
|
bool c2 = sceneAddr != 0 && srcAddr == sceneAddr;
|
||||||
bool c2 = sceneAddr != 0 && srcAddr == sceneAddr; // source == captured SceneColor
|
bool c3 = srcFmt == Format.R11G11B10Float;
|
||||||
bool c3 = srcFmt == Format.R11G11B10Float; // source format
|
bool c4 = haveDst && dstFmt == Format.R11G11B10Float && dw >= 1500 && dw <= 1700;
|
||||||
bool c4 = haveDst && dstFmt == Format.R11G11B10Float && dw >= 1500 && dw <= 1700; // dest == HDR 1600
|
bool c5 = sw > 0 && dw > 0 && sw < dw;
|
||||||
bool c5 = sw > 0 && dw > 0 && sw < dw; // coherent dims (upscale)
|
bool c6 = idx3 && depthOff && blendOff;
|
||||||
bool c6 = idx3 && depthOff && blendOff; // fullscreen triangle, no depth/blend
|
|
||||||
|
|
||||||
bool gate = c1 && c2 && c3 && c4 && c5 && c6;
|
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 vs = (ulong)state.ShaderState[1].Offset;
|
||||||
ulong fs = (ulong)state.ShaderState[5].Offset;
|
ulong fs = (ulong)state.ShaderState[5].Offset;
|
||||||
@@ -141,7 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
else if (c2)
|
else if (c2)
|
||||||
{
|
{
|
||||||
// Reads the captured scene but is NOT the upscale gate -- proves specificity (should be rare/zero).
|
|
||||||
_nearMiss++;
|
_nearMiss++;
|
||||||
if (_verbose && _nearMiss <= 30)
|
if (_verbose && _nearMiss <= 30)
|
||||||
{
|
{
|
||||||
@@ -161,8 +138,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
$"strict(candidates={_candidates} pairs={_candidatePairs.Count}) " +
|
$"strict(candidates={_candidates} pairs={_candidatePairs.Count}) " +
|
||||||
$"RELAXED(candidates={_relaxedCandidates} pairs={_relaxedPairs.Count}) nearMiss={_nearMiss}");
|
$"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)
|
if (_relaxedPairs.Count > 1)
|
||||||
{
|
{
|
||||||
foreach ((ulong pvs, ulong pfs) in _relaxedPairs)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppLayoutProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppLdrSkip
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR") == "1";
|
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 =
|
public static readonly bool SkipRank1 =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NORANK1") == "1";
|
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 =
|
public static readonly bool AbMode =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR_AB") == "1";
|
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 =
|
public static readonly bool MissWhy =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_MISSWHY") == "1";
|
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;
|
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 WarmCaptures = 200;
|
||||||
|
|
||||||
private const int CalibrationFrames = 600;
|
private const int CalibrationFrames = 600;
|
||||||
@@ -129,19 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private const float MarginPoints = 0.10f;
|
private const float MarginPoints = 0.10f;
|
||||||
private const int ReportMs = 5000;
|
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 SuspendFrames = 3000;
|
||||||
private const int SuspendsBeforeGiveUp = 4;
|
private const int SuspendsBeforeGiveUp = 4;
|
||||||
|
|
||||||
@@ -169,18 +54,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _frameMark;
|
private static int _frameMark;
|
||||||
private static int _seenMark;
|
private static int _seenMark;
|
||||||
|
|
||||||
// Par image, ecrits par le fil GPU seulement (voir MissWhy).
|
|
||||||
private static int _frSkipped;
|
private static int _frSkipped;
|
||||||
private static int _frAllowed;
|
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 _missA;
|
||||||
private static int _missB;
|
private static int _missB;
|
||||||
private static int _missC;
|
private static int _missC;
|
||||||
private static long _lastReportMs;
|
private static long _lastReportMs;
|
||||||
|
|
||||||
// Mode A/B : indice 1 = restriction ACTIVE (avec), indice 0 = restriction LEVEE (sans).
|
|
||||||
private static bool _abArmed;
|
private static bool _abArmed;
|
||||||
private static int _abWin;
|
private static int _abWin;
|
||||||
private static bool _abStarted;
|
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[] _abReads = new int[2];
|
||||||
private static readonly int[] _abIntruders = 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;
|
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)
|
public static bool Allows(GpuChannel channel)
|
||||||
{
|
{
|
||||||
_qualThisFrame = true;
|
_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;
|
int mark = _frameMark;
|
||||||
|
|
||||||
if (_seenMark != mark)
|
if (_seenMark != mark)
|
||||||
@@ -237,18 +109,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return false;
|
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++;
|
_frAllowed++;
|
||||||
|
|
||||||
return true;
|
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)
|
private static bool IsIntegerColorPass(GpuChannel channel)
|
||||||
{
|
{
|
||||||
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
|
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)
|
internal static void NoteRead(bool intruder)
|
||||||
{
|
{
|
||||||
if (!AbMode || !_abStarted)
|
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)
|
public static void OnFrame(bool captured)
|
||||||
{
|
{
|
||||||
if (!_qualThisFrame)
|
if (!_qualThisFrame)
|
||||||
{
|
{
|
||||||
// Image sans passe 3D (menu, chargement) : elle n'avait pas de camera a prendre.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_qualThisFrame = false;
|
_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 (MissWhy && !captured && ArmedNow)
|
||||||
{
|
{
|
||||||
if (_frSkipped == 0)
|
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++;
|
_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 (_phase == PhaseWarming)
|
||||||
{
|
{
|
||||||
if (captured)
|
if (captured)
|
||||||
@@ -394,8 +247,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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 (_phase == PhaseSuspended)
|
||||||
{
|
{
|
||||||
if (--_suspendLeft <= 0)
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -10,19 +7,6 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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| < 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
|
static class MvppP2BProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -30,10 +14,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
private const int LogIntervalMs = 2000;
|
private const int LogIntervalMs = 2000;
|
||||||
private const float FrustumMargin = 1.3f;
|
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 SlotCount = 18;
|
||||||
private const int ScanSampleShift = 4; // scan every 16th draw
|
private const int ScanSampleShift = 4;
|
||||||
private const int MaxScanFloats = 5120; // covers cb5's 20 KB palettes
|
private const int MaxScanFloats = 5120;
|
||||||
|
|
||||||
private static readonly int[] _scanSlots = { 1, 5, 9, 13 };
|
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 bool _sampleDumped;
|
||||||
private static readonly int[] _slotBound = new int[SlotCount];
|
private static readonly int[] _slotBound = new int[SlotCount];
|
||||||
private static readonly int[] _scanned = new int[4];
|
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 readonly int[][][] _hits = CreateHits();
|
||||||
|
|
||||||
private static int[][][] 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)
|
if (!_sampleDumped && _named >= 500)
|
||||||
{
|
{
|
||||||
_sampleDumped = true;
|
_sampleDumped = true;
|
||||||
@@ -201,7 +182,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
(ax >= MinTranslation || ay >= MinTranslation || az >= MinTranslation);
|
(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)
|
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;
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -10,16 +7,6 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppP2Probe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -27,9 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
private const int LogIntervalMs = 2000;
|
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 const float CoherentMaxDelta = 1.0f;
|
||||||
|
|
||||||
private struct DrawRecord
|
private struct DrawRecord
|
||||||
@@ -37,8 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
public Matrix4x4 Mat0;
|
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> _prev = new();
|
||||||
private static Dictionary<ulong, DrawRecord> _curr = new();
|
private static Dictionary<ulong, DrawRecord> _curr = new();
|
||||||
private static readonly Dictionary<ulong, int> _sigCounts = 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 _ambiguous;
|
||||||
private static int _matched;
|
private static int _matched;
|
||||||
private static int _coherent;
|
private static int _coherent;
|
||||||
private static double _sumDtRow; // |delta translation| of coherent pairs, row-major candidate [12..14]
|
private static double _sumDtRow;
|
||||||
private static double _sumDtCol; // column-major candidate [3,7,11]
|
private static double _sumDtCol;
|
||||||
private static float _maxDelta; // max element delta over ALL matched pairs (mismatch witness)
|
private static float _maxDelta;
|
||||||
private static long _sumCbufBytes;
|
private static long _sumCbufBytes;
|
||||||
private static int _maxCbufBytes;
|
private static int _maxCbufBytes;
|
||||||
|
|
||||||
/// <summary>Present enqueue = frame boundary: close the frame, swap tables, maybe log.</summary>
|
|
||||||
public static void OnFrameEnqueued()
|
public static void OnFrameEnqueued()
|
||||||
{
|
{
|
||||||
if (!_enabled)
|
if (!_enabled)
|
||||||
@@ -69,8 +50,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
lock (_sync)
|
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)
|
foreach (KeyValuePair<ulong, int> kv in _sigCounts)
|
||||||
{
|
{
|
||||||
if (kv.Value > 1)
|
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(
|
public static void OnDraw(
|
||||||
GpuChannel channel,
|
GpuChannel channel,
|
||||||
ref ThreedClassState state,
|
ref ThreedClassState state,
|
||||||
@@ -111,8 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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
|
try
|
||||||
{
|
{
|
||||||
OnDrawImpl(channel, ref state, firstIndex, indexCount, drawFirstVertex, drawVertexCount);
|
OnDrawImpl(channel, ref state, firstIndex, indexCount, drawFirstVertex, drawVertexCount);
|
||||||
@@ -132,7 +108,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
int drawFirstVertex,
|
int drawFirstVertex,
|
||||||
int drawVertexCount)
|
int drawVertexCount)
|
||||||
{
|
{
|
||||||
// Only the scaled 3D scene pass; UI/native passes stay at 1x.
|
|
||||||
if (channel.TextureManager.RenderTargetScale == 1f)
|
if (channel.TextureManager.RenderTargetScale == 1f)
|
||||||
{
|
{
|
||||||
return;
|
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).");
|
"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;
|
ulong sig = 14695981039346656037UL;
|
||||||
sig = FnvStep(sig, state.ShaderState[1].Offset);
|
sig = FnvStep(sig, state.ShaderState[1].Offset);
|
||||||
sig = FnvStep(sig, state.ShaderState[5].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)drawFirstVertex);
|
||||||
sig = FnvStep(sig, (ulong)(uint)drawVertexCount);
|
sig = FnvStep(sig, (ulong)(uint)drawVertexCount);
|
||||||
|
|
||||||
// cbuf5 (vertex stage) coverage + matrix 0 snapshot.
|
|
||||||
ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(0, 5);
|
ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(0, 5);
|
||||||
int size = channel.BufferManager.GetGraphicsUniformBufferSize(0, 5);
|
int size = channel.BufferManager.GetGraphicsUniformBufferSize(0, 5);
|
||||||
|
|
||||||
@@ -248,9 +221,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
_coherent++;
|
_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 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];
|
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);
|
_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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,19 +5,6 @@ using System.IO;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppPreSyncProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
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)
|
public static void DumpGuest(Image.Texture texture)
|
||||||
{
|
{
|
||||||
if (!_enabled || texture == null)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -9,22 +6,11 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_PROBE") == "1";
|
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 =
|
private static readonly int _dumpSlot =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUMP"), out int s) ? s : -1;
|
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUMP"), out int s) ? s : -1;
|
||||||
|
|
||||||
@@ -43,9 +29,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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
|
try
|
||||||
{
|
{
|
||||||
OnDrawImpl(channel);
|
OnDrawImpl(channel);
|
||||||
@@ -59,7 +42,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
private static void OnDrawImpl(GpuChannel channel)
|
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)
|
if (channel.TextureManager.RenderTargetScale == 1f)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -95,8 +77,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bound ranges are already TRANSLATED: physical addresses, not GPU VAs
|
|
||||||
// (SetGraphicsUniformBuffer runs TranslateAndCreateBuffer). Read physical.
|
|
||||||
ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot);
|
ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot);
|
||||||
int size = Math.Min(channel.BufferManager.GetGraphicsUniformBufferSize(stage, slot), MaxBytesPerBuffer);
|
int size = Math.Min(channel.BufferManager.GetGraphicsUniformBufferSize(stage, slot), MaxBytesPerBuffer);
|
||||||
|
|
||||||
@@ -122,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 16-byte aligned windows of 16 floats.
|
|
||||||
for (int i = 0; i + 16 <= data.Length; i += 4)
|
for (int i = 0; i + 16 <= data.Length; i += 4)
|
||||||
{
|
{
|
||||||
ReadOnlySpan<float> window = data.Slice(i, 16);
|
ReadOnlySpan<float> window = data.Slice(i, 16);
|
||||||
@@ -151,7 +130,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_lastFingerprint[key] = fp;
|
_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)
|
private static bool LooksLikeMat4(ReadOnlySpan<float> m)
|
||||||
{
|
{
|
||||||
int nonZero = 0;
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,60 +5,6 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppProjAudit
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -71,10 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
private static long _lastMs;
|
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)
|
public static void Audit(in Matrix4x4 vp, float camX, float camY, float camZ)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
if (!Enabled)
|
||||||
@@ -91,41 +30,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_lastMs = now;
|
_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 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);
|
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 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);
|
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 dot23 = vp.M31 * vp.M41 + vp.M32 * vp.M42 + vp.M33 * vp.M43;
|
||||||
float s = dot23 >= 0f ? 1f : -1f;
|
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 tz = s != 0f ? vp.M44 / s : 0f;
|
||||||
float b = vp.M34 - a * tz;
|
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 aSigned = a * s;
|
||||||
float near = MathF.Abs(aSigned - 1f) > 1e-6f ? b / (aSigned - 1f) : float.NaN;
|
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;
|
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 wAtCam = vp.M41 * camX + vp.M42 * camY + vp.M43 * camZ + vp.M44;
|
||||||
float camDist = MathF.Sqrt(camX * camX + camY * camY + camZ * camZ);
|
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;
|
float rt = float.NaN;
|
||||||
|
|
||||||
if (Matrix4x4.Invert(vp, out Matrix4x4 inv))
|
if (Matrix4x4.Invert(vp, out Matrix4x4 inv))
|
||||||
@@ -142,8 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (MathF.Abs(back.W) > 1e-20f)
|
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) +
|
rt = MathF.Sqrt(back.X / back.W * (back.X / back.W) +
|
||||||
back.Y / back.W * (back.Y / back.W)) * 1920f;
|
back.Y / back.W * (back.Y / back.W)) * 1920f;
|
||||||
}
|
}
|
||||||
@@ -151,7 +72,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
rt = -1f; // not invertible at all
|
rt = -1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Info?.Print(LogClass.Gpu,
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,80 +6,26 @@ using System.IO;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppRtDumpProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP") == "1";
|
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 =
|
private static readonly int _startSeconds =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_START"), out int st) && st > 0 ? st : 90;
|
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 =
|
private static readonly int _everySeconds =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_EVERY"), out int s) && s > 0 ? s : 10;
|
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 =
|
private static readonly int _maxBursts =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_MAX"), out int m) && m > 0 ? m : 4;
|
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 =
|
private static readonly bool _dumpAll =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_ALL") == "1";
|
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 =
|
private static readonly bool _dumpInputs =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_INPUTS") == "1";
|
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 =
|
private static readonly bool _frameMode =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_FRAME") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_FRAME") == "1";
|
||||||
|
|
||||||
@@ -95,11 +39,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static bool _frameArmLogged;
|
private static bool _frameArmLogged;
|
||||||
private static readonly HashSet<string> _doneThisBurst = new();
|
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)
|
public static void NotePresented(Image.Texture texture)
|
||||||
{
|
{
|
||||||
if (!_enabled)
|
if (!_enabled)
|
||||||
@@ -126,13 +65,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mode IMAGE : ce present est la frontière entre deux images. ---
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_bursting)
|
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)
|
if (texture != null)
|
||||||
{
|
{
|
||||||
TryDump(texture, "PRESENTED");
|
TryDump(texture, "PRESENTED");
|
||||||
@@ -173,8 +109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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++;
|
_burstIndex++;
|
||||||
_doneThisBurst.Clear();
|
_doneThisBurst.Clear();
|
||||||
_burstDir = Path.Combine("rtdump", $"image{_burstIndex:D2}");
|
_burstDir = Path.Combine("rtdump", $"image{_burstIndex:D2}");
|
||||||
@@ -212,9 +146,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
long now = Environment.TickCount64;
|
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 (_frameMode)
|
||||||
{
|
{
|
||||||
if (!_bursting)
|
if (!_bursting)
|
||||||
@@ -245,8 +176,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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;
|
_bursting = true;
|
||||||
_burstEndsMs = now + 1000;
|
_burstEndsMs = now + 1000;
|
||||||
_burstIndex++;
|
_burstIndex++;
|
||||||
@@ -288,11 +217,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
TryDump(ds, "depth");
|
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)
|
if (_dumpInputs)
|
||||||
{
|
{
|
||||||
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) => TryDump(tex, $"input{stage}"));
|
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) => TryDump(tex, $"input{stage}"));
|
||||||
@@ -313,27 +237,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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}";
|
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 &&
|
if (role != "PRESENTED" && !_dumpAll && !_dumpInputs &&
|
||||||
!tex.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
|
!tex.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return;
|
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;
|
ulong gpuAddr = 0;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -342,7 +253,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// pas d'adresse exploitable : on garde 0, le nom reste unique par le slot
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_doneThisBurst.Add(gpuAddr != 0 ? $"@{gpuAddr:X}" : name))
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,32 +5,6 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppScanProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -42,9 +13,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private const int PassWindowMs = 5000;
|
private const int PassWindowMs = 5000;
|
||||||
private const int DeepScanIntervalMs = 1000;
|
private const int DeepScanIntervalMs = 1000;
|
||||||
private const int DeepScanFallbackMs = 500;
|
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 MaxBytesPerBuffer = 4096;
|
||||||
private const int MaxDetailLines = 28;
|
private const int MaxDetailLines = 28;
|
||||||
|
|
||||||
@@ -54,8 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private const int MaxViewPaired = 10;
|
private const int MaxViewPaired = 10;
|
||||||
private const int MaxProducts = MaxProjPaired * MaxViewPaired * 2;
|
private const int MaxProducts = MaxProjPaired * MaxViewPaired * 2;
|
||||||
|
|
||||||
// ---- Stage A: pass inventory (cheap per-draw counters, 5 s windows) ----
|
|
||||||
|
|
||||||
private struct DepthForm
|
private struct DepthForm
|
||||||
{
|
{
|
||||||
public int Width;
|
public int Width;
|
||||||
@@ -65,12 +31,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
public int Count;
|
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 _liveMainW;
|
||||||
private static int _liveMainH;
|
private static int _liveMainH;
|
||||||
private static long _liveMainArea;
|
private static long _liveMainArea;
|
||||||
@@ -88,8 +48,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _colorFormCount;
|
private static int _colorFormCount;
|
||||||
private static int _colorFormOverflow;
|
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 long _lastDeepScanMs;
|
||||||
private static bool _lastScanOnMainPass;
|
private static bool _lastScanOnMainPass;
|
||||||
private static bool _wantDeepScan;
|
private static bool _wantDeepScan;
|
||||||
@@ -113,9 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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
|
try
|
||||||
{
|
{
|
||||||
OnDrawImpl(channel);
|
OnDrawImpl(channel);
|
||||||
@@ -131,8 +86,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
long now = Environment.TickCount64;
|
long now = Environment.TickCount64;
|
||||||
|
|
||||||
// ---- Stage A: per-draw counters (int adds + a tiny linear form table) ----
|
|
||||||
|
|
||||||
_draws++;
|
_draws++;
|
||||||
|
|
||||||
Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil;
|
Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil;
|
||||||
@@ -156,8 +109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_liveMainW = w;
|
_liveMainW = w;
|
||||||
_liveMainH = h;
|
_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)
|
if (_mainAspect <= 0f)
|
||||||
{
|
{
|
||||||
_mainAspect = (float)w / h;
|
_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);
|
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;
|
Image.Texture c0 = channel.TextureManager.RenderTargetColor0;
|
||||||
|
|
||||||
if (c0 != null)
|
if (c0 != null)
|
||||||
@@ -190,22 +136,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_passWindowStartMs = now;
|
_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)
|
if (!_wantDeepScan && now - _lastDeepScanMs >= DeepScanIntervalMs)
|
||||||
{
|
{
|
||||||
_wantDeepScan = true;
|
_wantDeepScan = true;
|
||||||
_wantDeepScanSinceMs = now;
|
_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 &&
|
bool onMainPass = ds != null && ds.Info.Width != ds.Info.Height &&
|
||||||
(long)ds.Info.Width * ds.Info.Height >= _liveMainArea;
|
(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());
|
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
|
||||||
|
|
||||||
// Promote the window's main target as the aspect reference for the next window.
|
|
||||||
if (_liveMainH > 0)
|
if (_liveMainH > 0)
|
||||||
{
|
{
|
||||||
_mainAspect = (float)_liveMainW / _liveMainH;
|
_mainAspect = (float)_liveMainW / _liveMainH;
|
||||||
@@ -345,27 +280,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_deepScans = 0;
|
_deepScans = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Deep scan ----
|
|
||||||
|
|
||||||
private enum MatClass
|
private enum MatClass
|
||||||
{
|
{
|
||||||
Proj, // perspective projection, row-major
|
Proj,
|
||||||
ProjT, // perspective projection, column-major (transposed storage)
|
ProjT,
|
||||||
View, // orthonormal view, row-major
|
View,
|
||||||
ViewT, // orthonormal view, column-major
|
ViewT,
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct Candidate
|
private struct Candidate
|
||||||
{
|
{
|
||||||
public int Stage;
|
public int Stage;
|
||||||
public int Slot;
|
public int Slot;
|
||||||
public int Offset; // in BYTES from the start of the buffer
|
public int Offset;
|
||||||
public MatClass Class;
|
public MatClass Class;
|
||||||
public float A; // proj: m00 (fx) | view: camera pos X
|
public float A;
|
||||||
public float B; // proj: m11 (fy) | view: camera pos Y
|
public float B;
|
||||||
public float C; // view: camera pos Z (unused for proj)
|
public float C;
|
||||||
public bool InRun; // sits in a stride-64 chain = skinning palette signature
|
public bool InRun;
|
||||||
public bool Identity; // near-identity = structurally meaningless
|
public bool Identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct BufRef
|
private struct BufRef
|
||||||
@@ -380,7 +313,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static readonly BufRef[] _bufs = new BufRef[MaxBuffers];
|
private static readonly BufRef[] _bufs = new BufRef[MaxBuffers];
|
||||||
private static int _bufCount;
|
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 float[] _products = new float[MaxProducts * 16];
|
||||||
private static readonly int[] _prodProjIdx = new int[MaxProducts];
|
private static readonly int[] _prodProjIdx = new int[MaxProducts];
|
||||||
private static readonly int[] _prodViewIdx = 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[] _tmpB = new float[16];
|
||||||
private static readonly float[] _tmpT = 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
|
private struct VpSolo
|
||||||
{
|
{
|
||||||
public int Stage;
|
public int Stage;
|
||||||
@@ -419,15 +338,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
public float Motion;
|
public float Motion;
|
||||||
public float Straightness;
|
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;
|
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;
|
public ulong Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,39 +347,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static readonly VpSolo[] _vpSolos = new VpSolo[MaxVpSolos];
|
private static readonly VpSolo[] _vpSolos = new VpSolo[MaxVpSolos];
|
||||||
private static int _vpSoloCount;
|
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;
|
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 bool _hasLock;
|
||||||
private static ulong _lockAddress;
|
private static ulong _lockAddress;
|
||||||
private static int _lockOffset;
|
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
|
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 ulong Address;
|
||||||
public int Offset;
|
public int Offset;
|
||||||
public float LastX;
|
public float LastX;
|
||||||
@@ -477,7 +364,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
public float MaxStep;
|
public float MaxStep;
|
||||||
public int Seen;
|
public int Seen;
|
||||||
|
|
||||||
// Ring of recent positions, used for the straightness ratio (see TrackMotion).
|
|
||||||
public int RingHead;
|
public int RingHead;
|
||||||
public int RingCount;
|
public int RingCount;
|
||||||
public float PathLength;
|
public float PathLength;
|
||||||
@@ -489,7 +375,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static readonly Tracked[] _tracks = new Tracked[MaxTracks];
|
private static readonly Tracked[] _tracks = new Tracked[MaxTracks];
|
||||||
private static int _trackCount;
|
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[] _ringX = new float[MaxTracks * RingSize];
|
||||||
private static readonly float[] _ringY = new float[MaxTracks * RingSize];
|
private static readonly float[] _ringY = new float[MaxTracks * RingSize];
|
||||||
private static readonly float[] _ringZ = 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;
|
System.Text.StringBuilder detail = null;
|
||||||
int detailLines = 0;
|
int detailLines = 0;
|
||||||
|
|
||||||
// ---- Phase 1: collect every matrix-shaped candidate across ALL stages/slots ----
|
|
||||||
|
|
||||||
for (int stage = 0; stage < Constants.ShaderStages; stage++)
|
for (int stage = 0; stage < Constants.ShaderStages; stage++)
|
||||||
{
|
{
|
||||||
uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(stage);
|
uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(stage);
|
||||||
@@ -596,13 +479,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
MarkRuns(firstCand);
|
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)
|
for (int i = 0; i + 16 <= data.Length; i += 4)
|
||||||
{
|
{
|
||||||
ReadOnlySpan<float> m = data.Slice(i, 16);
|
ReadOnlySpan<float> m = data.Slice(i, 16);
|
||||||
@@ -622,8 +498,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
ReadOnlySpan<float> vpm = transposed ? _tmpT : m;
|
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)
|
if (MathF.Abs(fy / fx - 1f) < 0.2f)
|
||||||
{
|
{
|
||||||
_lastStandaloneSquare++;
|
_lastStandaloneSquare++;
|
||||||
@@ -637,8 +511,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
ViewProjPos(vpm, fx, fy, out float px, out float py, out float pz);
|
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) ||
|
if (!float.IsFinite(px) || !float.IsFinite(py) || !float.IsFinite(pz) ||
|
||||||
MathF.Abs(px) > 1e6f || MathF.Abs(py) > 1e6f || MathF.Abs(pz) > 1e6f)
|
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,
|
AppendDetail(ref detail, ref detailLines,
|
||||||
$"VP-SOLO{(transposed ? "^T" : " ")} stage{stage} cbuf{slot} +0x{i * 4:X3} " +
|
$"VP-SOLO{(transposed ? "^T" : " ")} stage{stage} cbuf{slot} +0x{i * 4:X3} " +
|
||||||
$"@{channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot):X10} " +
|
$"@{channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot):X10} " +
|
||||||
$"fx={fx:0.###} fy={fy:0.###} aspect={fy / fx:0.###} campos=[{px:0.#} {py:0.#} {pz:0.#}]");
|
$"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)
|
for (int i = 0; i + 48 <= data.Length; i += 4)
|
||||||
{
|
{
|
||||||
if (IsOrthonormalView(data.Slice(i, 16)) &&
|
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 projCount = 0;
|
||||||
int viewCount = 0;
|
int viewCount = 0;
|
||||||
int viewKept = 0;
|
int viewKept = 0;
|
||||||
@@ -726,8 +588,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
BuildProducts(channel);
|
BuildProducts(channel);
|
||||||
|
|
||||||
// ---- Phase 3: is any of those products actually STORED in guest memory? ----
|
|
||||||
|
|
||||||
int vpProducts = SearchProducts(channel, ref detail, ref detailLines, ref foundFp);
|
int vpProducts = SearchProducts(channel, ref detail, ref detailLines, ref foundFp);
|
||||||
|
|
||||||
if (layoutFp != _lastCbufLayoutFp)
|
if (layoutFp != _lastCbufLayoutFp)
|
||||||
@@ -743,9 +603,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_lastCanonicalCount = canonical;
|
_lastCanonicalCount = canonical;
|
||||||
_lastStandaloneVp = standaloneVp;
|
_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;
|
System.Text.StringBuilder verdict = null;
|
||||||
int verdictLines = 0;
|
int verdictLines = 0;
|
||||||
LogVerdict(ref verdict, ref verdictLines);
|
LogVerdict(ref verdict, ref verdictLines);
|
||||||
@@ -759,7 +616,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
_lastFoundSetFp = foundFp;
|
_lastFoundSetFp = foundFp;
|
||||||
|
|
||||||
// Projections first (the solid signal), then the views that survived the filter.
|
|
||||||
for (int i = 0; i < _candCount && detailLines < MaxDetailLines; i++)
|
for (int i = 0; i < _candCount && detailLines < MaxDetailLines; i++)
|
||||||
{
|
{
|
||||||
ref Candidate c = ref _cands[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)
|
private static float TrackMotion(in VpSolo c, out float straightness)
|
||||||
{
|
{
|
||||||
straightness = 1f;
|
straightness = 1f;
|
||||||
@@ -850,19 +689,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
ref Tracked t = ref _tracks[idx];
|
ref Tracked t = ref _tracks[idx];
|
||||||
t.Seen++;
|
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;
|
t.MaxStep *= 0.5f;
|
||||||
|
|
||||||
if (t.HasLast)
|
if (t.HasLast)
|
||||||
@@ -872,28 +698,16 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
float dz = c.Z - t.LastZ;
|
float dz = c.Z - t.LastZ;
|
||||||
float step = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
|
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)
|
if (float.IsFinite(step) && step < 1e5f && step > t.MaxStep)
|
||||||
{
|
{
|
||||||
t.MaxStep = step;
|
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;
|
int slot = idx * RingSize;
|
||||||
|
|
||||||
if (t.RingCount == RingSize)
|
if (t.RingCount == RingSize)
|
||||||
{
|
{
|
||||||
// Drop the step leaving the window.
|
|
||||||
t.PathLength -= _ringStep[slot + t.RingHead];
|
t.PathLength -= _ringStep[slot + t.RingHead];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -952,8 +766,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
float targetAspect = _mainAspect;
|
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 bestMotion = 0f;
|
||||||
float bestStraight = 0f;
|
float bestStraight = 0f;
|
||||||
int considered = 0;
|
int considered = 0;
|
||||||
@@ -999,20 +811,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
continue;
|
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)
|
if (bestMotion > 0f && a.Motion < bestMotion * 0.1f)
|
||||||
{
|
{
|
||||||
continue;
|
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)
|
if (bestStraight > 0f && a.Straightness < bestStraight * 0.35f)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -1020,8 +823,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
movers++;
|
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;
|
int votes = 0;
|
||||||
|
|
||||||
for (int j = 0; j < _vpSoloCount; j++)
|
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)
|
if (_hasLock && lockedIdx < 0)
|
||||||
{
|
{
|
||||||
_hasLock = false;
|
_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;
|
int winner;
|
||||||
string mode;
|
string mode;
|
||||||
|
|
||||||
@@ -1100,11 +890,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
winner = elected;
|
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;
|
bool trustworthy = targetAspect > 0f && bestMotion > 0f && _lastScanOnMainPass;
|
||||||
|
|
||||||
mode = trustworthy ? (_hasLock ? "RE-ELU" : "ELU") : "PROVISOIRE";
|
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, " +
|
$"mouvement={w.Motion:0.##} -- {(lockedIdx >= 0 ? lockedVotes : electedVotes)} sources d'accord, " +
|
||||||
$"{movers} mobiles sur {considered} au bon aspect ({_vpSoloCount} bruts){pass}{divergence}");
|
$"{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,
|
if (MvppSoloCamera.TryGetPublishedForProbe(out System.Numerics.Matrix4x4 pubVp,
|
||||||
out float pubX, out float pubY, out float pubZ))
|
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 int _dgConsecutive;
|
||||||
|
|
||||||
private static bool TryReadBuffer(GpuChannel channel, int stage, int slot, out ReadOnlySpan<float> data)
|
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
|
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));
|
data = MemoryMarshal.Cast<byte, float>(channel.MemoryManager.Physical.GetSpan(address, size));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -1218,12 +990,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return true;
|
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)
|
private static void MarkRuns(int firstCand)
|
||||||
{
|
{
|
||||||
for (int i = firstCand; i < _candCount; i++)
|
for (int i = firstCand; i < _candCount; i++)
|
||||||
@@ -1300,7 +1066,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
viewUsed++;
|
viewUsed++;
|
||||||
|
|
||||||
// Both conventions: proj x view (row-vector engines) and view x proj.
|
|
||||||
Multiply(_tmpA, _tmpB, _products.AsSpan(_prodCount * 16, 16));
|
Multiply(_tmpA, _tmpB, _products.AsSpan(_prodCount * 16, 16));
|
||||||
_prodProjIdx[_prodCount] = pi;
|
_prodProjIdx[_prodCount] = pi;
|
||||||
_prodViewIdx[_prodCount] = vi;
|
_prodViewIdx[_prodCount] = vi;
|
||||||
@@ -1344,7 +1109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
ReadOnlySpan<float> exp = _products.AsSpan(k * 16, 16);
|
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 direct = Close(w[0], exp[0]) && Close(w[5], exp[5]) && MatchesLoose(w, exp, false);
|
||||||
bool transposed = !direct &&
|
bool transposed = !direct &&
|
||||||
Close(w[0], exp[0]) && Close(w[5], exp[5]) && MatchesLoose(w, exp, true);
|
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;
|
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)
|
private static bool IsViewProj(ReadOnlySpan<float> m, out float fx, out float fy)
|
||||||
{
|
{
|
||||||
fx = 0f;
|
fx = 0f;
|
||||||
fy = 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]);
|
float n3 = MathF.Sqrt(m[12] * m[12] + m[13] * m[13] + m[14] * m[14]);
|
||||||
|
|
||||||
if (MathF.Abs(n3 - 1f) > 0.02f)
|
if (MathF.Abs(n3 - 1f) > 0.02f)
|
||||||
@@ -1437,9 +1185,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return false;
|
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 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]);
|
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;
|
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 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 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;
|
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;
|
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]);
|
float n2 = MathF.Sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]);
|
||||||
|
|
||||||
if (n2 > 1e-4f)
|
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 =
|
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[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 &&
|
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;
|
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)
|
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;
|
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)
|
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]);
|
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]);
|
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]);
|
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;
|
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)
|
private static bool IsOrthonormalView(ReadOnlySpan<float> m)
|
||||||
{
|
{
|
||||||
if (MathF.Abs(m[12]) > 1e-3f || MathF.Abs(m[13]) > 1e-3f ||
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppScenePass
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -86,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _catchUpFramesSeen;
|
private static int _catchUpFramesSeen;
|
||||||
private static long _lastReportMs;
|
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)
|
public static bool Allows(GpuChannel channel)
|
||||||
{
|
{
|
||||||
_qualThisFrame = true;
|
_qualThisFrame = true;
|
||||||
@@ -110,12 +46,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return false;
|
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)
|
internal static bool IsSceneColorPass(GpuChannel channel)
|
||||||
{
|
{
|
||||||
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
|
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;
|
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)
|
public static void OnFrame(bool captured)
|
||||||
{
|
{
|
||||||
if (!_qualThisFrame)
|
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;
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -12,20 +9,8 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
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 =
|
private static readonly int MaxDumps =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_SURVIVE_DUMPS"), out int maxDumps) && maxDumps > 0
|
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_SURVIVE_DUMPS"), out int maxDumps) && maxDumps > 0
|
||||||
? maxDumps
|
? maxDumps
|
||||||
@@ -37,12 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static long _nextDumpMs = -1;
|
private static long _nextDumpMs = -1;
|
||||||
private static int _dumpCount;
|
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)
|
public static BufferRange GetRange(GpuContext context)
|
||||||
{
|
{
|
||||||
if (_handle == BufferHandle.Null)
|
if (_handle == BufferHandle.Null)
|
||||||
@@ -56,19 +35,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
$"binding {MvppSurviveProbe.HostBinding}) and cleared to zero.");
|
$"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);
|
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)
|
public static BufferRange GetDummyRange(GpuContext context)
|
||||||
{
|
{
|
||||||
if (_dummyHandle == BufferHandle.Null)
|
if (_dummyHandle == BufferHandle.Null)
|
||||||
@@ -82,14 +51,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return new BufferRange(_dummyHandle, 0, MvppSurviveProbe.BufferSize, true);
|
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)
|
public static void MaybeDump(GpuContext context)
|
||||||
{
|
{
|
||||||
if (_handle == BufferHandle.Null || _dumpCount >= MaxDumps)
|
if (_handle == BufferHandle.Null || _dumpCount >= MaxDumps)
|
||||||
@@ -113,7 +74,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_nextDumpMs = now + DumpIntervalMs;
|
_nextDumpMs = now + DumpIntervalMs;
|
||||||
_dumpCount++;
|
_dumpCount++;
|
||||||
|
|
||||||
// Make the fragment shader stores visible, then read the whole map back.
|
|
||||||
context.Renderer.Pipeline.Barrier();
|
context.Renderer.Pipeline.Barrier();
|
||||||
|
|
||||||
long nonZero = 0;
|
long nonZero = 0;
|
||||||
@@ -130,16 +90,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
for (int i = 0; i < words.Length; i++)
|
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)
|
if (words[i] != 0)
|
||||||
{
|
{
|
||||||
nonZero++;
|
nonZero++;
|
||||||
pgmPixels[i] = 255;
|
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 x = i & (MvppSurviveProbe.MapStride - 1);
|
||||||
int y = i >> 12;
|
int y = i >> 12;
|
||||||
|
|
||||||
@@ -179,7 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
$"sceneDepthPin={MvppCameraCapture.SceneDepthHostWidth}x{MvppCameraCapture.SceneDepthHostHeight} " +
|
$"sceneDepthPin={MvppCameraCapture.SceneDepthHostWidth}x{MvppCameraCapture.SceneDepthHostHeight} " +
|
||||||
$"-> {path} + raw .bin");
|
$"-> {path} + raw .bin");
|
||||||
|
|
||||||
// Fresh window for the next dump.
|
|
||||||
context.Renderer.Pipeline.ClearBuffer(_handle, 0, MvppSurviveProbe.BufferSize, 0);
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -10,29 +7,14 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppUiProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE") == "1";
|
||||||
|
|
||||||
private const int MaxEpochsPerFrame = 64; // hard cap so a pathological frame cannot grow unbounded
|
private const int MaxEpochsPerFrame = 64;
|
||||||
private const int FullDetailFrames = 8; // log every epoch for the first frames, then only the verdict
|
private const int FullDetailFrames = 8;
|
||||||
private const int VerdictThrottleMs = 2000; // steady-state: one verdict line per change or per 2 s
|
private const int VerdictThrottleMs = 2000;
|
||||||
|
|
||||||
private struct Epoch
|
private struct Epoch
|
||||||
{
|
{
|
||||||
@@ -44,8 +26,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
public bool AnyDepth;
|
public bool AnyDepth;
|
||||||
public bool AnyBlend;
|
public bool AnyBlend;
|
||||||
public int Draws;
|
public int Draws;
|
||||||
public int NoDepthDraws; // [HUDSPLIT] dessins sans test de profondeur = candidats HUD
|
public int NoDepthDraws;
|
||||||
public int FirstNoDepthAt; // rang du premier d'entre eux dans l'epoque (1 = des le debut)
|
public int FirstNoDepthAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly List<Epoch> _epochs = new();
|
private static readonly List<Epoch> _epochs = new();
|
||||||
@@ -53,60 +35,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _frameNo;
|
private static int _frameNo;
|
||||||
private static int _framesLoggedFull;
|
private static int _framesLoggedFull;
|
||||||
private static long _lastVerdictMs;
|
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 =
|
private static readonly int _dumpAfterSeconds =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE_DUMP"), out int ds) && ds > 0 ? ds : 0;
|
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE_DUMP"), out int ds) && ds > 0 ? ds : 0;
|
||||||
|
|
||||||
private static long _dumpArmedAtMs;
|
private static long _dumpArmedAtMs;
|
||||||
private static bool _dumpDone;
|
private static bool _dumpDone;
|
||||||
private static int _dumpSeq;
|
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 =
|
private static readonly bool _hudless =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS") == "1";
|
||||||
|
|
||||||
private const int HudlessMinDraws = 20;
|
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 =
|
private static readonly bool _hudlessFeed =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS_FEED") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS_FEED") == "1";
|
||||||
|
|
||||||
@@ -120,13 +63,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private static int _renderWidthSeen;
|
private static int _renderWidthSeen;
|
||||||
private static int _hudlessHits;
|
private static int _hudlessHits;
|
||||||
private static int _hudlessFrames;
|
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 long _hudlessLogMs;
|
||||||
private static int _hudlessInFrame;
|
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 ulong _hudlessBestAddr;
|
||||||
private static int _hudlessBestRank;
|
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 int _hudlessEchecsLogues;
|
||||||
private static string _lastVerdictSig = "";
|
private static string _lastVerdictSig = "";
|
||||||
|
|
||||||
@@ -137,8 +80,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
return;
|
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
|
try
|
||||||
{
|
{
|
||||||
OnDrawImpl(channel, ref state, context);
|
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()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!_enabled)
|
if (!_enabled)
|
||||||
@@ -180,14 +116,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
float scale = channel.TextureManager.RenderTargetScale;
|
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)
|
if (_prevScale == 1f && scale > 1f && _epochs.Count > 0)
|
||||||
{
|
{
|
||||||
FlushFrame();
|
FlushFrame();
|
||||||
@@ -208,10 +136,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
h = col0.Info.Height;
|
h = col0.Info.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool depth = state.DepthTestEnable; // Boolean32 -> implicit bool
|
bool depth = state.DepthTestEnable;
|
||||||
bool blend = state.BlendEnable[0];
|
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)
|
if (_epochs.Count > 0)
|
||||||
{
|
{
|
||||||
ref Epoch last = ref System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_epochs)[_epochs.Count - 1];
|
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.AnyDepth |= depth;
|
||||||
last.AnyBlend |= blend;
|
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 (!depth)
|
||||||
{
|
{
|
||||||
if (last.NoDepthDraws == 0)
|
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)
|
if (_hudless && _epochs.Count > 0)
|
||||||
{
|
{
|
||||||
Epoch fini = _epochs[^1];
|
Epoch fini = _epochs[^1];
|
||||||
@@ -256,19 +177,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
_hudlessInFrame++;
|
_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)
|
if (fini.Draws >= _hudlessBestDraws)
|
||||||
{
|
{
|
||||||
_hudlessBestDraws = fini.Draws;
|
_hudlessBestDraws = fini.Draws;
|
||||||
_hudlessBestAddr = fini.Addr;
|
_hudlessBestAddr = fini.Addr;
|
||||||
_hudlessBestRank = _epochs.Count;
|
_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)
|
if (_hudlessFeed && _epochTex != null && context != null)
|
||||||
{
|
{
|
||||||
CopyHudless(context);
|
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();
|
TryDumpEpoch();
|
||||||
_epochTex = col0;
|
_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)
|
private static void CopyHudless(GpuContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -322,8 +228,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
int h = src.Info.Height;
|
int h = src.Info.Height;
|
||||||
Format fmt = src.Info.FormatInfo.Format;
|
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)
|
if (_hudlessCopy == null || _hudlessCopyW != w || _hudlessCopyH != h || _hudlessCopyFmt != fmt)
|
||||||
{
|
{
|
||||||
_hudlessCopy?.Release();
|
_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()
|
private static void TryDumpEpoch()
|
||||||
{
|
{
|
||||||
if (_dumpAfterSeconds == 0 || _dumpDone || _epochTex == null)
|
if (_dumpAfterSeconds == 0 || _dumpDone || _epochTex == null)
|
||||||
@@ -420,9 +320,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
_frameNo++;
|
_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;
|
int sceneIdx = -1, compIdx = -1;
|
||||||
long sceneArea = -1, compArea = -1;
|
long sceneArea = -1, compArea = -1;
|
||||||
|
|
||||||
@@ -446,8 +343,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
ulong compAddr = compIdx >= 0 ? _epochs[compIdx].Addr : 0;
|
ulong compAddr = compIdx >= 0 ? _epochs[compIdx].Addr : 0;
|
||||||
|
|
||||||
bool blendIntoComposite = false; // HUD alpha-blends onto the final buffer itself
|
bool blendIntoComposite = false;
|
||||||
ulong separateUiAddr = 0; // a distinct RT that alpha-blends (would be capturable)
|
ulong separateUiAddr = 0;
|
||||||
int separateUiW = 0, separateUiH = 0;
|
int separateUiW = 0, separateUiH = 0;
|
||||||
|
|
||||||
for (int i = 0; i < _epochs.Count; i++)
|
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;
|
bool hdr = e.Fmt == Format.R11G11B10Float || e.Fmt == Format.R16G16B16A16Float;
|
||||||
if (hdr || !e.AnyBlend)
|
if (hdr || !e.AnyBlend)
|
||||||
{
|
{
|
||||||
continue; // only LDR blended (overlay-like) epochs matter for the UI question
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.Addr == compAddr)
|
if (e.Addr == compAddr)
|
||||||
@@ -476,7 +373,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
blendIntoComposite ? "INPLACE(NO-GO)" :
|
blendIntoComposite ? "INPLACE(NO-GO)" :
|
||||||
"UNCLEAR";
|
"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}";
|
string sig = $"{verdict}|comp0x{compAddr:X}|ui0x{separateUiAddr:X}";
|
||||||
long now = Environment.TickCount64;
|
long now = Environment.TickCount64;
|
||||||
if (sig != _lastVerdictSig || now - _lastVerdictMs >= VerdictThrottleMs)
|
if (sig != _lastVerdictSig || now - _lastVerdictMs >= VerdictThrottleMs)
|
||||||
@@ -499,13 +395,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
$"scene={sceneStr} composite={compStr} blendIntoComposite={blendIntoComposite} separateUiRT={uiStr}");
|
$"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;
|
bool periodic = now - _lastDetailMs >= 5000;
|
||||||
|
|
||||||
if (_framesLoggedFull < FullDetailFrames || periodic)
|
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)
|
if (_hudless)
|
||||||
{
|
{
|
||||||
_hudlessFrames++;
|
_hudlessFrames++;
|
||||||
@@ -538,7 +424,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_hudlessMultiple++;
|
_hudlessMultiple++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [v2] Election de fin d'image : UN candidat retenu, et un seul.
|
|
||||||
if (_hudlessBestDraws > 0)
|
if (_hudlessBestDraws > 0)
|
||||||
{
|
{
|
||||||
_hudlessRetenus++;
|
_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)
|
else if (_hudlessEchecsLogues < 8)
|
||||||
{
|
{
|
||||||
_hudlessEchecsLogues++;
|
_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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,24 +6,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
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
|
static class MvppViewportProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -38,37 +17,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private sealed class Stats
|
private sealed class Stats
|
||||||
{
|
{
|
||||||
public int Draws;
|
public int Draws;
|
||||||
public int Narrow; // viewport does not cover the target horizontally
|
public int Narrow;
|
||||||
public int Short; // ... vertically
|
public int Short;
|
||||||
public int TransformOff; // ViewportTransformEnable == 0 (screen-scissor path)
|
public int TransformOff;
|
||||||
public float MinX, MaxX, MinW, MaxW, MinY, MaxY, MinH, MaxH;
|
public float MinX, MaxX, MinW, MaxW, MinY, MaxY, MinH, MaxH;
|
||||||
public bool Seeded;
|
public bool Seeded;
|
||||||
|
|
||||||
// [v2, 21/07] The viewport came back covering the target on 91 398 draws, so the OTHER half of
|
public int ClipZero;
|
||||||
// the same mechanism is what matters now: the SCISSOR, and the SCREEN scissor that feeds the
|
public int ClipFull;
|
||||||
// texture-width heuristic. Neither has ever been measured on this game's scene passes -- the one
|
public int ClipCuts;
|
||||||
// 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 ClipMinX, ClipMaxX, ClipMinW, ClipMaxW;
|
public int ClipMinX, ClipMaxX, ClipMinW, ClipMaxW;
|
||||||
public bool ClipSeeded;
|
public bool ClipSeeded;
|
||||||
public int A2c; // alpha-to-coverage actif sur ce draw
|
public int A2c;
|
||||||
public int A2cDither; // ... ET dither actif => chemin du discard en damier
|
public int A2cDither;
|
||||||
public int NegScaleX; // ScaleX < 0 -> horizontal mirror the backend cannot express
|
public int NegScaleX;
|
||||||
public int SwizzleXOdd; // swizzle X is not PositiveX
|
public int SwizzleXOdd;
|
||||||
|
|
||||||
public void AddClip(int x, int w)
|
public void AddClip(int x, int w)
|
||||||
{
|
{
|
||||||
@@ -86,8 +49,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
if (w > ClipMaxW) { ClipMaxW = w; }
|
if (w > ClipMaxW) { ClipMaxW = w; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ScissorOn; // scissor enabled at all
|
public int ScissorOn;
|
||||||
public int ScissorCuts; // scissor leaves uncovered pixels left or right of the target
|
public int ScissorCuts;
|
||||||
public int ScissorSeen;
|
public int ScissorSeen;
|
||||||
public int SciMinX1, SciMaxX1, SciMinX2, SciMaxX2;
|
public int SciMinX1, SciMaxX1, SciMinX2, SciMaxX2;
|
||||||
public int ScrMinW, ScrMaxW, ScrMinH, ScrMaxH;
|
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();
|
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[] _addrU = new int[16];
|
||||||
private static readonly int[] _addrV = new int[16];
|
private static readonly int[] _addrV = new int[16];
|
||||||
private static int _addrBindings;
|
private static int _addrBindings;
|
||||||
@@ -177,7 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// A bench instrument must never take the emulator down with it.
|
|
||||||
_enabled = false;
|
_enabled = false;
|
||||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP VPPROBE: disabled after unexpected error: {e}");
|
Logger.Warning?.Print(LogClass.Gpu, $"MVPP VPPROBE: disabled after unexpected error: {e}");
|
||||||
}
|
}
|
||||||
@@ -187,7 +139,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
if (!_announced)
|
if (!_announced)
|
||||||
{
|
{
|
||||||
// Armed proof before any measurement: a switch you cannot SEE in the log is worth nothing.
|
|
||||||
_announced = true;
|
_announced = true;
|
||||||
Logger.Info?.Print(LogClass.Gpu,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
"MVPP VPPROBE: ON -- viewport vs render target, read-only, one line per shape per second.");
|
"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;
|
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];
|
ref ViewportTransform transform = ref state.ViewportTransform[0];
|
||||||
|
|
||||||
float scaleX = MathF.Abs(transform.ScaleX);
|
float scaleX = MathF.Abs(transform.ScaleX);
|
||||||
@@ -236,8 +185,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
st.TransformOff++;
|
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)
|
if (vpX > 1f || vpX + vpW < rtW - 1f)
|
||||||
{
|
{
|
||||||
st.Narrow++;
|
st.Narrow++;
|
||||||
@@ -248,9 +195,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
st.Short++;
|
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];
|
ScissorState sc = state.ScissorState[0];
|
||||||
bool scEnabled = sc.Enable;
|
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;
|
ScreenScissorState scr = state.ScreenScissorState;
|
||||||
st.AddScreen(scr.Width, scr.Height);
|
st.AddScreen(scr.Width, scr.Height);
|
||||||
|
|
||||||
// [v3] The clip rectangle the emulator decodes and throws away.
|
|
||||||
ViewportExtents ext = state.ViewportExtents[0];
|
ViewportExtents ext = state.ViewportExtents[0];
|
||||||
int clipX = ext.X;
|
int clipX = ext.X;
|
||||||
int clipW = ext.Width;
|
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)
|
if (transform.ScaleX < 0f)
|
||||||
{
|
{
|
||||||
st.NegScaleX++;
|
st.NegScaleX++;
|
||||||
@@ -305,12 +244,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
st.SwizzleXOdd++;
|
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)
|
if ((state.MultisampleControl & 1) != 0)
|
||||||
{
|
{
|
||||||
st.A2c++;
|
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)
|
if ((++_addrSampled & 15) == 0)
|
||||||
{
|
{
|
||||||
channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) =>
|
channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) =>
|
||||||
@@ -360,8 +291,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
continue;
|
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 leftMin = s.MinX;
|
||||||
float rightMin = entry.Key.W - (s.MinX + s.MinW);
|
float rightMin = entry.Key.W - (s.MinX + s.MinW);
|
||||||
|
|
||||||
@@ -396,8 +325,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (_addrBindings > 0)
|
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,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
$"MVPP ADDRPROBE: {_addrBindings} bindings echantillonnes | U: {DescribeModes(_addrU)} | V: {DescribeModes(_addrV)}");
|
$"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 scale = _channel.TextureManager.RenderTargetScale;
|
||||||
float divisor = scale * scale;
|
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)
|
if (GAL.MvppDev.Enabled && scale != System.MathF.Floor(scale) && (_fractProbeCount++ & 31) == 0)
|
||||||
{
|
{
|
||||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
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 ShaderStateIndex = 26;
|
||||||
public const int RtColorMaskIndex = 14;
|
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 StencilTestStateIndex = 4;
|
||||||
public const int DepthTestStateIndex = 5;
|
public const int DepthTestStateIndex = 5;
|
||||||
public const int DepthBiasStateIndex = 11;
|
public const int DepthBiasStateIndex = 11;
|
||||||
@@ -39,58 +37,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private readonly GpuContext _context;
|
private readonly GpuContext _context;
|
||||||
private readonly GpuChannel _channel;
|
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();
|
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 =
|
private static readonly bool _camPassProbe =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_PASSPROBE") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_PASSPROBE") == "1";
|
||||||
private static readonly System.Collections.Generic.HashSet<(int, int, bool)> _camPassProbeSeen = new();
|
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 =
|
private static readonly bool _worldResJitter =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES") == "1";
|
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 =
|
private static readonly bool _worldResLow =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES_LOW") == "1";
|
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 =
|
private static readonly bool _injectFullJit =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_INJECT_FULLJIT") == "1";
|
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 =
|
private static readonly bool _alphaTestProbe =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_ALPHATEST_PROBE") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_ALPHATEST_PROBE") == "1";
|
||||||
private static readonly System.Collections.Generic.HashSet<(bool, int, int)> _alphaTestProbeSeen = new();
|
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 =
|
private static readonly bool _jitterval =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTERVAL") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTERVAL") == "1";
|
||||||
private static long _jittervalLastFrame = -1;
|
private static long _jittervalLastFrame = -1;
|
||||||
@@ -98,32 +63,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private readonly DrawState _drawState;
|
private readonly DrawState _drawState;
|
||||||
private readonly AdvancedBlendManager _blendManager;
|
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 int _jvColorW, _jvColorH, _jvDepthW, _jvDepthH;
|
||||||
|
|
||||||
private readonly StateUpdateTracker<ThreedClassState> _updateTracker;
|
private readonly StateUpdateTracker<ThreedClassState> _updateTracker;
|
||||||
|
|
||||||
private readonly ShaderProgramInfo[] _currentProgramInfo;
|
private readonly ShaderProgramInfo[] _currentProgramInfo;
|
||||||
|
|
||||||
// [SCENEPROBE] guest shader VAs of the current draw (read-only, for the scene-pass probe).
|
|
||||||
private ulong _probeSceneFsAddr;
|
private ulong _probeSceneFsAddr;
|
||||||
private ulong _probeSceneVsAddr;
|
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 =
|
private static readonly bool _cutoutDrawProbe =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CUTOUT_PROBE") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CUTOUT_PROBE") == "1";
|
||||||
private static int _cpTotalDraws, _cpCutoutDraws;
|
private static int _cpTotalDraws, _cpCutoutDraws;
|
||||||
private static long _cpSummaryMs;
|
private static long _cpSummaryMs;
|
||||||
private static readonly System.Collections.Generic.Dictionary<(int, int), int> _cpCutoutViewports = new();
|
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 =
|
private static readonly bool _surviveProbe =
|
||||||
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled;
|
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled;
|
||||||
private static int _surviveProbeTarget;
|
private static int _surviveProbeTarget;
|
||||||
@@ -132,16 +86,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private ShaderSpecializationState _shaderSpecState;
|
private ShaderSpecializationState _shaderSpecState;
|
||||||
private readonly SpecializationStateUpdater _currentSpecState;
|
private readonly SpecializationStateUpdater _currentSpecState;
|
||||||
|
|
||||||
// MV++ double-transform (b1) sub-step 2: generate+publish the MvppVariant program.
|
|
||||||
private static readonly bool _mvppVariantEnabled =
|
private static readonly bool _mvppVariantEnabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_VARIANT") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_VARIANT") == "1";
|
||||||
private static bool _variantRestoreLogged;
|
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 =
|
private static readonly bool _mvppN1Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_N1") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_N1") == "1";
|
||||||
private static bool _n1ResolveLogged;
|
private static bool _n1ResolveLogged;
|
||||||
@@ -429,30 +377,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_prevTfEnable = true;
|
_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)
|
if (_cutoutDrawProbe)
|
||||||
{
|
{
|
||||||
CutoutDrawProbeTick();
|
CutoutDrawProbeTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// [CONSTDIFF] Read-only per-draw: trace the builder's prev-frame matrix (journal 221).
|
|
||||||
if (Image.MvppConstDiffProbe.Enabled)
|
if (Image.MvppConstDiffProbe.Enabled)
|
||||||
{
|
{
|
||||||
Image.MvppConstDiffProbe.OnDraw(_channel, _probeSceneFsAddr);
|
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);
|
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);
|
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)
|
if (Image.MvppScenePassProbe.Enabled)
|
||||||
{
|
{
|
||||||
Image.MvppScenePassProbe.OnDraw(
|
Image.MvppScenePassProbe.OnDraw(
|
||||||
@@ -462,8 +400,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
Image.MvppFeedbackProbe.Frame);
|
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)
|
if (Image.MvppMap64Probe.Enabled)
|
||||||
{
|
{
|
||||||
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
|
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
|
||||||
@@ -478,8 +414,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
(int)(MathF.Abs(vp0.ScaleY) * 2f * vpScale));
|
(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)
|
if (Image.MvppMvBufProbe.Enabled)
|
||||||
{
|
{
|
||||||
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
|
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
|
||||||
@@ -511,10 +445,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
probeMask);
|
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)
|
if (Image.MvppWriterCensusProbe.Enabled)
|
||||||
{
|
{
|
||||||
Span<uint> censusMask = stackalloc uint[Constants.TotalRenderTargets];
|
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);
|
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)
|
if (Image.MvppTwinMapProbe.Enabled)
|
||||||
{
|
{
|
||||||
Span<uint> twinMask = stackalloc uint[Constants.TotalRenderTargets];
|
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()
|
private void CutoutDrawProbeTick()
|
||||||
{
|
{
|
||||||
_cpTotalDraws++;
|
_cpTotalDraws++;
|
||||||
@@ -653,31 +579,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_channel.BufferManager.CommitGraphicsBindings(_drawState.DrawIndexed);
|
_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)
|
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;
|
Image.Texture dsCur = _channel.TextureManager.RenderTargetDepthStencil;
|
||||||
ulong sceneDepthAddr = MvppCameraCapture.SceneDepthAddress;
|
ulong sceneDepthAddr = MvppCameraCapture.SceneDepthAddress;
|
||||||
|
|
||||||
int wantTarget = (dsCur != null && sceneDepthAddr != 0 &&
|
int wantTarget = (dsCur != null && sceneDepthAddr != 0 &&
|
||||||
dsCur.Range.GetSubRange(0).Address == sceneDepthAddr) ? 1 : 2;
|
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 &&
|
if (wantTarget == 1 &&
|
||||||
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.MipMarkEnabled)
|
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.MipMarkEnabled)
|
||||||
{
|
{
|
||||||
@@ -714,10 +623,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
Logger.Info?.Print(LogClass.Gpu,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
"MVPP-SURVIVE: scene-window bind-swap ACTIVE (mask<->dummy at binding 80, first scene match).");
|
"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(
|
_context.Renderer.Pipeline.SetStorageBuffers(
|
||||||
[new BufferAssignment(
|
[new BufferAssignment(
|
||||||
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BackendMirrorBinding,
|
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);
|
MvppSurviveProbeBuffer.MaybeDump(_context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -792,7 +695,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
private void UpdateTfBufferState()
|
private void UpdateTfBufferState()
|
||||||
{
|
{
|
||||||
Span<TfBufferState> tfBufferStateSpan = _state.State.TfBufferState.AsSpan();
|
Span<TfBufferState> tfBufferStateSpan = _state.State.TfBufferState.AsSpan();
|
||||||
|
|
||||||
for (int index = 0; index < Constants.TotalTransformFeedbackBuffers; index++)
|
for (int index = 0; index < Constants.TotalTransformFeedbackBuffers; index++)
|
||||||
{
|
{
|
||||||
TfBufferState tfb = tfBufferStateSpan[index];
|
TfBufferState tfb = tfBufferStateSpan[index];
|
||||||
@@ -965,8 +868,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
oldScaleX != _channel.TextureManager.RenderTargetScaleX ||
|
oldScaleX != _channel.TextureManager.RenderTargetScaleX ||
|
||||||
oldScaleY != _channel.TextureManager.RenderTargetScaleY)
|
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);
|
_context.SupportBufferUpdater.SetRenderTargetScale(_channel.TextureManager.RenderTargetScaleX);
|
||||||
|
|
||||||
UpdateViewportTransform();
|
UpdateViewportTransform();
|
||||||
@@ -1015,7 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
Span<Rectangle<int>> regions = stackalloc Rectangle<int>[Constants.TotalViewports];
|
Span<Rectangle<int>> regions = stackalloc Rectangle<int>[Constants.TotalViewports];
|
||||||
Span<ScissorState> scissorStateSpan = _state.State.ScissorState.AsSpan();
|
Span<ScissorState> scissorStateSpan = _state.State.ScissorState.AsSpan();
|
||||||
|
|
||||||
for (int index = 0; index < Constants.TotalViewports; index++)
|
for (int index = 0; index < Constants.TotalViewports; index++)
|
||||||
{
|
{
|
||||||
ScissorState scissor = scissorStateSpan[index];
|
ScissorState scissor = scissorStateSpan[index];
|
||||||
@@ -1055,7 +956,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
width = (int)MathF.Ceiling(width * scaleX);
|
width = (int)MathF.Ceiling(width * scaleX);
|
||||||
height = (int)MathF.Ceiling(height * scaleY);
|
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",
|
"SCISSOR",
|
||||||
null, null,
|
null, null,
|
||||||
gx, gy, gx + gw, gy + gh,
|
gx, gy, gx + gw, gy + gh,
|
||||||
@@ -1148,18 +1049,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
Span<ViewportTransform> viewportTransformSpan = _state.State.ViewportTransform.AsSpan();
|
Span<ViewportTransform> viewportTransformSpan = _state.State.ViewportTransform.AsSpan();
|
||||||
Span<ViewportExtents> viewportExtentsSpan = _state.State.ViewportExtents.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;
|
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)
|
if (DlssJitterState.Enabled && DlssJitterState.Inject && DlssJitterState.CamPassOnly)
|
||||||
{
|
{
|
||||||
ref ViewportTransform vp0 = ref viewportTransformSpan[0];
|
ref ViewportTransform vp0 = ref viewportTransformSpan[0];
|
||||||
@@ -1172,8 +1063,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
DlssJitterState.InputWidth > 0 &&
|
DlssJitterState.InputWidth > 0 &&
|
||||||
vpW >= DlssJitterState.InputWidth / 2f && vpW <= DlssJitterState.InputWidth + 2f;
|
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)
|
if (cameraPass)
|
||||||
{
|
{
|
||||||
DlssJitterState.CamVpW = vpW;
|
DlssJitterState.CamVpW = vpW;
|
||||||
@@ -1182,17 +1071,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
bool depthOn = _state.State.RtDepthStencilEnable;
|
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 &&
|
bool worldResPass = _worldResJitter && !cameraPass &&
|
||||||
aspect > 1.5f && aspect < 1.95f &&
|
aspect > 1.5f && aspect < 1.95f &&
|
||||||
DlssJitterState.InputWidth > 0 &&
|
DlssJitterState.InputWidth > 0 &&
|
||||||
vpW >= DlssJitterState.InputWidth / 2f &&
|
vpW >= DlssJitterState.InputWidth / 2f &&
|
||||||
vpW <= DlssJitterState.InputWidth * 0.95f;
|
vpW <= DlssJitterState.InputWidth * 0.95f;
|
||||||
|
|
||||||
// [WORLDRES-LOW] The half-render-res world passes, below the July window.
|
|
||||||
bool worldResLowPass = _worldResLow && !cameraPass &&
|
bool worldResLowPass = _worldResLow && !cameraPass &&
|
||||||
aspect > 1.5f && aspect < 1.95f &&
|
aspect > 1.5f && aspect < 1.95f &&
|
||||||
DlssJitterState.InputWidth > 0 &&
|
DlssJitterState.InputWidth > 0 &&
|
||||||
@@ -1202,17 +1086,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
if (cameraPass || worldResPass)
|
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 denomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth;
|
||||||
float denomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight;
|
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)
|
if (worldResLowPass)
|
||||||
{
|
{
|
||||||
denomW = DlssJitterState.InputWidth;
|
denomW = DlssJitterState.InputWidth;
|
||||||
@@ -1223,8 +1099,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
jitterNdcY = DlssJitterState.OffsetY * 2f / denomH;
|
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)
|
if (_jitterval && cameraPass && DlssJitterState.FrameId != _jittervalLastFrame)
|
||||||
{
|
{
|
||||||
_jittervalLastFrame = DlssJitterState.FrameId;
|
_jittervalLastFrame = DlssJitterState.FrameId;
|
||||||
@@ -1235,10 +1109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
float appliedPxX = inW > 0 ? offX * vpW / inW : 0f;
|
float appliedPxX = inW > 0 ? offX * vpW / inW : 0f;
|
||||||
float appliedPxY = inH > 0 ? offY * vpH / inH : 0f;
|
float appliedPxY = inH > 0 ? offY * vpH / inH : 0f;
|
||||||
var sc = _state.State.ScreenScissorState;
|
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 jvDenomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth;
|
||||||
float jvDenomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight;
|
float jvDenomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight;
|
||||||
float truePxX = jvDenomW > 0f ? offX * vpW / jvDenomW : 0f;
|
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}");
|
$"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)
|
if (_camPassProbe)
|
||||||
{
|
{
|
||||||
var probeKey = ((int)vpW, (int)vpH, depthOn);
|
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 vpWidth = MathF.Abs(vp0.ScaleX) * 2f * jScale;
|
||||||
float vpHeight = MathF.Abs(vp0.ScaleY) * 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;
|
float passAspect = vpHeight > 0f ? vpWidth / vpHeight : 0f;
|
||||||
bool squarePass = passAspect > 0.95f && passAspect < 1.05f;
|
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)
|
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];
|
ref ViewportTransform vp0 = ref viewportTransformSpan[0];
|
||||||
float vpWidth = MathF.Abs(vp0.ScaleX) * 2f;
|
float vpWidth = MathF.Abs(vp0.ScaleX) * 2f;
|
||||||
float vpHeight = MathF.Abs(vp0.ScaleY) * 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);
|
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);
|
int vpKey = ((int)vpWidth << 16) | ((int)vpHeight & 0xFFFF);
|
||||||
if (_jitterUiProbeSeen.Add(vpKey))
|
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,
|
// Viewport size is only used on the shader when YNegate is enabled,
|
||||||
// and if the fragment shader accesses gl_FragCoord,
|
// and if the fragment shader accesses gl_FragCoord,
|
||||||
// so there's no need to update it in other cases.
|
// 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)
|
if ((yNegate && _fsReadsFragCoord) || _surviveProbe)
|
||||||
{
|
{
|
||||||
UpdateSupportBufferViewportSize();
|
UpdateSupportBufferViewportSize();
|
||||||
@@ -1418,9 +1269,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
_context.Renderer.Pipeline.SetDepthMode(mode);
|
_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)
|
if (DlssCameraState.Enabled)
|
||||||
{
|
{
|
||||||
DlssCameraState.GuestDepthMinusOneToOne = mode == DepthMode.MinusOneToOne;
|
DlssCameraState.GuestDepthMinusOneToOne = mode == DepthMode.MinusOneToOne;
|
||||||
@@ -1568,7 +1416,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
Span<VertexAttribDescriptor> vertexAttribs = stackalloc VertexAttribDescriptor[Constants.TotalVertexAttribs];
|
Span<VertexAttribDescriptor> vertexAttribs = stackalloc VertexAttribDescriptor[Constants.TotalVertexAttribs];
|
||||||
Span<VertexAttribState> vertexAttribStateSpan = _state.State.VertexAttribState.AsSpan();
|
Span<VertexAttribState> vertexAttribStateSpan = _state.State.VertexAttribState.AsSpan();
|
||||||
|
|
||||||
for (int index = 0; index < Constants.TotalVertexAttribs; index++)
|
for (int index = 0; index < Constants.TotalVertexAttribs; index++)
|
||||||
{
|
{
|
||||||
VertexAttribState vertexAttrib = vertexAttribStateSpan[index];
|
VertexAttribState vertexAttrib = vertexAttribStateSpan[index];
|
||||||
@@ -1713,7 +1561,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
int drawFirstVertex = _drawState.DrawFirstVertex;
|
int drawFirstVertex = _drawState.DrawFirstVertex;
|
||||||
int drawVertexCount = _drawState.DrawVertexCount;
|
int drawVertexCount = _drawState.DrawVertexCount;
|
||||||
uint vbEnableMask = 0;
|
uint vbEnableMask = 0;
|
||||||
|
|
||||||
Span<VertexBufferState> vertexBufferStateSpan = _state.State.VertexBufferState.AsSpan();
|
Span<VertexBufferState> vertexBufferStateSpan = _state.State.VertexBufferState.AsSpan();
|
||||||
Span<BufferPipelineDescriptor> vertexBuffersSpan = _pipeline.VertexBuffers.AsSpan();
|
Span<BufferPipelineDescriptor> vertexBuffersSpan = _pipeline.VertexBuffers.AsSpan();
|
||||||
Span<GpuVa> vertexBufferEndAddressSpan = _state.State.VertexBufferEndAddress.AsSpan();
|
Span<GpuVa> vertexBufferEndAddressSpan = _state.State.VertexBufferEndAddress.AsSpan();
|
||||||
@@ -1892,7 +1740,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
Span<Boolean32> blendEnableSpan = _state.State.BlendEnable.AsSpan();
|
Span<Boolean32> blendEnableSpan = _state.State.BlendEnable.AsSpan();
|
||||||
Span<BlendState> blendStateSpan = _state.State.BlendState.AsSpan();
|
Span<BlendState> blendStateSpan = _state.State.BlendState.AsSpan();
|
||||||
Span<BlendDescriptor> blendDescriptorsSpan = _pipeline.BlendDescriptors.AsSpan();
|
Span<BlendDescriptor> blendDescriptorsSpan = _pipeline.BlendDescriptors.AsSpan();
|
||||||
|
|
||||||
for (int index = 0; index < Constants.TotalRenderTargets; index++)
|
for (int index = 0; index < Constants.TotalRenderTargets; index++)
|
||||||
{
|
{
|
||||||
bool enable = blendEnableSpan[index];
|
bool enable = blendEnableSpan[index];
|
||||||
@@ -1944,7 +1792,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
{
|
{
|
||||||
dualSourceBlendEnabled = true;
|
dualSourceBlendEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<BlendDescriptor> blendDescriptorsSpan = _pipeline.BlendDescriptors.AsSpan();
|
Span<BlendDescriptor> blendDescriptorsSpan = _pipeline.BlendDescriptors.AsSpan();
|
||||||
|
|
||||||
for (int index = 0; index < Constants.TotalRenderTargets; index++)
|
for (int index = 0; index < Constants.TotalRenderTargets; index++)
|
||||||
@@ -2050,9 +1898,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
addressesSpan[index] = baseAddress + shader.Offset;
|
addressesSpan[index] = baseAddress + shader.Offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [SCENEPROBE] stable per-pass identity for the current draw (guest shader VAs).
|
_probeSceneFsAddr = addressesSpan[5];
|
||||||
_probeSceneFsAddr = addressesSpan[5]; // fragment
|
_probeSceneVsAddr = addressesSpan[1];
|
||||||
_probeSceneVsAddr = addressesSpan[1]; // vertex (main)
|
|
||||||
|
|
||||||
int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex
|
int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex
|
||||||
? _state.State.TexturePoolState.MaximumId
|
? _state.State.TexturePoolState.MaximumId
|
||||||
@@ -2067,10 +1914,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
ref _currentSpecState.GetGraphicsState(),
|
ref _currentSpecState.GetGraphicsState(),
|
||||||
addresses);
|
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)
|
if (_mvppVariantEnabled)
|
||||||
{
|
{
|
||||||
CachedShaderProgram gsVariant = shaderCache.GetGraphicsShader(
|
CachedShaderProgram gsVariant = shaderCache.GetGraphicsShader(
|
||||||
@@ -2085,17 +1928,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
MvppVelState.VariantProgram = gsVariant.HostProgram;
|
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)
|
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.VariantN1Binding = MvppN1ReservedBinding;
|
||||||
MvppVelState.VariantN1Set = 0;
|
MvppVelState.VariantN1Set = 0;
|
||||||
|
|
||||||
@@ -2154,7 +1988,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
_currentProgramInfo[stageIndex] = info;
|
_currentProgramInfo[stageIndex] = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (gs.Shaders[5]?.Info.UsesFragCoord == true)
|
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.
|
// 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);
|
_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)
|
if (MvppVelState.VariantBound)
|
||||||
{
|
{
|
||||||
MvppVelState.VariantBound = false;
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,26 +5,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
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
|
static class MvppTwodProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
@@ -36,15 +13,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
|||||||
private static bool _announced;
|
private static bool _announced;
|
||||||
private static long _nextLogMs;
|
private static long _nextLogMs;
|
||||||
private static int _blits;
|
private static int _blits;
|
||||||
private static int _negatives; // une coordonnée source ou destination est négative
|
private static int _negatives;
|
||||||
private static int _resized; // la région source n'a pas la même taille que la destination
|
private static int _resized;
|
||||||
private static readonly Dictionary<string, int> _shapes = new();
|
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()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!_enabled)
|
if (!_enabled)
|
||||||
@@ -114,12 +86,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
|||||||
_resized++;
|
_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}";
|
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;
|
_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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -295,7 +295,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
|||||||
|
|
||||||
FormatInfo dstCopyTextureFormat = dstCopyTexture.Format.Convert();
|
FormatInfo dstCopyTextureFormat = dstCopyTexture.Format.Convert();
|
||||||
|
|
||||||
// Dernier étage jamais instrumenté du dossier XC2 : le moteur 2D. Lecture seule, gated.
|
|
||||||
MvppTwodProbe.OnBlit(
|
MvppTwodProbe.OnBlit(
|
||||||
srcX1, srcY1, srcX2, srcY2,
|
srcX1, srcY1, srcX2, srcY2,
|
||||||
dstX1, dstY1, dstX2, dstY2,
|
dstX1, dstY1, dstX2, dstY2,
|
||||||
@@ -364,15 +363,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
|||||||
return;
|
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)
|
if (srcTexture.Info.Samples > 1 || dstTexture.Info.Samples > 1)
|
||||||
{
|
{
|
||||||
srcTexture.PropagateScale(dstTexture);
|
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 scaleX = srcTexture.EffectiveScaleX;
|
||||||
float scaleY = srcTexture.EffectiveScaleY;
|
float scaleY = srcTexture.EffectiveScaleY;
|
||||||
float dstScaleX = dstTexture.EffectiveScaleX;
|
float dstScaleX = dstTexture.EffectiveScaleX;
|
||||||
@@ -392,7 +389,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
|||||||
|
|
||||||
bool linearFilter = _state.State.SetPixelsFromMemorySampleModeFilter == SetPixelsFromMemorySampleModeFilter.Bilinear;
|
bool linearFilter = _state.State.SetPixelsFromMemorySampleModeFilter == SetPixelsFromMemorySampleModeFilter.Bilinear;
|
||||||
|
|
||||||
Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated)
|
Image.MvppSeamProbe.OnScaledCopy(
|
||||||
"2D-BLIT",
|
"2D-BLIT",
|
||||||
srcTexture, dstTexture,
|
srcTexture, dstTexture,
|
||||||
srcX1 / srcTexture.Info.SamplesInX, srcY1 / srcTexture.Info.SamplesInY,
|
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,
|
dstRegion.X1, dstRegion.Y1, dstRegion.X2, dstRegion.Y2,
|
||||||
linearFilter ? "linear" : "point");
|
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);
|
srcTexture.HostTexture.CopyTo(dstTexture.HostTexture, srcRegion, dstRegion, linearFilter);
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
public Window Window { get; }
|
public Window Window { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal Image.Texture LastPresentDepthStencil { get; set; }
|
internal Image.Texture LastPresentDepthStencil { get; set; }
|
||||||
|
|
||||||
@@ -105,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal DirtyHacks DirtyHacks { get; }
|
internal DirtyHacks DirtyHacks { get; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Host hardware capabilities.
|
/// Host hardware capabilities.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -435,7 +431,7 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
SyncActions.RemoveAt(i--);
|
SyncActions.RemoveAt(i--);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < SyncpointActions.Count; i++)
|
for (int i = 0; i < SyncpointActions.Count; i++)
|
||||||
{
|
{
|
||||||
if (SyncpointActions[i].SyncAction(syncPoint))
|
if (SyncpointActions[i].SyncAction(syncPoint))
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// If the backend GPU has 0 memory capacity, the cache size defaults to `DefaultTextureSizeCapacity`.
|
/// If the backend GPU has 0 memory capacity, the cache size defaults to `DefaultTextureSizeCapacity`.
|
||||||
///
|
///
|
||||||
/// Reads the current Device total CPU Memory, to determine the maximum amount of VRAM available. Capped to 50% of Current GPU Memory.
|
/// Reads the current Device total CPU Memory, to determine the maximum amount of VRAM available. Capped to 50% of Current GPU Memory.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="context">The GPU context that the cache belongs to</param>
|
/// <param name="context">The GPU context that the cache belongs to</param>
|
||||||
@@ -119,11 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
_totalSize += texture.Size;
|
_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)
|
if (CacheProbeEnabled)
|
||||||
{
|
{
|
||||||
long tickMs = System.Environment.TickCount64;
|
long tickMs = System.Environment.TickCount64;
|
||||||
@@ -184,15 +179,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes the least used texture from the cache.
|
/// Removes the least used texture from the cache.
|
||||||
/// </summary>
|
/// </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 =
|
internal static readonly bool CacheProbeEnabled =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_CACHE_PROBE") == "1";
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -10,34 +7,11 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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_<handle>),
|
|
||||||
/// 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
|
static class MvppBuilderInProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_BUILDERIN") == "1";
|
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 =
|
private static readonly (ulong Addr, string Name)[] Passes =
|
||||||
{
|
{
|
||||||
(0x1000AD730UL, "builder"),
|
(0x1000AD730UL, "builder"),
|
||||||
@@ -51,7 +25,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static readonly HashSet<string> _census = new();
|
private static readonly HashSet<string> _census = new();
|
||||||
private static int _censusLines;
|
private static int _censusLines;
|
||||||
|
|
||||||
/// <summary>Cheap identity of the draw's first colour target, for throttling.</summary>
|
|
||||||
private static int TargetKey(TextureManager texMgr)
|
private static int TargetKey(TextureManager texMgr)
|
||||||
{
|
{
|
||||||
if (texMgr == null)
|
if (texMgr == null)
|
||||||
@@ -110,10 +83,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
private static string _shape = "?";
|
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; }
|
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)
|
private static bool MatchesUnknownTarget(TextureManager texMgr, out string shape)
|
||||||
{
|
{
|
||||||
shape = null;
|
shape = null;
|
||||||
@@ -165,12 +136,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order)
|
private const int FragmentStageIndex = 4;
|
||||||
private const int CbufSlot = 3; // fp_c3
|
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 static readonly int[] WantedC3 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13 };
|
||||||
|
|
||||||
private struct Bound
|
private struct Bound
|
||||||
@@ -190,15 +158,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static bool _armed;
|
private static bool _armed;
|
||||||
private static bool _failLogged;
|
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 const int MaxReports = 40;
|
||||||
private static int _reports;
|
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<ulong, long> _lastBeat = new();
|
||||||
private static readonly Dictionary<string, string> _lastSig = new();
|
private static readonly Dictionary<string, string> _lastSig = new();
|
||||||
private static readonly Dictionary<string, int> _perConfig = new();
|
private static readonly Dictionary<string, int> _perConfig = new();
|
||||||
@@ -239,10 +200,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
H = info.Height,
|
H = info.Height,
|
||||||
Handle = handle,
|
Handle = handle,
|
||||||
Binding = binding,
|
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,
|
Levels = texture.Info.Levels,
|
||||||
Tex = texture,
|
Tex = texture,
|
||||||
Filter = sampler == null
|
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)
|
public static void OnDraw(GpuChannel channel, TextureManager texMgr, ulong fsAddr)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
if (!Enabled)
|
||||||
@@ -269,20 +221,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
try
|
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);
|
CensusTick(texMgr, fsAddr);
|
||||||
|
|
||||||
string pass = PassName(fsAddr);
|
string pass = PassName(fsAddr);
|
||||||
|
|
||||||
if (pass == null)
|
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))
|
if (!MatchesUnknownTarget(texMgr, out string shape))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -299,9 +243,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
_builderDraws++;
|
_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)
|
if (fsAddr == Passes[0].Addr)
|
||||||
{
|
{
|
||||||
foreach (Bound bb in _inputs)
|
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)");
|
"[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;
|
long now = Environment.TickCount64;
|
||||||
ulong beatKey = fsAddr ^ ((ulong)TargetKey(texMgr) << 40);
|
ulong beatKey = fsAddr ^ ((ulong)TargetKey(texMgr) << 40);
|
||||||
_lastBeat.TryGetValue(beatKey, out long beat);
|
_lastBeat.TryGetValue(beatKey, out long beat);
|
||||||
@@ -340,10 +275,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_texMgr = texMgr;
|
_texMgr = texMgr;
|
||||||
string signature = BuildSignature(channel, out string[] lines);
|
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;
|
string config = pass + "|" + _shape;
|
||||||
|
|
||||||
_lastSig.TryGetValue(config, out string prev);
|
_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)
|
private static string BuildSignature(GpuChannel channel, out string[] lines)
|
||||||
{
|
{
|
||||||
List<string> outLines = new();
|
List<string> outLines = new();
|
||||||
@@ -425,11 +352,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return sig.ToString();
|
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)
|
private static void AppendTargets(TextureManager texMgr, List<string> lines, StringBuilder sig, StringBuilder shape)
|
||||||
{
|
{
|
||||||
if (texMgr == null)
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,42 +6,20 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppCacheProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_CACHEOPS") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_CACHEOPS") == "1";
|
||||||
|
|
||||||
private static long _nextLogMs;
|
private static long _nextLogMs;
|
||||||
private static int _copyDeps; // copies de dépendance créées entre textures qui se chevauchent
|
private static int _copyDeps;
|
||||||
private static int _syncs; // APPELS à SynchronizeMemory (la plupart ressortent sans rien faire)
|
private static int _syncs;
|
||||||
private static int _mismatch; // ... dont les deux textures n'ont PAS la même largeur
|
private static int _mismatch;
|
||||||
private static int _realSyncs; // appels qui rechargent VRAIMENT des données (texture sale)
|
private static int _realSyncs;
|
||||||
private static int _realNoData; // ... dont la texture n'avait pas encore de données (SynchronizeFull)
|
private static int _realNoData;
|
||||||
private static readonly Dictionary<string, int> _realShapes = new(); // formes qui rechargent, par fréquence
|
private static readonly Dictionary<string, int> _realShapes = new();
|
||||||
private static readonly Dictionary<string, int> _pairs = 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)
|
public static void OnRealSync(int w, int h, Format fmt, bool hadData)
|
||||||
{
|
{
|
||||||
if (!_enabled)
|
if (!_enabled)
|
||||||
@@ -63,7 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_realShapes[key] = _realShapes.TryGetValue(key, out int n) ? n + 1 : 1;
|
_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)
|
public static void OnCopyDependency(Texture a, Texture b)
|
||||||
{
|
{
|
||||||
if (!_enabled || a == null || b == null)
|
if (!_enabled || a == null || b == null)
|
||||||
@@ -83,9 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_mismatch++;
|
_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" : "")}";
|
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;
|
_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()
|
public static void OnSynchronize()
|
||||||
{
|
{
|
||||||
if (_enabled)
|
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()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!_enabled)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppConstDiffProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_CONSTDIFF") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_CONSTDIFF") == "1";
|
||||||
|
|
||||||
private const ulong BuilderFsAddress = 0x1000AD730UL;
|
private const ulong BuilderFsAddress = 0x1000AD730UL;
|
||||||
private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order)
|
private const int FragmentStageIndex = 4;
|
||||||
private const int CbufSlot = 3; // fp_c3
|
private const int CbufSlot = 3;
|
||||||
|
|
||||||
private static bool _armedLogged;
|
private static bool _armedLogged;
|
||||||
private static bool _failLogged;
|
private static bool _failLogged;
|
||||||
@@ -86,8 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGraphicsUniformBufferAddress renvoie une adresse DEJA physique
|
|
||||||
// (Range traduite) : lecture directe, PAS de Translate.
|
|
||||||
ulong addr = channel.BufferManager.GetGraphicsUniformBufferAddress(FragmentStageIndex, slot);
|
ulong addr = channel.BufferManager.GetGraphicsUniformBufferAddress(FragmentStageIndex, slot);
|
||||||
|
|
||||||
if (addr == 0 || addr == ulong.MaxValue)
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,26 +6,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppContentProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -92,9 +69,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
double sumAbs = 0;
|
double sumAbs = 0;
|
||||||
ulong hash = 14695981039346656037UL;
|
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++)
|
for (int i = 0; i < words; i++)
|
||||||
{
|
{
|
||||||
uint w = (uint)(bytes[i * 4] |
|
uint w = (uint)(bytes[i * 4] |
|
||||||
|
|||||||
@@ -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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -9,36 +6,19 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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 -> 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
|
static class MvppDofProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_DOF_PROBE") == "1";
|
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 DofWidth = 512;
|
||||||
private const int DofHeight = 288;
|
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 const int SmallRtMax = 640;
|
||||||
|
|
||||||
private static readonly HashSet<string> _smallRtSizes = new();
|
private static readonly HashSet<string> _smallRtSizes = new();
|
||||||
private static readonly Dictionary<string, long> _dofInputs = 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();
|
public static readonly HashSet<ulong> DofInputVas = new();
|
||||||
private static long _lastSummaryMs;
|
private static long _lastSummaryMs;
|
||||||
private static long _lastHeartbeatMs;
|
private static long _lastHeartbeatMs;
|
||||||
@@ -46,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static bool _armed;
|
private static bool _armed;
|
||||||
private static int _announced;
|
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()
|
public static void AnnounceOnce()
|
||||||
{
|
{
|
||||||
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
|
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."));
|
(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)
|
public static void OnInput(Texture texture, Texture renderTarget, ShaderStage stage)
|
||||||
{
|
{
|
||||||
if (texture == null || renderTarget == null)
|
if (texture == null || renderTarget == null)
|
||||||
@@ -85,11 +52,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
_totalBinds++;
|
_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)
|
if (!_armed)
|
||||||
{
|
{
|
||||||
_armed = true;
|
_armed = true;
|
||||||
@@ -105,8 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
$"DOFPROBE alive: binds={_totalBinds} smallRTsizes={_smallRtSizes.Count} dofInputs={_dofInputs.Count}");
|
$"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)
|
if (rt.Width <= SmallRtMax)
|
||||||
{
|
{
|
||||||
string rtSize = $"{rt.Width}x{rt.Height} {rt.FormatInfo.Format}";
|
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)
|
if (rt.Width != DofWidth || rt.Height != DofHeight)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -134,7 +93,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
va = 0;
|
va = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
DofInputVas.Add(va); // cross-reference key for the feedback probe
|
DofInputVas.Add(va);
|
||||||
|
|
||||||
string key =
|
string key =
|
||||||
$"stage={stage} @0x{va:X}/{info.FormatInfo.Format}/{info.Width}x{info.Height}/lv{info.Levels}/{info.Target}";
|
$"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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,23 +5,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppFeedbackProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -34,8 +14,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
public long LastWriteFrame = long.MinValue;
|
public long LastWriteFrame = long.MinValue;
|
||||||
public long FirstAccessFrame = long.MinValue;
|
public long FirstAccessFrame = long.MinValue;
|
||||||
public long CaseA; // write(N) -> read-first(N+1): temporal history
|
public long CaseA;
|
||||||
public long CaseB; // write(N) -> write-first(N+1): transient reused
|
public long CaseB;
|
||||||
public string Format;
|
public string Format;
|
||||||
public int Width;
|
public int Width;
|
||||||
public int Height;
|
public int Height;
|
||||||
@@ -47,7 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static long _lastReportMs;
|
private static long _lastReportMs;
|
||||||
private static int _announced;
|
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()
|
public static void AnnounceOnce()
|
||||||
{
|
{
|
||||||
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
|
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."));
|
(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);
|
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)
|
public static (long caseA, long caseB) Classify(ulong va)
|
||||||
{
|
{
|
||||||
lock (_rts)
|
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)
|
public static void OnWrite(Texture texture)
|
||||||
{
|
{
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
@@ -107,8 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
rec.FirstAccessFrame = _frame;
|
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)
|
if (rec.LastWriteFrame == _frame - 1)
|
||||||
{
|
{
|
||||||
rec.CaseB++;
|
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)
|
public static void OnRead(Texture texture)
|
||||||
{
|
{
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
@@ -139,15 +112,13 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (!_rts.TryGetValue(va, out Rec rec))
|
if (!_rts.TryGetValue(va, out Rec rec))
|
||||||
{
|
{
|
||||||
return; // never a render target -> cannot be a history buffer
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rec.FirstAccessFrame != _frame)
|
if (rec.FirstAccessFrame != _frame)
|
||||||
{
|
{
|
||||||
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)
|
if (rec.LastWriteFrame == _frame - 1)
|
||||||
{
|
{
|
||||||
rec.CaseA++;
|
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()
|
public static void OnFrameBoundary()
|
||||||
{
|
{
|
||||||
lock (_rts)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,28 +5,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppFullSyncProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -40,10 +15,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static bool _armedLogged;
|
private static bool _armedLogged;
|
||||||
private static int _count;
|
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()
|
public static void ReportArmed()
|
||||||
{
|
{
|
||||||
if (!Enabled || _armedLogged)
|
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.");
|
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 readonly Dictionary<string, int> _shapeCounts = new();
|
||||||
|
|
||||||
private static int _fullResLogged;
|
private static int _fullResLogged;
|
||||||
@@ -83,8 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_shapeCounts.TryGetValue(key, out int n);
|
_shapeCounts.TryGetValue(key, out int n);
|
||||||
_shapeCounts[key] = n + 1;
|
_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)
|
if (info.Width == 1280 && info.Height == 720 && _fullResLogged < FullResLogCap)
|
||||||
{
|
{
|
||||||
_fullResLogged++;
|
_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 nonZero = 0;
|
||||||
int sampled = 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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -7,39 +5,11 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppGobProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_GOBZ_PROBE") == "1";
|
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 =
|
public static readonly bool ClampDisabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_NO_GOBZ_CLAMP") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_NO_GOBZ_CLAMP") == "1";
|
||||||
|
|
||||||
@@ -54,8 +24,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
_total++;
|
_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}";
|
string key = $"{width}x{height}x{layers} {target} {format} gobZ={gobZ}";
|
||||||
|
|
||||||
_shapes.TryGetValue(key, out int n);
|
_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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -11,31 +8,6 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppMap64Probe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -43,7 +15,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
private static bool _armedLogged;
|
private static bool _armedLogged;
|
||||||
|
|
||||||
/// <summary>Positive control (TextureCache ctor). Silence without it is unreadable.</summary>
|
|
||||||
public static void ReportArmed()
|
public static void ReportArmed()
|
||||||
{
|
{
|
||||||
if (!Enabled || _armedLogged)
|
if (!Enabled || _armedLogged)
|
||||||
@@ -75,7 +46,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
private static readonly List<In> _inputs = new();
|
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)
|
public static void OnInput(Texture texture, ShaderStage stage, int handle)
|
||||||
{
|
{
|
||||||
if (!Enabled || texture == null)
|
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();
|
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 readonly Dictionary<string, HashSet<int>> _instances = new();
|
||||||
|
|
||||||
private static long _totalDraws;
|
private static long _totalDraws;
|
||||||
@@ -116,10 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static long _groupSyncs;
|
private static long _groupSyncs;
|
||||||
private static long _summaryMs;
|
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)
|
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, int vpW, int vpH)
|
||||||
{
|
{
|
||||||
_totalDraws++;
|
_totalDraws++;
|
||||||
@@ -211,7 +174,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static long _groupSyncLogged;
|
private static long _groupSyncLogged;
|
||||||
private const long PerEventLogCap = 40;
|
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)
|
public static void OnFullSync(Texture texture, ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
if (!Enabled || texture == null || !IsWatched(texture.Info))
|
if (!Enabled || texture == null || !IsWatched(texture.Info))
|
||||||
@@ -221,7 +183,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
_fullSyncs++;
|
_fullSyncs++;
|
||||||
|
|
||||||
// The texture is tiny (64x36x4 = 9 KiB guest range): scan EVERY byte, no sampling caveat.
|
|
||||||
bool empty = true;
|
bool empty = true;
|
||||||
for (int i = 0; i < data.Length; i++)
|
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)
|
public static void OnGroupSync(Texture texture)
|
||||||
{
|
{
|
||||||
if (!Enabled || texture == null || !IsWatched(texture.Info))
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -10,30 +7,6 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppMvBufProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -62,7 +35,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
|
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame accounting (advanced from the present boundary, always on GPU thread).
|
|
||||||
private static long _frames;
|
private static long _frames;
|
||||||
private static int _clearsThisFrame;
|
private static int _clearsThisFrame;
|
||||||
private static int _writesThisFrame;
|
private static int _writesThisFrame;
|
||||||
@@ -79,21 +51,13 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static readonly HashSet<string> _clearSignatures = new();
|
private static readonly HashSet<string> _clearSignatures = new();
|
||||||
private static readonly HashSet<(ulong, int, int)> _writers = 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 readonly HashSet<string> _writerStates = new();
|
||||||
private static long _drawsBlendOnMv;
|
private static long _drawsBlendOnMv;
|
||||||
private static long _drawsPartialMaskOnMv;
|
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> _clearedIds = new();
|
||||||
private static readonly HashSet<int> _sampledIds = 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()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
if (!Enabled)
|
||||||
@@ -101,8 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return;
|
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)
|
if (_clears + _writerDraws > 0)
|
||||||
{
|
{
|
||||||
_frames++;
|
_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)
|
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))
|
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)
|
public static void OnSampled(Texture texture)
|
||||||
{
|
{
|
||||||
if (!Enabled || !IsMvBuffer(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)
|
public static void OnDraw(TextureManager texMgr, ulong fsAddr, int vpW, int vpH, ReadOnlySpan<bool> blendEnable, ReadOnlySpan<uint> writeMasks)
|
||||||
{
|
{
|
||||||
if (texMgr == null)
|
if (texMgr == null)
|
||||||
@@ -244,8 +203,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (isNew && (blend || mask != 0xF))
|
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,
|
Logger.Warning?.Print(LogClass.Gpu,
|
||||||
$"[MVBUF/WRITER-STATE] {state}{(blend && mask != 0 ? " *** BLEND INTO MV ***" : "")}");
|
$"[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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -8,28 +5,6 @@ using System;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppMvSyncProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -51,7 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
|
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Frame boundary (Window present): arming witness + 3s heartbeat, zeros included.</summary>
|
|
||||||
public static void OnPresent()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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)
|
public static void OnSyncDecision(Texture storage, bool partial, int regionCount, bool anyModified)
|
||||||
{
|
{
|
||||||
if (!Enabled || !IsMvBuffer(storage))
|
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)
|
public static void OnCopyIn(Texture storage)
|
||||||
{
|
{
|
||||||
if (!Enabled || !IsMvBuffer(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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,16 +6,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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 > 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
|
static class MvppPairProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -29,12 +16,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static int _disabled;
|
private static int _disabled;
|
||||||
private static long _lastSummaryMs;
|
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)
|
public static void OnPair(Texture texture, Sampler sampler)
|
||||||
{
|
{
|
||||||
if (texture == null || sampler == null)
|
if (texture == null || sampler == null)
|
||||||
@@ -44,9 +25,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
TextureInfo info = texture.Info;
|
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 ||
|
if (info.Levels != 1 ||
|
||||||
info.Width < 64 ||
|
info.Width < 64 ||
|
||||||
info.Height < 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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -11,18 +8,6 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppScenePassProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -35,13 +20,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
public int W;
|
public int W;
|
||||||
public int H;
|
public int H;
|
||||||
public ShaderStage Stage;
|
public ShaderStage Stage;
|
||||||
public int Handle; // [E0] texture-CB handle -> GLSL sampler name fp_t_tcb_<Handle:X>
|
public int Handle;
|
||||||
public int Binding; // [E0] runtime binding slot -> GLSL layout(binding=N)
|
public int Binding;
|
||||||
public AddressMode WrapU; // [CLAMPPROBE] sampler address mode U (edge/border/wrap) of this input
|
public AddressMode WrapU;
|
||||||
public AddressMode WrapV; // [CLAMPPROBE] sampler address mode V
|
public AddressMode WrapV;
|
||||||
// [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 MinFilter MinF;
|
public MinFilter MinF;
|
||||||
public MagFilter MagF;
|
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)
|
public static void OnInput(Texture texture, ShaderStage stage, int handle, int binding, Sampler sampler)
|
||||||
{
|
{
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
@@ -87,7 +68,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (_inputs.Count >= 256)
|
if (_inputs.Count >= 256)
|
||||||
{
|
{
|
||||||
return; // safety cap; a real draw never samples this many
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureInfo info = texture.Info;
|
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)
|
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, long frame)
|
||||||
{
|
{
|
||||||
lock (_inputs)
|
lock (_inputs)
|
||||||
@@ -127,8 +103,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
TextureInfo rt = colorRt.Info;
|
TextureInfo rt = colorRt.Info;
|
||||||
bool isScene = rt.Width == 1280 && rt.Height == 720 && rt.FormatInfo.Format == Format.R11G11B10Float;
|
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;
|
bool isDof = rt.Width == 512 && rt.Height == 288 && rt.FormatInfo.Format == Format.R16G16B16A16Float;
|
||||||
if (!isScene && !isDof)
|
if (!isScene && !isDof)
|
||||||
{
|
{
|
||||||
@@ -137,7 +111,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (!_seenShaders.Add(fsAddr))
|
if (!_seenShaders.Add(fsAddr))
|
||||||
{
|
{
|
||||||
return; // this pass already reported
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong fbVa = Va(colorRt);
|
ulong fbVa = Va(colorRt);
|
||||||
@@ -146,8 +120,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
Logger.Info?.Print(LogClass.Gpu,
|
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} =====");
|
$"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++)
|
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
||||||
{
|
{
|
||||||
Texture ct = texMgr.GetColorTarget(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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,21 +5,9 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
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[] _watchX;
|
||||||
private static readonly int[] _watchY;
|
private static readonly int[] _watchY;
|
||||||
@@ -81,11 +66,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
private static bool _announced;
|
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()
|
public static void Announce()
|
||||||
{
|
{
|
||||||
if (_announced)
|
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}");
|
$"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(
|
public static void OnScaledCopy(
|
||||||
string op,
|
string op,
|
||||||
Texture src, Texture dst,
|
Texture src, Texture dst,
|
||||||
@@ -116,11 +92,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A diagnostic on the GPU thread must never take the process down.
|
|
||||||
try
|
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 &&
|
if (src != null && dst != null &&
|
||||||
src.EffectiveScaleX == 1f && src.EffectiveScaleY == 1f &&
|
src.EffectiveScaleX == 1f && src.EffectiveScaleY == 1f &&
|
||||||
dst.EffectiveScaleX == 1f && dst.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)
|
private static string EdgeHit(float sA, float sB, float dA, float dB, int[] watch, string axis)
|
||||||
{
|
{
|
||||||
foreach (int w in watch)
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -8,32 +5,11 @@ using System;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppTaaProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_TAAPROBE") == "1";
|
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 const int Slots = 24;
|
||||||
|
|
||||||
private static readonly ulong[] _va = new ulong[Slots];
|
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}.");
|
$"TAAPROBE gate check: RYUJINX_MVPP_TAAPROBE={(v ?? "<unset>")} -> enabled={Enabled}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Frontiere d'image invitee.</summary>
|
|
||||||
public static void OnPresent()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (Enabled)
|
if (Enabled)
|
||||||
@@ -85,11 +60,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return -1;
|
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)
|
public static void OnInput(Texture input, Texture target, ShaderStage stage)
|
||||||
{
|
{
|
||||||
if (!Enabled || input == null || target == null)
|
if (!Enabled || input == null || target == null)
|
||||||
@@ -97,9 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return;
|
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;
|
long area = (long)target.Info.Width * target.Info.Height;
|
||||||
|
|
||||||
if (area > _mainArea)
|
if (area > _mainArea)
|
||||||
@@ -114,7 +81,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// La cible pleine resolution de ce dessin est notee comme "vue a cette image".
|
|
||||||
ulong tva = Va(target);
|
ulong tva = Va(target);
|
||||||
int ti = Find(tva);
|
int ti = Find(tva);
|
||||||
|
|
||||||
@@ -129,8 +95,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_count++;
|
_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)
|
if (input.Info.Width != _mainW || input.Info.Height != _mainH)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -138,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
int ii = Find(Va(input));
|
int ii = Find(Va(input));
|
||||||
|
|
||||||
// Ecrite a une image PRECEDENTE et lue maintenant = historique temporel.
|
|
||||||
if (ii < 0 || _lastSeen[ii] >= _frame)
|
if (ii < 0 || _lastSeen[ii] >= _frame)
|
||||||
{
|
{
|
||||||
return;
|
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.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -9,17 +6,6 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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 <- 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
|
static class MvppTraceProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -78,7 +64,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Records the edge "renderTarget <- input" for one read (call site gated on Enabled).</summary>
|
|
||||||
public static void OnEdge(Texture renderTarget, Texture input)
|
public static void OnEdge(Texture renderTarget, Texture input)
|
||||||
{
|
{
|
||||||
if (renderTarget == null || input == null)
|
if (renderTarget == null || input == null)
|
||||||
@@ -97,7 +82,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
lock (_graph)
|
lock (_graph)
|
||||||
{
|
{
|
||||||
Node rt = GetOrAdd(rtVa, renderTarget.Info);
|
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);
|
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)
|
private static void Walk(ulong va, int depth, HashSet<ulong> visited)
|
||||||
{
|
{
|
||||||
if (depth > MaxDepth || !visited.Add(va))
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -9,25 +6,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppTwinFixProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -38,8 +16,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
private static readonly HashSet<(ShaderStage, int)> _redirectTuples = new();
|
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)
|
public static bool NeedsRebind(Texture cached)
|
||||||
{
|
{
|
||||||
if (!Enabled || cached == null)
|
if (!Enabled || cached == null)
|
||||||
@@ -60,7 +36,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return cached.Range.GetSubRange(0).Address == skyVa;
|
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)
|
public static Texture MaybeRedirect(Texture texture, ShaderStage stage, int handle)
|
||||||
{
|
{
|
||||||
if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture))
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -9,20 +6,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppTwinMapProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -37,24 +20,16 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
public long WriteDraws;
|
public long WriteDraws;
|
||||||
public int WritersLogged;
|
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 GpuVa;
|
||||||
public ulong Backing;
|
public ulong Backing;
|
||||||
public ulong LastTranslation;
|
public ulong LastTranslation;
|
||||||
|
|
||||||
// v4 (journal 178): live Texture reference for TWINFIX's read-side redirect.
|
|
||||||
public WeakReference<Texture> LastTexture;
|
public WeakReference<Texture> LastTexture;
|
||||||
|
|
||||||
// v5 (journal 179): per-twin clear attribution + masked-bind census.
|
|
||||||
public long Clears;
|
public long Clears;
|
||||||
public int ClearsLogged;
|
public int ClearsLogged;
|
||||||
public long MaskedBinds;
|
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 BlackClears;
|
||||||
public long NeutralClears;
|
public long NeutralClears;
|
||||||
}
|
}
|
||||||
@@ -63,10 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static readonly HashSet<(ulong, ShaderStage, int)> _reads = new();
|
private static readonly HashSet<(ulong, ShaderStage, int)> _reads = new();
|
||||||
private static WeakReference<Memory.MemoryManager> _mm;
|
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 _poolMatches;
|
||||||
private static long _poolMismatches;
|
private static long _poolMismatches;
|
||||||
private static readonly HashSet<(ulong, ulong, int)> _mismatchTuples = new();
|
private static readonly HashSet<(ulong, ulong, int)> _mismatchTuples = new();
|
||||||
@@ -76,8 +47,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return t.Range.GetSubRange(0).Address;
|
return t.Range.GetSubRange(0).Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>v4 (TWINFIX): conservative twin classification. True only with exactly two twins,
|
|
||||||
/// one clearly sky-only (<= 4 writers) and one clearly material (>= 50 writers).</summary>
|
|
||||||
public static bool TryClassify(out ulong skyVa, out Texture materialTwin)
|
public static bool TryClassify(out ulong skyVa, out Texture materialTwin)
|
||||||
{
|
{
|
||||||
skyVa = 0;
|
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)
|
if (sky == null || mat == null)
|
||||||
{
|
{
|
||||||
TwinStats black = null, neutral = 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()
|
public static ulong[] TwinVas()
|
||||||
{
|
{
|
||||||
lock (_twins)
|
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)
|
public static void OnDraw(TextureManager texMgr, ulong fsAddr, Memory.MemoryManager memoryManager, ReadOnlySpan<uint> writeMasks)
|
||||||
{
|
{
|
||||||
if (!Enabled || texMgr == null || fsAddr == 0)
|
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.");
|
"[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;
|
bool any = false;
|
||||||
|
|
||||||
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
||||||
@@ -251,7 +211,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return stats;
|
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)
|
public static void OnClear(Texture target, uint componentMask, float r, float g, float b, float a)
|
||||||
{
|
{
|
||||||
if (!Enabled || target == null || !MvppMvSyncProbe.IsMvBuffer(target))
|
if (!Enabled || target == null || !MvppMvSyncProbe.IsMvBuffer(target))
|
||||||
@@ -266,7 +225,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
TwinStats stats = GetOrAdd(va, target, null);
|
TwinStats stats = GetOrAdd(va, target, null);
|
||||||
stats.Clears++;
|
stats.Clears++;
|
||||||
|
|
||||||
// [v6] clear-colour census for the fallback classification.
|
|
||||||
if (r < 0.1f && g < 0.1f && b < 0.1f)
|
if (r < 0.1f && g < 0.1f && b < 0.1f)
|
||||||
{
|
{
|
||||||
stats.BlackClears++;
|
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)
|
public static void OnRead(Texture texture, ShaderStage stage, int handle, TexturePool pool, int textureId, Memory.MemoryManager memoryManager)
|
||||||
{
|
{
|
||||||
if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture))
|
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}");
|
$"[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)
|
if (pool != null && memoryManager != null && textureId >= 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -342,7 +297,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
catch
|
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;
|
TwinStats stats = kv.Value;
|
||||||
summary += $" twin@0x{kv.Key:X}: writers={stats.Writers.Count} draws={stats.WriteDraws} maskedBinds={stats.MaskedBinds} CLEARS={stats.Clears} |";
|
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)
|
if (mm != null && stats.GpuVa != 0)
|
||||||
{
|
{
|
||||||
ulong translated = mm.Translate(stats.GpuVa);
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -9,25 +6,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppTwinXferProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
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, ShaderStage, int, bool)> _imageTuples = new();
|
||||||
private static readonly HashSet<(ulong, ulong, bool)> _dmaPairs = 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 const ulong TwinWindow = 0x400000;
|
||||||
|
|
||||||
private static bool IsMv(Texture t)
|
private static bool IsMv(Texture t)
|
||||||
@@ -57,7 +33,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return t.Range.GetSubRange(0).Address;
|
return t.Range.GetSubRange(0).Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Frame boundary (Gpu Window present): arming witness + heartbeat, zeros included.</summary>
|
|
||||||
public static void OnPresent()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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)
|
public static void OnCopy(string path, Texture src, Texture dst)
|
||||||
{
|
{
|
||||||
if (!Enabled || (!IsMv(src) && !IsMv(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)
|
public static void OnDma(ulong srcVa, ulong dstVa, int xCount, int yCount, bool copy2D, bool srcLinear, bool dstLinear)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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)
|
public static void OnImageBind(Texture texture, ShaderStage stage, int handle, bool isStore)
|
||||||
{
|
{
|
||||||
if (!Enabled || !IsMv(texture))
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,45 +5,17 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppViewAliasProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_VIEWPROBE") == "1";
|
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 readonly HashSet<string> _reported = new();
|
||||||
|
|
||||||
private static bool _armedLogged;
|
private static bool _armedLogged;
|
||||||
private static bool _firstViewLogged;
|
private static bool _firstViewLogged;
|
||||||
private static int _viewCount;
|
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()
|
public static void ReportArmed()
|
||||||
{
|
{
|
||||||
if (!Enabled || _armedLogged)
|
if (!Enabled || _armedLogged)
|
||||||
@@ -68,9 +37,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
TextureInfo p = parent.Info;
|
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++;
|
_viewCount++;
|
||||||
|
|
||||||
if (!_firstViewLogged)
|
if (!_firstViewLogged)
|
||||||
@@ -82,14 +48,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
$"over parent {p.FormatInfo.Format} {p.Width}x{p.Height} | compat {compatibility}");
|
$"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)
|
if ((_viewCount % 500) == 0)
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Gpu, $"[VIEWALIAS] tally: {_viewCount} views created so far, {_reported.Count} distinct cross-format cases.");
|
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)
|
if (child.FormatInfo.Format == p.FormatInfo.Format)
|
||||||
{
|
{
|
||||||
return;
|
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.Common.Logging;
|
||||||
using Ryujinx.Graphics.Shader.Translation;
|
using Ryujinx.Graphics.Shader.Translation;
|
||||||
@@ -9,26 +6,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
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
|
static class MvppWriterCensusProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
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> _unarmedFs = new();
|
||||||
private static readonly HashSet<ulong> _maskedOffFs = new();
|
private static readonly HashSet<ulong> _maskedOffFs = new();
|
||||||
|
|
||||||
/// <summary>Frame boundary (Window present): arming witness + 3s heartbeat, zeros included.</summary>
|
|
||||||
public static void OnPresent()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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)
|
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ReadOnlySpan<uint> writeMasks, Ryujinx.Graphics.Shader.ShaderProgramInfo fragInfo)
|
||||||
{
|
{
|
||||||
if (texMgr == null || fsAddr == 0)
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
public bool IsSrgb { get; }
|
public bool IsSrgb { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [PAIRPROBE, read-only diagnostics] Unpacked filter/LOD parameters, kept because the
|
|
||||||
/// descriptor is discarded after construction. Never read by any rendering path.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MinFilter ProbeMinFilter { get; }
|
public MinFilter ProbeMinFilter { get; }
|
||||||
public MagFilter ProbeMagFilter { get; }
|
public MagFilter ProbeMagFilter { get; }
|
||||||
@@ -29,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
public float ProbeMipLodBias { get; }
|
public float ProbeMipLodBias { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public AddressMode ProbeAddressU { get; }
|
public AddressMode ProbeAddressU { get; }
|
||||||
public AddressMode ProbeAddressV { get; }
|
public AddressMode ProbeAddressV { get; }
|
||||||
@@ -49,18 +42,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ISampler _anisoSampler;
|
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 =
|
private static readonly bool _forceMipsTrilinear =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_TRILINEAR") == "1";
|
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 =
|
private static readonly float _forceMipsBias =
|
||||||
float.TryParse(
|
float.TryParse(
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_BIAS"),
|
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_BIAS"),
|
||||||
@@ -69,8 +53,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
out float fmBias) ? fmBias : 0f;
|
out float fmBias) ? fmBias : 0f;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private readonly ISampler _trilinearSampler;
|
private readonly ISampler _trilinearSampler;
|
||||||
|
|
||||||
@@ -191,7 +173,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (_trilinearSampler != null && texture?.HasForcedMips == true)
|
if (_trilinearSampler != null && texture?.HasForcedMips == true)
|
||||||
{
|
{
|
||||||
return _trilinearSampler; // [FORCEMIPS_TRILINEAR] blend between generated levels
|
return _trilinearSampler;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _anisoSampler != null && texture?.CanForceAnisotropy == true ? _anisoSampler : _hostSampler;
|
return _anisoSampler != null && texture?.CanForceAnisotropy == true ? _anisoSampler : _hostSampler;
|
||||||
|
|||||||
@@ -44,23 +44,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
private SizeInfo _sizeInfo;
|
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 =
|
private static readonly bool _mipProbe =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPPROBE") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPPROBE") == "1";
|
||||||
private static int _mipProbeAstc, _mipProbeUnreadable, _mipProbeCand, _mipProbeLogged;
|
private static int _mipProbeAstc, _mipProbeUnreadable, _mipProbeCand, _mipProbeLogged;
|
||||||
private static long _mipProbeSummaryMs;
|
private static long _mipProbeSummaryMs;
|
||||||
private bool _mipProbed;
|
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 =
|
private static readonly bool _forceMips =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS") == "1";
|
||||||
private static readonly int _forceMipsExtraLevels =
|
private static readonly int _forceMipsExtraLevels =
|
||||||
@@ -73,33 +62,14 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private static int _forceMipsLost;
|
private static int _forceMipsLost;
|
||||||
private static long _forceMipsSummaryMs;
|
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 static int _forceMipsRtDemotes;
|
||||||
private bool _forceMipsRtDemoted;
|
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 =
|
private static readonly int _forceMipsRefreshMs =
|
||||||
int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_REFRESH_MS"), out int fmRefresh)
|
int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_REFRESH_MS"), out int fmRefresh)
|
||||||
? Math.Max(0, fmRefresh)
|
? Math.Max(0, fmRefresh)
|
||||||
: 0;
|
: 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 =
|
private static readonly bool _mipDump =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPDUMP") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPDUMP") == "1";
|
||||||
private const int MipDumpMaxTextures = 16;
|
private const int MipDumpMaxTextures = 16;
|
||||||
@@ -111,10 +81,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private long _forcedMipsLastRegenMs;
|
private long _forcedMipsLastRegenMs;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public bool HasForcedMips => _forcedMipLevels > 1;
|
public bool HasForcedMips => _forcedMipLevels > 1;
|
||||||
|
|
||||||
@@ -154,18 +120,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
public float ScaleFactor { get; private set; }
|
public float ScaleFactor { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public float EffectiveScaleX => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null
|
public float EffectiveScaleX => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null
|
||||||
? HostTexture.Width * Info.SamplesInX / (float)Info.Width
|
? HostTexture.Width * Info.SamplesInX / (float)Info.Width
|
||||||
: ScaleFactor;
|
: ScaleFactor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exact vertical scale of the host texture (host size / guest size). See <see cref="EffectiveScaleX"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float EffectiveScaleY => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null
|
public float EffectiveScaleY => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null
|
||||||
? HostTexture.Height * Info.SamplesInY / (float)Info.Height
|
? HostTexture.Height * Info.SamplesInY / (float)Info.Height
|
||||||
@@ -246,7 +206,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public MultiRange Range { get; private set; }
|
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;
|
internal PhysicalMemory PhysicalMemory => _physicalMemory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -328,9 +287,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
InitializeTexture(context, physicalMemory, info, sizeInfo, range);
|
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)
|
if (_texProbe && info.Target == Target.Texture2D && info.Width >= 16)
|
||||||
{
|
{
|
||||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
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 =
|
private static readonly bool _texProbe =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_TEXPROBE") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_TEXPROBE") == "1";
|
||||||
|
|
||||||
@@ -387,16 +342,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
Debug.Assert(!isView);
|
Debug.Assert(!isView);
|
||||||
|
|
||||||
TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor);
|
TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor);
|
||||||
createInfo = BoostLevelsForForcedMips(createInfo); // [FORCEMIPS] no-op when off
|
createInfo = BoostLevelsForForcedMips(createInfo);
|
||||||
HostTexture = _context.Renderer.CreateTexture(createInfo);
|
HostTexture = _context.Renderer.CreateTexture(createInfo);
|
||||||
|
|
||||||
SynchronizeMemory(); // Load the data.
|
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();
|
EnsureForcedMips();
|
||||||
|
|
||||||
if (ScaleMode == TextureScaleMode.Scaled)
|
if (ScaleMode == TextureScaleMode.Scaled)
|
||||||
@@ -420,32 +370,20 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor);
|
TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor);
|
||||||
createInfo = BoostLevelsForForcedMips(createInfo); // [FORCEMIPS] no-op when off
|
createInfo = BoostLevelsForForcedMips(createInfo);
|
||||||
HostTexture = _context.Renderer.CreateTexture(createInfo);
|
HostTexture = _context.Renderer.CreateTexture(createInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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)
|
private TextureCreateInfo BoostLevelsForForcedMips(TextureCreateInfo createInfo)
|
||||||
{
|
{
|
||||||
_forcedMipLevels = 0;
|
_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 ||
|
if (!_forceMips ||
|
||||||
_forceMipsRtDemoted || // [MIPS-RT fix] une cible de rendu ne se re-booste jamais
|
_forceMipsRtDemoted ||
|
||||||
ScaleMode == TextureScaleMode.Scaled ||
|
ScaleMode == TextureScaleMode.Scaled ||
|
||||||
Info.Target != Target.Texture2D ||
|
Info.Target != Target.Texture2D ||
|
||||||
Info.Levels != 1 ||
|
Info.Levels != 1 ||
|
||||||
@@ -459,12 +397,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return createInfo;
|
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);
|
int aspectWidth = (int)MathF.Ceiling((createInfo.Height / 9f) * 16f);
|
||||||
|
|
||||||
if (Math.Abs(createInfo.Width - aspectWidth) <= 16)
|
if (Math.Abs(createInfo.Width - aspectWidth) <= 16)
|
||||||
@@ -505,13 +437,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void DemoteForcedMipsOnRenderTargetBind()
|
public void DemoteForcedMipsOnRenderTargetBind()
|
||||||
{
|
{
|
||||||
@@ -535,7 +460,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
ITexture newStorage = storage.GetScaledHostTexture(storage.ScaleFactor, true);
|
ITexture newStorage = storage.GetScaledHostTexture(storage.ScaleFactor, true);
|
||||||
storage.ReplaceStorage(newStorage);
|
storage.ReplaceStorage(newStorage);
|
||||||
|
|
||||||
// Les vues doivent revivre sur la nouvelle storage (miroir du chemin SetScale).
|
|
||||||
foreach (Texture view in storage._views)
|
foreach (Texture view in storage._views)
|
||||||
{
|
{
|
||||||
TextureCreateInfo viewCreateInfo = TextureCache.GetCreateInfo(view.Info, storage._context.Capabilities, view.ScaleFactor);
|
TextureCreateInfo viewCreateInfo = TextureCache.GetCreateInfo(view.Info, storage._context.Capabilities, view.ScaleFactor);
|
||||||
@@ -547,11 +471,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void EnsureForcedMips()
|
public void EnsureForcedMips()
|
||||||
{
|
{
|
||||||
@@ -597,7 +516,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
dstView.Release();
|
dstView.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// [MIPDUMP] First regeneration of the first N boosted textures: photograph the chain.
|
|
||||||
if (_mipDump && !_mipDumped && _mipDumpCount < MipDumpMaxTextures)
|
if (_mipDump && !_mipDumped && _mipDumpCount < MipDumpMaxTextures)
|
||||||
{
|
{
|
||||||
_mipDumped = true;
|
_mipDumped = true;
|
||||||
@@ -624,12 +542,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="index">Sequential dump index (for file naming)</param>
|
|
||||||
private void DumpForcedMipChain(int index)
|
private void DumpForcedMipChain(int index)
|
||||||
{
|
{
|
||||||
string dir = System.IO.Path.Combine(System.Environment.CurrentDirectory, "_MIPDUMP");
|
string dir = System.IO.Path.Combine(System.Environment.CurrentDirectory, "_MIPDUMP");
|
||||||
@@ -694,13 +607,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [FORCEMIPS] Builds the creation info for a transient single-level view used by the mip
|
|
||||||
/// regeneration blits.
|
|
||||||
/// </summary>
|
/// </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)
|
private static TextureCreateInfo ForcedMipViewInfo(TextureCreateInfo baseInfo, int width, int height)
|
||||||
{
|
{
|
||||||
return new TextureCreateInfo(
|
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>
|
/// <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)
|
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)
|
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>
|
/// <returns>A host texture containing a scaled version of this texture</returns>
|
||||||
private ITexture GetScaledHostTexture(float scale, bool copy, ITexture storage = null)
|
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))
|
if (GAL.MvppDev.Enabled && copy && scale == 1f && ScaleFactor != MathF.Floor(ScaleFactor))
|
||||||
{
|
{
|
||||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||||
@@ -1023,7 +924,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void SynchronizeMemory()
|
public void SynchronizeMemory()
|
||||||
{
|
{
|
||||||
MvppCacheProbe.OnSynchronize(); // read-only (gated)
|
MvppCacheProbe.OnSynchronize();
|
||||||
|
|
||||||
if (Target == Target.TextureBuffer)
|
if (Target == Target.TextureBuffer)
|
||||||
{
|
{
|
||||||
@@ -1037,14 +938,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
_dirty = false;
|
_dirty = false;
|
||||||
|
|
||||||
// Le VRAI travail : seul un appel qui trouve la texture SALE recharge des données. Les
|
MvppCacheProbe.OnRealSync(Info.Width, Info.Height, Info.FormatInfo.Format, _hasData);
|
||||||
// ~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)
|
|
||||||
|
|
||||||
if (_hasData)
|
if (_hasData)
|
||||||
{
|
{
|
||||||
MvppMap64Probe.OnGroupSync(this); // [MAP64] read-only (gated): partial syncs onto the small DoF maps
|
MvppMap64Probe.OnGroupSync(this);
|
||||||
Group.SynchronizeMemory(this);
|
Group.SynchronizeMemory(this);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1078,14 +976,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Range);
|
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);
|
MvppFullSyncProbe.OnFullSync(Info, data, IsView);
|
||||||
|
|
||||||
// [MAP64, READ-ONLY] Same question, filtered on the small DoF maps (gated).
|
|
||||||
MvppMap64Probe.OnFullSync(this, data);
|
MvppMap64Probe.OnFullSync(this, data);
|
||||||
|
|
||||||
// [MIPPROBE, READ-ONLY] Measure mip alpha coverage once per texture. Gated; does nothing to data/upload.
|
|
||||||
if (_mipProbe && !_mipProbed)
|
if (_mipProbe && !_mipProbed)
|
||||||
{
|
{
|
||||||
_mipProbed = true;
|
_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 &&
|
if (Info.FormatInfo.Format.IsAstc &&
|
||||||
!_context.Capabilities.SupportsAstcCompression &&
|
!_context.Capabilities.SupportsAstcCompression &&
|
||||||
_depth == 1 &&
|
_depth == 1 &&
|
||||||
@@ -1158,7 +1048,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (_forcedMipLevels > 1)
|
if (_forcedMipLevels > 1)
|
||||||
{
|
{
|
||||||
_forcedMipsDirty = true; // [FORCEMIPS] level 0 changed; regenerated lazily at the next sampling bind
|
_forcedMipsDirty = true;
|
||||||
_forceMipsMarks++;
|
_forceMipsMarks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,9 +1056,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [MIPPROBE / Mesure H1, READ-ONLY] Decodes each mip level to a TEMP buffer, measures the alpha
|
|
||||||
/// coverage (fraction of texels with alpha > 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>
|
/// </summary>
|
||||||
private void MipCoverageProbe(ReadOnlySpan<byte> data)
|
private void MipCoverageProbe(ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
@@ -1186,7 +1073,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (!astc && !rgba8)
|
if (!astc && !rgba8)
|
||||||
{
|
{
|
||||||
// Mipped texture whose alpha we can't read cheaply (BCn native, etc.) -- log the landscape.
|
|
||||||
System.Threading.Interlocked.Increment(ref _mipProbeUnreadable);
|
System.Threading.Interlocked.Increment(ref _mipProbeUnreadable);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1203,7 +1089,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
for (int level = 0; level < levels; level++)
|
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)
|
if (off < 0 || off >= data.Length)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -1246,7 +1132,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (level == 0 && !binary)
|
if (level == 0 && !binary)
|
||||||
{
|
{
|
||||||
return; // not a cutout texture -> skip
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1299,7 +1185,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (_forcedMipLevels > 1)
|
if (_forcedMipLevels > 1)
|
||||||
{
|
{
|
||||||
_forcedMipsDirty = true; // [FORCEMIPS]
|
_forcedMipsDirty = true;
|
||||||
_forceMipsMarks++;
|
_forceMipsMarks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,7 +1206,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (_forcedMipLevels > 1 && level == 0)
|
if (_forcedMipLevels > 1 && level == 0)
|
||||||
{
|
{
|
||||||
_forcedMipsDirty = true; // [FORCEMIPS]
|
_forcedMipsDirty = true;
|
||||||
_forceMipsMarks++;
|
_forceMipsMarks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1344,7 +1230,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (_forcedMipLevels > 1 && level == 0)
|
if (_forcedMipLevels > 1 && level == 0)
|
||||||
{
|
{
|
||||||
_forcedMipsDirty = true; // [FORCEMIPS] tile streamed into the pool
|
_forcedMipsDirty = true;
|
||||||
_forceMipsMarks++;
|
_forceMipsMarks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2086,12 +1972,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (MvppDestProbe.Enabled)
|
if (MvppDestProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppDestProbe.OnModify(this, viaModifying: false, bound: true); // read-only (gated)
|
MvppDestProbe.OnModify(this, viaModifying: false, bound: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_forcedMipLevels > 1)
|
if (_forcedMipLevels > 1)
|
||||||
{
|
{
|
||||||
_forcedMipsDirty = true; // [FORCEMIPS] GPU-side write (copy/RT); regenerated lazily if sampled
|
_forcedMipsDirty = true;
|
||||||
_forceMipsMarks++;
|
_forceMipsMarks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2115,7 +2001,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (MvppDestProbe.Enabled)
|
if (MvppDestProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppDestProbe.OnModify(this, viaModifying: true, bound: bound); // read-only (gated)
|
MvppDestProbe.OnModify(this, viaModifying: true, bound: bound);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bound)
|
if (bound)
|
||||||
@@ -2149,9 +2035,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
DisposeTextures();
|
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)
|
if (_forcedMipLevels > 1)
|
||||||
{
|
{
|
||||||
_forceMipsLost++;
|
_forceMipsLost++;
|
||||||
|
|||||||
@@ -898,10 +898,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (MvppPairProbe.Enabled)
|
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);
|
hostSampler = sampler?.GetHostSampler(texture);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,9 +111,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void MvppEnumerateInputs(System.Action<Texture> report)
|
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)
|
public void MvppEnumerateInputsWithSampler(System.Action<int, Texture, Sampler> report)
|
||||||
{
|
{
|
||||||
for (int stage = 0; stage < _textureBindings.Length; stage++)
|
for (int stage = 0; stage < _textureBindings.Length; stage++)
|
||||||
@@ -186,9 +179,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void MvppEnumerateImages(System.Action<Texture, bool> report)
|
public void MvppEnumerateImages(System.Action<Texture, bool> report)
|
||||||
{
|
{
|
||||||
@@ -213,9 +203,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public int MvppArrayBindingCount()
|
public int MvppArrayBindingCount()
|
||||||
{
|
{
|
||||||
@@ -236,10 +223,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void MvppEnumerateCompute(System.Action<bool, bool, Texture> report)
|
public void MvppEnumerateCompute(System.Action<bool, bool, Texture> report)
|
||||||
{
|
{
|
||||||
@@ -364,10 +347,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (MvppPairProbe.Enabled)
|
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);
|
return (texture, sampler);
|
||||||
}
|
}
|
||||||
@@ -397,13 +380,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
switch (stage)
|
switch (stage)
|
||||||
{
|
{
|
||||||
case ShaderStage.Fragment:
|
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;
|
float scale = texture.EffectiveScaleX;
|
||||||
|
|
||||||
if (scale != 1)
|
if (scale != 1)
|
||||||
@@ -633,11 +609,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
MvppDofProbe.AnnounceOnce(); // [DOFPROBE] one-shot gate-state line on the real draw path, even when OFF
|
MvppDofProbe.AnnounceOnce();
|
||||||
MvppFeedbackProbe.AnnounceOnce(); // [FEEDBACKPROBE] one-shot gate-state line, even when OFF
|
MvppFeedbackProbe.AnnounceOnce();
|
||||||
MvppTraceProbe.AnnounceOnce(); // [TRACEPROBE] one-shot gate-state line, even when OFF
|
MvppTraceProbe.AnnounceOnce();
|
||||||
MvppScenePassProbe.AnnounceOnce(); // [SCENEPROBE] one-shot gate-state line, even when OFF
|
MvppScenePassProbe.AnnounceOnce();
|
||||||
MvppTaaProbe.AnnounceOnce(); // [TAAPROBE] one-shot gate-state line, even when OFF
|
MvppTaaProbe.AnnounceOnce();
|
||||||
|
|
||||||
bool specStateMatches = true;
|
bool specStateMatches = true;
|
||||||
|
|
||||||
@@ -683,45 +659,42 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
state.CachedTexture != null &&
|
state.CachedTexture != null &&
|
||||||
state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence &&
|
state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence &&
|
||||||
state.CachedSampler?.IsDisposed != true &&
|
state.CachedSampler?.IsDisposed != true &&
|
||||||
!MvppTwinFixProbe.NeedsRebind(state.CachedTexture)) // [TWINFIX] one forced re-resolve once classified
|
!MvppTwinFixProbe.NeedsRebind(state.CachedTexture))
|
||||||
{
|
{
|
||||||
// The texture is already bound.
|
// The texture is already bound.
|
||||||
state.CachedTexture.SynchronizeMemory();
|
state.CachedTexture.SynchronizeMemory();
|
||||||
|
|
||||||
if (MvppDofProbe.Enabled)
|
if (MvppDofProbe.Enabled)
|
||||||
{
|
{
|
||||||
// [DOFPROBE] cached (fast) path: a stable input re-bound this draw.
|
|
||||||
MvppDofProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
MvppDofProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppTaaProbe.Enabled)
|
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);
|
MvppTaaProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppFeedbackProbe.Enabled)
|
if (MvppFeedbackProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppFeedbackProbe.OnRead(state.CachedTexture); // [FEEDBACKPROBE] input read (fast path)
|
MvppFeedbackProbe.OnRead(state.CachedTexture);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppTraceProbe.Enabled)
|
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)
|
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)
|
MvppBuilderInProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler);
|
||||||
MvppMap64Probe.OnInput(state.CachedTexture, stage, bindingInfo.Handle); // [MAP64] per-draw input (fast path, self-gated)
|
MvppMap64Probe.OnInput(state.CachedTexture, stage, bindingInfo.Handle);
|
||||||
MvppMvBufProbe.OnSampled(state.CachedTexture); // [MVBUF] identity of the sampled MV buffer (self-gated)
|
MvppMvBufProbe.OnSampled(state.CachedTexture);
|
||||||
MvppTwinMapProbe.OnRead(state.CachedTexture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated)
|
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 &&
|
if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 &&
|
||||||
UpdateScale(state.CachedTexture, usageFlags, index, stage))
|
UpdateScale(state.CachedTexture, usageFlags, index, stage))
|
||||||
@@ -745,48 +718,42 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (MvppPairProbe.Enabled)
|
if (MvppPairProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppPairProbe.OnPair(texture, sampler); // read-only (gated)
|
MvppPairProbe.OnPair(texture, sampler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppDofProbe.Enabled)
|
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);
|
MvppDofProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppTaaProbe.Enabled)
|
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);
|
MvppTaaProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppFeedbackProbe.Enabled)
|
if (MvppFeedbackProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppFeedbackProbe.OnRead(texture); // [FEEDBACKPROBE] input read (resolve path)
|
MvppFeedbackProbe.OnRead(texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppTraceProbe.Enabled)
|
if (MvppTraceProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), texture); // [TRACEPROBE] dep edge (resolve path)
|
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppScenePassProbe.Enabled)
|
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)
|
MvppBuilderInProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler);
|
||||||
MvppMap64Probe.OnInput(texture, stage, bindingInfo.Handle); // [MAP64] per-draw input (resolve path, self-gated)
|
MvppMap64Probe.OnInput(texture, stage, bindingInfo.Handle);
|
||||||
MvppMvBufProbe.OnSampled(texture); // [MVBUF] identity of the sampled MV buffer (self-gated)
|
MvppMvBufProbe.OnSampled(texture);
|
||||||
MvppTwinMapProbe.OnRead(texture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated)
|
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 = MvppTwinFixProbe.MaybeRedirect(texture, stage, bindingInfo.Handle);
|
||||||
|
|
||||||
texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty
|
texture?.EnsureForcedMips();
|
||||||
|
|
||||||
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
|
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
|
||||||
|
|
||||||
@@ -901,7 +868,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
// The texture is already bound.
|
// The texture is already bound.
|
||||||
cachedTexture.SynchronizeMemory();
|
cachedTexture.SynchronizeMemory();
|
||||||
|
|
||||||
MvppTwinXferProbe.OnImageBind(cachedTexture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated
|
MvppTwinXferProbe.OnImageBind(cachedTexture, stage, bindingInfo.Handle, isStore);
|
||||||
|
|
||||||
if (isStore)
|
if (isStore)
|
||||||
{
|
{
|
||||||
@@ -942,7 +909,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MvppTwinXferProbe.OnImageBind(texture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated
|
MvppTwinXferProbe.OnImageBind(texture, stage, bindingInfo.Handle, isStore);
|
||||||
|
|
||||||
if (isStore)
|
if (isStore)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
|
_textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
|
||||||
_overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
|
_overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
|
||||||
|
|
||||||
// [VIEWALIAS] / [FULLSYNC] / [MAP64] Positive controls: make the probes' silence interpretable.
|
|
||||||
MvppViewAliasProbe.ReportArmed();
|
MvppViewAliasProbe.ReportArmed();
|
||||||
MvppFullSyncProbe.ReportArmed();
|
MvppFullSyncProbe.ReportArmed();
|
||||||
MvppMap64Probe.ReportArmed();
|
MvppMap64Probe.ReportArmed();
|
||||||
@@ -878,7 +877,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
info = info.CreateInfoWithFormat(overlap.Info.FormatInfo);
|
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());
|
MvppViewAliasProbe.OnViewCreated(info, overlap, oInfo.FirstLayer, oInfo.FirstLevel, oInfo.Compatibility.ToString());
|
||||||
|
|
||||||
texture = overlap.CreateView(info, sizeInfo, range.Value, oInfo.FirstLayer, oInfo.FirstLevel);
|
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="caps">GPU capabilities</param>
|
||||||
/// <param name="scale">Texture scale factor, to be applied to the texture size</param>
|
/// <param name="scale">Texture scale factor, to be applied to the texture size</param>
|
||||||
/// <returns>The texture creation information</returns>
|
/// <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 =
|
internal static readonly bool DlssSrSpike =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_DLSS_SR") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_DLSS_SR") == "1";
|
||||||
|
|
||||||
@@ -1336,13 +1331,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (DlssSrSpike)
|
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);
|
width = (int)MathF.Ceiling(width * scale);
|
||||||
height = (int)MathF.Ceiling(height * scale);
|
height = (int)MathF.Ceiling(height * scale);
|
||||||
activeWidth = width;
|
activeWidth = width;
|
||||||
@@ -1376,18 +1364,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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)
|
public static TextureCreateInfo ClampViewToStorage(TextureCreateInfo createInfo, ITexture storage, int firstLevel, float scale)
|
||||||
{
|
{
|
||||||
if (!DlssSrSpike || scale == 1f || storage == null || firstLevel <= 0)
|
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="info">Texture information</param>
|
||||||
/// <param name="caps">Host GPU capabilities</param>
|
/// <param name="caps">Host GPU capabilities</param>
|
||||||
/// <returns>A host compatible format</returns>
|
/// <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 =
|
internal static readonly bool ForceMipsMasks =
|
||||||
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_MASKS") == "1";
|
System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_MASKS") == "1";
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="info">Texture information</param>
|
/// <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)
|
public static bool ForceMipsWantsUncompressed(in TextureInfo info)
|
||||||
{
|
{
|
||||||
return ForceMipsMasks &&
|
return ForceMipsMasks &&
|
||||||
@@ -107,7 +97,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4)
|
? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4)
|
||||||
: new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4);
|
: new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.FormatInfo.Format.IsAstcSrgb)
|
if (info.FormatInfo.Format.IsAstcSrgb)
|
||||||
{
|
{
|
||||||
return GraphicsConfig.EnableTextureRecompression
|
return GraphicsConfig.EnableTextureRecompression
|
||||||
|
|||||||
@@ -266,11 +266,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
return dirty;
|
return dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CheckDirtyCallback(int baseHandle, int regionCount, bool split, bool consume)
|
bool CheckDirtyCallback(int baseHandle, int regionCount, bool split, bool consume)
|
||||||
{
|
{
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
|
|
||||||
for (int i = 0; i < regionCount; i++)
|
for (int i = 0; i < regionCount; i++)
|
||||||
{
|
{
|
||||||
TextureGroupHandle group = _handles[baseHandle + i];
|
TextureGroupHandle group = _handles[baseHandle + i];
|
||||||
@@ -301,7 +301,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
EvaluateRelevantHandles(texture, _discardDataCallback, out _);
|
EvaluateRelevantHandles(texture, _discardDataCallback, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DiscardDataCallback(int baseHandle, int regionCount, bool split, bool bound)
|
bool DiscardDataCallback(int baseHandle, int regionCount, bool split, bool bound)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < regionCount; i++)
|
for (int i = 0; i < regionCount; i++)
|
||||||
@@ -363,7 +363,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (group.NeedsCopy && group.Copy(_context))
|
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.
|
anyModified |= true; // The copy target has been modified.
|
||||||
handleDirty = false;
|
handleDirty = false;
|
||||||
}
|
}
|
||||||
@@ -389,7 +389,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
bool partialPath = anyNotDirty || (_handles.Length > 1 && (anyModified || split));
|
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)
|
if (partialPath)
|
||||||
{
|
{
|
||||||
@@ -728,7 +728,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
group.SignalModified(_context);
|
group.SignalModified(_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, out _);
|
}, out _);
|
||||||
}
|
}
|
||||||
@@ -743,10 +743,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
ModifiedSequence = _context.GetModifiedSequence();
|
ModifiedSequence = _context.GetModifiedSequence();
|
||||||
|
|
||||||
ClearIncompatibleOverlaps(texture);
|
ClearIncompatibleOverlaps(texture);
|
||||||
|
|
||||||
EvaluateRelevantHandles(texture, _signalModifyingCallback, out _, bound);
|
EvaluateRelevantHandles(texture, _signalModifyingCallback, out _, bound);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SignalModifyingCallback(int baseHandle, int regionCount, bool split, bool bound)
|
bool SignalModifyingCallback(int baseHandle, int regionCount, bool split, bool bound)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < regionCount; i++)
|
for (int i = 0; i < regionCount; i++)
|
||||||
@@ -755,7 +755,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
group.SignalModifying(bound, _context);
|
group.SignalModifying(bound, _context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,7 +830,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
int targetLevelHandles = _hasMipViews ? levels : 1;
|
int targetLevelHandles = _hasMipViews ? levels : 1;
|
||||||
|
|
||||||
result = false;
|
result = false;
|
||||||
|
|
||||||
if (_isBuffer)
|
if (_isBuffer)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -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;
|
public float RenderTargetScale { get; private set; } = 1f;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public float RenderTargetScaleX { get; private set; } = 1f;
|
public float RenderTargetScaleX { get; private set; } = 1f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exact vertical scale (host/guest ratio) of the currently bound render targets.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float RenderTargetScaleY { get; private set; } = 1f;
|
public float RenderTargetScaleY { get; private set; } = 1f;
|
||||||
|
|
||||||
// [COUTURE PROBE -- TEMPORARY, DISPOSABLE] throttle for the edge-seam hunt log.
|
|
||||||
private long _coutureLogMs;
|
private long _coutureLogMs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -72,7 +67,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
|
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
|
||||||
_rtColorsBound = new bool[Constants.TotalRenderTargets];
|
_rtColorsBound = new bool[Constants.TotalRenderTargets];
|
||||||
|
|
||||||
MvppSeamProbe.Announce(); // one status line per boot, even if no call site ever fires
|
MvppSeamProbe.Announce();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -84,43 +79,36 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_cpBindingsManager.SetBindings(bindings);
|
_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)
|
public void MvppEnumerateComputeBindings(System.Action<bool, bool, Texture> report)
|
||||||
{
|
{
|
||||||
_cpBindingsManager.MvppEnumerateCompute(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)
|
public void MvppEnumerateGraphicsInputs(System.Action<Texture> report)
|
||||||
{
|
{
|
||||||
_gpBindingsManager.MvppEnumerateInputs(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()
|
public int MvppGraphicsArrayBindingCount()
|
||||||
{
|
{
|
||||||
return _gpBindingsManager.MvppArrayBindingCount();
|
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)
|
public void MvppEnumerateGraphicsImages(System.Action<Texture, bool> report)
|
||||||
{
|
{
|
||||||
_gpBindingsManager.MvppEnumerateImages(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)
|
public void MvppEnumerateGraphicsInputsStage(System.Action<int, Texture> report)
|
||||||
{
|
{
|
||||||
_gpBindingsManager.MvppEnumerateInputsStage(report);
|
_gpBindingsManager.MvppEnumerateInputsStage(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>[GLOWPROBE, read-only] Bound textures WITH their sampler.</summary>
|
|
||||||
public void MvppEnumerateGraphicsInputsWithSampler(System.Action<int, Texture, Sampler> report)
|
public void MvppEnumerateGraphicsInputsWithSampler(System.Action<int, Texture, Sampler> report)
|
||||||
{
|
{
|
||||||
_gpBindingsManager.MvppEnumerateInputsWithSampler(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)
|
public void MvppEnumerateRenderTargets(System.Action<int, Texture> report)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _rtColors.Length; i++)
|
for (int i = 0; i < _rtColors.Length; i++)
|
||||||
@@ -212,14 +200,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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)
|
private bool EffectiveScaleMismatch(Texture texture)
|
||||||
{
|
{
|
||||||
return TextureCache.DlssSrSpike &&
|
return TextureCache.DlssSrSpike &&
|
||||||
@@ -236,9 +217,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
public bool SetRenderTargetColor(int index, Texture color)
|
public bool SetRenderTargetColor(int index, Texture color)
|
||||||
{
|
{
|
||||||
bool hasValue = color != null;
|
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)));
|
bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && (RenderTargetScale != color.ScaleFactor || EffectiveScaleMismatch(color)));
|
||||||
|
|
||||||
if (_rtColors[index] != color)
|
if (_rtColors[index] != color)
|
||||||
@@ -256,12 +234,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
color.SynchronizeMemory();
|
color.SynchronizeMemory();
|
||||||
color.SignalModifying(true);
|
color.SignalModifying(true);
|
||||||
color.DemoteForcedMipsOnRenderTargetBind(); // [MIPS-RT fix] no-op hors boost (2 tests de champ)
|
color.DemoteForcedMipsOnRenderTargetBind();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MvppFeedbackProbe.Enabled && color != null)
|
if (MvppFeedbackProbe.Enabled && color != null)
|
||||||
{
|
{
|
||||||
MvppFeedbackProbe.OnWrite(color); // [FEEDBACKPROBE] colour target bound = write (gated)
|
MvppFeedbackProbe.OnWrite(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
_rtColors[index] = color;
|
_rtColors[index] = color;
|
||||||
@@ -278,7 +256,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
public bool SetRenderTargetDepthStencil(Texture depthStencil)
|
public bool SetRenderTargetDepthStencil(Texture depthStencil)
|
||||||
{
|
{
|
||||||
bool hasValue = depthStencil != null;
|
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)
|
if (_rtDepthStencil != depthStencil)
|
||||||
{
|
{
|
||||||
@@ -300,8 +278,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_rtDepthStencil = depthStencil;
|
_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)
|
if (depthStencil != null)
|
||||||
{
|
{
|
||||||
_context.LastPresentDepthStencil = depthStencil;
|
_context.LastPresentDepthStencil = depthStencil;
|
||||||
@@ -311,26 +287,18 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Currently bound depth-stencil render target (if any).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Texture RenderTargetDepthStencil => _rtDepthStencil;
|
public Texture RenderTargetDepthStencil => _rtDepthStencil;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public Texture RenderTargetColor0 => _rtColors[0];
|
public Texture RenderTargetColor0 => _rtColors[0];
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public int ColorTargetsLength => _rtColors.Length;
|
public int ColorTargetsLength => _rtColors.Length;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public Texture GetColorTarget(int index) => (uint)index < (uint)_rtColors.Length ? _rtColors[index] : null;
|
public Texture GetColorTarget(int index) => (uint)index < (uint)_rtColors.Length ? _rtColors[index] : null;
|
||||||
|
|
||||||
@@ -446,10 +414,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
RenderTargetScale = targetScale;
|
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 scaleX = targetScale;
|
||||||
float scaleY = targetScale;
|
float scaleY = targetScale;
|
||||||
|
|
||||||
@@ -498,11 +462,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
RenderTargetScaleX = scaleX;
|
RenderTargetScaleX = scaleX;
|
||||||
RenderTargetScaleY = scaleY;
|
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 &&
|
if (TextureCache.DlssSrSpike && targetScale != 1f &&
|
||||||
Environment.TickCount64 - _coutureLogMs >= 1000)
|
Environment.TickCount64 - _coutureLogMs >= 1000)
|
||||||
{
|
{
|
||||||
@@ -705,12 +664,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void MvppInvalidateHostRenderTargets()
|
public void MvppInvalidateHostRenderTargets()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -598,16 +598,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
int gobBlocksInZ = descriptor.UnpackGobBlocksInZ();
|
int gobBlocksInZ = descriptor.UnpackGobBlocksInZ();
|
||||||
if (target != Target.Texture3D && gobBlocksInZ > 1 && depthOrLayers > 1)
|
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)
|
if (MvppGobProbe.Enabled)
|
||||||
{
|
{
|
||||||
MvppGobProbe.NoteClamp(width, height, depthOrLayers, gobBlocksInZ, target.ToString(), formatInfo.Format.ToString());
|
MvppGobProbe.NoteClamp(width, height, depthOrLayers, gobBlocksInZ, target.ToString(), formatInfo.Format.ToString());
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// End address of the buffer in guest memory.
|
/// End address of the buffer in guest memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong EndAddress => Address + Size;
|
public ulong EndAddress => Address + Size;
|
||||||
|
|
||||||
public Buffer Next { get; set; }
|
public Buffer Next { get; set; }
|
||||||
public Buffer Previous { get; set; }
|
public Buffer Previous { get; set; }
|
||||||
|
|
||||||
@@ -283,14 +283,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="address">Start address of the range to synchronize</param>
|
/// <param name="address">Start address of the range to synchronize</param>
|
||||||
/// <param name="size">Size in bytes 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 =
|
private static readonly bool _bufSeqForce =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_BUFSEQ") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_BUFSEQ") == "1";
|
||||||
|
|
||||||
@@ -298,17 +290,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
private static long _bufSeqCalls;
|
private static long _bufSeqCalls;
|
||||||
private static long _bufSeqLogMs;
|
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 =
|
private static readonly bool _bufCoalEnabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_BUFCOAL") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_BUFCOAL") == "1";
|
||||||
|
|
||||||
@@ -323,10 +304,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
private ulong _coalEnd;
|
private ulong _coalEnd;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private void LoadRegionCoalesced(ulong mAddress, ulong mSize)
|
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()
|
private void FlushCoalesced()
|
||||||
{
|
{
|
||||||
if (_coalStart != ulong.MaxValue)
|
if (_coalStart != ulong.MaxValue)
|
||||||
@@ -416,7 +391,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
BackingState.RecordSet();
|
BackingState.RecordSet();
|
||||||
_context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size));
|
_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();
|
CopyToDependantVirtualBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +421,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
if (_bufCoalEnabled)
|
if (_bufCoalEnabled)
|
||||||
{
|
{
|
||||||
// [BUFCOAL] la fenetre en attente ne survit jamais a la synchro qui l'a produite.
|
|
||||||
FlushCoalesced();
|
FlushCoalesced();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,7 +492,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_referenceCount == 0)
|
if (_referenceCount == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -541,7 +515,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncPreRangeAction(ulong address, ulong size)
|
void SyncPreRangeAction(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
_preFlush.CopyModified(address, size);
|
_preFlush.CopyModified(address, size);
|
||||||
@@ -573,7 +547,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncRangeAction(ulong address, ulong size)
|
void SyncRangeAction(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
_memoryTrackingGranular.RegisterAction(address, size, _externalFlushDelegate);
|
_memoryTrackingGranular.RegisterAction(address, size, _externalFlushDelegate);
|
||||||
@@ -587,7 +561,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public void InheritModifiedRanges(Buffer from)
|
public void InheritModifiedRanges(Buffer from)
|
||||||
{
|
{
|
||||||
from._bufferInherited = true;
|
from._bufferInherited = true;
|
||||||
|
|
||||||
if (from._modifiedRanges is { HasRanges: true })
|
if (from._modifiedRanges is { HasRanges: true })
|
||||||
{
|
{
|
||||||
if (from._syncActionRegistered && !_syncActionRegistered)
|
if (from._syncActionRegistered && !_syncActionRegistered)
|
||||||
@@ -710,7 +684,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
_context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize));
|
_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);
|
CopyToDependantVirtualBuffers(mAddress, mSize);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,19 +523,19 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
address = Math.Min(address, overlaps[0].Address);
|
address = Math.Min(address, overlaps[0].Address);
|
||||||
endAddress = Math.Max(endAddress, overlaps[^1].EndAddress);
|
endAddress = Math.Max(endAddress, overlaps[^1].EndAddress);
|
||||||
|
|
||||||
for (int i = 0; i < overlaps.Length; i++)
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
{
|
{
|
||||||
anySparseCompatible |= overlaps[i].SparseCompatible;
|
anySparseCompatible |= overlaps[i].SparseCompatible;
|
||||||
}
|
}
|
||||||
|
|
||||||
Buffer[] overlapsArray = overlaps.ToArray();
|
Buffer[] overlapsArray = overlaps.ToArray();
|
||||||
|
|
||||||
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
||||||
|
|
||||||
ulong newSize = endAddress - address;
|
ulong newSize = endAddress - address;
|
||||||
|
|
||||||
_buffers.Add(CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray));
|
_buffers.Add(CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray));
|
||||||
@@ -560,7 +560,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment)
|
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment)
|
||||||
{
|
{
|
||||||
bool sparseAligned = alignment >= SparseBufferAlignmentSize;
|
bool sparseAligned = alignment >= SparseBufferAlignmentSize;
|
||||||
|
|
||||||
ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
ReadOnlySpan<Buffer> overlaps = _buffers.FindOverlapsAsSpan(address, size);
|
||||||
|
|
||||||
if (overlaps.Length != 0)
|
if (overlaps.Length != 0)
|
||||||
@@ -582,7 +582,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
endAddress = Math.Max(endAddress, overlaps[^1].EndAddress);
|
endAddress = Math.Max(endAddress, overlaps[^1].EndAddress);
|
||||||
int oldOverlapCount;
|
int oldOverlapCount;
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
address = Math.Min(address, overlaps[0].Address);
|
address = Math.Min(address, overlaps[0].Address);
|
||||||
@@ -596,11 +596,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
while (oldOverlapCount != overlaps.Length);
|
while (oldOverlapCount != overlaps.Length);
|
||||||
|
|
||||||
ulong newSize = endAddress - address;
|
ulong newSize = endAddress - address;
|
||||||
|
|
||||||
Buffer[] overlapsArray = overlaps.ToArray();
|
Buffer[] overlapsArray = overlaps.ToArray();
|
||||||
|
|
||||||
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
_buffers.RemoveRange(overlaps[0], overlaps[^1]);
|
||||||
|
|
||||||
_buffers.Add(CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray));
|
_buffers.Add(CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -608,7 +608,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
{
|
{
|
||||||
// No overlap, just create a new buffer.
|
// No overlap, just create a new buffer.
|
||||||
_buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseAligned, []));
|
_buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseAligned, []));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -643,7 +643,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
NotifyBuffersModified?.Invoke();
|
NotifyBuffersModified?.Invoke();
|
||||||
|
|
||||||
RecreateMultiRangeBuffers(address, size);
|
RecreateMultiRangeBuffers(address, size);
|
||||||
|
|
||||||
return newBuffer;
|
return newBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +690,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy);
|
MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy);
|
||||||
MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, 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);
|
MvppPalProbe.OnGpuCopy(dstRange.GetSubRange(0).Address, size);
|
||||||
|
|
||||||
if (srcRange.Count == 1 && dstRange.Count == 1)
|
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);
|
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);
|
MvppPalProbe.OnXfbBind(index, range);
|
||||||
|
|
||||||
_transformFeedbackBuffers[index] = new BufferBounds(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));
|
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);
|
MvppPalProbe.OnComputeBind(index, range, (int)flags);
|
||||||
|
|
||||||
_cpStorageBuffers.SetBounds(index, range, 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));
|
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);
|
MvppPalProbe.OnBind(stage, index, range, _channel.MemoryManager.Physical);
|
||||||
|
|
||||||
if (!buffers.Buffers[index].Range.Equals(range))
|
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.Common.Logging;
|
||||||
using Ryujinx.Memory.Range;
|
using Ryujinx.Memory.Range;
|
||||||
@@ -9,32 +6,12 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Memory
|
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
|
static class MvppPalProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_PAL_PROBE") == "1";
|
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 MaxTracked = 32;
|
||||||
private const int HashStride = 4093;
|
private const int HashStride = 4093;
|
||||||
private const int AnomalyLogCap = 24;
|
private const int AnomalyLogCap = 24;
|
||||||
@@ -62,10 +39,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
private static long _anomaliesLogged;
|
private static long _anomaliesLogged;
|
||||||
private static long _summaryMs;
|
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)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,7 +76,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
if (rec.SeenThisFrame)
|
if (rec.SeenThisFrame)
|
||||||
{
|
{
|
||||||
return; // one hash per buffer per frame
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rec.SeenThisFrame = true;
|
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();
|
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)
|
public static void OnComputeBind(int index, MultiRange range, int flags)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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, int)> _xfbBinds = new();
|
||||||
private static readonly HashSet<(ulong, ulong)> _copyHits = 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)
|
public static void OnXfbBind(int index, MultiRange range)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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)
|
public static void OnGpuCopy(ulong dstAddress, ulong size)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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)
|
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);
|
MvppUpVolProbe.OnUpload(address, size, site, pm);
|
||||||
|
|
||||||
if (!Enabled)
|
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()
|
public static void OnPresent()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
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 Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
@@ -8,18 +5,6 @@ using System.Threading;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Memory
|
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
|
static class MvppUpVolProbe
|
||||||
{
|
{
|
||||||
public static readonly bool Enabled =
|
public static readonly bool Enabled =
|
||||||
@@ -34,15 +19,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
private static long _site0Bytes;
|
private static long _site0Bytes;
|
||||||
private static long _site1Calls;
|
private static long _site1Calls;
|
||||||
private static long _site1Bytes;
|
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[] _bucketCalls = new long[4];
|
||||||
private static readonly long[] _bucketBytes = 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 ulong _benchAddr;
|
||||||
private static long _benchSize;
|
private static long _benchSize;
|
||||||
private static PhysicalMemory _benchPm;
|
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()
|
private static void RunMirrorBench()
|
||||||
{
|
{
|
||||||
long benchSize = Interlocked.Exchange(ref _benchSize, 0);
|
long benchSize = Interlocked.Exchange(ref _benchSize, 0);
|
||||||
@@ -139,7 +116,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
if (pm == null || benchSize < 0x4000)
|
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);
|
int sz = (int)Math.Min(benchSize, 0x40000);
|
||||||
@@ -152,10 +129,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
|
|
||||||
try
|
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);
|
ReadOnlySpan<byte> src = pm.GetSpan(benchAddr, sz);
|
||||||
long t0 = System.Diagnostics.Stopwatch.GetTimestamp();
|
long t0 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
src.CopyTo(_benchScratch);
|
src.CopyTo(_benchScratch);
|
||||||
@@ -163,7 +136,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
src.CopyTo(_benchScratch);
|
src.CopyTo(_benchScratch);
|
||||||
long t2 = System.Diagnostics.Stopwatch.GetTimestamp();
|
long t2 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
// Meme taille, source = tableau gere ordinaire (chaud)
|
|
||||||
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
|
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
|
||||||
long t3 = System.Diagnostics.Stopwatch.GetTimestamp();
|
long t3 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
|
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
_data.RenderScale.AsSpan().Fill(defaultScale);
|
_data.RenderScale.AsSpan().Fill(defaultScale);
|
||||||
DirtyRenderScale(0, SupportBuffer.RenderScaleMaxCount);
|
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.X = 0f;
|
||||||
_data.JitterOffset.Y = 0f;
|
_data.JitterOffset.Y = 0f;
|
||||||
MarkDirty(SupportBuffer.JitterOffsetOffset, SupportBuffer.FieldSize);
|
MarkDirty(SupportBuffer.JitterOffsetOffset, SupportBuffer.FieldSize);
|
||||||
@@ -99,11 +93,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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)
|
public void SetJitter(float x, float y)
|
||||||
{
|
{
|
||||||
if (_data.JitterOffset.X != x || _data.JitterOffset.Y != y)
|
if (_data.JitterOffset.X != x || _data.JitterOffset.Y != y)
|
||||||
@@ -133,7 +123,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public void UpdateRenderScale(int index, float scale)
|
public void UpdateRenderScale(int index, float scale)
|
||||||
{
|
{
|
||||||
Span<Vector4<float>> renderScaleSpan = _data.RenderScale.AsSpan();
|
Span<Vector4<float>> renderScaleSpan = _data.RenderScale.AsSpan();
|
||||||
|
|
||||||
if (renderScaleSpan[1 + index].X != scale)
|
if (renderScaleSpan[1 + index].X != scale)
|
||||||
{
|
{
|
||||||
renderScaleSpan[1 + index].X = scale;
|
renderScaleSpan[1 + index].X = scale;
|
||||||
@@ -164,7 +154,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
|||||||
public void SetRenderTargetIsBgra(int index, bool isBgra)
|
public void SetRenderTargetIsBgra(int index, bool isBgra)
|
||||||
{
|
{
|
||||||
Span<Vector4<int>> fragmentIsBgraSpan = _data.FragmentIsBgra.AsSpan();
|
Span<Vector4<int>> fragmentIsBgraSpan = _data.FragmentIsBgra.AsSpan();
|
||||||
|
|
||||||
bool isBgraChanged = fragmentIsBgraSpan[index].X != 0 != isBgra;
|
bool isBgraChanged = fragmentIsBgraSpan[index].X != 0 != isBgra;
|
||||||
|
|
||||||
if (isBgraChanged)
|
if (isBgraChanged)
|
||||||
|
|||||||
@@ -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.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
@@ -9,24 +6,13 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu
|
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
|
static class MvppDestProbe
|
||||||
{
|
{
|
||||||
private static bool _enabled =
|
private static bool _enabled =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DESTPROBE") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DESTPROBE") == "1";
|
||||||
|
|
||||||
private const int MaxWritesLogged = 400; // hard cap on DEST-WRITE lines
|
private const int MaxWritesLogged = 400;
|
||||||
private const int StackSampleWrites = 60; // capture a caller stack only for the first N matches (cost control)
|
private const int StackSampleWrites = 60;
|
||||||
|
|
||||||
private static readonly HashSet<ulong> _presentedAddrs = new();
|
private static readonly HashSet<ulong> _presentedAddrs = new();
|
||||||
private static int _frame;
|
private static int _frame;
|
||||||
@@ -34,18 +20,14 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
private static int _writesLogged;
|
private static int _writesLogged;
|
||||||
private static int _stackSamples;
|
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 readonly HashSet<ulong> _trackedAddrs = new();
|
||||||
private static int _trackedLogged;
|
private static int _trackedLogged;
|
||||||
private const int MaxTrackedLogged = 200;
|
private const int MaxTrackedLogged = 200;
|
||||||
|
|
||||||
public static bool Enabled => _enabled;
|
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);
|
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)
|
public static void AddTracked(ulong addr)
|
||||||
{
|
{
|
||||||
if (_enabled && addr != 0 && _trackedAddrs.Count < 32)
|
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;
|
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)
|
public static void RegisterPresented(Image.Texture tex)
|
||||||
{
|
{
|
||||||
if (!_enabled || tex == null)
|
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)
|
public static void OnModify(Image.Texture tex, bool viaModifying, bool bound)
|
||||||
{
|
{
|
||||||
if (!_enabled || tex == null || (_presentedAddrs.Count == 0 && _trackedAddrs.Count == 0))
|
if (!_enabled || tex == null || (_presentedAddrs.Count == 0 && _trackedAddrs.Count == 0))
|
||||||
@@ -96,7 +75,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignalModifying(false) is an UNBIND, not a write -- ignore it.
|
|
||||||
if (viaModifying && !bound)
|
if (viaModifying && !bound)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -112,8 +90,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
return;
|
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 (isTracked && !isPres)
|
||||||
{
|
{
|
||||||
if (_trackedLogged >= MaxTrackedLogged)
|
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()
|
private static string Caller()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
|||||||
private const ushort FileFormatVersionMajor = 1;
|
private const ushort FileFormatVersionMajor = 1;
|
||||||
private const ushort FileFormatVersionMinor = 2;
|
private const ushort FileFormatVersionMinor = 2;
|
||||||
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
|
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 uint CodeGenVersion = 7355;
|
||||||
|
|
||||||
private const string SharedTocFileName = "shared.toc";
|
private const string SharedTocFileName = "shared.toc";
|
||||||
@@ -326,11 +322,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
|||||||
throw new DiskCacheLoadException(DiskCacheLoadResult.IncompatibleVersion);
|
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;
|
bool loadHostCache = header.CodeGenVersion == CodeGenVersion && !_mvppCodegenProbeActive;
|
||||||
|
|
||||||
int programIndex = 0;
|
int programIndex = 0;
|
||||||
@@ -566,9 +557,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
|||||||
/// <param name="program">Cached program</param>
|
/// <param name="program">Cached program</param>
|
||||||
/// <param name="hostCode">Optional host binary code</param>
|
/// <param name="hostCode">Optional host binary code</param>
|
||||||
/// <param name="streams">Output streams to use</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 =
|
private static readonly bool _mvppCodegenProbeActive =
|
||||||
Ryujinx.Graphics.Shader.Translation.MvppPositionProbe.Enabled ||
|
Ryujinx.Graphics.Shader.Translation.MvppPositionProbe.Enabled ||
|
||||||
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.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)
|
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)
|
if (_mvppCodegenProbeActive)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -849,9 +836,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
|||||||
dataInfo.UsesInstanceId,
|
dataInfo.UsesInstanceId,
|
||||||
dataInfo.UsesDrawParameters,
|
dataInfo.UsesDrawParameters,
|
||||||
dataInfo.UsesRtLayer,
|
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,
|
false,
|
||||||
dataInfo.ClipDistancesWritten,
|
dataInfo.ClipDistancesWritten,
|
||||||
dataInfo.FragmentOutputMap);
|
dataInfo.FragmentOutputMap);
|
||||||
|
|||||||
@@ -59,20 +59,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
|||||||
private readonly Dictionary<ulong, CachedShaderProgram> _cpPrograms;
|
private readonly Dictionary<ulong, CachedShaderProgram> _cpPrograms;
|
||||||
private readonly Dictionary<ShaderAddresses, CachedShaderProgram> _gpPrograms;
|
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;
|
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 =
|
private static readonly string _mvppDumpDir =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DUMPSPV");
|
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DUMPSPV");
|
||||||
private static int _mvppDumpCount;
|
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 =
|
private static readonly bool _dumpMap =
|
||||||
Environment.GetEnvironmentVariable("RYUJINX_DUMP_MAP") == "1";
|
Environment.GetEnvironmentVariable("RYUJINX_DUMP_MAP") == "1";
|
||||||
|
|
||||||
@@ -331,9 +323,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
|||||||
ShaderAddresses addresses,
|
ShaderAddresses addresses,
|
||||||
TranslationFlags extraFlags = TranslationFlags.None)
|
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;
|
bool isVariant = extraFlags != TranslationFlags.None;
|
||||||
Dictionary<ShaderAddresses, CachedShaderProgram> gpCache = isVariant ? _gpVariantPrograms : _gpPrograms;
|
Dictionary<ShaderAddresses, CachedShaderProgram> gpCache = isVariant ? _gpVariantPrograms : _gpPrograms;
|
||||||
|
|
||||||
@@ -342,9 +331,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
|||||||
return gpShaders;
|
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);
|
bool diskHit = _graphicsShaderCache.TryFind(channel, ref poolState, ref graphicsState, addresses, out gpShaders, out CachedGraphicsGuestCode cachedGuestCode);
|
||||||
|
|
||||||
if (diskHit && !isVariant)
|
if (diskHit && !isVariant)
|
||||||
@@ -497,9 +483,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
|||||||
|
|
||||||
if (isVariant)
|
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)
|
if (_mvppDumpDir != null && _mvppDumpCount < 2048)
|
||||||
{
|
{
|
||||||
try
|
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;
|
_gpVariantPrograms[addresses] = gpShaders;
|
||||||
|
|
||||||
Logger.Info?.Print(LogClass.Gpu,
|
Logger.Info?.Print(LogClass.Gpu,
|
||||||
@@ -653,7 +634,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
|||||||
|
|
||||||
Span<TfState> tfStateSpan = state.TfState.AsSpan();
|
Span<TfState> tfStateSpan = state.TfState.AsSpan();
|
||||||
Span<Array32<uint>> tfVaryingLocationsSpan = state.TfVaryingLocations.AsSpan();
|
Span<Array32<uint>> tfVaryingLocationsSpan = state.TfVaryingLocations.AsSpan();
|
||||||
|
|
||||||
for (int i = 0; i < Constants.TotalTransformFeedbackBuffers; i++)
|
for (int i = 0; i < Constants.TotalTransformFeedbackBuffers; i++)
|
||||||
{
|
{
|
||||||
TfState tf = tfStateSpan[i];
|
TfState tf = tfStateSpan[i];
|
||||||
@@ -854,8 +835,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
|||||||
|
|
||||||
ShaderDumpPaths paths = dumper?.Dump(code, context.Stage == ShaderStage.Compute) ?? default;
|
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)
|
if (_dumpMap && paths.HasPath)
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Gpu,
|
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.TextureAndSampler, ResourceType.BufferTexture, textureSetIndex, textureBinding, texturesPerStage);
|
||||||
AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, imageSetIndex, imageBinding, imagesPerStage);
|
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)
|
foreach (BufferDescriptor cb in info.CBuffers)
|
||||||
{
|
{
|
||||||
if (cb.Binding == Ryujinx.Graphics.Shader.Translation.MvppVariantInjector.MvppN1HostBinding)
|
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 &&
|
if (Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled &&
|
||||||
info.Stage == ShaderStage.Fragment)
|
info.Stage == ShaderStage.Fragment)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,19 +58,13 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
public object UserObj { get; }
|
public object UserObj { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public float JitterX { get; }
|
public float JitterX { get; }
|
||||||
public float JitterY { 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; }
|
public long JitterFrameId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public System.Numerics.Matrix4x4 CameraVp { get; }
|
public System.Numerics.Matrix4x4 CameraVp { get; }
|
||||||
public bool CameraVpValid { get; }
|
public bool CameraVpValid { get; }
|
||||||
@@ -119,12 +113,12 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
private int _framesAvailable;
|
private int _framesAvailable;
|
||||||
private long _mvppDepthLogMs;
|
private long _mvppDepthLogMs;
|
||||||
private long _mvppFifoLogMs;
|
private long _mvppFifoLogMs;
|
||||||
private long _jitTraceMs; // [JITTRACE] cadence de l'etape B
|
private long _jitTraceMs;
|
||||||
private int _mvppTakes;
|
private int _mvppTakes;
|
||||||
private int _mvppTakeHits;
|
private int _mvppTakeHits;
|
||||||
private Image.Texture _mvppHeldSceneDepth;
|
private Image.Texture _mvppHeldSceneDepth;
|
||||||
private int _mvppHeldSceneDepthSeq;
|
private int _mvppHeldSceneDepthSeq;
|
||||||
private long _colorPubDiagMs; // [COLORCAP snapshot] throttled publish diagnostic
|
private long _colorPubDiagMs;
|
||||||
|
|
||||||
public bool IsFrameAvailable => _framesAvailable != 0;
|
public bool IsFrameAvailable => _framesAvailable != 0;
|
||||||
|
|
||||||
@@ -213,16 +207,10 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
|
|
||||||
MultiRange range = new(address, (ulong)size);
|
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 jitterX = DlssJitterState.Enabled ? DlssJitterState.OffsetX : 0f;
|
||||||
float jitterY = DlssJitterState.Enabled ? DlssJitterState.OffsetY : 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;
|
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;
|
System.Numerics.Matrix4x4 cameraVp = default;
|
||||||
bool cameraVpValid = DlssCameraState.Enabled && DlssCameraState.SnapshotCurrent(out cameraVp);
|
bool cameraVpValid = DlssCameraState.Enabled && DlssCameraState.SnapshotCurrent(out cameraVp);
|
||||||
if (DlssCameraState.Enabled)
|
if (DlssCameraState.Enabled)
|
||||||
@@ -230,10 +218,8 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
Engine.Threed.MvppCameraCapture.OnFrameEnqueued();
|
Engine.Threed.MvppCameraCapture.OnFrameEnqueued();
|
||||||
}
|
}
|
||||||
|
|
||||||
// MV++ Phase 2.0: frame boundary for the draw-identity matching probe (no-op flag off).
|
|
||||||
Engine.Threed.MvppP2Probe.OnFrameEnqueued();
|
Engine.Threed.MvppP2Probe.OnFrameEnqueued();
|
||||||
|
|
||||||
// MV++ Phase 2.2: frame boundary for the velocity pass (coverage clear).
|
|
||||||
Engine.Threed.MvppVelocityPass.OnFrameEnqueued();
|
Engine.Threed.MvppVelocityPass.OnFrameEnqueued();
|
||||||
|
|
||||||
_frameQueue.Enqueue(new PresentationTexture(
|
_frameQueue.Enqueue(new PresentationTexture(
|
||||||
@@ -270,55 +256,38 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
|
|
||||||
pt.Cache.Tick();
|
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);
|
Engine.Threed.MvppPreSyncProbe.Before(texture);
|
||||||
|
|
||||||
texture.SynchronizeMemory();
|
texture.SynchronizeMemory();
|
||||||
|
|
||||||
Engine.Threed.MvppPreSyncProbe.After(texture);
|
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.MvppLayoutProbe.Compare(pt.Info, texture);
|
||||||
Engine.Threed.MvppPreSyncProbe.DumpGuest(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);
|
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.MvppUiProbe.OnPresent();
|
||||||
Engine.Threed.MvppDrawStepProbe.OnPresent(texture);
|
Engine.Threed.MvppDrawStepProbe.OnPresent(texture);
|
||||||
Engine.Twod.MvppTwodProbe.OnPresent();
|
Engine.Twod.MvppTwodProbe.OnPresent();
|
||||||
Image.MvppCacheProbe.OnPresent();
|
Image.MvppCacheProbe.OnPresent();
|
||||||
Image.MvppMvBufProbe.OnPresent(); // [MVBUF] frame boundary (self-gated)
|
Image.MvppMvBufProbe.OnPresent();
|
||||||
Image.MvppMvSyncProbe.OnPresent(); // [MVSYNC] arming witness + heartbeat (self-gated)
|
Image.MvppMvSyncProbe.OnPresent();
|
||||||
Image.MvppWriterCensusProbe.OnPresent(); // [CENSUS] arming witness + heartbeat (self-gated)
|
Image.MvppWriterCensusProbe.OnPresent();
|
||||||
Image.MvppTwinXferProbe.OnPresent(); // [TWINXFER] arming witness + heartbeat (self-gated)
|
Image.MvppTwinXferProbe.OnPresent();
|
||||||
Memory.MvppPalProbe.OnPresent(); // [PAL] frame boundary (self-gated)
|
Memory.MvppPalProbe.OnPresent();
|
||||||
Image.MvppTaaProbe.OnPresent(); // [TAAPROBE] frontiere d'image (auto-gardee)
|
Image.MvppTaaProbe.OnPresent();
|
||||||
// [JITTRACE] cadence de l'etape B, voir plus bas.
|
|
||||||
|
|
||||||
if (Image.MvppFeedbackProbe.Enabled)
|
if (Image.MvppFeedbackProbe.Enabled)
|
||||||
{
|
{
|
||||||
Image.MvppFeedbackProbe.OnFrameBoundary(); // [FEEDBACKPROBE] guest frame boundary (gated)
|
Image.MvppFeedbackProbe.OnFrameBoundary();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Image.MvppTraceProbe.Enabled)
|
if (Image.MvppTraceProbe.Enabled)
|
||||||
{
|
{
|
||||||
Image.MvppTraceProbe.OnFrameBoundary(); // [TRACEPROBE] dependency-graph report (gated)
|
Image.MvppTraceProbe.OnFrameBoundary();
|
||||||
}
|
}
|
||||||
|
|
||||||
float cropScaleX = texture.EffectiveScaleX;
|
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);
|
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.PresentX = pt.JitterX;
|
||||||
DlssJitterState.PresentY = pt.JitterY;
|
DlssJitterState.PresentY = pt.JitterY;
|
||||||
DlssJitterState.PresentFrameId = pt.JitterFrameId;
|
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)
|
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);
|
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 orderedJx = Engine.Threed.MvppSoloCamera.LastJitterX;
|
||||||
float orderedJy = Engine.Threed.MvppSoloCamera.LastJitterY;
|
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;
|
_ = orderedJx;
|
||||||
_ = orderedJy;
|
_ = orderedJy;
|
||||||
DlssCameraState.PublishPresent(in orderedVp, orderedValid);
|
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)
|
if (Engine.Threed.MvppFamHold.Enabled)
|
||||||
{
|
{
|
||||||
Engine.Threed.MvppFamHold.OnPresent(DlssCameraState.LastConsumeFresh, in orderedVp);
|
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)
|
if (Engine.Threed.MvppExtProbe.Enabled)
|
||||||
{
|
{
|
||||||
Engine.Threed.MvppExtProbe.OnPresent(
|
Engine.Threed.MvppExtProbe.OnPresent(
|
||||||
DlssCameraState.LastConsumeFresh, in orderedVp, DlssCameraState.TeleportSeq);
|
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 &&
|
if (Engine.Threed.MvppCameraCapture.JitTrace &&
|
||||||
Environment.TickCount64 - _jitTraceMs >= 1000)
|
Environment.TickCount64 - _jitTraceMs >= 1000)
|
||||||
{
|
{
|
||||||
@@ -452,19 +377,11 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
DlssCameraState.PublishPresent(pt.CameraVp, pt.CameraVpValid);
|
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;
|
Image.Texture presentDepth = null;
|
||||||
if (DlssCameraState.Enabled)
|
if (DlssCameraState.Enabled)
|
||||||
{
|
{
|
||||||
Image.Texture scene = Engine.Threed.MvppCameraCapture.TakeSceneDepth();
|
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)
|
if (scene != null)
|
||||||
{
|
{
|
||||||
_mvppHeldSceneDepth = scene;
|
_mvppHeldSceneDepth = scene;
|
||||||
@@ -472,9 +389,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
}
|
}
|
||||||
else if (_mvppHeldSceneDepth != null && _mvppHeldSceneDepth.InvalidatedSequence != _mvppHeldSceneDepthSeq)
|
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;
|
_mvppHeldSceneDepth = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +403,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
_mvppTakeHits++;
|
_mvppTakeHits++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MV++ diag: what depth is published at present time? (heartbeat, capture runs only)
|
|
||||||
if (MvppDev.Enabled && DlssCameraState.Enabled && Environment.TickCount64 - _mvppDepthLogMs >= 5000)
|
if (MvppDev.Enabled && DlssCameraState.Enabled && Environment.TickCount64 - _mvppDepthLogMs >= 5000)
|
||||||
{
|
{
|
||||||
_mvppDepthLogMs = Environment.TickCount64;
|
_mvppDepthLogMs = Environment.TickCount64;
|
||||||
@@ -506,9 +419,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
Engine.Threed.MvppCameraCapture.StatSquareRejects = 0;
|
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)
|
if (GAL.MvppColorSnapshot.Enabled)
|
||||||
{
|
{
|
||||||
Image.Texture sceneColor = Engine.Threed.MvppCameraCapture.TakeSceneColor();
|
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)
|
public int DecodeFrame(Surface output, ReadOnlySpan<byte> bitstream)
|
||||||
{
|
{
|
||||||
FFmpegApi.av_frame_unref(output.Frame);
|
FFmpegApi.av_frame_unref(output.Frame);
|
||||||
@@ -130,8 +168,11 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg
|
|||||||
result = _decodeFrame(_context, output.Frame, &gotFrame, _packet);
|
result = _decodeFrame(_context, output.Frame, &gotFrame, _packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool delayedTried = false;
|
||||||
|
|
||||||
if (gotFrame == 0)
|
if (gotFrame == 0)
|
||||||
{
|
{
|
||||||
|
delayedTried = true;
|
||||||
FFmpegApi.av_frame_unref(output.Frame);
|
FFmpegApi.av_frame_unref(output.Frame);
|
||||||
|
|
||||||
// If the frame was not delivered, it was probably delayed.
|
// 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);
|
FFmpegApi.av_frame_unref(output.Frame);
|
||||||
|
|
||||||
|
if (_vidProbe)
|
||||||
|
{
|
||||||
|
VidProbeTick(1, 0, delayedTried ? 1 : 0, 1, result);
|
||||||
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_vidProbe)
|
||||||
|
{
|
||||||
|
VidProbeTick(1, 1, delayedTried ? 1 : 0, result < 0 ? 1 : 0, result < 0 ? result : 0);
|
||||||
|
}
|
||||||
|
|
||||||
return 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
Reference in New Issue
Block a user