The Beast Roofer Edition v1.2.4
Removes a bug introduced in v1.2.3: the DLSS clip-space jitter block was emitted into every vertex shader and read the Position output before writing it. Reading an unwritten output is undefined, so vertex positions could come out as garbage - whole 3D passes landing nowhere (the green screen reported on GYLT, Ghostbusters, Blair Witch and others) and the black block artifacts in Xenoblade Chronicles 2's motion-blur pass, which had been mistaken for a long-standing emulation defect and worked around by disabling the effect. With jitter off - how this fork ships - the vertex epilogue is now identical to the base emulator again, and the motion blur no longer needs to be disabled. CodeGenVersion bumped so existing shader caches are rebuilt. Also adds a DLSS sharpening slider (0-100, off by default) and an experimental advanced motion stack checkbox (off by default).
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# Release notes v1.2.3 — The Beast Roofer Edition (EN + FR)
|
||||
|
||||
_Draft for the Gitea release page. Bilingual per fork rule. No internal jargon._
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
### Improved: cloud and sky ghosting with DLSS — now also with native-resolution 4K mods
|
||||
|
||||
Two improvements in one:
|
||||
|
||||
- **Native-resolution configurations** (emulator resolution 1x + an in-engine 4K render
|
||||
mod, e.g. TOTK Optimizer at 3840x2160): the DLSS motion-quality system was **silently
|
||||
inactive** in this configuration — clouds and sky ghosted exactly as raw DLSS. It now
|
||||
engages in every resolution configuration, scaled or native.
|
||||
- **Camera zooms** (aiming a bow, spyglass): the anti-transition protection used to mute
|
||||
the sky handling during every zoom, letting clouds ghost briefly in broad daylight. It
|
||||
now only triggers on real scene cuts (teleports, shrine transitions), never on zooms.
|
||||
|
||||
**Known behavior:** during roughly the first minute after loading a save, some cloud
|
||||
ghosting can appear, fade, and come back while the system arms itself (it has to find the
|
||||
game's camera, its depth buffer, and build up motion history). It then settles for the
|
||||
rest of the session. This release is an **improvement, not perfection** — brief cloud
|
||||
ghosting can still occur in specific moments.
|
||||
|
||||
### Improved: black lines / dark frame echo during camera movement
|
||||
|
||||
The recurring "thin black frame" some players saw while panning the camera had a real
|
||||
mechanism behind it: the game leaves a 2-pixel black border on its rendered image at
|
||||
scaled resolutions, and the DLSS history smears that border inward during camera motion —
|
||||
a dotted echo proportional to pan speed. The border is now repaired before DLSS ever sees
|
||||
the image. As a side effect, cloud trails that were fed by the same polluted history are
|
||||
also reduced.
|
||||
|
||||
### Fixed: crash when toggling fullscreen (F11) or resizing the window with DLSS active
|
||||
|
||||
Toggling fullscreen or resizing while DLSS ran could crash the emulator (GPU errors on the
|
||||
swapchain recreation path). The recreation sequence was rebuilt — the old chain and its
|
||||
views now retire only once the display is done with them, and the acquire loop follows the
|
||||
Vulkan rules to the letter. Validated with hundreds of consecutive fullscreen toggles.
|
||||
|
||||
### New: Frame Generation self-recovery
|
||||
|
||||
Frame Generation could silently stop injecting frames (display back to 1x, no error
|
||||
anywhere) until a fullscreen toggle happened to revive it. A watchdog now detects that
|
||||
state and applies the same remedy automatically within ~8 seconds. If you ever need to
|
||||
disable it: set the environment variable `RYUJINX_DLSS_FG_WATCHDOG=0`.
|
||||
|
||||
### Fixed: game list could come up empty after a crash
|
||||
|
||||
If the emulator was killed or crashed at the exact moment it was saving its configuration,
|
||||
the config file could be truncated — and your game directories list came up empty on the
|
||||
next launch. Configuration files are now written atomically: a crash can no longer damage
|
||||
them.
|
||||
|
||||
### Fixed: changing DLSS settings could restart with a partial state
|
||||
|
||||
Changing certain DLSS settings restarts the emulator; the restart could come back up with
|
||||
only part of the motion-quality profile applied (cloud ghosting returning, border fix
|
||||
lost). The full profile is now re-derived from scratch on every restart.
|
||||
|
||||
### Fixed: texture corruption with "Force mipmaps" + native-resolution 4K mods
|
||||
|
||||
With the (off by default) *Force mipmaps* option enabled **and** an in-engine 4K render
|
||||
mod at native emulator resolution, procedural textures (shrine portals, some terrain)
|
||||
could show purple/rainbow corruption, most often around shrine transitions. Cause: mip
|
||||
levels were being generated behind small render targets the game redraws every frame. Such
|
||||
textures are now excluded automatically the moment the game renders into them. Art
|
||||
textures keep the full benefit of the option.
|
||||
|
||||
### Better diagnostics
|
||||
|
||||
When the DLSS motion-quality system cannot run, it now says why in the log ("camera not
|
||||
paired", "depth not paired"...) instead of staying silent. If you report an issue, your
|
||||
log will tell us much more than before.
|
||||
|
||||
### Reminder — hardware requirements for DLSS
|
||||
|
||||
A lot of reports boil down to this, so let's be explicit:
|
||||
|
||||
- **DLSS and DLAA require an NVIDIA RTX GPU** (RTX 20 series or newer). They **cannot**
|
||||
work on GTX cards (10/16 series) — the hardware simply lacks the units DLSS runs on.
|
||||
This is an NVIDIA limitation, not something the fork can patch around.
|
||||
- **Frame Generation requires an RTX 40 series or newer.**
|
||||
- **No RTX card? Use NIS** (NVIDIA Image Scaling, included in this fork): it runs on any
|
||||
GPU and remains the recommended upscaler for GTX and non-NVIDIA cards.
|
||||
- A small number of RTX configurations still crash when DLSS activates; this is under
|
||||
active investigation — see the pinned diagnostic-build ticket if you are affected.
|
||||
|
||||
### Testing scope
|
||||
|
||||
This release was built and validated on a single machine — Zelda TOTK and BOTW as main
|
||||
test games, both scaled and native-4K-mod configurations, plus a package-level test of
|
||||
the exact archives published here. Other
|
||||
GPUs, drivers and games may behave differently: if something regresses for you, open an
|
||||
issue with your log and it becomes tomorrow's fix.
|
||||
|
||||
---
|
||||
|
||||
## Français
|
||||
|
||||
### Amélioré : ghosting des nuages et du ciel avec DLSS — maintenant aussi avec les mods 4K en résolution native
|
||||
|
||||
Deux améliorations en une :
|
||||
|
||||
- **Configurations en résolution native** (résolution émulateur 1x + un mod de rendu 4K
|
||||
in-engine, ex. TOTK Optimizer en 3840x2160) : le système de qualité de mouvement DLSS
|
||||
était **silencieusement inactif** dans cette configuration — les nuages et le ciel
|
||||
ghostaient comme avec le DLSS brut. Il s'active désormais dans toutes les configurations
|
||||
de résolution, scalées ou natives.
|
||||
- **Zooms de caméra** (visée à l'arc, longue-vue) : la protection anti-transitions coupait
|
||||
le traitement du ciel pendant chaque zoom, laissant les nuages ghoster brièvement en
|
||||
plein jour. Elle ne se déclenche plus que sur les vraies coupures de scène (téléportations,
|
||||
transitions de sanctuaire), jamais sur les zooms.
|
||||
|
||||
**Comportement connu :** pendant environ la première minute après le chargement d'une
|
||||
partie, un peu de ghosting de nuages peut apparaître, s'estomper et revenir pendant que le
|
||||
système s'arme (il doit trouver la caméra du jeu, son tampon de profondeur, et bâtir son
|
||||
historique de mouvement). Ensuite ça se stabilise pour le reste de la session. Cette
|
||||
version est une **amélioration, pas la perfection** — un bref ghosting de nuages reste
|
||||
possible dans certains moments précis.
|
||||
|
||||
### Amélioré : lignes noires / écho de cadre sombre pendant les mouvements de caméra
|
||||
|
||||
Le fameux « cadre noir fin » que certains joueurs voyaient en tournant la caméra avait un
|
||||
vrai mécanisme derrière : le jeu laisse une bordure noire de 2 pixels sur son image aux
|
||||
résolutions scalées, et l'historique DLSS étale cette bordure vers l'intérieur pendant le
|
||||
mouvement — un écho pointillé proportionnel à la vitesse du pan. La bordure est désormais
|
||||
réparée avant que DLSS ne voie l'image. Effet de bord bienvenu : les traînées de nuages
|
||||
nourries par ce même historique pollué sont réduites elles aussi.
|
||||
|
||||
### Corrigé : plantage au passage plein écran (F11) ou au redimensionnement avec DLSS actif
|
||||
|
||||
Basculer en plein écran ou redimensionner la fenêtre pendant que DLSS tournait pouvait
|
||||
faire planter l'émulateur (erreurs GPU sur le chemin de recréation du swapchain). La
|
||||
séquence de recréation a été rebâtie — l'ancienne chaîne et ses vues ne partent que quand
|
||||
l'affichage en a fini avec elles, et la boucle d'acquisition suit les règles Vulkan à la
|
||||
lettre. Validé avec des centaines de bascules plein écran consécutives.
|
||||
|
||||
### Nouveau : auto-récupération de la génération d'images (Frame Generation)
|
||||
|
||||
La génération d'images pouvait s'arrêter en silence (affichage revenu à 1x, aucune erreur
|
||||
nulle part) jusqu'à ce qu'un passage plein écran la ressuscite par hasard. Un chien de
|
||||
garde détecte maintenant cet état et applique le même remède automatiquement en ~8
|
||||
secondes. Pour le désactiver au besoin : variable d'environnement
|
||||
`RYUJINX_DLSS_FG_WATCHDOG=0`.
|
||||
|
||||
### Corrigé : liste de jeux vide après un plantage
|
||||
|
||||
Si l'émulateur était tué ou plantait au moment exact où il sauvegardait sa configuration,
|
||||
le fichier de config pouvait être tronqué — et votre liste de dossiers de jeux revenait
|
||||
vide au lancement suivant. Les fichiers de configuration sont désormais écrits de façon
|
||||
atomique : un plantage ne peut plus les abîmer.
|
||||
|
||||
### Corrigé : changer les réglages DLSS pouvait redémarrer avec un état partiel
|
||||
|
||||
Changer certains réglages DLSS redémarre l'émulateur ; le redémarrage pouvait revenir avec
|
||||
seulement une partie du profil de qualité de mouvement appliquée (ghosting de nuages de
|
||||
retour, correctif de bordure perdu). Le profil complet est maintenant reconstruit de zéro
|
||||
à chaque redémarrage.
|
||||
|
||||
### Corrigé : corruption de textures avec « Forcer les mipmaps » + mods 4K en résolution native
|
||||
|
||||
Avec l'option *Forcer les mipmaps* (désactivée par défaut) **et** un mod de rendu 4K
|
||||
in-engine en résolution émulateur native, des textures procédurales (portails de
|
||||
sanctuaire, certains terrains) pouvaient montrer une corruption mauve/arc-en-ciel, surtout
|
||||
autour des transitions de sanctuaire. Cause : des niveaux de mip étaient générés derrière
|
||||
de petites cibles de rendu que le jeu redessine chaque frame. Ces textures sont désormais
|
||||
exclues automatiquement dès que le jeu dessine dedans. Les textures d'art gardent tout le
|
||||
bénéfice de l'option.
|
||||
|
||||
### Meilleurs diagnostics
|
||||
|
||||
Quand le système de qualité de mouvement DLSS ne peut pas tourner, il dit maintenant
|
||||
pourquoi dans le journal (« caméra non appariée », « profondeur non appariée »...) au lieu
|
||||
de rester muet. Si vous rapportez un problème, votre journal nous en dira beaucoup plus
|
||||
qu'avant.
|
||||
|
||||
### Rappel — matériel requis pour le DLSS
|
||||
|
||||
Beaucoup de rapports se résument à ceci, alors soyons explicites :
|
||||
|
||||
- **Le DLSS et le DLAA exigent une carte NVIDIA RTX** (série RTX 20 ou plus récente). Ils
|
||||
**ne peuvent pas** fonctionner sur les cartes GTX (séries 10/16) — le matériel n'a tout
|
||||
simplement pas les unités sur lesquelles le DLSS tourne. C'est une limite NVIDIA, pas
|
||||
quelque chose que le fork peut contourner.
|
||||
- **La génération d'images (Frame Generation) exige une RTX série 40 ou plus récente.**
|
||||
- **Pas de carte RTX ? Utilisez NIS** (NVIDIA Image Scaling, inclus dans ce fork) : il
|
||||
fonctionne sur n'importe quel GPU et reste l'upscaler recommandé pour les cartes GTX et
|
||||
non-NVIDIA.
|
||||
- Un petit nombre de configurations RTX plantent encore à l'activation du DLSS ; c'est en
|
||||
cours d'investigation active — voyez le ticket épinglé de build diagnostic si vous êtes
|
||||
concerné.
|
||||
|
||||
### Portée des tests
|
||||
|
||||
Cette version a été bâtie et validée sur une seule machine — Zelda TOTK et BOTW comme
|
||||
jeux de test principaux, en configurations scalée et 4K natif avec mod, plus un test au
|
||||
niveau du paquet sur les archives exactes publiées ici. D'autres GPU, pilotes et jeux peuvent se comporter différemment : si quelque chose
|
||||
régresse chez vous, ouvrez un ticket avec votre journal et ça devient le correctif de
|
||||
demain.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Release notes v1.2.4 — The Beast Roofer Edition (EN + FR)
|
||||
|
||||
_Pour la page de release Gitea. Bilingue (règle du fork). Sans jargon interne._
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
I am shipping this update because I caught a bug I had introduced myself in v1.2.3, the
|
||||
one that turns the screen green in some games. It could in principle affect any game, so
|
||||
please take this update even if yours looked fine.
|
||||
|
||||
One correction while I am at it: the black block artifacts in Xenoblade Chronicles 2, the
|
||||
ones I had described as a long-standing emulation defect and worked around by skipping the
|
||||
motion-blur pass, were caused by that same bug of mine. They were never an old emulator
|
||||
problem, they were mine. They are gone now, and the motion blur stays on.
|
||||
|
||||
### Fixed: green screen in several games (GYLT, Ghostbusters: Spirits Unleashed, and others)
|
||||
|
||||
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
|
||||
cause was a real bug in this fork, introduced in v1.2.3: a constant used by the DLSS
|
||||
system was never delivered to the GPU when DLSS jitter was off, and every 3D shader ended
|
||||
up reading uninitialized memory. Depending on what the graphics driver had left in that
|
||||
memory, a game could look perfectly fine, glitch occasionally, or go fully green.
|
||||
|
||||
The offending code has been removed: with jitter off — which is how this fork ships — the
|
||||
vertex shaders are now translated exactly as the base emulator does, so there is nothing
|
||||
left to corrupt. To be clear about what this is: we broke it, and we undid it.
|
||||
|
||||
The same bug also produced black block artifacts in motion-blur scenes on this fork (that
|
||||
pass expands points into quads; hand it undefined vertex positions and it draws blocks).
|
||||
Those are gone too, and the motion blur no longer has to be disabled to avoid them.
|
||||
Verified on the previously-broken games. If a specific title still shows issues, it is a
|
||||
separate cause — please report it on the community.
|
||||
|
||||
**Every v1.2.3 user should take this update**: the bug could in principle touch any
|
||||
game, even ones that appeared to work.
|
||||
|
||||
### New: DLSS Sharpening slider
|
||||
|
||||
A **DLSS Sharpening** slider (0–100) in Settings → Graphics, right under Frame Generation.
|
||||
It applies the same contrast-adaptive sharpening used by the FSR filter, directly on the
|
||||
DLSS output — HDR is handled correctly and it costs almost nothing. 0 = off (the default;
|
||||
existing setups are unchanged). Around 25–40 is subtle; higher is noticeably crisper but
|
||||
can make halos and shimmer more visible in motion. Applied on the next launch, like the
|
||||
other DLSS settings.
|
||||
|
||||
An experimental `Advanced motion stack` checkbox is also present, **off by default**. It
|
||||
is validated on one engine family only and can add trembling in motion elsewhere — leave
|
||||
it off unless you want to experiment.
|
||||
|
||||
### Known issue: Frame Generation still produces ghost images
|
||||
|
||||
With Frame Generation enabled, ghost or "temporal" images can still appear — doubled
|
||||
edges and trails on moving content. **Turning Frame Generation off removes them.** This
|
||||
is the number one thing being worked on right now: the generated frames are built from
|
||||
motion vectors we reconstruct, and those describe the camera better than they describe
|
||||
individual moving objects. If ghosting bothers you more than the extra smoothness helps,
|
||||
set Frame Generation to Off in the graphics settings.
|
||||
|
||||
### Known issue: Xenoblade Chronicles 2 can hang while loading
|
||||
|
||||
On Xenoblade Chronicles 2, loading (or the menu) occasionally stops progressing — the
|
||||
picture keeps drawing, but the game stops advancing. Closing and reopening the emulator
|
||||
clears it. We captured this live several times and it is **not solved yet**: the freeze
|
||||
happens on the game's own side, and it predates the changes in this release. If you hit
|
||||
it, closing and relaunching is the workaround for now.
|
||||
|
||||
---
|
||||
|
||||
## Français
|
||||
|
||||
J'envoie cette mise à jour parce que j'ai attrapé un bug que j'avais moi-même introduit en
|
||||
v1.2.3, celui qui rend l'écran vert dans certains jeux. Il pouvait en principe toucher
|
||||
n'importe quel jeu, alors prenez la mise à jour même si le vôtre semblait correct.
|
||||
|
||||
Une correction au passage : les blocs noirs dans Xenoblade Chronicles 2, ceux que j'avais
|
||||
décrits comme un vieux défaut d'émulation et que je contournais en désactivant la passe de
|
||||
flou de mouvement, venaient du même bug — le mien. Ça n'a jamais été un problème ancien de
|
||||
l'émulateur, c'était moi. Ils sont partis, et le flou de mouvement reste actif.
|
||||
|
||||
### Corrigé : écran vert dans plusieurs jeux (GYLT, Ghostbusters: Spirits Unleashed, et d'autres)
|
||||
|
||||
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.
|
||||
La cause était un vrai bug de ce fork, introduit en v1.2.3 : une constante du système
|
||||
DLSS n'était jamais livrée au GPU quand le jitter DLSS était éteint, et tous les shaders
|
||||
3D lisaient de la mémoire non initialisée. Selon ce que le pilote graphique avait laissé
|
||||
dans cette mémoire, un jeu pouvait sembler parfait, glitcher par moments, ou devenir
|
||||
entièrement vert.
|
||||
|
||||
Le code fautif a été retiré : avec le jitter éteint — c'est ainsi que ce fork est livré —
|
||||
les vertex shaders sont désormais traduits exactement comme le fait l'émulateur de base,
|
||||
il n'y a donc plus rien à corrompre. Pour être clair sur ce que c'est : on l'avait cassé,
|
||||
on l'a défait.
|
||||
|
||||
Le même bug produisait aussi les blocs noirs dans les scènes avec flou de mouvement sur ce
|
||||
fork (cette passe dilate des points en carrés ; donnez-lui des positions indéfinies, elle
|
||||
dessine des blocs). Ils disparaissent également, et il n'est plus nécessaire de désactiver
|
||||
le flou de mouvement pour les éviter. Vérifié sur les jeux qui étaient cassés. Si un titre
|
||||
précis montre encore des problèmes, c'est une cause distincte — signalez-le sur la
|
||||
communauté.
|
||||
|
||||
**Tous les utilisateurs de la v1.2.3 devraient prendre cette mise à jour** : le bug
|
||||
pouvait en principe toucher n'importe quel jeu, même ceux qui semblaient fonctionner.
|
||||
|
||||
### Nouveau : curseur de netteté DLSS
|
||||
|
||||
Un curseur **DLSS Sharpening** (0–100) dans Paramètres → Graphismes, juste sous la Frame
|
||||
Generation. Il applique la même netteté adaptative que le filtre FSR, directement sur la
|
||||
sortie DLSS — le HDR est géré correctement et le coût est négligeable. 0 = désactivé (le
|
||||
défaut ; les configurations existantes ne changent pas). Autour de 25–40 c'est subtil ;
|
||||
plus haut, c'est nettement plus croustillant mais les halos et le scintillement en
|
||||
mouvement deviennent plus visibles. Appliqué au prochain lancement, comme les autres
|
||||
réglages DLSS.
|
||||
|
||||
Une case `Advanced motion stack` expérimentale est également présente, **désactivée par
|
||||
défaut**. Elle n'est validée que sur une famille de moteur et peut au contraire ajouter du
|
||||
tremblement en mouvement ailleurs — laissez-la éteinte sauf si vous voulez expérimenter.
|
||||
|
||||
### Limite connue : la Frame Generation produit encore des images fantômes
|
||||
|
||||
Avec la Frame Generation activée, des images fantômes ou « temporelles » peuvent encore
|
||||
apparaître — contours dédoublés et traînées sur ce qui bouge. **Désactiver la Frame
|
||||
Generation les fait disparaître.** C'est le chantier numéro un en ce moment : les images
|
||||
générées sont construites à partir de vecteurs de mouvement que nous reconstruisons, et
|
||||
ceux-ci décrivent mieux la caméra que les objets qui bougent individuellement. Si les
|
||||
fantômes vous gênent plus que la fluidité supplémentaire ne vous apporte, mettez la Frame
|
||||
Generation sur Off dans les paramètres graphiques.
|
||||
|
||||
### Limite connue : Xenoblade Chronicles 2 peut se bloquer au chargement
|
||||
|
||||
Sur Xenoblade Chronicles 2, le chargement (ou le menu) cesse parfois d'avancer — l'image
|
||||
continue de s'afficher, mais le jeu n'avance plus. Fermer et rouvrir l'émulateur règle le
|
||||
problème. Nous l'avons capturé en direct à plusieurs reprises et il **n'est pas encore
|
||||
résolu** : le blocage se produit du côté du jeu lui-même, et il est antérieur aux
|
||||
changements de cette version. Si ça vous arrive, fermer et relancer reste le contournement
|
||||
pour l'instant.
|
||||
@@ -38,6 +38,14 @@ namespace Ryujinx.Graphics.GAL
|
||||
public static Matrix4x4 PresentVp;
|
||||
public static bool PresentVpValid;
|
||||
|
||||
/// <summary>
|
||||
/// [GAMEJITTER 28/07] Le decalage sous-pixel que LE JEU appliquait a l'image presentee, en
|
||||
/// NDC (x_ndc += Jx). Republie au dequeue, comme <see cref="PresentVp"/>, donc apparie a la
|
||||
/// meme image par le meme ordre. En pixels : Jx * largeurRendu / 2.
|
||||
/// </summary>
|
||||
public static float PresentJitterX;
|
||||
public static float PresentJitterY;
|
||||
|
||||
// Present hand-off (GPU thread -> render thread). TryConsumeOrdered below pairs the game
|
||||
// camera to the presented frame ON the GPU thread; this ring carries that paired result
|
||||
// across to the render thread. A single static slot raced here: the GPU thread overwrote
|
||||
@@ -46,14 +54,18 @@ namespace Ryujinx.Graphics.GAL
|
||||
// exactly the heavy scenes where DLSS matters most). Order does the pairing instead: one
|
||||
// entry enqueued per GAL present (GPU thread), one dequeued per executed present (render
|
||||
// thread), so a lagging consumer still reads the camera of the frame it is presenting.
|
||||
private static readonly ConcurrentQueue<(Matrix4x4 Vp, bool Valid)> _presentRing = new();
|
||||
private static readonly ConcurrentQueue<(Matrix4x4 Vp, bool Valid, float Jx, float Jy)> _presentRing = new();
|
||||
|
||||
public static void PublishPresent(in Matrix4x4 vp, bool valid) => PublishPresent(in vp, valid, 0f, 0f);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues the camera paired with the frame being enqueued for presentation (GPU thread).
|
||||
/// [GAMEJITTER] Le decalage sous-pixel voyage dans le MEME anneau, donc il ne peut pas se
|
||||
/// desynchroniser de la camera ni de l'image. La surcharge sans decalage pousse zero.
|
||||
/// </summary>
|
||||
public static void PublishPresent(in Matrix4x4 vp, bool valid)
|
||||
public static void PublishPresent(in Matrix4x4 vp, bool valid, float jx, float jy)
|
||||
{
|
||||
_presentRing.Enqueue((vp, valid));
|
||||
_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
|
||||
@@ -72,10 +84,12 @@ namespace Ryujinx.Graphics.GAL
|
||||
/// </summary>
|
||||
public static void ConsumePresent()
|
||||
{
|
||||
if (_presentRing.TryDequeue(out (Matrix4x4 Vp, bool Valid) entry))
|
||||
if (_presentRing.TryDequeue(out (Matrix4x4 Vp, bool Valid, float Jx, float Jy) entry))
|
||||
{
|
||||
PresentVp = entry.Vp;
|
||||
PresentVpValid = entry.Valid;
|
||||
PresentJitterX = entry.Jx;
|
||||
PresentJitterY = entry.Jy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +134,46 @@ namespace Ryujinx.Graphics.GAL
|
||||
/// </summary>
|
||||
public static bool GuestDepthMinusOneToOne;
|
||||
|
||||
/// <summary>
|
||||
/// [CUTONJUMP 27/07] Incremented by the camera source whenever the guest's camera makes a
|
||||
/// jump no continuous movement can explain - a warp, a zone load, a cutscene taking over.
|
||||
/// The upscaler compares it against the value it last saw and, on a change, resets its
|
||||
/// history for that frame.
|
||||
///
|
||||
/// It is a SEQUENCE and not a flag on purpose: a flag can be set and cleared between two
|
||||
/// reads and vanish, and it also makes "how many jumps happened" unanswerable. A counter
|
||||
/// only ever moves forward, so the consumer cannot miss one and the count is a diagnostic
|
||||
/// on its own.
|
||||
///
|
||||
/// This carries no world units and no threshold: the decision is taken at the source,
|
||||
/// where the camera's own recent motion provides the scale. See MvppSoloCamera.
|
||||
/// </summary>
|
||||
public static int TeleportSeq;
|
||||
|
||||
/// <summary>
|
||||
/// [DUPSKIP 02/08] Séquence des présentations invitées portant AU MOINS UN dessin (une
|
||||
/// « vraie » image rendue). Producteur : MvppCameraCapture à la frontière de present
|
||||
/// (Interlocked, gaté RYUJINX_DLSS_DUPSKIP). Consommateur : DlssUpscaler.TryRun — si la
|
||||
/// séquence n'a pas bougé depuis son dernier Evaluate, la présentation courante est un
|
||||
/// DOUBLON (le jeu re-présente sans avoir rendu) : on re-blitte la sortie précédente au
|
||||
/// lieu de ré-accumuler le temporel dessus (journal (348)-(350) : BOTW ~30 vraies
|
||||
/// images/s en rotation caméra, présentées ~60 → « images fantômes »). Même patron que
|
||||
/// TeleportSeq — une séquence, pas un drapeau (et PAS la FIFO : voir l'avertissement
|
||||
/// du 28/07 ci-dessous). 0 = producteur jamais armé → le consommateur reste inerte
|
||||
/// (garde-fou anti-gel).
|
||||
/// </summary>
|
||||
public static long RenderedFrameSeq;
|
||||
|
||||
// ⛔ NE PAS CHANGER LE TYPE D'ELEMENT DE CETTE FILE. Essaye le 28/07 : j'y avais ajoute deux
|
||||
// flottants pour faire voyager le decalage sous-pixel avec la matrice. Resultat mesure a la
|
||||
// trace : la capture poussait bien (etape A non nulle) et le present trouvait la file VIDE
|
||||
// (etape B, valide=False, frais=False), alors que la meme file donnait pushes=30 fresh=30
|
||||
// holds=0 quelques heures plus tot. `Queue<T>` n'est pas protegee contre les acces
|
||||
// simultanes -- le commentaire ci-dessus affirme un seul thread, mais les journaux montrent
|
||||
// OnDrawImpl d'un cote et Present de l'autre. Grossir l'element d'une structure deja limite
|
||||
// suffit a la faire rendre du vide, et l'appariement camera s'effondre avec.
|
||||
//
|
||||
// Le decalage passe donc par l'ANNEAU DE PRESENTATION, qui est une ConcurrentQueue.
|
||||
private static readonly Queue<Matrix4x4> _fifo = new();
|
||||
private static Matrix4x4 _fifoLast;
|
||||
private static bool _fifoHasLast;
|
||||
@@ -133,6 +187,12 @@ namespace Ryujinx.Graphics.GAL
|
||||
public static int StatDrops;
|
||||
public static int StatMaxDepth;
|
||||
|
||||
// [28/07] NE RIEN AJOUTER ICI SANS Y PENSER A DEUX FOIS. Ce fichier vit dans
|
||||
// Ryujinx.Graphics.GAL, et la copie de test tourne sur un GAL.dll du 10/07 : y ajouter un
|
||||
// champ oblige a redeployer cette DLL, donc a injecter d'un coup tous les changements de
|
||||
// GAL accumules depuis. Fait le 28/07 a 10h07 pour une simple sonde de journal, suivi d'un
|
||||
// defaut neuf en jeu. Faire transiter les sondes autrement.
|
||||
|
||||
/// <summary>[JITTERVAL temporal probe, 10/07] Result of the most recent
|
||||
/// <see cref="TryConsumeOrdered"/>: true = fresh dequeue, false = hold of the last camera.
|
||||
/// Log-only (read by the gated JITTERVAL-PRES/INJ lines); nothing in the render path reads it.</summary>
|
||||
@@ -141,6 +201,10 @@ namespace Ryujinx.Graphics.GAL
|
||||
/// <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)
|
||||
{
|
||||
_fifo.Enqueue(vp);
|
||||
@@ -165,6 +229,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
/// static camera no new value was written, so the last consumed one still describes
|
||||
/// this frame.
|
||||
/// </summary>
|
||||
// [28/07] Restauree A L'IDENTIQUE de la version qui donnait pushes=30 fresh=30 holds=0.
|
||||
public static bool TryConsumeOrdered(out Matrix4x4 vp)
|
||||
{
|
||||
if (_fifo.TryDequeue(out vp))
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
Register<BufferDisposeCommand>(CommandType.BufferDispose);
|
||||
Register<BufferGetDataCommand>(CommandType.BufferGetData);
|
||||
Register<BufferSetDataCommand>(CommandType.BufferSetData);
|
||||
Register<BufferSetDataBatchCommand>(CommandType.BufferSetDataBatch); // [BUFBATCH]
|
||||
|
||||
Register<CounterEventDisposeCommand>(CommandType.CounterEventDispose);
|
||||
Register<CounterEventFlushCommand>(CommandType.CounterEventFlush);
|
||||
|
||||
@@ -113,5 +113,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
TryHostConditionalRendering,
|
||||
TryHostConditionalRenderingFlush,
|
||||
MvppInjectDlss,
|
||||
BufferSetDataBatch,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Graphics.GAL.Multithreading.Model;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer
|
||||
{
|
||||
/// <summary>
|
||||
/// [BUFBATCH 02/08, journal (368)-(369)] Une entree du lot : ou ecrire (buffer, offset) et
|
||||
/// combien d'octets consommer dans l'arene du lot (les donnees sont concatenees dans l'ordre).
|
||||
/// </summary>
|
||||
struct BufferSetDataBatchEntry
|
||||
{
|
||||
public BufferHandle Buffer;
|
||||
public int Offset;
|
||||
public int Size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [BUFBATCH] Lot de SetBufferData consecutifs : K entrees + une arene de donnees concatenees,
|
||||
/// portees par DEUX SpanRef (l'element de queue ne grossit pas — voir l'avertissement
|
||||
/// « grossir l'element de la Queue la brise »). Ordre pool FIFO : le producteur insere les
|
||||
/// ENTREES puis l'ARENE ; le consommateur consomme dans le meme ordre (copie des entrees en
|
||||
/// pile avant Dispose, car Get/Dispose du pool sont strictement sequentiels).
|
||||
/// </summary>
|
||||
struct BufferSetDataBatchCommand : IGALCommand, IGALCommand<BufferSetDataBatchCommand>
|
||||
{
|
||||
public readonly CommandType CommandType => CommandType.BufferSetDataBatch;
|
||||
private SpanRef<BufferSetDataBatchEntry> _entries;
|
||||
private SpanRef<byte> _data;
|
||||
private int _count;
|
||||
|
||||
public void Set(SpanRef<BufferSetDataBatchEntry> entries, SpanRef<byte> data, int count)
|
||||
{
|
||||
_entries = entries;
|
||||
_data = data;
|
||||
_count = count;
|
||||
}
|
||||
|
||||
public static void Run(ref BufferSetDataBatchCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||
{
|
||||
// Copie des entrees AVANT Dispose : le pool est un anneau, Dispose libere la zone
|
||||
// pour le producteur — on ne garde jamais un Span pool au-dela de son Dispose.
|
||||
Span<BufferSetDataBatchEntry> entries = command._count <= 64
|
||||
? stackalloc BufferSetDataBatchEntry[64]
|
||||
: new BufferSetDataBatchEntry[command._count];
|
||||
|
||||
command._entries.Get(threaded)[..command._count].CopyTo(entries);
|
||||
command._entries.Dispose(threaded);
|
||||
entries = entries[..command._count];
|
||||
|
||||
ReadOnlySpan<byte> data = command._data.Get(threaded);
|
||||
|
||||
int dataOffset = 0;
|
||||
foreach (BufferSetDataBatchEntry entry in entries)
|
||||
{
|
||||
renderer.SetBufferData(threaded.Buffers.MapBuffer(entry.Buffer), entry.Offset, data.Slice(dataOffset, entry.Size));
|
||||
dataOffset += entry.Size;
|
||||
}
|
||||
|
||||
command._data.Dispose(threaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,85 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
return _spanPool.Insert(data);
|
||||
}
|
||||
|
||||
// [BUFBATCH 02/08, journal (368)-(369)] Lots de SetBufferData consecutifs
|
||||
// (RYUJINX_BUFBATCH=1, OFF par defaut = chemin stock). Mesure (362)-(368) : le jeu ecrit
|
||||
// ses buffers au tick invite (~40-50 000 uploads/s CONSTANTS) ; a ~6 us de machinerie par
|
||||
// commande, le cout par IMAGE RENDUE explose quand la cadence chute (boucle de
|
||||
// retroaction, marches 12-20 img/s). Ce gate accumule les SetBufferData CONSECUTIFS dans
|
||||
// une arene productrice privee et n'emet qu'UNE commande par lot. L'ordre global est
|
||||
// preserve : TOUTE commande d'un autre type flushe d'abord le lot en attente (crochet
|
||||
// unique dans New<T>). Le lot ne detient AUCUNE ressource du pool avant son flush.
|
||||
// Serialisation : memes hypotheses que le producteur stock (New/_producerPtr non
|
||||
// verrouilles) — quiconque a le droit d'enqueue a le droit d'accumuler.
|
||||
private static readonly bool _bufBatchEnabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_BUFBATCH") == "1";
|
||||
|
||||
private const int BufBatchMaxEntries = 256;
|
||||
private const int BufBatchArenaBytes = 512 * 1024;
|
||||
|
||||
private BufferSetDataBatchEntry[] _bufBatchEntries;
|
||||
private byte[] _bufBatchArena;
|
||||
private int _bufBatchCount;
|
||||
private int _bufBatchArenaUsed;
|
||||
private bool _bufBatchArmedLogged;
|
||||
|
||||
private unsafe void BatchBufferSetData(BufferHandle buffer, int offset, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (!_bufBatchArmedLogged)
|
||||
{
|
||||
_bufBatchArmedLogged = true;
|
||||
_bufBatchEntries = new BufferSetDataBatchEntry[BufBatchMaxEntries];
|
||||
_bufBatchArena = new byte[BufBatchArenaBytes];
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"[BUFBATCH] ARME (RYUJINX_BUFBATCH=1) - lots de SetBufferData (max {BufBatchMaxEntries} entrees / {BufBatchArenaBytes / 1024} Ko).");
|
||||
}
|
||||
|
||||
// Trop gros pour l'arene : flush du lot puis chemin stock (l'ordre reste correct).
|
||||
if (data.Length > BufBatchArenaBytes)
|
||||
{
|
||||
FlushBufferBatch();
|
||||
New<BufferSetDataCommand>()->Set(buffer, offset, CopySpan(data));
|
||||
QueueCommand();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_bufBatchCount == BufBatchMaxEntries || _bufBatchArenaUsed + data.Length > BufBatchArenaBytes)
|
||||
{
|
||||
FlushBufferBatch();
|
||||
}
|
||||
|
||||
data.CopyTo(_bufBatchArena.AsSpan(_bufBatchArenaUsed, data.Length));
|
||||
_bufBatchEntries[_bufBatchCount++] = new BufferSetDataBatchEntry
|
||||
{
|
||||
Buffer = buffer,
|
||||
Offset = offset,
|
||||
Size = 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()
|
||||
{
|
||||
if (_bufBatchCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int count = _bufBatchCount;
|
||||
int used = _bufBatchArenaUsed;
|
||||
_bufBatchCount = 0;
|
||||
_bufBatchArenaUsed = 0;
|
||||
|
||||
SpanRef<BufferSetDataBatchEntry> entries = CopySpan<BufferSetDataBatchEntry>(_bufBatchEntries.AsSpan(0, count));
|
||||
SpanRef<byte> data = CopySpan<byte>(_bufBatchArena.AsSpan(0, used));
|
||||
|
||||
New<BufferSetDataBatchCommand>()->Set(entries, data, count);
|
||||
QueueCommand();
|
||||
}
|
||||
|
||||
private TableRef<T> Ref<T>(T reference)
|
||||
{
|
||||
return new TableRef<T>(this, reference);
|
||||
@@ -169,6 +248,14 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
|
||||
internal unsafe T* New<T>() where T : unmanaged, IGALCommand
|
||||
{
|
||||
// [BUFBATCH] point d'ordre unique : toute commande d'un AUTRE type vide d'abord le
|
||||
// lot de SetBufferData en attente. typeof(T) est resolu par le JIT par
|
||||
// instanciation ; gate OFF => _bufBatchCount reste 0 => branche morte.
|
||||
if (_bufBatchEnabled && _bufBatchCount != 0 && typeof(T) != typeof(BufferSetDataBatchCommand))
|
||||
{
|
||||
FlushBufferBatch();
|
||||
}
|
||||
|
||||
while (_producerPtr == (Volatile.Read(ref _consumerPtr) + QueueCount - 1) % QueueCount)
|
||||
{
|
||||
// If incrementing the producer pointer would overflow, we need to wait.
|
||||
@@ -460,6 +547,13 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
|
||||
public unsafe void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (_bufBatchEnabled)
|
||||
{
|
||||
BatchBufferSetData(buffer, offset, data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
New<BufferSetDataCommand>()->Set(buffer, offset, CopySpan(data));
|
||||
QueueCommand();
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
|
||||
_context.Renderer.Pipeline.DispatchCompute(qmd.CtaRasterWidth, qmd.CtaRasterHeight, qmd.CtaRasterDepth);
|
||||
|
||||
MvppBlitProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); // read-only (gated)
|
||||
Threed.MvppDrawStepProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); // read-only (gated)
|
||||
|
||||
_3dEngine.ForceShaderUpdate();
|
||||
}
|
||||
|
||||
@@ -216,6 +216,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
|
||||
_3dEngine.CreatePendingSyncs();
|
||||
_3dEngine.FlushUboDirty();
|
||||
|
||||
// [TWINXFER] Raw copy-engine tap, BEFORE any branch (buffer-domain blind spot). Self-gated.
|
||||
Image.MvppTwinXferProbe.OnDma(srcGpuVa, dstGpuVa, xCount, yCount, copy2D, srcLinear, dstLinear);
|
||||
|
||||
if (copy2D)
|
||||
{
|
||||
// Buffer to texture copy.
|
||||
@@ -325,6 +328,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
|
||||
target.PropagateScale(source);
|
||||
}
|
||||
|
||||
Image.MvppTwinXferProbe.OnCopy("DMA", source, target); // [TWINXFER] read-only, self-gated
|
||||
source.HostTexture.CopyTo(target.HostTexture, 0, 0);
|
||||
target.SignalModified();
|
||||
return;
|
||||
|
||||
@@ -105,6 +105,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
}
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_UBO_FORCEDIRTY=1 (EXP 11, gated OFF by default): always mark
|
||||
// the inline-updated UBO range dirty, ignoring the redundancy check's verdict. The XC2
|
||||
// velocity chain's only per-frame data path is the constant buffers (journal 141: every
|
||||
// vertex SSBO is static); if the redundancy check ever wrongly reports "unchanged" (e.g. a
|
||||
// game double-write making guest memory match while the HOST copy is still old), the GPU
|
||||
// reads one-frame-stale matrices -- garbage velocities, only in motion, per draw. Forcing
|
||||
// the dirty flag costs a redundant upload but cannot be wrong. Artifact gone with this on
|
||||
// => the redundancy-check family is the root; see journal (142).
|
||||
private static readonly bool _uboForceDirty =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_UBO_FORCEDIRTY") == "1";
|
||||
|
||||
private static long _uboForceDirtyHits;
|
||||
private static long _uboForceDirtyLogMs;
|
||||
|
||||
/// <summary>
|
||||
/// Flushes any queued UBO updates.
|
||||
/// </summary>
|
||||
@@ -116,9 +130,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
Span<byte> data = MemoryMarshal.Cast<int, byte>(_ubData.AsSpan(0, (int)(_ubByteCount / 4)));
|
||||
|
||||
if (memoryManager.Physical.WriteWithRedundancyCheck(_ubBeginCpuAddress, data))
|
||||
bool changed = memoryManager.Physical.WriteWithRedundancyCheck(_ubBeginCpuAddress, data);
|
||||
|
||||
if (changed || _uboForceDirty)
|
||||
{
|
||||
memoryManager.Physical.BufferCache.ForceDirty(memoryManager, _ubFollowUpAddress - _ubByteCount, _ubByteCount);
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
// Only reached with the experiment on: these are exactly the flushes the
|
||||
// redundancy check would have skipped. Their count is the witness.
|
||||
_uboForceDirtyHits++;
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _uboForceDirtyLogMs >= 3000)
|
||||
{
|
||||
_uboForceDirtyLogMs = now;
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"[UBOFD] redundancy-skipped flushes forced dirty so far: {_uboForceDirtyHits}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ubFollowUpAddress = 0;
|
||||
|
||||
@@ -125,12 +125,29 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private void DrawEnd(ThreedClass engine, int firstIndex, int indexCount, int drawFirstVertex, int drawVertexCount)
|
||||
{
|
||||
MvppProbe.OnDraw(_channel);
|
||||
MvppScanProbe.OnDraw(_channel);
|
||||
MvppHazardProbe.OnDraw(_channel);
|
||||
MvppRtDumpProbe.OnDraw(_channel);
|
||||
MvppGlowProbe.OnDraw(_channel, ref _state.State);
|
||||
MvppViewportProbe.OnDraw(_channel, ref _state.State);
|
||||
MvppDrawStepProbe.OnDraw(_channel);
|
||||
MvppCompInputsProbe.OnDraw(_channel, ref _state.State);
|
||||
MvppCameraCapture.OnDraw(_channel);
|
||||
MvppP2Probe.OnDraw(_channel, ref _state.State, firstIndex, indexCount, drawFirstVertex, drawVertexCount);
|
||||
MvppP2BProbe.OnDraw(_channel);
|
||||
MvppUiProbe.OnDraw(_channel, ref _state.State);
|
||||
MvppUiProbe.OnDraw(_channel, ref _state.State, _context);
|
||||
MvppCompositeProbe.OnDraw(_channel, ref _state.State);
|
||||
|
||||
// [DOFSKIP] Compatibility option, inert unless RYUJINX_DOF_SCATTER_SKIP=1. Matched by
|
||||
// pipeline shape, so it survives game versions AND a warm shader cache.
|
||||
if (MvppDofSkip.ShouldSkip(_channel))
|
||||
{
|
||||
_drawState.DrawIndexed = false;
|
||||
_instancedDrawPending = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ConditionalRenderEnabled renderEnable = ConditionalRendering.GetRenderEnable(
|
||||
_context,
|
||||
_channel.MemoryManager,
|
||||
@@ -223,6 +240,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
_context.Renderer.Pipeline.EndHostConditionalRendering();
|
||||
}
|
||||
|
||||
// [POST 21/07] Capture la cible APRES l'ecriture de ce draw (chemin normal non-instancie).
|
||||
// La sonde pre-draw (l.133) montre le contenu AVANT ; comparer les deux dit si ce draw
|
||||
// ECRIT la corruption ou la trouve deja la.
|
||||
MvppDrawStepProbe.OnDrawPost(_channel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -938,6 +960,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
engine.UpdateState(updateMask);
|
||||
|
||||
int probeScX = -1, probeScY = -1, probeScW = -1, probeScH = -1; // [MVBUF] read-only capture of the effective clear scissor
|
||||
|
||||
if (needsCustomScissor)
|
||||
{
|
||||
int scissorX = screenScissorState.X;
|
||||
@@ -970,6 +994,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
new(scissorX, scissorY, scissorW, scissorH)
|
||||
];
|
||||
|
||||
probeScX = scissorX; // [MVBUF]
|
||||
probeScY = scissorY;
|
||||
probeScW = scissorW;
|
||||
probeScH = scissorH;
|
||||
|
||||
_context.Renderer.Pipeline.SetScissors(scissors);
|
||||
}
|
||||
|
||||
@@ -981,6 +1010,24 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
ColorF color = new(clearColor.Red, clearColor.Green, clearColor.Blue, clearColor.Alpha);
|
||||
|
||||
// [MVBUF] Read-only (gated): record engine clears hitting the object-MV buffer.
|
||||
Image.MvppMvBufProbe.OnClear(
|
||||
_channel.TextureManager.GetColorTarget(index),
|
||||
index,
|
||||
componentMask,
|
||||
color,
|
||||
needsCustomScissor,
|
||||
probeScX,
|
||||
probeScY,
|
||||
probeScW,
|
||||
probeScH);
|
||||
|
||||
// [TWINMAP v5] per-twin clear attribution (self-gated).
|
||||
Image.MvppTwinMapProbe.OnClear(
|
||||
_channel.TextureManager.GetColorTarget(index),
|
||||
componentMask,
|
||||
clearColor.Red, clearColor.Green, clearColor.Blue, clearColor.Alpha);
|
||||
|
||||
_context.Renderer.Pipeline.ClearRenderTargetColor(index, layer, layerCount, componentMask, color);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// IDENTITY TRACE of the camera actually read each frame. Gate: RYUJINX_MVPP_CAMTRACE=1.
|
||||
/// LOGGING ONLY - one line per accepted read, nothing is altered, nothing is fed downstream.
|
||||
///
|
||||
/// WHY IT EXISTS (27/07, XC2). Alex reports the geometry sliding even when NOTHING moves.
|
||||
/// Measured on his run: at the screen centre the motion vector is ~0.5 to 1.2 px and FLIPS
|
||||
/// SIGN frame to frame while the scene is still, and the projective audit shows the camera
|
||||
/// position returning, in a loop, to five values that repeat EXACTLY to three decimals on all
|
||||
/// three axes, spread over 0.14 world units. Noise never repeats exactly, so those are not
|
||||
/// measurement error: they are DISTINCT cameras, and the per-frame read lands on a different
|
||||
/// one from frame to frame.
|
||||
///
|
||||
/// That was already anticipated in MvppSoloCamera's own notes - the game runs its adjustable
|
||||
/// gameplay camera and renders the minimap from another - and CAMGUARD was written for it.
|
||||
/// But CAMGUARD tests a TELEPORT budget (150 world units per second, floor 5), sized on the
|
||||
/// intruders seen so far (150 and 34699 units). A rival camera 0.14 units away passes that
|
||||
/// test untouched, and a genuine camera moving at the measured median (5.8 units per second =
|
||||
/// ~0.1 per frame) is the SAME order of magnitude as the gap between the rivals. So distance
|
||||
/// alone can never separate them, whatever the threshold: the discriminator has to be
|
||||
/// IDENTITY, not metric.
|
||||
///
|
||||
/// WHAT THIS MEASURES. The per-frame read is keyed on the SLOT, deliberately, because the
|
||||
/// camera's ADDRESS rotates through a small ring buffer. Nothing checks that the address
|
||||
/// behind the slot still belongs to THAT ring. This trace prints, for every accepted read,
|
||||
/// the address it came from and the position it yielded.
|
||||
///
|
||||
/// HOW TO READ IT. Group the lines by address:
|
||||
/// - a handful of addresses, each always giving the SAME position, cycling => the slot is
|
||||
/// shared by several cameras and the fix is to pin the read to the elected ring;
|
||||
/// - one address giving positions that jitter => the ring holds different FRAMES of one
|
||||
/// camera and the fix is a freshness rule, not an identity one;
|
||||
/// - addresses and positions both stable => this whole theory is wrong and the false motion
|
||||
/// is produced downstream of the camera.
|
||||
/// The three outcomes ask for three different fixes, which is the point of measuring first.
|
||||
/// </summary>
|
||||
static class MvppCamTrace
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAMTRACE") == "1";
|
||||
|
||||
// ~10 seconds at 60 fps. Long enough to see a cycle repeat many times, short enough that
|
||||
// the log stays readable and the run stays fast.
|
||||
private const int MaxLines = 600;
|
||||
|
||||
// Ordinary reads measured at 4.8 max once the three fixes are armed; the intruders they
|
||||
// removed were 250. Ten sits between the two by two orders of magnitude on either side.
|
||||
private const float JumpThreshold = 50f;
|
||||
|
||||
private static int _lines;
|
||||
private static int _jumps;
|
||||
private static ulong _prevAddress;
|
||||
private static float _prevX, _prevY, _prevZ;
|
||||
private static bool _hasPrev;
|
||||
|
||||
/// <summary>
|
||||
/// Called on the success path of the per-frame read, with the address currently behind the
|
||||
/// elected slot and the position that read produced.
|
||||
/// </summary>
|
||||
public static void Note(ulong address, float px, float py, float pz, float jx, float jy, bool deJittered)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// [27/07] The first version stopped after MaxLines and went silent for the rest of the
|
||||
// session - so Alex's one remaining flick, which happened during a fight nine minutes
|
||||
// in, left no trace at all. Past the opening window the probe keeps watching but only
|
||||
// speaks when a read jumps: measured on the run that followed the three fixes, ordinary
|
||||
// reads peak at 4.8 while the intruders were 250, so a jump this size is an event and
|
||||
// not a busy log. Cost outside an event: three subtractions.
|
||||
bool opening = _lines < MaxLines;
|
||||
|
||||
if (!opening)
|
||||
{
|
||||
float ddx = px - _prevX;
|
||||
float ddy = py - _prevY;
|
||||
float ddz = pz - _prevZ;
|
||||
float jump = MathF.Sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
|
||||
|
||||
_prevAddress = address;
|
||||
_prevX = px;
|
||||
_prevY = py;
|
||||
_prevZ = pz;
|
||||
|
||||
if (jump > JumpThreshold)
|
||||
{
|
||||
_jumps++;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"MVPP camtrace JUMP #{_jumps}: addr={address:X10} " +
|
||||
$"pos=[{px:0.####} {py:0.####} {pz:0.####}] step={jump:0.##}.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_lines++;
|
||||
|
||||
// The step since the previous accepted read is what the reprojection turns into motion
|
||||
// vectors, so it is printed rather than left to be recomputed by hand afterwards.
|
||||
float step = 0f;
|
||||
bool addrChanged = false;
|
||||
|
||||
if (_hasPrev)
|
||||
{
|
||||
float dx = px - _prevX;
|
||||
float dy = py - _prevY;
|
||||
float dz = pz - _prevZ;
|
||||
step = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
addrChanged = address != _prevAddress;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP camtrace {_lines,4}: addr={address:X10}{(addrChanged ? " *NEW*" : " ")} " +
|
||||
$"pos=[{px:0.####} {py:0.####} {pz:0.####}] step={step:0.#####} " +
|
||||
$"jitter=({jx:0.######} {jy:0.######}){(deJittered ? " REMOVED" : " kept")}" +
|
||||
(_lines == MaxLines ? " (trace complete, no further lines)" : ""));
|
||||
|
||||
_prevAddress = address;
|
||||
_prevX = px;
|
||||
_prevY = py;
|
||||
_prevZ = pz;
|
||||
_hasPrev = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,48 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAP") == "1";
|
||||
|
||||
// [CAPWHY 02/08] Compteurs de branches du chemin de capture, lecture seule, gate OFF par
|
||||
// defaut. Question (journal (347)) : sur les images AFFAMEES du village BOTW, ou meurt le
|
||||
// chemin — OnDrawImpl jamais appelee, cache reussi (?!), relocalisation, ou repli solo ?
|
||||
// Meme patron que les stats de fenetre : ecrits sur le fil GPU, vides au meme point de
|
||||
// flush que la ligne "MVPP capture window" (approximation identique a l'existant).
|
||||
private static readonly bool _capWhy =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPWHY") == "1";
|
||||
|
||||
// [DUPSKIP 02/08] Producteur du signal anti-doublons : a chaque present invite portant au
|
||||
// moins un dessin, incremente GAL.DlssCameraState.RenderedFrameSeq (voir sa doc). Reutilise
|
||||
// le verrou par image de CAPWHY v2 ; ferme = aucun comportement.
|
||||
private static readonly bool _dupSkip =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_DLSS_DUPSKIP") == "1";
|
||||
|
||||
// [FGFEED 02/08] Meme signal, autre consommateur : Vulkan Window.Present AVALE les
|
||||
// presentations-doublons pour que Streamline/FG ne voie que les vraies images et
|
||||
// interpole entre elles (journal (353)). Le producteur est strictement identique a
|
||||
// DUPSKIP ; seul le consommateur change. Ferme = aucun comportement.
|
||||
private static readonly bool _fgFeed =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_DLSS_FGFEED") == "1";
|
||||
|
||||
private static int _cwQual; // dessins ayant passe le prefiltre (OnDraw)
|
||||
private static int _cwNeed; // ... dont l'image n'avait pas encore capture
|
||||
private static int _cwImpl; // entrees dans OnDrawImpl
|
||||
private static int _cwCachedOk; // lecture au cache reussie (publie)
|
||||
private static int _cwHeld; // quarantaine gate (slot tenu ouvert)
|
||||
private static int _cwRescanOk; // relocalisation reussie (publie)
|
||||
private static int _cwSoloCall; // repli solo atteint
|
||||
private static int _cwSoloOk; // repli solo a publie
|
||||
// "rien" = impl - cachedOk - held - rescanOk - soloOk, derivable, pas de compteur dedie.
|
||||
|
||||
// [CAPWHY v2 02/08] Discriminant demande par la correction d'Alex (« les FPS ne descendent
|
||||
// pas ») : par PRESENTATION, l'image avait-elle des dessins DU TOUT (avant prefiltre), et
|
||||
// en avait-elle apres ? Separe « le jeu n'a pas rendu » (monde 1) de « tout est mort au
|
||||
// prefiltre » (monde 2). Verrous par image poses sur le fil GPU, releves au present.
|
||||
private static int _cwRaw; // dessins vus a l'entree de OnDraw (avant prefiltre)
|
||||
private static int _cwFrameHadRaw; // verrou : cette image a vu >= 1 dessin brut
|
||||
private static int _cwFrameHadQual; // verrou : cette image a vu >= 1 dessin qualifie
|
||||
private static int _cwFramesNoDraw; // presentations sans AUCUN dessin brut
|
||||
private static int _cwFramesPrefiltred; // presentations avec des bruts mais 0 qualifie
|
||||
private static int _cwFramesOk; // presentations avec >= 1 dessin qualifie
|
||||
|
||||
private static int _capturedThisFrame;
|
||||
private static int _cachedSlot = -1;
|
||||
private static int _cachedOffset;
|
||||
@@ -136,6 +178,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private static readonly bool _fifoMode =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VPFIFO") == "1";
|
||||
|
||||
// [VPSOLO] Last matrix pushed into the ordered FIFO by the solo fallback, so it pushes
|
||||
// only on change (the triplet producer gates on its block changing; without an
|
||||
// equivalent here the queue would be fed an identical matrix every single frame).
|
||||
private static bool _hasSoloPushed;
|
||||
private static Matrix4x4 _lastSoloPushed;
|
||||
|
||||
static MvppCameraCapture()
|
||||
{
|
||||
DlssCameraState.Enabled = _enabled;
|
||||
@@ -149,6 +197,30 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
return;
|
||||
}
|
||||
|
||||
if (_capWhy || _dupSkip || _fgFeed)
|
||||
{
|
||||
if (_capWhy)
|
||||
{
|
||||
_cwRaw++;
|
||||
}
|
||||
|
||||
_cwFrameHadRaw = 1;
|
||||
}
|
||||
|
||||
// [VPSOLO] Aspect reference for the solo fallback, fed on EVERY draw with a bound
|
||||
// depth -- both regimes, before any filtering. Feeding it only from the native arm
|
||||
// left a scaled config (res_scale > 1) with no reference at all, which silently
|
||||
// blocks the election for ever. Guarded: nothing runs unless the fallback is on.
|
||||
if (MvppSoloCamera.Enabled)
|
||||
{
|
||||
Image.Texture aspectDs = channel.TextureManager.RenderTargetDepthStencil;
|
||||
|
||||
if (aspectDs != null)
|
||||
{
|
||||
MvppSoloCamera.NoteDepth(aspectDs.Info.Width, aspectDs.Info.Height);
|
||||
}
|
||||
}
|
||||
|
||||
// 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):
|
||||
@@ -181,17 +253,75 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
}
|
||||
}
|
||||
|
||||
// [CTXPROBE 29/07 SOIR] Lecture seule. Recense TOUTES les passes qui arrivent jusqu'ici, pas
|
||||
// seulement la premiere : c'est ce chiffre qui dit s'il y a un choix a faire, puisque la
|
||||
// capture ci-dessous prend la premiere venue et se tait ensuite. Voir MvppCtxProbe.
|
||||
if (MvppCtxProbe.Enabled)
|
||||
{
|
||||
MvppCtxProbe.NoteQualifyingDraw(channel);
|
||||
}
|
||||
|
||||
bool needCapture = Volatile.Read(ref _capturedThisFrame) == 0;
|
||||
bool probing = (_pairProbe || _fifoMode) && _cachedSlot >= 0;
|
||||
|
||||
if (_capWhy)
|
||||
{
|
||||
_cwQual++;
|
||||
_cwFrameHadQual = 1;
|
||||
|
||||
if (needCapture)
|
||||
{
|
||||
_cwNeed++;
|
||||
}
|
||||
}
|
||||
|
||||
// [SCENEPASS 29/07 SOIR] Le pre-filtre ci-dessus reconnait "une passe 3D", pas "LA passe
|
||||
// de la scene", et la capture prend la premiere venue. Mesure CTXPROBE du soir : la passe
|
||||
// 8 bits porte 67 % des intruses, la couleur HDR de scene 1,7 %. On saute donc la
|
||||
// tentative sur les passes qui ne sont pas la scene -- la capture se fera plus loin dans
|
||||
// la MEME image. Filet de securite et auto-desarmement : voir MvppScenePass.
|
||||
if (needCapture && MvppScenePass.Enabled && !MvppScenePass.Allows(channel))
|
||||
{
|
||||
needCapture = false;
|
||||
}
|
||||
|
||||
// [NOLDR 29/07 SOIR] L'inverse de SCENEPASS, et c'est la mesure qui a impose l'inversion :
|
||||
// la passe de scene HDR existe dans 98,9 % des images mais la camera n'y est lisible que
|
||||
// dans 36,5 % des cas, donc n'autoriser QUE celle-la affame la camera. On ECARTE plutot
|
||||
// la pire passe -- la 8 bits, 19,6 % d'intruses, quatre intruses sur cinq -- et on garde
|
||||
// toutes les autres comme occasions de capture. Voir MvppLdrSkip.
|
||||
if (needCapture && MvppLdrSkip.Active && !MvppLdrSkip.Allows(channel))
|
||||
{
|
||||
needCapture = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StashSceneDepth(channel);
|
||||
|
||||
// [CAPTIME_FIX 31/07] Mesure du 31/07 : sur 55 % des images le jeu reecrit sa camera
|
||||
// APRES notre capture, vers le 39e dessin sur ~160. On laisse donc passer les
|
||||
// dessins tant que le lieu elu porte encore la valeur deja publiee, et la capture
|
||||
// normale se fait des qu'une valeur NOUVELLE apparait. Une seule capture par image,
|
||||
// par le chemin normal : SNAPGUARD, la file et les gardes gardent leur hypothese.
|
||||
// Eteint par defaut ; le doute profite toujours au comportement existant.
|
||||
if (needCapture && MvppSoloCamera.StaleAtElectedLocation(channel))
|
||||
{
|
||||
needCapture = false;
|
||||
}
|
||||
|
||||
if (needCapture)
|
||||
{
|
||||
OnDrawImpl(channel);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
if (probing)
|
||||
{
|
||||
@@ -671,6 +801,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
vp[12], vp[13], vp[14], vp[15]);
|
||||
|
||||
DlssCameraState.PushOrdered(in m);
|
||||
MvppFamHold.NotePush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,6 +814,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
_colorCapFrameId++; // [COLORCAP probe] one id per presented frame, for the capture prototype log
|
||||
|
||||
// [HANGWATCH 29/07] Une ecriture d'entier, sur un fil de fond independant qui, lui,
|
||||
// survivra au gel du fil graphique. Voir MvppHangWatch.
|
||||
if (MvppHangWatch.Enabled)
|
||||
{
|
||||
MvppHangWatch.Ping();
|
||||
}
|
||||
|
||||
// [CTXPROBE 29/07 SOIR] Re-arme le rang de dessin, au meme endroit que la capture elle-meme.
|
||||
if (MvppCtxProbe.Enabled)
|
||||
{
|
||||
MvppCtxProbe.OnFrame();
|
||||
}
|
||||
|
||||
if (_gateEnabled)
|
||||
{
|
||||
if (Volatile.Read(ref _capturedThisFrame) == 1)
|
||||
@@ -700,8 +844,56 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
_rivalSeenThisFrame = false;
|
||||
}
|
||||
|
||||
// [SCENEPASS 29/07 SOIR] ICI, avant le re-armement : c'est le seul endroit ou l'on sait
|
||||
// si l'image a fini par obtenir une camera. Une image restee sans camera alors qu'elle
|
||||
// avait des passes 3D est une image potentiellement perdue -- le filet la compte.
|
||||
if (MvppScenePass.Enabled)
|
||||
{
|
||||
MvppScenePass.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1);
|
||||
}
|
||||
|
||||
if (MvppLdrSkip.Active)
|
||||
{
|
||||
MvppLdrSkip.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1);
|
||||
}
|
||||
|
||||
// [CAPTIME_AB 31/07] Meme endroit, meme raison que les deux au-dessus : c'est le seul
|
||||
// point ou l'on sait si l'image a fini par obtenir une camera. Inerte tant que
|
||||
// RYUJINX_MVPP_CAPTIME_AB n'est pas renseigne.
|
||||
MvppSoloCamera.OnPresentBoundary(Volatile.Read(ref _capturedThisFrame) == 1);
|
||||
MvppSoloCamera.LogWhy();
|
||||
|
||||
Volatile.Write(ref _capturedThisFrame, 0);
|
||||
|
||||
// [CAPWHY v2] Classement de la presentation qui se termine, puis reset des verrous.
|
||||
// [DUPSKIP] La meme frontiere nourrit la sequence des vraies images rendues.
|
||||
if (_capWhy || _dupSkip || _fgFeed)
|
||||
{
|
||||
if ((_dupSkip || _fgFeed) && _cwFrameHadRaw == 1)
|
||||
{
|
||||
System.Threading.Interlocked.Increment(ref DlssCameraState.RenderedFrameSeq);
|
||||
}
|
||||
|
||||
if (_capWhy)
|
||||
{
|
||||
if (_cwFrameHadRaw == 0)
|
||||
{
|
||||
_cwFramesNoDraw++;
|
||||
}
|
||||
else if (_cwFrameHadQual == 0)
|
||||
{
|
||||
_cwFramesPrefiltred++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cwFramesOk++;
|
||||
}
|
||||
}
|
||||
|
||||
_cwFrameHadRaw = 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++;
|
||||
@@ -717,6 +909,16 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
$"vp-jumps {_statVpJumps}, blk-changes {_statBlockChanges}, " +
|
||||
$"sq-rejects {StatSquareRejects}, rescans {_rescans}, " +
|
||||
$"rivals {_statRivals}, late {_statLineageLate}, resyncs {_statResyncs}.");
|
||||
|
||||
if (_capWhy)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP capture why: raw {_cwRaw}, qual {_cwQual}, need {_cwNeed}, impl {_cwImpl}, " +
|
||||
$"cachedOk {_cwCachedOk}, held {_cwHeld}, rescanOk {_cwRescanOk}, " +
|
||||
$"soloCall {_cwSoloCall}, soloOk {_cwSoloOk}, " +
|
||||
$"rien {_cwImpl - _cwCachedOk - _cwHeld - _cwRescanOk - _cwSoloOk} | " +
|
||||
$"img: sansDessin {_cwFramesNoDraw}, toutPrefiltre {_cwFramesPrefiltred}, ok {_cwFramesOk}.");
|
||||
}
|
||||
}
|
||||
|
||||
_pubWindowMs = nowPub;
|
||||
@@ -729,6 +931,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
_statBlockChanges = 0;
|
||||
_statRivals = 0;
|
||||
_statLineageLate = 0;
|
||||
_cwQual = 0;
|
||||
_cwNeed = 0;
|
||||
_cwImpl = 0;
|
||||
_cwCachedOk = 0;
|
||||
_cwHeld = 0;
|
||||
_cwRescanOk = 0;
|
||||
_cwSoloCall = 0;
|
||||
_cwSoloOk = 0;
|
||||
_cwRaw = 0;
|
||||
_cwFramesNoDraw = 0;
|
||||
_cwFramesPrefiltred = 0;
|
||||
_cwFramesOk = 0;
|
||||
}
|
||||
|
||||
if (_pairProbe)
|
||||
@@ -750,13 +964,95 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
}
|
||||
}
|
||||
|
||||
// [DUPPAIR 28/07] Sonde du POINT DE PRODUCTION de la camera, cadencee a 1 Hz et sans
|
||||
// dependance au mode dev (qui coute des images). Mesure du 28/07 cote consommation :
|
||||
// pendant les pans, 6 a 15 valeurs distinctes par seconde pour 30 images -- et le
|
||||
// manque est INVERSEMENT correle a la vitesse de la camera. Rien n'est perdu en route
|
||||
// (drops=0), donc la valeur n'est jamais produite. Reste a savoir si on lit et que la
|
||||
// valeur ne bouge pas, ou si la lecture echoue -- et alors pour quel motif.
|
||||
private static readonly bool _dupPair =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUPPAIR") == "1";
|
||||
|
||||
// [JITTRACE 28/07] Voir les etapes A / B / C. Journal seul, une ligne par seconde par etape.
|
||||
internal static readonly bool JitTrace =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_JITTRACE") == "1";
|
||||
|
||||
private static readonly bool _jitTrace = JitTrace;
|
||||
private static long _jitTraceMs;
|
||||
|
||||
private static int _soloDistinct;
|
||||
private static int _soloSame;
|
||||
private static long _dupLogMs;
|
||||
private static readonly int[] _dupFailBase = new int[16];
|
||||
private static readonly int[] _dupFailNow = new int[16];
|
||||
|
||||
private static void DupPairLog()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _dupLogMs < 1000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dupLogMs = now;
|
||||
|
||||
MvppSoloCamera.CopyFailCounts(_dupFailNow);
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append($"MVPP PRODUCTION: lectures distinctes={_soloDistinct} identiques={_soloSame} | echecs: ");
|
||||
|
||||
int n = Math.Min(MvppSoloCamera.FailReasonCount, _dupFailNow.Length);
|
||||
int total = 0;
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
total += _dupFailNow[i] - _dupFailBase[i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int d = _dupFailNow[i] - _dupFailBase[i];
|
||||
|
||||
if (d > 0)
|
||||
{
|
||||
sb.Append($"{MvppSoloCamera.FailNames[i]} {d} ({(total > 0 ? 100f * d / total : 0f):0.#} %) · ");
|
||||
}
|
||||
|
||||
_dupFailBase[i] = _dupFailNow[i];
|
||||
}
|
||||
|
||||
sb.Append($"total {total}");
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
|
||||
Logger.Info?.Print(LogClass.Gpu, MvppSoloCamera.AltSummaryAndReset());
|
||||
|
||||
_soloDistinct = 0;
|
||||
_soloSame = 0;
|
||||
}
|
||||
|
||||
private static void OnDrawImpl(GpuChannel channel)
|
||||
{
|
||||
if (_dupPair)
|
||||
{
|
||||
DupPairLog();
|
||||
}
|
||||
|
||||
if (_capWhy)
|
||||
{
|
||||
_cwImpl++;
|
||||
}
|
||||
|
||||
_gateHeldSlot = false;
|
||||
|
||||
// Cached location first: one read + three cheap checks per frame in steady state.
|
||||
if (_cachedSlot >= 0 && TryCapture(channel, _cachedSlot, _cachedOffset))
|
||||
{
|
||||
if (_capWhy)
|
||||
{
|
||||
_cwCachedOk++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -765,6 +1061,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
// The cached block validated but the gate quarantined it (rival camera):
|
||||
// the lineage lives at the SAME location on a LATER draw -- a full rescan
|
||||
// would only re-find the rival. Keep the slot open, skip the scan.
|
||||
if (_capWhy)
|
||||
{
|
||||
_cwHeld++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -791,10 +1092,106 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP capture: camera block found at stage0 cbuf{slot} +0x{offset:X3} (scan #{_rescans}).");
|
||||
|
||||
if (_capWhy)
|
||||
{
|
||||
_cwRescanOk++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [VPSOLO fallback, 21/07 -- RYUJINX_MVPP_VPSOLO=1, off by default]
|
||||
// Reached ONLY when the triplet scan above found nothing, so a game whose camera the
|
||||
// historic contract recognises never gets here and its behaviour is unchanged by
|
||||
// construction rather than by testing. Games that store their view-projection ALONE
|
||||
// (Xenoblade 2: view and proj in separate buffers, column-major, canonical triplet
|
||||
// count 0 across every measured run) had MV++ silently disarmed for the whole
|
||||
// session; this gives them a camera. See MvppSoloCamera for how the matrix is
|
||||
// recognised and how the location is elected.
|
||||
if (_capWhy && MvppSoloCamera.Enabled)
|
||||
{
|
||||
_cwSoloCall++;
|
||||
}
|
||||
|
||||
if (MvppSoloCamera.Enabled && MvppSoloCamera.TryGetViewProjection(channel, out Matrix4x4 soloVp))
|
||||
{
|
||||
if (_capWhy)
|
||||
{
|
||||
_cwSoloOk++;
|
||||
}
|
||||
|
||||
DlssCameraState.PublishCurrent(in soloVp);
|
||||
Volatile.Write(ref _capturedThisFrame, 1);
|
||||
_captures++;
|
||||
_statPubFrames++;
|
||||
|
||||
// Publishing is only half the pipe. In VPFIFO mode the present side pairs the
|
||||
// camera to the presented frame by CONSUMING an ordered queue, and the only
|
||||
// producer for that queue is ProbeDistinctValues -- which requires _cachedSlot,
|
||||
// i.e. a triplet that was found. On a game where the triplet does not exist the
|
||||
// queue therefore stayed empty for ever: measured 21/07 on XC2, pub 150/151 on
|
||||
// the GPU side while the consumer logged "pairOk=0" every 5 s for a whole
|
||||
// 3-minute run, so the reprojection pass never once ran. Push here too, and only
|
||||
// on a CHANGED value, exactly like the triplet producer does -- a queue fed the
|
||||
// same matrix every frame would just grow.
|
||||
if (_fifoMode && (!_hasSoloPushed || soloVp != _lastSoloPushed))
|
||||
{
|
||||
_hasSoloPushed = true;
|
||||
_lastSoloPushed = soloVp;
|
||||
|
||||
// [GAMEJITTER 28/07] Le decalage sous-pixel MESURE SUR CETTE LECTURE part avec
|
||||
// la matrice : c'est la seule facon qu'il reste apparie a la bonne image.
|
||||
float pushJx = MvppSoloCamera.LastJitterX;
|
||||
float pushJy = MvppSoloCamera.LastJitterY;
|
||||
|
||||
// ⛔ [28/07 17h40] Surcharge a 3 parametres NEUTRALISEE, meme dette que dans
|
||||
// Window.cs : PushOrdered(vp, jx, jy) a ete ajoutee au GAL a 11:46, le GAL.dll
|
||||
// de l'installation date de 10:07 et ne la contient pas -> MissingMethodException
|
||||
// des que le projet Gpu est recompile. La surcharge a 1 parametre existe des
|
||||
// deux cotes. Zero perte : GAMEJITTER ne transporte rien (mesure du 28/07,
|
||||
// `brut (0,000;0,000)` cote DLSS). A retablir avec la reconstruction du GAL.
|
||||
_ = pushJx;
|
||||
_ = pushJy;
|
||||
DlssCameraState.PushOrdered(in soloVp);
|
||||
MvppFamHold.NotePush();
|
||||
_soloDistinct++;
|
||||
|
||||
// [JITTRACE 28/07] ETAPE A. La valeur est mesuree d'un cote et vaut zero de
|
||||
// l'autre ; deux explications de ma part se sont deja revelees fausses. On
|
||||
// trace donc a CHAQUE saut au lieu de raisonner. Ici : ce qu'on POUSSE.
|
||||
if (_jitTrace && Environment.TickCount64 - _jitTraceMs >= 1000)
|
||||
{
|
||||
_jitTraceMs = Environment.TickCount64;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"JITTRACE A (pousse) : jx={pushJx:0.000000} jy={pushJy:0.000000} " +
|
||||
$"soit ({pushJx * 960f:0.000};{pushJy * 540f:0.000}) px a 1920x1080");
|
||||
}
|
||||
}
|
||||
else if (_fifoMode)
|
||||
{
|
||||
// [DUPPAIR 28/07] Lecture reussie qui rend la MEME matrice que la derniere
|
||||
// poussee. C'est la moitie manquante du diagnostic : une image sans nouvelle
|
||||
// camera vient soit d'ici (on lit, mais la valeur ne bouge pas), soit d'un
|
||||
// echec de lecture (compte par motif dans MvppSoloCamera). Les deux ne se
|
||||
// corrigent pas au meme endroit.
|
||||
_soloSame++;
|
||||
}
|
||||
|
||||
// Same scene/aux attribution as the triplet path: did the draw that carried this
|
||||
// camera have the pinned scene depth bound?
|
||||
GAL.ITexture soloDs = channel.TextureManager.RenderTargetDepthStencil?.HostTexture;
|
||||
|
||||
if (soloDs != null && soloDs.Width == SceneDepthHostWidth && soloDs.Height == SceneDepthHostHeight)
|
||||
{
|
||||
_statCapScene++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_statCapAux++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center-pixel MV spikes of +-40 px at 1-3 Hz with a near-still camera can only come
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Recensement du tuilage GOB de toutes les textures pleine résolution (RYUJINX_COMPIN=1). READ-ONLY.
|
||||
///
|
||||
/// La corruption XC2 a une période de 8 lignes = hauteur d'un GOB ⇒ erreur de tuilage block-linear.
|
||||
/// Plutôt que de traquer la passe exacte (mes filtres étaient tour à tour trop larges puis trop serrés),
|
||||
/// on recense directement le gobBlocksInY de CHAQUE texture aux dimensions d'un G-buffer (1280x720,
|
||||
/// 640x360, 512x288), cibles de rendu ET entrées échantillonnées. Une texture dont le gobBlocksInY
|
||||
/// détonne de ses voisines de MÊME taille est lue/écrite de travers : c'est le bug.
|
||||
/// Chaque forme distincte (dims + format + gobY + rôle) est logguée une seule fois.
|
||||
/// </summary>
|
||||
static class MvppCompInputsProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_COMPIN") == "1";
|
||||
|
||||
private static bool _announced;
|
||||
private static readonly HashSet<string> _seen = new();
|
||||
|
||||
private static bool RtSized(Image.TextureInfo i)
|
||||
{
|
||||
bool sized = (i.Width == 1280 && i.Height == 720) ||
|
||||
(i.Width == 640 && i.Height == 360) ||
|
||||
(i.Width == 512 && i.Height == 288);
|
||||
|
||||
string fmt = i.FormatInfo.Format.ToString();
|
||||
bool uncompressed = !fmt.StartsWith("Bc", StringComparison.Ordinal) &&
|
||||
!fmt.StartsWith("Astc", StringComparison.Ordinal);
|
||||
|
||||
return sized && uncompressed;
|
||||
}
|
||||
|
||||
private static void Note(Image.Texture tex, string role)
|
||||
{
|
||||
if (tex == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Image.TextureInfo i = tex.Info;
|
||||
|
||||
if (!RtSized(i))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// On logue le gobBlocksInY brut : l'anomalie = deux textures de MÊME taille avec des gobY
|
||||
// différents. Le stride est ajouté car un pitch block-linear faux est l'autre forme du bug.
|
||||
string sig = $"{role} {i.Width}x{i.Height} {i.FormatInfo.Format} gobY={i.GobBlocksInY} lin={i.IsLinear} stride={i.Stride}";
|
||||
|
||||
if (_seen.Add(sig))
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP COMPIN: {sig}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnDraw(GpuChannel channel, ref ThreedClassState state)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_announced)
|
||||
{
|
||||
_announced = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
"MVPP COMPIN: ON -- recensement gobBlocksInY des textures pleine res (cibles + entrees).");
|
||||
}
|
||||
|
||||
channel.TextureManager.MvppEnumerateRenderTargets((slot, rt) => Note(rt, $"CIBLE slot{slot}"));
|
||||
channel.TextureManager.MvppEnumerateGraphicsInputs(tex => Note(tex, "ENTREE"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP COMPIN: desactive apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// [CTXPROBE 29/07 SOIR] Sonde de CONTEXTE DE BRANCHEMENT (RYUJINX_MVPP_CTXPROBE=1, coupee par
|
||||
/// defaut). LECTURE SEULE : ne touche ni la matrice rendue, ni l'election, ni un garde-fou.
|
||||
/// Aucune ecriture disque. Deux appels d'instrumentation, rien d'autre.
|
||||
///
|
||||
/// LA QUESTION QU'ELLE POSE. Identifier l'intruse par son CONTENU est ferme depuis le 27/07
|
||||
/// (commentaire de SNAPGUARD) : elle n'est pas a l'origine, elle a le bon rapport d'image, et
|
||||
/// c'est une vue-projection structurellement PARFAITE. Rien dans ses chiffres ne la distingue
|
||||
/// d'une vraie camera. Reste une chose qu'on n'a jamais regardee : non pas a quoi elle
|
||||
/// ressemble, mais OU et QUAND le jeu la branche.
|
||||
///
|
||||
/// LE MECANISME SOUPCONNE. <see cref="MvppCameraCapture.OnDraw"/> capture la camera au PREMIER
|
||||
/// dessin de l'image qui passe le pre-filtre (profondeur non carree / cible mise a l'echelle),
|
||||
/// puis se tait jusqu'a l'image suivante (_capturedThisFrame). Ce pre-filtre reconnait "une
|
||||
/// passe 3D", pas "LA passe de la scene principale". Si le jeu dessine une passe secondaire
|
||||
/// (reflet, miroir, carte, vue fixe) AVANT la scene, on lit SA camera -- une vraie camera, a un
|
||||
/// siege fixe du monde, structurellement parfaite. Ce qui collerait a tout le dossier : siege
|
||||
/// fixe, matrice parfaite, entree par le slot ELU (meme slot, autre passe), salves de plus de
|
||||
/// 20 images, dependance a l'angle et a l'endroit, et le fait qu'un filtre d'amplitude attenue
|
||||
/// sans jamais tuer -- on filtre apres coup un choix fait trop tot.
|
||||
///
|
||||
/// CE QU'ELLE MESURE. Deux recensements, tous deux a GROS VOLUME (lecon du 29/07 : ne jamais
|
||||
/// juger sur des evenements rares) :
|
||||
/// 1. CONTEXTE DE LECTURE -- pour chaque lecture de la camera, la cible couleur et la
|
||||
/// profondeur branchees a cet instant, plus le rang du dessin dans l'image. Chaque
|
||||
/// contexte compte ses lectures ET ses intruses.
|
||||
/// 2. RECENSEMENT DES PASSES QUALIFIANTES -- toutes les passes qui passent le pre-filtre dans
|
||||
/// l'image, pas seulement la premiere. Ce chiffre decide si un correctif est meme
|
||||
/// POSSIBLE : s'il n'y a qu'une passe qualifiante par image, il n'y a rien a choisir et
|
||||
/// l'hypothese meurt ici.
|
||||
///
|
||||
/// COMMENT LA LIRE. Si les intruses se concentrent sur un contexte distinct des lectures
|
||||
/// saines, on tient un discriminant que le contenu ne donnait pas. Si tous les contextes sont
|
||||
/// identiques, l'hypothese est morte -- et c'est un resultat, pas un echec.
|
||||
///
|
||||
/// Minuteur PROPRE, delibere : celui d'ALTPROBE etait enferme dans le bloc d'une autre sonde et
|
||||
/// n'a jamais ete emis une seule fois de tout le dossier.
|
||||
/// </summary>
|
||||
static class MvppCtxProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CTXPROBE") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// [29/07 soir] La lecture d'ESSAI sur la passe HDR, derriere SA PROPRE variable
|
||||
/// (RYUJINX_MVPP_HDRTRY=1). Pourquoi separee : le run qui l'a introduite a rendu une
|
||||
/// publication de 60,4 % contre 83,2 % de reference, alors que rien d'autre n'avait change
|
||||
/// -- une sonde en lecture seule ne devrait pas deplacer ce chiffre. Soit c'est l'endroit
|
||||
/// (la reference etait mesuree ailleurs), soit CETTE lecture perturbe le chemin de lecture.
|
||||
/// Un interrupteur separe permet de trancher au meme endroit, sans perdre le recensement.
|
||||
/// 📌 Dans ce dossier, un ecart spectaculaire est d'abord un bug d'instrument.
|
||||
/// </summary>
|
||||
public static readonly bool TrialRead =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HDRTRY") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Seuil de separation, pris des mesures du 29/07 et non d'un choix : le plus grand pas
|
||||
/// LEGITIME observe vaut 4,9 u (pas ordinaire 0,2, 99e centile 0,7), la plus petite
|
||||
/// INTRUSE 415 u. 100 u tombe entre deux ordres de grandeur ou aucune lecture n'a jamais
|
||||
/// ete mesuree. Ce seuil ne SERT a rien d'autre qu'a etiqueter une ligne de journal.
|
||||
/// </summary>
|
||||
private const float IntruderDist = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// Au-dela de cet age, la derniere position acceptee ne peut plus servir de reference : le
|
||||
/// joueur a eu le temps de se deplacer, donc l'ecart mesure n'est plus un saut de camera.
|
||||
/// 60 ms ≈ deux images. Mesure du 29/07 au soir : les lectures saines ont un age moyen de
|
||||
/// 37 ms, les faux positifs 2 443 ms -- les deux populations sont separees par presque deux
|
||||
/// ordres de grandeur, donc la valeur exacte du seuil n'est pas critique.
|
||||
/// </summary>
|
||||
private const long FreshRefMs = 60;
|
||||
|
||||
// Releve a 32 le 29/07 au soir : l'adresse de la profondeur est entree dans la cle, donc une
|
||||
// meme forme peut desormais occuper plusieurs lignes. Le debordement reste journalise.
|
||||
private const int MaxCtx = 32;
|
||||
private const int ReportMs = 5000;
|
||||
|
||||
private struct Ctx
|
||||
{
|
||||
public bool Used;
|
||||
public int Fmt;
|
||||
public int CW;
|
||||
public int CH;
|
||||
public int DW;
|
||||
public int DH;
|
||||
public ulong ColorAddr;
|
||||
public ulong DepthAddr;
|
||||
public int Reads;
|
||||
public int Intruders;
|
||||
public int MinRank;
|
||||
public int MaxRank;
|
||||
public float MaxDist;
|
||||
}
|
||||
|
||||
private static readonly Ctx[] _readCtx = new Ctx[MaxCtx];
|
||||
private static readonly Ctx[] _qualCtx = new Ctx[MaxCtx];
|
||||
|
||||
// [RANG REPARE 29/07 soir] La v1 remettait _rank a zero depuis OnFrame, c'est-a-dire depuis
|
||||
// le fil de PRESENTATION, pendant que OnDraw l'incrementait depuis le fil GPU -- d'ou des
|
||||
// "rang 0" impossibles et une colonne inutilisable. Ici OnFrame ne touche plus qu'un JETON,
|
||||
// et la remise a zero se fait paresseusement sur le fil GPU, seul ecrivain de _rank. Plus de
|
||||
// course : le seul echange entre fils est un int, indechirable.
|
||||
private static int _rank;
|
||||
private static int _frameMark;
|
||||
private static int _seenMark;
|
||||
|
||||
private static int _frames;
|
||||
private static int _qualSum;
|
||||
private static int _qualMin = int.MaxValue;
|
||||
private static int _qualMax;
|
||||
|
||||
private static int _reads;
|
||||
private static int _intruders;
|
||||
private static int _ctxOverflow;
|
||||
|
||||
// [VERDICT PASSE HDR, 29/07 soir] La question qui decide du chantier SCENEPASS. Le
|
||||
// recensement a montre OU sont les intruses (8,1 % sur la passe 8 bits, 0,08 % sur la
|
||||
// couleur HDR) ; deplacer la capture n'a de sens que si la camera est LISIBLE au moment de
|
||||
// la passe HDR. Une lecture d'essai par image, a la premiere passe HDR rencontree.
|
||||
// [IDENTITE DES INTRUSES, 29/07 soir, 3e passe] La question qui peut FERMER le dossier. Le
|
||||
// residu vaut 20 intruses par run, et toutes plafonnent a 415,2 u -- or (272) a etabli que
|
||||
// "415,2 depuis [403,3 -28 436,4]" est la transition de camera au CHARGEMENT : deterministe,
|
||||
// une fois par lancement, PAS un symptome. Si les 20 sont toutes de cette famille, il ne
|
||||
// reste rien a corriger. On les recense donc une par une, dedupliquees par distance arrondie
|
||||
// au dixieme, avec la position d'ou elles viennent : une valeur ET une position identiques
|
||||
// au chiffre pres d'un lancement a l'autre = evenement deterministe (regle de (272)).
|
||||
private const int MaxIntr = 16;
|
||||
private static readonly float[] _intrDist = new float[MaxIntr];
|
||||
private static readonly int[] _intrCount = new int[MaxIntr];
|
||||
private static readonly float[] _intrPx = new float[MaxIntr];
|
||||
private static readonly float[] _intrPy = new float[MaxIntr];
|
||||
private static readonly float[] _intrPz = new float[MaxIntr];
|
||||
private static int _intrN;
|
||||
private static int _intrOverflow;
|
||||
|
||||
// [RANG DES INTRUSES, 29/07 soir, 4e passe] Le seul axe jamais regarde, et disponible
|
||||
// uniquement depuis que la colonne "rang" est reparee. La question : les intruses arrivent-
|
||||
// elles a un rang systematiquement PLUS TOT que les lectures saines ? Si oui, "ne pas
|
||||
// capturer dans les N premiers dessins qualifiants" est un correctif generique, simple et
|
||||
// testable -- et ce serait le meme genre de discriminant que celui qui a tue la passe 8 bits.
|
||||
// Moyennes + histogramme grossier : on cherche un ecart franc, pas une decimale.
|
||||
private static long _rankSumClean;
|
||||
private static int _rankNClean;
|
||||
private static long _rankSumIntr;
|
||||
private static int _rankNIntr;
|
||||
private static readonly int[] _rankHistClean = new int[5];
|
||||
private static readonly int[] _rankHistIntr = new int[5];
|
||||
|
||||
private static int _staleJumps;
|
||||
private static long _ageSumClean;
|
||||
private static int _ageNClean;
|
||||
private static long _ageSumIntr;
|
||||
private static int _ageNIntr;
|
||||
private static long _ageMaxIntr;
|
||||
|
||||
private static bool _probedThisFrame;
|
||||
private static int _hdrFrames;
|
||||
private static int _hdrReadable;
|
||||
private static int _hdrUnreadable;
|
||||
private static int _hdrIntruders;
|
||||
|
||||
private static long _lastReportMs;
|
||||
|
||||
/// <summary>
|
||||
/// Appelee pour CHAQUE dessin qui passe le pre-filtre de <see cref="MvppCameraCapture"/>,
|
||||
/// que la camera y soit lue ou non. C'est ce recensement qui dit combien de passes
|
||||
/// qualifiantes existent par image, donc s'il y a un choix a faire.
|
||||
/// </summary>
|
||||
public static void NoteQualifyingDraw(GpuChannel channel)
|
||||
{
|
||||
int mark = _frameMark;
|
||||
|
||||
if (_seenMark != mark)
|
||||
{
|
||||
_seenMark = mark;
|
||||
_rank = 0;
|
||||
}
|
||||
|
||||
_rank++;
|
||||
|
||||
// [VERDICT PASSE HDR] Une seule lecture d'essai par image, a la PREMIERE passe de scene
|
||||
// HDR rencontree -- c'est exactement l'instant ou SCENEPASS voudrait capturer. Lecture
|
||||
// seule et sans effet de bord (voir MvppSoloCamera.TryProbeRead) : rien n'est publie,
|
||||
// aucun garde n'est traverse, la paire de jitter est restauree.
|
||||
if (TrialRead && !_probedThisFrame && MvppScenePass.IsSceneColorPass(channel))
|
||||
{
|
||||
_probedThisFrame = true;
|
||||
_hdrFrames++;
|
||||
|
||||
if (MvppSoloCamera.TryProbeRead(channel, out float pdist))
|
||||
{
|
||||
_hdrReadable++;
|
||||
|
||||
if (pdist >= IntruderDist)
|
||||
{
|
||||
_hdrIntruders++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_hdrUnreadable++;
|
||||
}
|
||||
}
|
||||
|
||||
Describe(channel, out int fmt, out int cw, out int ch, out int dw, out int dh,
|
||||
out ulong cAddr, out ulong dAddr);
|
||||
|
||||
Record(_qualCtx, fmt, cw, ch, dw, dh, cAddr, dAddr, _rank, 0f, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appelee sur la lecture principale de la camera, juste apres qu'elle a reussi
|
||||
/// structurellement et AVANT que les gardes ne tranchent -- sinon les intruses, que
|
||||
/// SNAPGUARD refuse, seraient invisibles et la sonde ne verrait que les lectures saines.
|
||||
/// <paramref name="dist"/> est le pas depuis la derniere position ACCEPTEE, c'est-a-dire
|
||||
/// exactement la distance que mesure SNAPGUARD : les chiffres des deux se comparent.
|
||||
/// </summary>
|
||||
public static void NoteRead(GpuChannel channel, float dist, float px, float py, float pz, long ageMs)
|
||||
{
|
||||
_reads++;
|
||||
|
||||
// [DEFINITION CORRIGEE, 29/07 soir, 7e passe] MESURE : les "intruses" avaient un age de
|
||||
// reference de 2 443 ms de moyenne (max 2 875) contre 37 ms pour les lectures saines.
|
||||
// 🐛 Donc elles n'etaient PAS des cameras rivales : c'etait ma definition qui se
|
||||
// declenchait sur une reference PERIMEE. Pendant 2,4 s le joueur se deplace, et la
|
||||
// lecture legitime suivante ressemble forcement a un saut de 400 u. Ca explique tout ce
|
||||
// qui m'intriguait : distances toujours vers 415 (ce qu'on parcourt en 2,4 s), "familles"
|
||||
// changeant de run en run, "position fixe" revue 19 fois (l'endroit ou les lectures
|
||||
// reprennent).
|
||||
//
|
||||
// Une intruse n'est donc comptee que si le saut est mesure contre une reference FRAICHE.
|
||||
// Les sauts a reference perimee sont comptes A PART : ils ne disparaissent pas du
|
||||
// journal, ils changent de nom -- ils mesurent les TROUS d'acceptation, ce qui est une
|
||||
// vraie grandeur, mais pas celle qu'on croyait.
|
||||
bool freshRef = ageMs <= FreshRefMs;
|
||||
bool bigJump = dist >= IntruderDist;
|
||||
bool intruder = bigJump && freshRef;
|
||||
|
||||
if (bigJump && !freshRef)
|
||||
{
|
||||
_staleJumps++;
|
||||
}
|
||||
|
||||
// [AGE DE LA REFERENCE, 29/07 soir, 6e passe] LE DOUTE SUR MA PROPRE DEFINITION.
|
||||
// "Intruse" = pas de 100 u ou plus depuis la DERNIERE POSITION ACCEPTEE. Or quand
|
||||
// SNAPGUARD refuse un moment, cette reference VIEILLIT pendant que le joueur avance :
|
||||
// une lecture parfaitement legitime peut alors ressembler a un saut de 400 u. Ce qui
|
||||
// collerait avec la famille qui DEFILAIT au plan Y du joueur ([115,1 -11,5 142,1] ->
|
||||
// [117,0 -11,5 143,7]) : peut-etre sa propre camera, lue apres une tenue longue.
|
||||
// Si les intruses ont un age de reference bien plus eleve que les lectures saines, une
|
||||
// partie du "residu" est un artefact de MA sonde, pas un defaut -- et il ne faut surtout
|
||||
// pas coder un correctif contre ca.
|
||||
if (intruder)
|
||||
{
|
||||
_ageSumIntr += ageMs;
|
||||
_ageNIntr++;
|
||||
|
||||
if (ageMs > _ageMaxIntr)
|
||||
{
|
||||
_ageMaxIntr = ageMs;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_ageSumClean += ageMs;
|
||||
_ageNClean++;
|
||||
}
|
||||
|
||||
if (intruder)
|
||||
{
|
||||
_intruders++;
|
||||
NoteIntruder(dist, px, py, pz);
|
||||
}
|
||||
|
||||
int bucket = _rank <= 1 ? 0 : _rank <= 3 ? 1 : _rank <= 10 ? 2 : _rank <= 50 ? 3 : 4;
|
||||
|
||||
if (intruder)
|
||||
{
|
||||
_rankSumIntr += _rank;
|
||||
_rankNIntr++;
|
||||
_rankHistIntr[bucket]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_rankSumClean += _rank;
|
||||
_rankNClean++;
|
||||
_rankHistClean[bucket]++;
|
||||
}
|
||||
|
||||
// [NOLDR A/B] Etiquette la lecture avec la moitie d'experience en cours. C'est le seul
|
||||
// endroit qui sait si une lecture est une intruse, et MvppLdrSkip le seul qui sache
|
||||
// quelle moitie tourne : les deux moities se comparent alors sur la MEME scene, ce qui
|
||||
// supprime la variable qui a fait juger trois correctifs sur du hasard ce matin.
|
||||
MvppLdrSkip.NoteRead(intruder);
|
||||
|
||||
Describe(channel, out int fmt, out int cw, out int ch, out int dw, out int dh,
|
||||
out ulong cAddr, out ulong dAddr);
|
||||
|
||||
Record(_readCtx, fmt, cw, ch, dw, dh, cAddr, dAddr, _rank, dist, intruder);
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastReportMs >= ReportMs)
|
||||
{
|
||||
_lastReportMs = now;
|
||||
Report();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Re-arme le rang de dessin, une fois par image presentee.</summary>
|
||||
public static void OnFrame()
|
||||
{
|
||||
if (_rank > 0)
|
||||
{
|
||||
_frames++;
|
||||
_qualSum += _rank;
|
||||
|
||||
if (_rank < _qualMin)
|
||||
{
|
||||
_qualMin = _rank;
|
||||
}
|
||||
|
||||
if (_rank > _qualMax)
|
||||
{
|
||||
_qualMax = _rank;
|
||||
}
|
||||
}
|
||||
|
||||
// On ne touche PLUS a _rank ici (fil de presentation) : on avance un jeton, et le fil GPU
|
||||
// fera la remise a zero lui-meme. C'est la reparation de la colonne "rang".
|
||||
_frameMark++;
|
||||
_probedThisFrame = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recense une intruse, dedupliquee par sa POSITION arrondie a l'unite.
|
||||
///
|
||||
/// 🐛 [Correction 29/07 soir] La v1 dedupliquait par DISTANCE, et c'etait faux : quand le
|
||||
/// joueur bouge, chaque image donne une distance neuve pour une intruse pourtant immobile
|
||||
/// (mesure : 412,1 · 411,9 · 411,7 · 411,4 … depuis des positions qui defilent). La table de
|
||||
/// 16 lignes debordait donc en quelques secondes -- 23 valeurs perdues -- et le compteur
|
||||
/// "vue N fois" ne voulait rien dire.
|
||||
///
|
||||
/// La position est la bonne cle, et elle repond a LA question qui reste : combien du residu
|
||||
/// est-il STATIONNAIRE ? Un siege immobile pendant que la camera acceptee bouge n'est pas la
|
||||
/// camera de scene, et ca serait un critere GENERIQUE -- contrairement a un seuil sur une
|
||||
/// hauteur du monde. Les mesures disent que les deux especes coexistent : la famille a 550 u
|
||||
/// tient une position quasi fixe ([53,8 −1,0 −337,6]) alors que celle a 410 u DEFILE avec le
|
||||
/// joueur, au meme plan Y que lui. Le compte par position dira laquelle domine, donc ce qu'un
|
||||
/// tel critere pourrait gagner -- AVANT d'ecrire la moindre ligne de correctif.
|
||||
/// </summary>
|
||||
private static void NoteIntruder(float dist, float px, float py, float pz)
|
||||
{
|
||||
for (int i = 0; i < _intrN; i++)
|
||||
{
|
||||
if (MathF.Abs(_intrPx[i] - px) < 1f &&
|
||||
MathF.Abs(_intrPy[i] - py) < 1f &&
|
||||
MathF.Abs(_intrPz[i] - pz) < 1f)
|
||||
{
|
||||
_intrCount[i]++;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_intrN >= MaxIntr)
|
||||
{
|
||||
_intrOverflow++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_intrDist[_intrN] = dist;
|
||||
_intrPx[_intrN] = px;
|
||||
_intrPy[_intrN] = py;
|
||||
_intrPz[_intrN] = pz;
|
||||
_intrCount[_intrN] = 1;
|
||||
_intrN++;
|
||||
}
|
||||
|
||||
private static void Describe(
|
||||
GpuChannel channel,
|
||||
out int fmt,
|
||||
out int cw,
|
||||
out int ch,
|
||||
out int dw,
|
||||
out int dh,
|
||||
out ulong cAddr,
|
||||
out ulong dAddr)
|
||||
{
|
||||
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
|
||||
Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil;
|
||||
|
||||
// Dimensions HOTES : c'est ce que le rendu occupe vraiment, mise a l'echelle comprise.
|
||||
GAL.ITexture colHost = col0?.HostTexture;
|
||||
GAL.ITexture dsHost = ds?.HostTexture;
|
||||
|
||||
fmt = col0 != null ? (int)col0.Info.FormatInfo.Format : -1;
|
||||
cw = colHost?.Width ?? 0;
|
||||
ch = colHost?.Height ?? 0;
|
||||
dw = dsHost?.Width ?? 0;
|
||||
dh = dsHost?.Height ?? 0;
|
||||
cAddr = col0 != null ? col0.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(
|
||||
Ctx[] table,
|
||||
int fmt,
|
||||
int cw,
|
||||
int ch,
|
||||
int dw,
|
||||
int dh,
|
||||
ulong cAddr,
|
||||
ulong dAddr,
|
||||
int rank,
|
||||
float dist,
|
||||
bool intruder)
|
||||
{
|
||||
for (int i = 0; i < MaxCtx; i++)
|
||||
{
|
||||
ref Ctx c = ref table[i];
|
||||
|
||||
if (!c.Used)
|
||||
{
|
||||
c.Used = true;
|
||||
c.Fmt = fmt;
|
||||
c.CW = cw;
|
||||
c.CH = ch;
|
||||
c.DW = dw;
|
||||
c.DH = dh;
|
||||
c.DepthAddr = dAddr;
|
||||
c.MinRank = rank;
|
||||
c.MaxRank = rank;
|
||||
}
|
||||
else if (c.Fmt != fmt || c.CW != cw || c.CH != ch || c.DW != dw || c.DH != dh ||
|
||||
c.DepthAddr != dAddr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
c.ColorAddr = cAddr;
|
||||
c.Reads++;
|
||||
|
||||
if (rank < c.MinRank)
|
||||
{
|
||||
c.MinRank = rank;
|
||||
}
|
||||
|
||||
if (rank > c.MaxRank)
|
||||
{
|
||||
c.MaxRank = rank;
|
||||
}
|
||||
|
||||
if (intruder)
|
||||
{
|
||||
c.Intruders++;
|
||||
}
|
||||
|
||||
if (dist > c.MaxDist)
|
||||
{
|
||||
c.MaxDist = dist;
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
private static void Report()
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE : {_reads} lectures · {_intruders} INTRUSES (pas >= {IntruderDist:0} u " +
|
||||
$"contre une reference FRAICHE) · {_staleJumps} gros sauts ecartes (reference perimee, " +
|
||||
$"pas des intruses) · " +
|
||||
$"passes qualifiantes par image : min {(_qualMin == int.MaxValue ? 0 : _qualMin)} / " +
|
||||
$"max {_qualMax} / moyenne {(_frames > 0 ? (float)_qualSum / _frames : 0f):0.##} sur {_frames} images" +
|
||||
$"{(_ctxOverflow > 0 ? $" · TABLE PLEINE, {_ctxOverflow} contextes non comptes" : "")}");
|
||||
|
||||
// LA LIGNE QUI DECIDE. Si "passe HDR" est proche de 100 % des images 3D ET que la camera
|
||||
// y est lisible presque toujours, alors deplacer la capture est jouable et c'est mon
|
||||
// garde-fou qui etait mal regle. Si l'un des deux s'effondre, l'approche est morte -- et
|
||||
// c'est un resultat, pas un echec.
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE VERDICT PASSE HDR : {_frames} images 3D · " +
|
||||
$"{_hdrFrames} avec une passe HDR ({(_frames > 0 ? 100f * _hdrFrames / _frames : 0f):0.#} %) · " +
|
||||
$"camera LISIBLE sur {_hdrReadable} " +
|
||||
$"({(_hdrFrames > 0 ? 100f * _hdrReadable / _hdrFrames : 0f):0.#} % de celles-la) · " +
|
||||
$"illisible sur {_hdrUnreadable} · " +
|
||||
$"intruses parmi les lisibles : {_hdrIntruders} " +
|
||||
$"({(_hdrReadable > 0 ? 100f * _hdrIntruders / _hdrReadable : 0f):0.##} %)");
|
||||
|
||||
// L'AGE DE LA REFERENCE : le test qui dit si mes "intruses" sont reelles ou si c'est ma
|
||||
// definition qui les fabrique. Ages comparables => intruses reelles. Age des intruses
|
||||
// bien plus eleve => une partie du residu est un artefact de la sonde.
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE AGE DE LA REFERENCE · saines : " +
|
||||
$"{(_ageNClean > 0 ? (float)_ageSumClean / _ageNClean : 0f):0.#} ms de moyenne sur {_ageNClean} · " +
|
||||
$"intruses : {(_ageNIntr > 0 ? (float)_ageSumIntr / _ageNIntr : 0f):0.#} ms sur {_ageNIntr} " +
|
||||
$"(max {_ageMaxIntr} ms)");
|
||||
|
||||
// LE RANG : intruses contre lectures saines. Un ecart franc ici = un correctif generique
|
||||
// ("ne pas capturer trop tot dans l'image"). Des profils identiques = axe mort, et c'est
|
||||
// un resultat aussi.
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE RANG · saines : moyenne " +
|
||||
$"{(_rankNClean > 0 ? (float)_rankSumClean / _rankNClean : 0f):0.#} sur {_rankNClean} " +
|
||||
$"[rang1 {Pct(_rankHistClean[0], _rankNClean)} · 2-3 {Pct(_rankHistClean[1], _rankNClean)} · " +
|
||||
$"4-10 {Pct(_rankHistClean[2], _rankNClean)} · 11-50 {Pct(_rankHistClean[3], _rankNClean)} · " +
|
||||
$"51+ {Pct(_rankHistClean[4], _rankNClean)}]");
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE RANG · intruses : moyenne " +
|
||||
$"{(_rankNIntr > 0 ? (float)_rankSumIntr / _rankNIntr : 0f):0.#} sur {_rankNIntr} " +
|
||||
$"[rang1 {Pct(_rankHistIntr[0], _rankNIntr)} · 2-3 {Pct(_rankHistIntr[1], _rankNIntr)} · " +
|
||||
$"4-10 {Pct(_rankHistIntr[2], _rankNIntr)} · 11-50 {Pct(_rankHistIntr[3], _rankNIntr)} · " +
|
||||
$"51+ {Pct(_rankHistIntr[4], _rankNIntr)}]");
|
||||
|
||||
// LA LIGNE QUI PEUT FERMER LE DOSSIER : si toutes les intruses tiennent sur une ou deux
|
||||
// valeurs, depuis une position fixe, ce sont des evenements DETERMINISTES (transition de
|
||||
// chargement) et non le symptome qu'on chasse.
|
||||
for (int i = 0; i < _intrN; i++)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE INTRUSE #{i} : pas {_intrDist[i]:0.0} u · " +
|
||||
$"depuis [{_intrPx[i]:0.0} {_intrPy[i]:0.0} {_intrPz[i]:0.0}] · " +
|
||||
$"vue {_intrCount[i]} fois");
|
||||
}
|
||||
|
||||
if (_intrOverflow > 0)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE INTRUSES : {_intrOverflow} valeurs distinctes non comptees (table pleine)");
|
||||
}
|
||||
|
||||
Dump("CONTEXTE DE LECTURE", _readCtx, true);
|
||||
Dump("PASSES QUALIFIANTES", _qualCtx, false);
|
||||
}
|
||||
|
||||
private static string Pct(int n, int total)
|
||||
{
|
||||
return total > 0 ? $"{100f * n / total:0}%" : "-";
|
||||
}
|
||||
|
||||
private static void Dump(string title, Ctx[] table, bool withIntruders)
|
||||
{
|
||||
for (int i = 0; i < MaxCtx; i++)
|
||||
{
|
||||
ref Ctx c = ref table[i];
|
||||
|
||||
if (!c.Used)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CTXPROBE {title} #{i} : " +
|
||||
$"COLOR {(c.Fmt < 0 ? "aucune" : $"{(GAL.Format)c.Fmt} {c.CW}x{c.CH} @0x{c.ColorAddr:X}")} · " +
|
||||
$"DEPTH {c.DW}x{c.DH} @0x{c.DepthAddr:X} · " +
|
||||
$"rang {c.MinRank}-{c.MaxRank} · {c.Reads} vues" +
|
||||
(withIntruders
|
||||
? $" · {c.Intruders} intruses · pas max {c.MaxDist:0.#} u"
|
||||
: ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Gpu.Image;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// [DOFSKIP] (RYUJINX_MVPP_DOF_SCATTER_SKIP=1, inert unless set). Compatibility option.
|
||||
///
|
||||
/// The name MUST keep the RYUJINX_MVPP_ prefix: DlssRestart only carries variables matching
|
||||
/// RYUJINX_DLSS* / RYUJINX_MVPP_* / RYUJINX_HDR_PQ across a cold restart, so a gate named
|
||||
/// outside those prefixes is silently dropped the moment a DLSS setting changes in the UI -
|
||||
/// the same partial-state bug already caught once on SKYDRIFT/SKYGRID/EDGE/BORDERFIX.
|
||||
///
|
||||
/// Skips the scatter/bokeh accumulation pass of the Xenoblade engine's motion blur. On
|
||||
/// Vulkan that pass renders a block artifact this fork has not been able to explain -
|
||||
/// journal 226..246 eliminated, by measurement, every shader of the chain (all bit-identical
|
||||
/// to OpenGL on an offline bench), its inputs, its constants, the sin precision, the LOD,
|
||||
/// the blit path and the synchronisation. The effect it produces exists to hide the console's
|
||||
/// 30 fps; at emulated framerates it buys nothing, so switching it off is a defensible
|
||||
/// trade rather than a mutilation. It IS a trade: the blur is gone, not fixed.
|
||||
///
|
||||
/// Identified by PIPELINE SHAPE, never by a hardcoded guest address or shader hash:
|
||||
/// the pass is the only one in the frame whose colour target is a 512x288-equivalent
|
||||
/// RGBA16F surface. Two consequences matter:
|
||||
/// - it keeps working across game versions and regions, where an address does not;
|
||||
/// - it works with the shader cache ENABLED, unlike the translation-time gate, which
|
||||
/// never matches on a cached shader (the guest address comes back as 0) and therefore
|
||||
/// required playing with the cache off - unusable in practice.
|
||||
///
|
||||
/// Matched by PROPORTION of the render width, so it survives DLSS and ResScale alike.
|
||||
/// </summary>
|
||||
static class MvppDofSkip
|
||||
{
|
||||
/// <summary>
|
||||
/// 0 = off (default), 1 = skip the pass, 2 = DRY RUN: detect and report, skip NOTHING.
|
||||
///
|
||||
/// Mode 2 exists because the shape rule was validated against a 400-target census taken in
|
||||
/// GAMEPLAY only - cutscenes were never censused. If a cutscene pass falls inside the same
|
||||
/// proportions it would be skipped while legitimate, leaving its surface untouched, which
|
||||
/// reads on screen as coloured blocks. Mode 2 answers "which targets would I have skipped"
|
||||
/// during a single cutscene without altering a single pixel, so the visual verdict and the
|
||||
/// measurement come from the SAME run.
|
||||
/// </summary>
|
||||
public static readonly int Mode = ParseMode();
|
||||
|
||||
public static bool Enabled => Mode != 0;
|
||||
|
||||
private static int ParseMode()
|
||||
{
|
||||
return Environment.GetEnvironmentVariable("RYUJINX_MVPP_DOF_SCATTER_SKIP") switch
|
||||
{
|
||||
"1" => 1,
|
||||
"2" => 2,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
// The pass renders at 512x288 when the game renders at 1280x720 - that is 40% of the
|
||||
// render width, in 16:9. Matching those PROPORTIONS instead of an absolute pixel size is
|
||||
// what makes the option survive every scaling path: DLSS multiplies the whole frame
|
||||
// (quality made 512x288 come through as 768x432, x1.5, which an absolute match missed
|
||||
// entirely) and ResScale multiplies it again, neither of them reported by
|
||||
// RenderTargetScale alone.
|
||||
private const float WidthRatio = 0.4f; // 512 / 1280
|
||||
private const float AspectRatio = 16f / 9f;
|
||||
private const float Tolerance = 0.02f;
|
||||
|
||||
private static bool _announced;
|
||||
private static long _skipped;
|
||||
private static long _lastLogMs;
|
||||
private static int _renderWidth;
|
||||
|
||||
// Census of the DISTINCT target shapes this rule matches, and of every render-width change.
|
||||
// Both are silent in mode 1 beyond the first announcement; mode 2 reports them. A second
|
||||
// distinct shape appearing only during cutscenes IS the false positive we are hunting: the
|
||||
// rule cannot tell it apart from the bokeh pass, so it skips a legitimate draw and leaves
|
||||
// its surface untouched.
|
||||
private const int CensusCap = 16;
|
||||
private static readonly long[] _shapes = new long[CensusCap];
|
||||
private static int _shapeCount;
|
||||
private static readonly object _censusLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// True when this draw is the scatter/bokeh accumulation and should not be issued.
|
||||
/// </summary>
|
||||
public static bool ShouldSkip(GpuChannel channel)
|
||||
{
|
||||
if (!Enabled || channel == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Image.Texture target;
|
||||
|
||||
try
|
||||
{
|
||||
target = channel.TextureManager.GetColorTarget(0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int rawW = target.Info.Width;
|
||||
int rawH = target.Info.Height;
|
||||
|
||||
// Track the widest colour target seen: that is the frame's render width, whatever
|
||||
// DLSS and ResScale multiplied it by. Everything else is judged relative to it.
|
||||
if (rawW > _renderWidth)
|
||||
{
|
||||
int previous = _renderWidth;
|
||||
_renderWidth = rawW;
|
||||
|
||||
// The reference only ever grows, so a wider target appearing in a cutscene silently
|
||||
// moves the 40% goalpost for every later frame. Worth seeing, not just inferring.
|
||||
if (Mode == 2 && previous != 0)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[DOFSKIP/DRY] render width reference moved: {previous} -> {rawW}. " +
|
||||
$"The 40% target is now {rawW * WidthRatio:F0}px wide.");
|
||||
}
|
||||
}
|
||||
|
||||
if (target.Info.FormatInfo.Format != GAL.Format.R16G16B16A16Float || _renderWidth == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float widthRatio = rawW / (float)_renderWidth;
|
||||
float aspect = rawH == 0 ? 0f : rawW / (float)rawH;
|
||||
|
||||
if (MathF.Abs(widthRatio - WidthRatio) > Tolerance ||
|
||||
MathF.Abs(aspect - AspectRatio) > Tolerance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_skipped++;
|
||||
|
||||
// Report every DISTINCT shape the rule claims, not just the first one. The original
|
||||
// code announced once and stayed silent forever, so a second matching shape appearing
|
||||
// later in the session - exactly the cutscene case - was invisible in the log.
|
||||
ReportShape(rawW, rawH, widthRatio);
|
||||
|
||||
if (!_announced)
|
||||
{
|
||||
_announced = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[DOFSKIP] armed: skipping the scatter/bokeh pass (target {rawW}x{rawH} RGBA16F = " +
|
||||
$"{widthRatio * 100f:F0}% of a {_renderWidth}px render, 16:9). " +
|
||||
"Motion blur will be absent - this is the option's purpose.");
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastLogMs >= 10000)
|
||||
{
|
||||
_lastLogMs = now;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[DOFSKIP{(Mode == 2 ? "/DRY" : "")}] draws matched so far: {_skipped}");
|
||||
}
|
||||
|
||||
// Dry run: everything above is measurement, nothing is skipped. The frame renders exactly
|
||||
// as it would with the option off, so the eye verdict and the census come from one run.
|
||||
return Mode != 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a matched target shape the first time it is seen. Deduplicated and capped, so a
|
||||
/// pass running once per frame costs one line for the whole session.
|
||||
/// </summary>
|
||||
private static void ReportShape(int width, int height, float widthRatio)
|
||||
{
|
||||
long key = ((long)width << 32) | (uint)height;
|
||||
|
||||
lock (_censusLock)
|
||||
{
|
||||
for (int i = 0; i < _shapeCount; i++)
|
||||
{
|
||||
if (_shapes[i] == key)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_shapeCount >= CensusCap)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shapes[_shapeCount++] = key;
|
||||
|
||||
string tag = Mode == 2 ? "DOFSKIP/DRY" : "DOFSKIP";
|
||||
string verb = Mode == 2 ? "WOULD MATCH" : "MATCH";
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[{tag}] {verb} #{_shapeCount}: {width}x{height} RGBA16F = " +
|
||||
$"{widthRatio * 100f:F1}% of a {_renderWidth}px render. " +
|
||||
(_shapeCount > 1
|
||||
? "SECOND DISTINCT SHAPE - the rule is claiming more than the bokeh pass."
|
||||
: "This is the expected bokeh target."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Capture PAS À PAS d'une seule image (RYUJINX_DRAWSTEP=1). READ-ONLY, off par défaut.
|
||||
///
|
||||
/// POURQUOI. RenderDoc refuse de tourner sur cet émulateur (deux tentatives, deux plantages).
|
||||
/// Or ce qu'il apporte tient en une chose : voir le contenu de la cible APRÈS CHAQUE DRAW, pour
|
||||
/// repérer l'instant exact où les pixels deviennent faux. Cette sonde fait la même chose en plus
|
||||
/// rustique : sur UNE image et une seule, elle écrit la cible couleur principale tous les N draws.
|
||||
/// On obtient une séquence qui montre la scène se construire, et on lit à quel moment le
|
||||
/// quadrillage ou les traînées apparaissent -- puis on remonte au draw responsable.
|
||||
///
|
||||
/// Le dossier XC2 au 21/07 : le défaut est DANS l'image que le jeu produit (établi sur une paire
|
||||
/// même-image), et aucun état de rastérisation n'est en cause (viewport, scissor, screen scissor,
|
||||
/// clip, miroir, swizzle : six mesures, zéro anomalie sur ~90 000 draws). Restent les draws
|
||||
/// eux-mêmes, et personne ne les a jamais regardés un par un.
|
||||
///
|
||||
/// COÛT ASSUMÉ : chaque capture est une lecture GPU->CPU synchrone qui vide le pipeline. Une
|
||||
/// trentaine d'affilée fige le jeu quelques secondes. C'est pour ça que ça ne tourne QUE sur une
|
||||
/// image, et jamais par défaut.
|
||||
/// </summary>
|
||||
static class MvppDrawStepProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP") == "1";
|
||||
|
||||
/// <summary>Secondes avant de capturer l'image (RYUJINX_DRAWSTEP_START, défaut 45).</summary>
|
||||
private static readonly int _startSeconds =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_START"), out int st) && st > 0 ? st : 45;
|
||||
|
||||
/// <summary>Un cliché tous les N draws (RYUJINX_DRAWSTEP_EVERY, défaut 50).</summary>
|
||||
private static readonly int _everyDraws =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_EVERY"), out int ev) && ev > 0 ? ev : 50;
|
||||
|
||||
/// <summary>
|
||||
/// Ne capturer qu'à partir de ce numéro de draw (RYUJINX_DRAWSTEP_FROM, défaut 0 = dès le début).
|
||||
/// [21/07] La frame artefactée a ~54 draws et le budget s'épuisait au draw 33, ratant la
|
||||
/// composition finale (34-54) où la corruption naît. Ce gate saute le G-buffer du début pour
|
||||
/// dépenser tout le budget DENSÉMENT sur la fin de frame.
|
||||
/// </summary>
|
||||
private static readonly int _fromDraw =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FROM"), out int fdr) && fdr > 0 ? fdr : 0;
|
||||
|
||||
/// <summary>Plafond de clichés PAR image (RYUJINX_DRAWSTEP_MAX, défaut 25).</summary>
|
||||
private static readonly int _maxSteps =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_MAX"), out int mx) && mx > 0 ? mx : 25;
|
||||
|
||||
/// <summary>
|
||||
/// Nombre d'images instrumentées (RYUJINX_DRAWSTEP_FRAMES, défaut 5), espacées de _gapMs.
|
||||
/// Une seule image obligerait le joueur à tomber pile sur l'instant où l'artefact est là ;
|
||||
/// avec plusieurs tentatives espacées, il lui suffit de tourner en continu.
|
||||
/// </summary>
|
||||
private static readonly int _frames =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FRAMES"), out int fr) && fr > 0 ? fr : 5;
|
||||
|
||||
/// <summary>Pause entre deux images instrumentées (RYUJINX_DRAWSTEP_GAP, défaut 3000 ms).</summary>
|
||||
private static readonly int _gapMs =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_GAP"), out int gp) && gp > 0 ? gp : 3000;
|
||||
|
||||
/// <summary>
|
||||
/// Déclenchement AU CLAVIER (F10 par défaut, RYUJINX_DRAWSTEP_VKEY pour changer le code).
|
||||
///
|
||||
/// La version à minuterie était injouable : elle demandait d'avoir chargé la partie, d'être au
|
||||
/// bon endroit ET que l'artefact soit visible à la seconde près. Ici c'est l'inverse -- Alex joue
|
||||
/// normalement, et quand il VOIT le défaut il appuie. Même principe que le F12 de RenderDoc.
|
||||
/// </summary>
|
||||
private static readonly int _vkey =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_VKEY"), out int vk) && vk > 0 ? vk : 0x79;
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
private static bool TriggerHeld()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (GetAsyncKeyState(_vkey) & 0x8000) != 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static long _armMs;
|
||||
private static long _nextMs;
|
||||
private static bool _announced;
|
||||
private static bool _capturing;
|
||||
private static bool _done;
|
||||
private static int _frameNo;
|
||||
private static int _csDispatches, _csBindings, _csImages, _csStores;
|
||||
private static int _drawNo;
|
||||
private static int _stepNo;
|
||||
private static string _dir;
|
||||
|
||||
/// <summary>
|
||||
/// Frontière d'image, appelée depuis Gpu/Window.Present. Ouvre la capture sur l'image suivante,
|
||||
/// puis la referme définitivement : une seule image est instrumentée par session.
|
||||
/// </summary>
|
||||
public static void OnPresent(Image.Texture presented)
|
||||
{
|
||||
if (!_enabled || _done)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (_armMs == 0)
|
||||
{
|
||||
_armMs = now;
|
||||
}
|
||||
|
||||
if (!_announced)
|
||||
{
|
||||
_announced = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP DRAWSTEP: armed -- APPUIE SUR F10 quand tu VOIS l'artefact. Chaque appui capture une " +
|
||||
$"image entiere ({_maxSteps} cliches, un tous les {_everyDraws} draws). {_frames} appuis au " +
|
||||
"maximum. Le jeu figera brievement a chaque fois : c'est la capture.");
|
||||
}
|
||||
|
||||
if (_capturing)
|
||||
{
|
||||
// L'image instrumentée vient de se terminer. On écrit d'abord CE QUE LE JOUEUR VOIT :
|
||||
// sans ça la séquence s'arrête sur un buffer HDR intermédiaire, qu'Alex ne peut pas
|
||||
// reconnaître -- c'est ce qui a rendu les 5 premières séquences inexploitables.
|
||||
if (presented?.HostTexture != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using GAL.PinnedSpan<byte> pdata = presented.HostTexture.GetData();
|
||||
ReadOnlySpan<byte> pbytes = pdata.Get();
|
||||
string pname = $"ZFINAL_presente_{presented.Info.Width}x{presented.Info.Height}_{presented.Info.FormatInfo.Format}.bin";
|
||||
File.WriteAllBytes(Path.Combine(_dir, pname), pbytes.ToArray());
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: {pname} ({pbytes.Length} octets) -- image telle que presentee.");
|
||||
}
|
||||
catch (Exception pe)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: image presentee illisible: {pe.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_capturing = false;
|
||||
_nextMs = now + _gapMs;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP DRAWSTEP: image {_frameNo}/{_frames} terminee -- {_stepNo} cliches sur {_drawNo} draws, dans {_dir}. " +
|
||||
$"COMPUTE: {_csDispatches} dispatchs, {_csBindings} bindings, {_csImages} images, {_csStores} en ecriture.");
|
||||
|
||||
if (_frameNo >= _frames)
|
||||
{
|
||||
_done = true;
|
||||
Logger.Info?.Print(LogClass.Gpu, "MVPP DRAWSTEP: toutes les images sont capturees.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Anti-rebond seulement : la décision appartient entièrement au joueur.
|
||||
if (now < _nextMs || !TriggerHeld())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_frameNo++;
|
||||
_dir = Path.Combine("drawstep", $"image{_frameNo:D2}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
_drawNo = 0;
|
||||
_stepNo = 0;
|
||||
_csDispatches = _csBindings = _csImages = _csStores = 0;
|
||||
_capturing = true;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: capture de l'image {_frameNo}/{_frames} -> {_dir}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: desactive apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch COMPUTE, appelé depuis ComputeClass. Mesuré le 21/07 : au dernier DRAW de l'image la
|
||||
/// scène est complète et propre, et dans l'image présentée la moitié du décor a disparu sous une
|
||||
/// zone verte à bords droits. Comme cette sonde voit TOUS les draws et rien d'autre, ce qui reste
|
||||
/// entre les deux, c'est le compute et les copies -- un étage jamais instrumenté dans ce dossier.
|
||||
///
|
||||
/// On capture ici les IMAGES DE SORTIE du dispatch (bindings image en écriture), puisqu'un compute
|
||||
/// n'écrit pas dans une cible de rendu. Le nom porte le rang du dispatch DANS la séquence de draws,
|
||||
/// pour qu'on puisse replacer l'événement au bon endroit de la frame.
|
||||
/// </summary>
|
||||
public static void OnDispatch(Image.TextureManager tm, int gridX, int gridY, int gridZ, ulong shaderVa)
|
||||
{
|
||||
if (!_enabled || !_capturing || _stepNo >= _maxSteps || tm == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int idx = 0;
|
||||
|
||||
// Compteurs VISIBLES : sans eux, "aucune capture" ne distingue pas "aucun dispatch" de
|
||||
// "dispatch sans image en ecriture". C'est la regle du projet, et je l'ai deja violee une fois.
|
||||
_csDispatches++;
|
||||
|
||||
tm.MvppEnumerateComputeBindings((isImage, isStore, tex) =>
|
||||
{
|
||||
_csBindings++;
|
||||
|
||||
if (isImage)
|
||||
{
|
||||
_csImages++;
|
||||
}
|
||||
|
||||
if (isImage && isStore)
|
||||
{
|
||||
_csStores++;
|
||||
}
|
||||
});
|
||||
|
||||
tm.MvppEnumerateComputeBindings((isImage, isStore, tex) =>
|
||||
{
|
||||
if (!isImage || !isStore || tex?.HostTexture == null || _stepNo >= _maxSteps)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using GAL.PinnedSpan<byte> data = tex.HostTexture.GetData();
|
||||
ReadOnlySpan<byte> bytes = data.Get();
|
||||
|
||||
string name = $"step{_stepNo:D3}_CS{idx}_apresDraw{_drawNo:D5}_grid{gridX}x{gridY}x{gridZ}_" +
|
||||
$"{tex.Info.Width}x{tex.Info.Height}_{tex.Info.FormatInfo.Format}.bin";
|
||||
File.WriteAllBytes(Path.Combine(_dir, name), bytes.ToArray());
|
||||
_stepNo++;
|
||||
idx++;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: {name} (shader 0x{shaderVa:X}).");
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: desactive apres erreur compute: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnDraw(GpuChannel channel)
|
||||
{
|
||||
if (!_enabled || !_capturing || _stepNo >= _maxSteps)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
++_drawNo;
|
||||
|
||||
if (_drawNo < _fromDraw || (_drawNo % _everyDraws) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// [21/07] TOUS les slots couleur, plus seulement le 0 : la texture PRÉSENTÉE est un
|
||||
// R8G8B8A8 alors que les derniers draws visaient le HDR R11G11B10 en slot 0. En ne
|
||||
// regardant que le slot 0 on capturait la mauvaise cible et on croyait la scène propre.
|
||||
int localDraw = _drawNo;
|
||||
|
||||
channel.TextureManager.MvppEnumerateRenderTargets((slot, rt) =>
|
||||
{
|
||||
if (rt?.HostTexture == null || _stepNo >= _maxSteps)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using GAL.PinnedSpan<byte> data = rt.HostTexture.GetData();
|
||||
ReadOnlySpan<byte> bytes = data.Get();
|
||||
|
||||
string name = $"step{_stepNo:D3}_draw{localDraw:D5}_slot{slot}_{rt.Info.Width}x{rt.Info.Height}_{rt.Info.FormatInfo.Format}.bin";
|
||||
File.WriteAllBytes(Path.Combine(_dir, name), bytes.ToArray());
|
||||
_stepNo++;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: {name} ({bytes.Length} octets).");
|
||||
});
|
||||
|
||||
// [DRAWSTEP-INPUTS 21/07] Dump aussi CE QUE CE DRAW ECHANTILLONNE. Trancher hérité vs né :
|
||||
// si une sortie capturée est striée ALORS QUE ses entrées le sont déjà -> corruption
|
||||
// HERITEE (remonter à qui a écrit cette entrée). Si les entrées sont propres et la sortie
|
||||
// striée -> corruption NEE à ce draw (shader / mise en place de la cible). C'est la
|
||||
// bifurcation que la capture par-slot du 21/07 ne pouvait pas résoudre : elle ne voyait
|
||||
// que les sorties.
|
||||
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) =>
|
||||
{
|
||||
if (tex?.HostTexture == null || _stepNo >= _maxSteps)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using GAL.PinnedSpan<byte> idata = tex.HostTexture.GetData();
|
||||
ReadOnlySpan<byte> ibytes = idata.Get();
|
||||
|
||||
string iname = $"step{_stepNo:D3}_draw{localDraw:D5}_INPUT_s{stage}_{tex.Info.Width}x{tex.Info.Height}_{tex.Info.FormatInfo.Format}.bin";
|
||||
File.WriteAllBytes(Path.Combine(_dir, iname), ibytes.ToArray());
|
||||
_stepNo++;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: {iname} (entree echantillonnee).");
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: desactive apres erreur de lecture: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture APRÈS l'écriture du draw (appelée en fin de DrawEnd, chemin normal). Même grille que
|
||||
/// OnDraw mais SANS ré-incrémenter _drawNo (déjà fait au pré). Nom suffixé _POST pour apparier
|
||||
/// pré et post et dire si CE draw écrit la corruption ou la trouve déjà présente dans la cible.
|
||||
/// </summary>
|
||||
public static void OnDrawPost(GpuChannel channel)
|
||||
{
|
||||
if (!_enabled || !_capturing || _stepNo >= _maxSteps)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_drawNo < _fromDraw || (_drawNo % _everyDraws) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int localDraw = _drawNo;
|
||||
|
||||
channel.TextureManager.MvppEnumerateRenderTargets((slot, rt) =>
|
||||
{
|
||||
if (rt?.HostTexture == null || _stepNo >= _maxSteps)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using GAL.PinnedSpan<byte> data = rt.HostTexture.GetData();
|
||||
ReadOnlySpan<byte> bytes = data.Get();
|
||||
|
||||
string name = $"step{_stepNo:D3}_draw{localDraw:D5}_slot{slot}_POST_{rt.Info.Width}x{rt.Info.Height}_{rt.Info.FormatInfo.Format}.bin";
|
||||
File.WriteAllBytes(Path.Combine(_dir, name), bytes.ToArray());
|
||||
_stepNo++;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: {name} (apres ecriture).");
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: desactive apres erreur POST: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Light-glow clipping probe (RYUJINX_GLOW_PROBE=1, OFF by default, read-only).
|
||||
///
|
||||
/// WHERE THIS COMES FROM. Ten suspects were eliminated by measurement on the Xenoblade 2
|
||||
/// artefact (shader cache, DLSS/MV++/FG, runtime mipmaps, recycled memory, history resets, the
|
||||
/// fork's gobBlocksInZ clamp, sampling a bound target, the 1080p mod, dynamic resolution, and
|
||||
/// the scene buffer itself which dumped perfectly clean). Capturing the actual render targets
|
||||
/// then found it: one 1280x720 R11G11B10Float buffer holds the game's LIGHT HALOS, and every
|
||||
/// halo sits inside a HARD-EDGED RECTANGLE -- the glow is sliced off at the box border instead
|
||||
/// of fading out. Detected on six independent captures: ~450 columns and ~300 rows of abrupt
|
||||
/// edges, where a normal scene image gives 0 to 3.
|
||||
///
|
||||
/// That matches every observation: the boxes follow the lights so they move when the camera
|
||||
/// turns; more lights indoors means more boxes; it is absent when no light is in frame; and it
|
||||
/// survived every switch because it is the GAME's own rendering, upstream of all of it.
|
||||
///
|
||||
/// WHAT THIS PROBE ANSWERS. Are those rectangles the SCISSOR the game sets for each glow draw?
|
||||
/// If the logged scissor boxes match the rectangles seen in the dumps, the mechanism is
|
||||
/// "content drawn larger than the box it is clipped to", and we know exactly what to look at
|
||||
/// next. If the scissors are full-screen, the rectangles come from the geometry or the texture
|
||||
/// instead, and that is a different fix. Read-only either way: it records and reports.
|
||||
/// </summary>
|
||||
static class MvppGlowProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_GLOW_PROBE") == "1";
|
||||
|
||||
private static readonly Dictionary<string, int> _boxes = new();
|
||||
private static readonly Dictionary<string, int> _inputs = new();
|
||||
private static long _lastLogMs;
|
||||
private static int _drawsToGlow;
|
||||
private static int _scissored;
|
||||
|
||||
public static void OnDraw(GpuChannel channel, ref ThreedClassState state)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OnDrawImpl(channel, ref state);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"GLOWPROBE: disabled after unexpected error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state)
|
||||
{
|
||||
// Only the HDR float target the halos live in. Identified from the dumps, by FORMAT and
|
||||
// shape rather than by any address or game-specific value.
|
||||
Image.Texture c0 = channel.TextureManager.RenderTargetColor0;
|
||||
|
||||
if (c0 == null || !c0.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_drawsToGlow++;
|
||||
|
||||
ScissorState sc = state.ScissorState[0];
|
||||
ScreenScissorState screen = state.ScreenScissorState;
|
||||
|
||||
int w = c0.Info.Width;
|
||||
int h = c0.Info.Height;
|
||||
|
||||
bool boxed = sc.Enable && (sc.X1 > 0 || sc.Y1 > 0 || sc.X2 < w || sc.Y2 < h);
|
||||
|
||||
if (boxed)
|
||||
{
|
||||
_scissored++;
|
||||
|
||||
// Round to 8 px so the same box seen over consecutive frames aggregates instead of
|
||||
// producing a line per pixel of camera drift.
|
||||
string key = $"{sc.X1 / 8 * 8},{sc.Y1 / 8 * 8} -> {sc.X2 / 8 * 8},{sc.Y2 / 8 * 8} " +
|
||||
$"({(sc.X2 - sc.X1) / 8 * 8}x{(sc.Y2 - sc.Y1) / 8 * 8})";
|
||||
|
||||
_boxes.TryGetValue(key, out int n);
|
||||
_boxes[key] = n + 1;
|
||||
}
|
||||
|
||||
// The scissor answered "no" (0 boxes over the whole run), so the rectangles are the
|
||||
// glow QUADS themselves -- which is normal geometry. What is NOT normal, proven
|
||||
// against a TOTK control where the same buffer shows zero hard edges, is that the halo
|
||||
// is still bright AT the quad border instead of having faded to nothing. So look at
|
||||
// what these draws READ: the halo texture and, above all, its ADDRESSING MODE, which
|
||||
// is exactly what decides the value returned past the texture edge. ClampToEdge repeats
|
||||
// the last texel for ever (a bright rim stays bright); ClampToBorder returns the border
|
||||
// colour (normally transparent black, which fades correctly).
|
||||
channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) =>
|
||||
{
|
||||
if (tex == null || smp == null || _inputs.Count >= 24)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string k = $"{tex.Info.Width}x{tex.Info.Height} {tex.Info.FormatInfo.Format} " +
|
||||
$"U={smp.ProbeAddressU} V={smp.ProbeAddressV} borderA={smp.ProbeBorderA:0.##} " +
|
||||
$"min={smp.ProbeMinFilter}";
|
||||
|
||||
_inputs.TryGetValue(k, out int c);
|
||||
_inputs[k] = c + 1;
|
||||
});
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastLogMs < 5000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastLogMs = now;
|
||||
|
||||
System.Text.StringBuilder sb = new();
|
||||
sb.Append($"GLOWPROBE: {_drawsToGlow} draws vers le buffer HDR {w}x{h}, {_scissored} avec une boite de decoupe");
|
||||
sb.Append($" (ecran {screen.X},{screen.Y} {screen.Width}x{screen.Height})");
|
||||
|
||||
if (_boxes.Count == 0)
|
||||
{
|
||||
sb.Append(" -- AUCUNE boite: les rectangles ne viennent PAS du scissor.");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($", {_boxes.Count} boites distinctes:");
|
||||
int shown = 0;
|
||||
|
||||
foreach (KeyValuePair<string, int> kv in _boxes)
|
||||
{
|
||||
if (shown++ >= 8)
|
||||
{
|
||||
sb.Append($" (+{_boxes.Count - 8})");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append($" [{kv.Key} x{kv.Value}]");
|
||||
}
|
||||
}
|
||||
|
||||
if (_inputs.Count > 0)
|
||||
{
|
||||
System.Text.StringBuilder ib = new("GLOWPROBE textures lues par ces draws:");
|
||||
int k = 0;
|
||||
|
||||
foreach (KeyValuePair<string, int> kv in _inputs)
|
||||
{
|
||||
if (k++ >= 10)
|
||||
{
|
||||
ib.Append($" (+{_inputs.Count - 10})");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
ib.Append($"\n [{kv.Key}] x{kv.Value}");
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, ib.ToString());
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
|
||||
|
||||
_inputs.Clear();
|
||||
_drawsToGlow = 0;
|
||||
_scissored = 0;
|
||||
_boxes.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// [HANGWATCH 29/07 fin de soirée] Surveillance qui SURVIT AU GEL
|
||||
/// (RYUJINX_MVPP_HANGWATCH=1, coupée par défaut). Préparée à la demande d'Alex, à laisser
|
||||
/// dormir : le chantier du gel n'est PAS ouvert, l'instrument est juste prêt.
|
||||
///
|
||||
/// LE PROBLÈME QU'ELLE RÉSOUT. Alex a un blocage fréquent : après un combat, sur un écran de
|
||||
/// chargement, **l'image se figeauze mais la musique continue**, le processus reste vivant (214
|
||||
/// fils, un seul qui tourne, la fenêtre répond), et fermer/rouvrir suffit à repartir. Vu sans
|
||||
/// FG comme avec, et bien avant qu'on la rallume ⇒ **la FG est hors de cause**.
|
||||
///
|
||||
/// ⚠️ ET ON EST AVEUGLES À L'INSTANT EXACT : toutes les lignes du journal viennent du fil
|
||||
/// graphique, donc quand il se coince, le journal se coince avec lui. On voit les dix minutes
|
||||
/// d'avant, jamais le moment. Aucune sonde posée sur ce fil ne peut répondre -- il faut un
|
||||
/// observateur EXTÉRIEUR. C'est tout l'objet de ce fichier.
|
||||
///
|
||||
/// CE QU'ELLE MESURE, ET POURQUOI CES DEUX CHIFFRES SUFFISENT À TRANCHER. Un fil de fond,
|
||||
/// indépendant, compare deux horloges :
|
||||
/// - la dernière PRÉSENTATION d'image (<see cref="Ping"/>, appelé depuis OnFrameEnqueued) ;
|
||||
/// - le compteur de DESSINS de <see cref="MvppCameraCapture.StatScaledDraws"/>.
|
||||
/// Deux familles, et elles ne se réparent pas au même endroit :
|
||||
/// A. les présentations s'arrêtent MAIS les dessins continuent d'avancer ⇒ le fil graphique
|
||||
/// est vivant, c'est la PRÉSENTATION qui est bloquée (chaîne d'affichage, file de
|
||||
/// présentation, interposition d'un proxy) ;
|
||||
/// B. les deux s'arrêtent ensemble ⇒ c'est le fil graphique LUI-MÊME qui est coincé (attente
|
||||
/// GPU, compilation de shaders, verrou).
|
||||
/// Un seul run avec cette surveillance armée donnera la lettre. Sans elle, on ne peut que
|
||||
/// deviner -- et deviner est exactement ce qui a coûté des runs à Alex le 29/07.
|
||||
///
|
||||
/// ⛔ ELLE NE RÉPARE RIEN ET NE TOUCHE À RIEN : aucune écriture d'état de rendu, aucun réveil
|
||||
/// forcé, aucune tentative de récupération. Elle observe et elle écrit une ligne. Le seul coût
|
||||
/// sur le chemin chaud est une écriture d'entier par image présentée.
|
||||
/// </summary>
|
||||
static class MvppHangWatch
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HANGWATCH") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Au-delà de ce silence, on considère l'affichage arrêté. 3 s = une centaine d'images
|
||||
/// manquées à 30 Hz : bien au-delà d'un simple à-coup ou d'une compilation de shaders
|
||||
/// ordinaire, et bien en dessous des dizaines de secondes qu'Alex laisse passer avant de
|
||||
/// fermer. Ni faux positifs de micro-saccade, ni détection trop tardive.
|
||||
/// </summary>
|
||||
private const long StallMs = 3000;
|
||||
|
||||
/// <summary>Rappel pendant que le gel dure, pour voir si les dessins avancent ou non.</summary>
|
||||
private const long RepeatMs = 5000;
|
||||
|
||||
private static long _lastPresentMs;
|
||||
private static int _started;
|
||||
|
||||
private static long _stallLoggedMs;
|
||||
private static int _drawsAtStall;
|
||||
private static bool _inStall;
|
||||
|
||||
/// <summary>
|
||||
/// Appelée à chaque image présentée. Volontairement minuscule : une écriture d'entier, rien
|
||||
/// d'autre, pas de verrou -- elle est sur le chemin chaud.
|
||||
/// </summary>
|
||||
public static void Ping()
|
||||
{
|
||||
Volatile.Write(ref _lastPresentMs, Environment.TickCount64);
|
||||
|
||||
if (Volatile.Read(ref _started) == 0 && Interlocked.Exchange(ref _started, 1) == 0)
|
||||
{
|
||||
Thread t = new(Loop)
|
||||
{
|
||||
Name = "MvppHangWatch",
|
||||
IsBackground = true,
|
||||
};
|
||||
|
||||
t.Start();
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP HANGWATCH : surveillance armee (silence d'affichage de {StallMs} ms = gel). " +
|
||||
"Elle observe seulement, elle ne repare rien.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Loop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
long last = Volatile.Read(ref _lastPresentMs);
|
||||
long silence = now - last;
|
||||
|
||||
if (silence < StallMs)
|
||||
{
|
||||
if (_inStall)
|
||||
{
|
||||
_inStall = false;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"MVPP HANGWATCH : l'affichage est REPARTI apres {silence} ms de silence " +
|
||||
"-- ce n'etait donc pas definitif. Noter ce qui se passait a l'ecran.");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Le chiffre qui tranche : les dessins avancent-ils PENDANT le gel ?
|
||||
int draws = MvppCameraCapture.StatScaledDraws;
|
||||
|
||||
if (!_inStall)
|
||||
{
|
||||
_inStall = true;
|
||||
_stallLoggedMs = now;
|
||||
_drawsAtStall = draws;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"MVPP HANGWATCH : AUCUNE IMAGE PRESENTEE depuis {silence} ms. " +
|
||||
$"Compteur de dessins a cet instant : {draws}. " +
|
||||
"Prochaine ligne dans quelques secondes -- si ce compteur a AVANCE, c'est la " +
|
||||
"PRESENTATION qui est bloquee (le fil graphique vit) ; s'il est FIGE, c'est le " +
|
||||
"fil graphique lui-meme qui est coince.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now - _stallLoggedMs < RepeatMs)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_stallLoggedMs = now;
|
||||
|
||||
int delta = draws - _drawsAtStall;
|
||||
_drawsAtStall = draws;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"MVPP HANGWATCH : gel de {silence / 1000} s. Dessins depuis la derniere ligne : " +
|
||||
$"{delta} ⇒ " +
|
||||
(delta > 0
|
||||
? "le fil graphique VIT, c'est la PRESENTATION qui est bloquee (famille A)."
|
||||
: "le fil graphique est COINCE lui aussi (famille B)."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Read-while-written texture hazard probe (RYUJINX_TEX_HAZARD=1, OFF by default, read-only).
|
||||
///
|
||||
/// TARGET: the Xenoblade 2 artefact -- large rectangles of real-but-wrong content, appearing
|
||||
/// when the camera TURNS, reportedly worse indoors where more objects are on screen.
|
||||
///
|
||||
/// Everything cheap has already been eliminated BY MEASUREMENT, each with its switch verified
|
||||
/// live in the log (never assumed):
|
||||
/// - shader cache purged, artefact unchanged
|
||||
/// - DLSS / MV++ / FG log proved mode=0, zero "using mode", zero "DLSS: available"
|
||||
/// - runtime mipmaps never applied when DLSS is off (checked in the code path)
|
||||
/// - recycled memory 2000+ fresh allocations zeroed, artefact unchanged
|
||||
/// - history-reset storm 4 resets in a minute (Kameleo20's storm was 85-98%)
|
||||
/// - gobBlocksInZ clamp probe fired ZERO times on this game
|
||||
///
|
||||
/// What is left that displaces REAL content in RECTANGLES is a synchronisation hazard: the game
|
||||
/// SAMPLES a texture that is at the same time a bound RENDER TARGET. It then reads a mix of
|
||||
/// already-written and still-stale tiles -- blocky, made of genuine pixels, and worse when more
|
||||
/// draws are in flight. Turning the camera is when a streaming game re-renders those buffers.
|
||||
///
|
||||
/// This probe changes NOTHING. Each draw it lists what is being written (colour render targets)
|
||||
/// and what is being read (sampled textures, every stage), and reports the ones that are the
|
||||
/// same memory. A hit names the texture: dimensions, format, address. No hit rules the whole
|
||||
/// mechanism out, and we look elsewhere.
|
||||
/// </summary>
|
||||
static class MvppHazardProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TEX_HAZARD") == "1";
|
||||
|
||||
private const int MaxTargets = 8;
|
||||
|
||||
private static readonly ulong[] _rtAddr = new ulong[MaxTargets];
|
||||
private static readonly Image.Texture[] _rtTex = new Image.Texture[MaxTargets];
|
||||
private static int _rtCount;
|
||||
|
||||
private static readonly Dictionary<string, int> _hits = new();
|
||||
private static long _lastLogMs;
|
||||
private static int _draws;
|
||||
private static int _hitDraws;
|
||||
|
||||
public static void OnDraw(GpuChannel channel)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Never take the process down from a diagnostic on the GPU thread.
|
||||
try
|
||||
{
|
||||
OnDrawImpl(channel);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"TEXHAZARD: disabled after unexpected error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDrawImpl(GpuChannel channel)
|
||||
{
|
||||
_draws++;
|
||||
_rtCount = 0;
|
||||
|
||||
channel.TextureManager.MvppEnumerateRenderTargets((index, tex) =>
|
||||
{
|
||||
if (tex != null && _rtCount < MaxTargets)
|
||||
{
|
||||
_rtTex[_rtCount] = tex;
|
||||
_rtAddr[_rtCount] = tex.Range.GetSubRange(0).Address;
|
||||
_rtCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (_rtCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool hitThisDraw = false;
|
||||
|
||||
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) =>
|
||||
{
|
||||
if (tex == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong addr = tex.Range.GetSubRange(0).Address;
|
||||
|
||||
for (int i = 0; i < _rtCount; i++)
|
||||
{
|
||||
if (_rtAddr[i] != addr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hitThisDraw = true;
|
||||
|
||||
// Distinct shapes, not one line per draw: the same few buffers repeat
|
||||
// thousands of times a second and what matters is WHICH ones.
|
||||
string key =
|
||||
$"{tex.Info.Width}x{tex.Info.Height} {tex.Info.FormatInfo.Format} " +
|
||||
$"{tex.Info.Target} @{addr:X10} (rt{i}, stage{stage})";
|
||||
|
||||
_hits.TryGetValue(key, out int n);
|
||||
_hits[key] = n + 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (hitThisDraw)
|
||||
{
|
||||
_hitDraws++;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastLogMs < 5000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastLogMs = now;
|
||||
|
||||
System.Text.StringBuilder sb = new();
|
||||
sb.Append($"TEXHAZARD: {_hitDraws}/{_draws} draws sample a bound render target");
|
||||
|
||||
if (_hits.Count == 0)
|
||||
{
|
||||
sb.Append(" -- AUCUN (mecanisme ecarte pour cette fenetre).");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($", {_hits.Count} textures distinctes:");
|
||||
int shown = 0;
|
||||
|
||||
foreach (KeyValuePair<string, int> kv in _hits)
|
||||
{
|
||||
if (shown++ >= 6)
|
||||
{
|
||||
sb.Append($" (+{_hits.Count - 6} autres)");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append($" [{kv.Key} x{kv.Value}]");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
|
||||
|
||||
_draws = 0;
|
||||
_hitDraws = 0;
|
||||
_hits.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Comparaison tuilage demandé / tuilage fourni pour la texture présentée (RYUJINX_LAYOUT=1). READ-ONLY.
|
||||
///
|
||||
/// Établi le 21/07 : la texture remise au present arrive DÉJÀ corrompue, avec une mosaïque en blocs =
|
||||
/// signature d'un détuilage block-linear raté. Ici on met face à face ce que le JEU a demandé
|
||||
/// (pt.Info, construit dans EnqueueFrameThreadSafe à partir de stride/isLinear/gobBlocksInY que le jeu
|
||||
/// fournit) et ce que le cache a réellement retrouvé/créé (texture.Info). Un écart sur isLinear,
|
||||
/// gobBlocksInY ou stride est la cause exacte cherchée.
|
||||
///
|
||||
/// Log throttlé + une ligne à CHAQUE changement de signature, pour ne rien rater sans spammer.
|
||||
/// </summary>
|
||||
static class MvppLayoutProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_LAYOUT") == "1";
|
||||
|
||||
private static bool _announced;
|
||||
private static long _nextMs;
|
||||
private static string _lastSig = "";
|
||||
|
||||
public static void Compare(Image.TextureInfo want, Image.Texture got)
|
||||
{
|
||||
if (!_enabled || got == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_announced)
|
||||
{
|
||||
_announced = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
"MVPP LAYOUT: ON -- demande (jeu) vs fourni (cache) pour la texture presentee, lecture seule.");
|
||||
}
|
||||
|
||||
Image.TextureInfo g = got.Info;
|
||||
|
||||
bool linMismatch = want.IsLinear != g.IsLinear;
|
||||
bool gobMismatch = want.GobBlocksInY != g.GobBlocksInY;
|
||||
bool strideMismatch = want.Stride != g.Stride;
|
||||
bool dimMismatch = want.Width != g.Width || want.Height != g.Height;
|
||||
bool fmtMismatch = want.FormatInfo.Format != g.FormatInfo.Format;
|
||||
|
||||
bool any = linMismatch || gobMismatch || strideMismatch || dimMismatch || fmtMismatch;
|
||||
|
||||
string sig =
|
||||
$"DEMANDE {want.Width}x{want.Height} lin={want.IsLinear} gobY={want.GobBlocksInY} stride={want.Stride} {want.FormatInfo.Format} " +
|
||||
$"|| FOURNI {g.Width}x{g.Height} lin={g.IsLinear} gobY={g.GobBlocksInY} stride={g.Stride} {g.FormatInfo.Format}" +
|
||||
$"{(any ? " <<< DESACCORD:" : " (accord)")}" +
|
||||
$"{(linMismatch ? " isLinear" : "")}{(gobMismatch ? " gobBlocksInY" : "")}{(strideMismatch ? " stride" : "")}" +
|
||||
$"{(dimMismatch ? " dimensions" : "")}{(fmtMismatch ? " format" : "")}";
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (sig != _lastSig || now >= _nextMs)
|
||||
{
|
||||
_lastSig = sig;
|
||||
_nextMs = now + 2000;
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP LAYOUT: {sig}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP LAYOUT: desactive apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// [NOLDR 29/07 SOIR] Ne PAS capturer la camera sur une passe a cible couleur ENTIERE (8 bits),
|
||||
/// tout en laissant passer les autres. Deux interrupteurs :
|
||||
/// RYUJINX_MVPP_NOLDR=1 -- le correctif, arme apres calibrage (voir le filet plus bas)
|
||||
/// RYUJINX_MVPP_NOLDR_AB=1 -- le MESUREUR : la restriction s'allume et s'eteint toute seule
|
||||
/// par tranches de 600 images, dans le MEME run
|
||||
/// Les deux sont coupes par defaut.
|
||||
///
|
||||
/// POURQUOI CE FICHIER REMPLACE <see cref="MvppScenePass"/>. Ce dernier n'autorisait QUE la
|
||||
/// passe de scene HDR : la camera y etait mesuree lisible dans trop peu d'images, la publication
|
||||
/// s'effondrait, la camera etait AFFAMEE -- et une camera manquee fait ecrire zero sur toute
|
||||
/// l'image. ⛔ Ne pas le re-armer. Ici on n'ELIT pas une passe, on ECARTE la pire.
|
||||
///
|
||||
/// LA MESURE QUI L'A IMPOSE (runs SANS lecture d'essai, donc non contamines) -- la passe 8 bits
|
||||
/// est la pire dans les TROIS runs propres, et la passe HDR la plus saine dans les trois :
|
||||
/// run 1 (etalon) : 8 bits 119/1340 = 8,9 % · HDR 3/230 = 1,3 %
|
||||
/// run SCENEPASS : 8 bits 100/1233 = 8,1 % · HDR 1/1280 = 0,08 %
|
||||
/// run temoin : 8 bits 8/382 = 2,1 % · HDR 1/130 = 0,8 %
|
||||
/// ✅ VERIFIE EN JEU : avec NOLDR arme, la passe 8 bits DISPARAIT de la table des lectures
|
||||
/// (382 -> 0) et les captures se deplacent sur la passe HDR (130 -> 280). Le mecanisme fait ce
|
||||
/// qu'il annonce ; c'est son COUT qui restait a mesurer.
|
||||
///
|
||||
/// LE CRITERE EST GENERIQUE -- forme de pipeline : une cible couleur ATTACHEE dont le format
|
||||
/// n'est PAS flottant. Pas de couleur attachee ⇒ passe. Couleur flottante ⇒ passe. Aucune
|
||||
/// adresse, aucun hash, aucune resolution, aucun nom de jeu.
|
||||
///
|
||||
/// ⚠️⚠️⚠️ MES DEUX FILETS PRECEDENTS ETAIENT FAUX, CHACUN A SA FACON. A RELIRE AVANT D'EN
|
||||
/// ECRIRE UN TROISIEME.
|
||||
/// 1. `SCENEPASS` declenchait sur "2 images d'affilee sans camera". Or la reference en rate
|
||||
/// DEJA 17 % toute seule ⇒ deux rates d'affilee est un evenement a ~3 %, NORMAL au repos :
|
||||
/// le filet sautait sur une condition de base, desarme en 92 secondes.
|
||||
/// 2. La v1 de CE fichier se calibrait sur les 600 premieres images 3D -- c'est-a-dire pendant
|
||||
/// le CHARGEMENT, ou le ratage vaut 90 %. Le seuil devenait 90 + 10 = 100 % :
|
||||
/// **impossible a atteindre**. La restriction a tourne sans garde-fou tout un run.
|
||||
/// 📌 La lecon commune : un detecteur qui ne peut pas se declencher est indiscernable d'un
|
||||
/// detecteur casse. Avant de croire un compteur, verifier ce qu'il compte DANS LE REGIME OU ON
|
||||
/// L'A MIS -- ici, ne rien calibrer avant que la camera soit chaude.
|
||||
///
|
||||
/// LE MESUREUR (mode A/B), ET POURQUOI IL EXISTE. Quatre runs ont rendu des taux de publication
|
||||
/// de 83 / 70 / 63 / 60 % selon l'ENDROIT, et des comptes d'intruses variant d'un facteur 10.
|
||||
/// Comparer deux runs differents ne prouve donc rien -- c'est ce qui a fait juger trois
|
||||
/// correctifs sur du hasard le 29/07 au matin. Le mode A/B supprime la variable : la restriction
|
||||
/// alterne DANS le meme run, sur la meme scene et le meme geste, et les deux moities se
|
||||
/// comparent directement. Chauffe d'abord (200 captures reussies) pour ne jamais mesurer le
|
||||
/// chargement.
|
||||
///
|
||||
/// Les compteurs sont approximatifs : OnDraw tourne sur le fil GPU, OnFrame sur celui de la
|
||||
/// presentation. Ordres de grandeur seulement.
|
||||
/// </summary>
|
||||
static class MvppLdrSkip
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// [NORANK1 29/07 soir, 5e passe] Ne pas capturer sur la PREMIERE tentative de l'image
|
||||
/// (RYUJINX_MVPP_NORANK1=1, coupe par defaut).
|
||||
///
|
||||
/// LA MESURE. Une fois la colonne "rang" reparee, run de 4 min avec `NOLDR` deja arme (donc
|
||||
/// les passes 8 bits deja ecartees -- l'effet mesure ici est INDEPENDANT) :
|
||||
/// lectures saines : rang moyen 33,6 · rang 1 = 48 % · 2-3 = 4 % · 4-10 = 9 % · 51+ = 22 %
|
||||
/// intruses : rang moyen 3,1 · rang 1 = 95 % · 2-3 = 0 % · 4-10 = 0 % · 51+ = 3 %
|
||||
/// En taux, la seule lecture qui vaille : au rang 1 une lecture est intruse dans 1,62 % des
|
||||
/// cas, au-dela du rang 1 dans 0,08 %. **Vingt fois moins.**
|
||||
///
|
||||
/// POURQUOI CA NE DEVRAIT PAS AFFAMER, contrairement a `MvppScenePass` : il y a ~153 passes
|
||||
/// qualifiantes par image, donc sauter la premiere laisse 152 occasions derriere. SCENEPASS
|
||||
/// n'avait que la passe HDR pour se rattraper, et la camera n'y etait pas toujours lisible.
|
||||
/// Le meme filet auto-calibre couvre les deux criteres et tranchera par la mesure.
|
||||
/// </summary>
|
||||
public static readonly bool SkipRank1 =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NORANK1") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Le MESUREUR : fait ALTERNER par tranches de 600 images le ou les criteres armes, dans le
|
||||
/// meme run. Il est agnostique au critere -- il alterne ce qui est allume. C'est le seul
|
||||
/// instrument de la soiree qui ait produit une comparaison fiable, parce qu'il supprime
|
||||
/// l'endroit comme variable.
|
||||
/// </summary>
|
||||
public static readonly bool AbMode =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR_AB") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// [MISSWHY 29/07 fin de soirée] Pourquoi une image rate sa capture quand la restriction est
|
||||
/// armée (RYUJINX_MVPP_MISSWHY=1, coupé par défaut).
|
||||
///
|
||||
/// LE FAIT À EXPLIQUER : sur huit runs, la restriction s'est suspendue DEUX fois, les deux
|
||||
/// dans des scènes où le ratage calibré était déjà haut. Une suspension coûte 100 s de gain.
|
||||
/// Deux explications possibles, et elles ne se réparent PAS de la même façon :
|
||||
/// A. dans ces images il n'existait AUCUNE autre passe pour se rattraper — on a écarté la
|
||||
/// 8 bits et il n'y avait rien derrière ⇒ la restriction est la cause ;
|
||||
/// B. il y avait des alternatives, elles ont été tentées, mais la lecture a échoué ⇒ la
|
||||
/// restriction n'est peut-être pas la cause (l'image aurait raté de toute façon), mais
|
||||
/// les passes 8 bits écartées auraient peut-être réussi. **Cas AMBIGU, dit comme tel.**
|
||||
/// C. rien n'a été écarté dans cette image ⇒ la restriction n'y est pour rien. C'est le
|
||||
/// ratage de fond, celui qui existe sans nous.
|
||||
/// Le compte des trois dit laquelle domine. ⛔ Je ne devine pas : A et B mènent à des
|
||||
/// correctifs différents, et C n'en demande aucun.
|
||||
///
|
||||
/// Les deux compteurs par image sont écrits UNIQUEMENT par le fil GPU (dans Allows), remis à
|
||||
/// zéro par lui aussi via le jeton, et seulement LUS par le fil de présentation à la fin de
|
||||
/// l'image — donc après le dernier dessin. Pas de course sur une écriture. (Case 1 de
|
||||
/// `docs/CHECKLIST-SONDE.md`.)
|
||||
/// </summary>
|
||||
public static readonly bool MissWhy =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_MISSWHY") == "1";
|
||||
|
||||
/// <summary>Le crochet de capture consulte ceci, pas les interrupteurs un par un.</summary>
|
||||
public static bool Active => Enabled || SkipRank1 || AbMode;
|
||||
|
||||
/// <summary>
|
||||
/// Captures reussies a attendre avant de mesurer OU d'armer quoi que ce soit. Sert
|
||||
/// uniquement a sortir de la phase de chargement, ou le ratage vaut 90 % et ou toute
|
||||
/// calibration est un mensonge (faute de la v1 de ce fichier).
|
||||
/// </summary>
|
||||
private const int WarmCaptures = 200;
|
||||
|
||||
private const int CalibrationFrames = 600;
|
||||
private const int WindowFrames = 600;
|
||||
private const float MarginPoints = 0.10f;
|
||||
private const int ReportMs = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// [SUSPENSION 29/07 soir, 4e passe] La v2 coupait la restriction pour TOUTE LA SESSION au
|
||||
/// premier excès. Mesure du soir : sur cinq runs, un seul a déclenché -- et c'était celui
|
||||
/// dont le ratage calibré était déjà le plus haut (11,2 %, fenêtre à 25,3 %) ⇒ la famine a
|
||||
/// l'air LOCALE à une scène. Couper pour la session jetait donc le gain partout ailleurs
|
||||
/// jusqu'à la fermeture du jeu.
|
||||
///
|
||||
/// Ici l'excès ne coupe plus définitivement : il SUSPEND pour 3 000 images (~100 s), puis on
|
||||
/// RECALIBRE sur place -- la scène a changé, l'ancien étalon ne vaut plus -- et on réarme.
|
||||
/// Abandon définitif seulement au 4e excès, avec une ligne de journal explicite. La borne qui
|
||||
/// protège reste la même (le ratage ne peut pas dépasser le calibré de plus de 10 points
|
||||
/// pendant plus d'une fenêtre), mais on récupère le gain dès que la scène le permet.
|
||||
/// </summary>
|
||||
private const int SuspendFrames = 3000;
|
||||
private const int SuspendsBeforeGiveUp = 4;
|
||||
|
||||
private const int PhaseWarming = 0;
|
||||
private const int PhaseCalibrating = 1;
|
||||
private const int PhaseArmed = 2;
|
||||
private const int PhaseDisarmed = 3;
|
||||
private const int PhaseSuspended = 4;
|
||||
|
||||
private static int _suspendLeft;
|
||||
private static int _suspends;
|
||||
|
||||
private static int _phase = PhaseWarming;
|
||||
private static int _warmCaptured;
|
||||
private static float _baseMissRate;
|
||||
private static float _lastWinMissRate = -1f;
|
||||
|
||||
private static int _winFrames;
|
||||
private static int _winMisses;
|
||||
|
||||
private static bool _qualThisFrame;
|
||||
private static int _skipsLdr;
|
||||
private static int _skipsRank;
|
||||
private static int _attempt;
|
||||
private static int _frameMark;
|
||||
private static int _seenMark;
|
||||
|
||||
// Par image, ecrits par le fil GPU seulement (voir MissWhy).
|
||||
private static int _frSkipped;
|
||||
private static int _frAllowed;
|
||||
|
||||
// A : rien d'autre n'etait disponible · B : alternatives tentees et echouees (ambigu)
|
||||
// C : rien n'a ete ecarte, ratage de fond.
|
||||
private static int _missA;
|
||||
private static int _missB;
|
||||
private static int _missC;
|
||||
private static long _lastReportMs;
|
||||
|
||||
// Mode A/B : indice 1 = restriction ACTIVE (avec), indice 0 = restriction LEVEE (sans).
|
||||
private static bool _abArmed;
|
||||
private static int _abWin;
|
||||
private static bool _abStarted;
|
||||
private static readonly int[] _abFrames = new int[2];
|
||||
private static readonly int[] _abMisses = new int[2];
|
||||
private static readonly int[] _abReads = new int[2];
|
||||
private static readonly int[] _abIntruders = new int[2];
|
||||
|
||||
/// <summary>Vrai quand la restriction s'applique a cet instant. Sert a etiqueter les mesures.</summary>
|
||||
internal static bool ArmedNow => AbMode ? _abArmed : _phase == PhaseArmed;
|
||||
|
||||
/// <summary>
|
||||
/// Appelee sur un dessin qualifiant tant que l'image n'a pas encore sa camera. Rend false
|
||||
/// pour SAUTER la tentative sur ce dessin : la capture pourra se faire plus loin dans la
|
||||
/// MEME image, sur une passe qui n'est pas la passe 8 bits.
|
||||
/// </summary>
|
||||
public static bool Allows(GpuChannel channel)
|
||||
{
|
||||
_qualThisFrame = true;
|
||||
|
||||
// Rang de la TENTATIVE dans l'image. Meme mecanique que la reparation de MvppCtxProbe :
|
||||
// OnFrame (fil de presentation) n'avance qu'un jeton, la remise a zero se fait ici, sur
|
||||
// le fil GPU, seul ecrivain de _attempt. Pas de course.
|
||||
int mark = _frameMark;
|
||||
|
||||
if (_seenMark != mark)
|
||||
{
|
||||
_seenMark = mark;
|
||||
_attempt = 0;
|
||||
_frSkipped = 0;
|
||||
_frAllowed = 0;
|
||||
}
|
||||
|
||||
_attempt++;
|
||||
|
||||
if (!ArmedNow)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SkipRank1 && _attempt == 1)
|
||||
{
|
||||
_skipsRank++;
|
||||
_frSkipped++;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Enabled && IsIntegerColorPass(channel))
|
||||
{
|
||||
_skipsLdr++;
|
||||
_frSkipped++;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Une tentative AUTORISEE : si l'image finit quand meme sans camera, c'est que la lecture
|
||||
// a echoue ici, pas que la restriction a ferme la porte.
|
||||
_frAllowed++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La passe a ecarter, reconnue a la FORME de sa cible : une couleur est attachee et son
|
||||
/// format n'est pas flottant. Une passe sans couleur et une passe flottante restent des
|
||||
/// occasions de capture -- c'est ce qui evite la famine qui a tue <see cref="MvppScenePass"/>.
|
||||
/// </summary>
|
||||
private static bool IsIntegerColorPass(GpuChannel channel)
|
||||
{
|
||||
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
|
||||
|
||||
if (col0 == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !IsFloat(col0.Info.FormatInfo.Format);
|
||||
}
|
||||
|
||||
private static bool IsFloat(GAL.Format f)
|
||||
{
|
||||
switch (f)
|
||||
{
|
||||
case GAL.Format.R16Float:
|
||||
case GAL.Format.R32Float:
|
||||
case GAL.Format.R16G16Float:
|
||||
case GAL.Format.R32G32Float:
|
||||
case GAL.Format.R16G16B16Float:
|
||||
case GAL.Format.R32G32B32Float:
|
||||
case GAL.Format.R16G16B16A16Float:
|
||||
case GAL.Format.R32G32B32A32Float:
|
||||
case GAL.Format.R11G11B10Float:
|
||||
case GAL.Format.R9G9B9E5Float:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Etiquette une lecture de camera avec la moitie d'experience en cours. Appelee par
|
||||
/// <see cref="MvppCtxProbe"/>, qui est le seul a savoir si une lecture est une intruse.
|
||||
/// </summary>
|
||||
internal static void NoteRead(bool intruder)
|
||||
{
|
||||
if (!AbMode || !_abStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int p = _abArmed ? 1 : 0;
|
||||
|
||||
_abReads[p]++;
|
||||
|
||||
if (intruder)
|
||||
{
|
||||
_abIntruders[p]++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Une fois par image presentee, AVANT le re-armement du drapeau de capture.</summary>
|
||||
public static void OnFrame(bool captured)
|
||||
{
|
||||
if (!_qualThisFrame)
|
||||
{
|
||||
// Image sans passe 3D (menu, chargement) : elle n'avait pas de camera a prendre.
|
||||
return;
|
||||
}
|
||||
|
||||
_qualThisFrame = false;
|
||||
|
||||
// [MISSWHY] Classement de l'image ratee, ICI : le dernier dessin est passe, les deux
|
||||
// compteurs de l'image sont complets, et le jeton n'a pas encore ete avance.
|
||||
if (MissWhy && !captured && ArmedNow)
|
||||
{
|
||||
if (_frSkipped == 0)
|
||||
{
|
||||
_missC++;
|
||||
}
|
||||
else if (_frAllowed == 0)
|
||||
{
|
||||
_missA++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_missB++;
|
||||
}
|
||||
}
|
||||
|
||||
// Jeton pour le rang de tentative : seul echange entre fils, un int, indechirable.
|
||||
_frameMark++;
|
||||
|
||||
// CHAUFFE, commune aux deux modes : rien n'est mesure ni arme avant que la camera
|
||||
// produise vraiment. C'est la correction de la faute de la v1, qui calibrait pendant le
|
||||
// chargement et en tirait un seuil inatteignable.
|
||||
if (_phase == PhaseWarming)
|
||||
{
|
||||
if (captured)
|
||||
{
|
||||
_warmCaptured++;
|
||||
}
|
||||
|
||||
if (_warmCaptured < WarmCaptures)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AbMode)
|
||||
{
|
||||
_abStarted = true;
|
||||
_abArmed = true;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR A/B : camera chaude ({WarmCaptures} captures), debut de " +
|
||||
$"l'alternance par tranches de {WindowFrames} images. La restriction " +
|
||||
"s'allume et s'eteint toute seule ; les deux moities se comparent sur la " +
|
||||
"MEME scene et le MEME geste.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR : camera chaude ({WarmCaptures} captures), debut du calibrage " +
|
||||
$"sur {CalibrationFrames} images SANS restriction.");
|
||||
}
|
||||
|
||||
_phase = AbMode ? PhaseArmed : PhaseCalibrating;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (AbMode)
|
||||
{
|
||||
int p = _abArmed ? 1 : 0;
|
||||
|
||||
_abFrames[p]++;
|
||||
|
||||
if (!captured)
|
||||
{
|
||||
_abMisses[p]++;
|
||||
}
|
||||
|
||||
if (++_abWin >= WindowFrames)
|
||||
{
|
||||
_abWin = 0;
|
||||
_abArmed = !_abArmed;
|
||||
}
|
||||
|
||||
ReportAb();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// SUSPENSION : la restriction est levee (ArmedNow est faux), on laisse la scene passer,
|
||||
// puis on RECALIBRE ici meme au lieu de reprendre un etalon perime.
|
||||
if (_phase == PhaseSuspended)
|
||||
{
|
||||
if (--_suspendLeft <= 0)
|
||||
{
|
||||
_phase = PhaseCalibrating;
|
||||
_winFrames = 0;
|
||||
_winMisses = 0;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR fin de suspension #{_suspends} : recalibrage sur " +
|
||||
$"{CalibrationFrames} images ICI, puis nouvelle tentative.");
|
||||
}
|
||||
|
||||
ReportPlain();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_winFrames++;
|
||||
|
||||
if (!captured)
|
||||
{
|
||||
_winMisses++;
|
||||
}
|
||||
|
||||
if (_phase == PhaseCalibrating && _winFrames >= CalibrationFrames)
|
||||
{
|
||||
_baseMissRate = (float)_winMisses / _winFrames;
|
||||
_phase = PhaseArmed;
|
||||
_winFrames = 0;
|
||||
_winMisses = 0;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR calibrage termine : ratage de reference {100f * _baseMissRate:0.#} % " +
|
||||
$"sur {CalibrationFrames} images, camera chaude, mesure ICI sans restriction. " +
|
||||
$"Restriction ARMEE ; coupure si une fenetre de {WindowFrames} images depasse ce " +
|
||||
$"taux de plus de {100f * MarginPoints:0} points.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_phase == PhaseArmed && _winFrames >= WindowFrames)
|
||||
{
|
||||
float rate = (float)_winMisses / _winFrames;
|
||||
_lastWinMissRate = rate;
|
||||
_winFrames = 0;
|
||||
_winMisses = 0;
|
||||
|
||||
if (rate > _baseMissRate + MarginPoints)
|
||||
{
|
||||
_suspends++;
|
||||
|
||||
if (_suspends >= SuspendsBeforeGiveUp)
|
||||
{
|
||||
_phase = PhaseDisarmed;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR ABANDON : {_suspends}e exces (ratage {100f * rate:0.#} % contre " +
|
||||
$"{100f * _baseMissRate:0.#} % calibre). Ecarter la passe 8 bits affame la camera " +
|
||||
"de facon repetee sur ce jeu. Retour au comportement d'avant pour le reste de " +
|
||||
"la session, et il faut le savoir : le correctif ne tient pas ici.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_phase = PhaseSuspended;
|
||||
_suspendLeft = SuspendFrames;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR suspendu #{_suspends} : ratage {100f * rate:0.#} % contre " +
|
||||
$"{100f * _baseMissRate:0.#} % calibre (+{100f * (rate - _baseMissRate):0.#} points, " +
|
||||
$"marge {100f * MarginPoints:0}). Restriction levee pour {SuspendFrames} images, " +
|
||||
"puis RECALIBRAGE sur place et nouvelle tentative -- la famine est peut-etre " +
|
||||
"propre a cette scene.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReportPlain();
|
||||
}
|
||||
|
||||
private static void ReportAb()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastReportMs < ReportMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastReportMs = now;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR A/B [tranche en cours : {(_abArmed ? "AVEC" : "SANS")}] · " +
|
||||
$"ecartes : {_skipsRank} au rang 1, {_skipsLdr} en 8 bits");
|
||||
|
||||
Dump("AVEC restriction", 1);
|
||||
Dump("SANS restriction", 0);
|
||||
}
|
||||
|
||||
private static void Dump(string label, int p)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR A/B {label} : {_abFrames[p]} images · " +
|
||||
$"ratage {(_abFrames[p] > 0 ? 100f * _abMisses[p] / _abFrames[p] : 0f):0.#} % · " +
|
||||
$"{_abReads[p]} lectures · {_abIntruders[p]} intruses " +
|
||||
$"({(_abReads[p] > 0 ? 100f * _abIntruders[p] / _abReads[p] : 0f):0.##} %)");
|
||||
}
|
||||
|
||||
private static string Share(int n, int total)
|
||||
{
|
||||
return total > 0 ? $"{100f * n / total:0}%" : "-";
|
||||
}
|
||||
|
||||
private static void ReportPlain()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastReportMs < ReportMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastReportMs = now;
|
||||
|
||||
string phase = _phase switch
|
||||
{
|
||||
PhaseWarming => $"CHAUFFE ({_warmCaptured}/{WarmCaptures})",
|
||||
PhaseCalibrating => $"CALIBRAGE ({_winFrames}/{CalibrationFrames})",
|
||||
PhaseArmed => "ARME",
|
||||
PhaseSuspended => $"SUSPENDU ({_suspendLeft} img restantes, exces #{_suspends})",
|
||||
_ => $"ABANDONNE apres {_suspends} exces",
|
||||
};
|
||||
|
||||
if (MissWhy)
|
||||
{
|
||||
int tot = _missA + _missB + _missC;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP MISSWHY : {tot} images ratees avec restriction armee · " +
|
||||
$"A (aucune alternative, la restriction EST la cause) {_missA} " +
|
||||
$"({Share(_missA, tot)}) · " +
|
||||
$"B (alternatives tentees et echouees, AMBIGU) {_missB} ({Share(_missB, tot)}) · " +
|
||||
$"C (rien d'ecarte, ratage de fond) {_missC} ({Share(_missC, tot)})");
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP NOLDR [{phase}] : ecartes {_skipsRank} au rang 1 / {_skipsLdr} en 8 bits · ratage calibre " +
|
||||
$"{(_phase <= PhaseCalibrating ? "(en cours)" : $"{100f * _baseMissRate:0.#} %")}" +
|
||||
$"{(_lastWinMissRate >= 0f ? $" · derniere fenetre {100f * _lastWinMissRate:0.#} %" : "")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Test décisif XC2 (RYUJINX_PRESYNC=1). READ-ONLY, off par défaut.
|
||||
///
|
||||
/// Capture la texture PRÉSENTÉE juste AVANT et juste APRÈS le `texture.SynchronizeMemory()` de
|
||||
/// `Window.Present` (Gpu/Window.cs ~272). Hypothèse : le render target composé sur GPU est aussi suivi
|
||||
/// comme mémoire invitée et marqué sale, donc cette resynchronisation recharge la mémoire invitée
|
||||
/// (périmée) par-dessus l'image rendue propre, juste avant l'affichage. C'est le seul intervalle du
|
||||
/// pipeline jamais instrumenté, et le seul compatible avec "propre au dernier draw, détruit à l'écran".
|
||||
///
|
||||
/// Déclenchement F10 (comme la sonde pas-à-pas), pour capturer quand l'artefact est visible. Écrit
|
||||
/// AVANT et APRÈS, plus un booléen "identiques ?". Si before propre + after détruit => cause trouvée,
|
||||
/// à la ligne près. Si identiques => l'hypothèse meurt et le coupable est ailleurs dans le present.
|
||||
/// </summary>
|
||||
static class MvppPreSyncProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_PRESYNC") == "1";
|
||||
|
||||
private static readonly int _vkey =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_PRESYNC_VKEY"), out int vk) && vk > 0 ? vk : 0x79;
|
||||
|
||||
private static readonly int _maxShots =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_PRESYNC_MAX"), out int mx) && mx > 0 ? mx : 4;
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
private static bool _announced;
|
||||
private static int _shot;
|
||||
private static long _nextMs;
|
||||
private static byte[] _before;
|
||||
private static bool _armedThisFrame;
|
||||
private static int _guestShots;
|
||||
private static long _nextGuestMs;
|
||||
|
||||
private static bool KeyHeld()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (GetAsyncKeyState(_vkey) & 0x8000) != 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vidage de la mémoire INVITÉE brute de la texture présentée, sur F10. Comparé hors-ligne au
|
||||
/// détuilage : si la mémoire invitée détuilée est PROPRE alors que la texture hôte est corrompue,
|
||||
/// le bug est dans la lecture (détuilage) ; si la mémoire invitée est DÉJÀ corrompue, quelque chose
|
||||
/// a écrit des blocs décalés en amont. Écrit aussi les octets bruts + les paramètres pour rejouer
|
||||
/// le détuilage à la main.
|
||||
/// </summary>
|
||||
public static void DumpGuest(Image.Texture texture)
|
||||
{
|
||||
if (!_enabled || texture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (_guestShots >= _maxShots || now < _nextGuestMs || !KeyHeld())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> guest = texture.PhysicalMemory.GetSpan(texture.Range);
|
||||
|
||||
Image.TextureInfo gi = texture.Info;
|
||||
string dir = Path.Combine("presync", $"guest{_guestShots:D2}");
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
File.WriteAllBytes(
|
||||
Path.Combine(dir, $"MEMOIRE_{gi.Width}x{gi.Height}_lin{gi.IsLinear}_gobY{gi.GobBlocksInY}_stride{gi.Stride}_{gi.FormatInfo.Format}.bin"),
|
||||
guest.ToArray());
|
||||
|
||||
using GAL.PinnedSpan<byte> host = texture.HostTexture.GetData();
|
||||
File.WriteAllBytes(
|
||||
Path.Combine(dir, $"HOTE_{gi.Width}x{gi.Height}_{gi.FormatInfo.Format}.bin"),
|
||||
host.Get().ToArray());
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP PRESYNC GUEST shot {_guestShots}: memoire invitee {guest.Length} octets + texture hote, " +
|
||||
$"{gi.Width}x{gi.Height} lin={gi.IsLinear} gobY={gi.GobBlocksInY} stride={gi.Stride} {gi.FormatInfo.Format} -> {dir}");
|
||||
|
||||
_guestShots++;
|
||||
_nextGuestMs = now + 2000;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP PRESYNC GUEST: desactive apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Before(Image.Texture texture)
|
||||
{
|
||||
if (!_enabled || texture?.HostTexture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_announced)
|
||||
{
|
||||
_announced = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
"MVPP PRESYNC: ON -- APPUIE SUR F10 quand tu VOIS l'artefact. Capture l'image presentee " +
|
||||
"AVANT et APRES la resynchronisation, pour voir si c'est ELLE qui la detruit.");
|
||||
}
|
||||
|
||||
_armedThisFrame = false;
|
||||
|
||||
if (_shot >= _maxShots)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now < _nextMs || !KeyHeld())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using GAL.PinnedSpan<byte> data = texture.HostTexture.GetData();
|
||||
_before = data.Get().ToArray();
|
||||
_armedThisFrame = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP PRESYNC: desactive apres erreur (before): {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void After(Image.Texture texture)
|
||||
{
|
||||
if (!_enabled || !_armedThisFrame || _before == null || texture?.HostTexture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_armedThisFrame = false;
|
||||
|
||||
try
|
||||
{
|
||||
using GAL.PinnedSpan<byte> data = texture.HostTexture.GetData();
|
||||
byte[] after = data.Get().ToArray();
|
||||
|
||||
bool identical = _before.Length == after.Length && _before.AsSpan().SequenceEqual(after);
|
||||
|
||||
long diffBytes = 0;
|
||||
|
||||
if (!identical && _before.Length == after.Length)
|
||||
{
|
||||
for (int i = 0; i < after.Length; i++)
|
||||
{
|
||||
if (_before[i] != after[i])
|
||||
{
|
||||
diffBytes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string dir = Path.Combine("presync", $"shot{_shot:D2}");
|
||||
Directory.CreateDirectory(dir);
|
||||
string tag = $"{texture.Info.Width}x{texture.Info.Height}_{texture.Info.FormatInfo.Format}";
|
||||
File.WriteAllBytes(Path.Combine(dir, $"AVANT_{tag}.bin"), _before);
|
||||
File.WriteAllBytes(Path.Combine(dir, $"APRES_{tag}.bin"), after);
|
||||
|
||||
double pct = after.Length > 0 ? 100.0 * diffBytes / after.Length : 0;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP PRESYNC shot {_shot}: {tag} -- {(identical ? "IDENTIQUES (la resync ne touche rien)" : $"DIFFERENTS, {pct:F1}% des octets changes par la resync")} -> {dir}");
|
||||
|
||||
_shot++;
|
||||
_nextMs = Environment.TickCount64 + 2000;
|
||||
_before = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP PRESYNC: desactive apres erreur (after): {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// PROJECTIVE AUDIT of the view-projection elected by <see cref="MvppSoloCamera"/>.
|
||||
/// Gate: RYUJINX_MVPP_PROJAUDIT=1. LOGGING ONLY - reads the matrix that was already
|
||||
/// accepted, computes scalars, prints one line a second. It changes no state, feeds no
|
||||
/// shader and returns nothing, so with the gate off it is two boolean tests per frame and
|
||||
/// with the gate on it cannot alter a single pixel.
|
||||
///
|
||||
/// WHY IT EXISTS (27/07, XC2). VPSOLO finally armed MV++ on Xenoblade 2 and the split in
|
||||
/// Alex's verdict is the whole clue: the SKY is perfect, the GEOMETRY slides when the camera
|
||||
/// moves. SKYROT reprojects the far plane with ROTATION ONLY - it needs neither depth nor the
|
||||
/// projective part. Geometry needs the full matrix AND depth. Sky right + geometry wrong
|
||||
/// therefore points at a matrix that is correct as a CAMERA and wrong as a PROJECTION.
|
||||
///
|
||||
/// And that is exactly what the election allows. MvppSoloCamera.IsViewProj checks the norm of
|
||||
/// row3, the norms fx/fy, the mutual orthogonality of rows 0/1/3, the collinearity of row2
|
||||
/// with row3, and rejects bare projections. NONE of those constrain the DEPTH MAPPING: the
|
||||
/// terms usually written A and B (row2.xyz magnitude and row2.w) are never validated, and
|
||||
/// ViewProjPos rebuilds the camera position from m[3], m[7] and m[15] without ever reading
|
||||
/// m[11]. A matrix can pass every rule while mapping depth in a convention the reprojection
|
||||
/// does not expect.
|
||||
///
|
||||
/// THE SUSPECT NUMBER. The in-game "MVPP center" probe reports worldW between 0.0002 and
|
||||
/// 0.077 for real geometry - a ship and a building. If worldW is the view-space distance in
|
||||
/// world units, those objects would sit four centimetres from the camera. The reprojection
|
||||
/// divides by that w, so a w that is wrong by three orders of magnitude produces exactly what
|
||||
/// was measured: vectors that flip sign frame to frame during a single continuous pan and
|
||||
/// that peg to whatever the clamp is (128 with MAXMOTION=128, 512 with 512).
|
||||
///
|
||||
/// WHAT THIS PRINTS, AND HOW TO READ IT.
|
||||
/// fx/fy : the focal scales. Their ratio must equal the render aspect (~1.778).
|
||||
/// n3 : |row3.xyz|. IsViewProj FORCES this to 1 +/- 2%, so it is printed to confirm
|
||||
/// the constraint is what makes the scale of w what it is.
|
||||
/// A : |row2.xyz|. For a standard perspective this is the depth compression. A
|
||||
/// near-zero A means an INFINITE far plane (or reverse-Z), and note that
|
||||
/// IsViewProj SKIPS its collinearity test entirely when A < 1e-4 - such a
|
||||
/// matrix is accepted without that check ever running.
|
||||
/// B : row2.w - A*tz, the depth offset.
|
||||
/// near/far : recovered from A and B under the OpenGL convention. If they come out
|
||||
/// absurd (negative, inverted, astronomically large) the matrix does not map
|
||||
/// depth the way the reprojection assumes, and that is the defect.
|
||||
/// wCenter : the perspective w for a point at the screen centre at the depth given, in
|
||||
/// the SAME units as the camera position. Compare it against camDist: they
|
||||
/// describe the same distance and must agree in order of magnitude.
|
||||
/// rt : round-trip error in pixels - unproject the screen centre with VP^-1 then
|
||||
/// project it back with VP. This tests CONDITIONING only, never correctness:
|
||||
/// a badly scaled matrix round-trips perfectly. A large rt means the matrix is
|
||||
/// near-singular, which is a separate and worse problem.
|
||||
///
|
||||
/// HONEST LIMIT. This audit cannot say what the TRUE matrix is - there is no ground truth
|
||||
/// available at this level. It can only say whether the accepted one is internally coherent
|
||||
/// and whether its depth mapping is plausible. That is enough to decide where to look next:
|
||||
/// a coherent matrix moves the search to the consumer (the reprojection shader), an incoherent
|
||||
/// one keeps it here, in the election.
|
||||
/// </summary>
|
||||
static class MvppProjAudit
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_PROJAUDIT") == "1";
|
||||
|
||||
private const long IntervalMs = 1000;
|
||||
|
||||
private static long _lastMs;
|
||||
|
||||
/// <summary>
|
||||
/// Called on the success path of MvppSoloCamera.TryGetViewProjection, with the matrix it
|
||||
/// is about to hand out and the focal scales it recovered. Throttled to one line a second.
|
||||
/// </summary>
|
||||
public static void Audit(in Matrix4x4 vp, float camX, float camY, float camZ)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastMs < IntervalMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastMs = now;
|
||||
|
||||
// Recovered here rather than passed in, so the hook at the call site stays a single
|
||||
// line and TryReadAt keeps its signature untouched. Same definition as IsViewProj.
|
||||
float fx = MathF.Sqrt(vp.M11 * vp.M11 + vp.M12 * vp.M12 + vp.M13 * vp.M13);
|
||||
float fy = MathF.Sqrt(vp.M21 * vp.M21 + vp.M22 * vp.M22 + vp.M23 * vp.M23);
|
||||
|
||||
// Row layout, matching the constructor in MvppSoloCamera.TryReadAt:
|
||||
// row0 = M11 M12 M13 M14 row2 = M31 M32 M33 M34
|
||||
// row1 = M21 M22 M23 M24 row3 = M41 M42 M43 M44
|
||||
float n3 = MathF.Sqrt(vp.M41 * vp.M41 + vp.M42 * vp.M42 + vp.M43 * vp.M43);
|
||||
float a = MathF.Sqrt(vp.M31 * vp.M31 + vp.M32 * vp.M32 + vp.M33 * vp.M33);
|
||||
|
||||
// Documented shape: row2.xyz = A*R.row2 and row3.xyz = s*R.row2 with s = +/-1, so the
|
||||
// sign is read off the dot product rather than assumed.
|
||||
float dot23 = vp.M31 * vp.M41 + vp.M32 * vp.M42 + vp.M33 * vp.M43;
|
||||
float s = dot23 >= 0f ? 1f : -1f;
|
||||
|
||||
// row3.w = s*tz => tz = row3.w / s ; row2.w = A*tz + B => B = row2.w - A*tz.
|
||||
float tz = s != 0f ? vp.M44 / s : 0f;
|
||||
float b = vp.M34 - a * tz;
|
||||
|
||||
// OpenGL perspective: A' = -(f+n)/(f-n), B' = -2fn/(f-n) with the sign carried by s.
|
||||
// Inverting gives n = B/(A-1) and f = B/(A+1); both are printed raw so an absurd pair
|
||||
// is visible rather than silently normalised into something plausible.
|
||||
float aSigned = a * s;
|
||||
float near = MathF.Abs(aSigned - 1f) > 1e-6f ? b / (aSigned - 1f) : float.NaN;
|
||||
float far = MathF.Abs(aSigned + 1f) > 1e-6f ? b / (aSigned + 1f) : float.NaN;
|
||||
|
||||
// The perspective w at the screen centre for a point on the camera axis: w is row3
|
||||
// dotted with the world point plus row3.w. Taking the camera itself gives the offset,
|
||||
// so the magnitude that matters is how w grows per world unit along the view axis --
|
||||
// with n3 forced to 1 that rate is 1, and w IS the distance in world units.
|
||||
float wAtCam = vp.M41 * camX + vp.M42 * camY + vp.M43 * camZ + vp.M44;
|
||||
float camDist = MathF.Sqrt(camX * camX + camY * camY + camZ * camZ);
|
||||
|
||||
// Conditioning: unproject the centre of the screen at mid depth, project it back.
|
||||
float rt = float.NaN;
|
||||
|
||||
if (Matrix4x4.Invert(vp, out Matrix4x4 inv))
|
||||
{
|
||||
Vector4 clip = new Vector4(0f, 0f, 0f, 1f);
|
||||
Vector4 world = Vector4.Transform(clip, inv);
|
||||
|
||||
if (MathF.Abs(world.W) > 1e-20f)
|
||||
{
|
||||
world /= world.W;
|
||||
world.W = 1f;
|
||||
|
||||
Vector4 back = Vector4.Transform(world, vp);
|
||||
|
||||
if (MathF.Abs(back.W) > 1e-20f)
|
||||
{
|
||||
// Half the render width is the worst case for a normalised device unit,
|
||||
// 1920 is only a scale for readability - the verdict is "near zero or not".
|
||||
rt = MathF.Sqrt(back.X / back.W * (back.X / back.W) +
|
||||
back.Y / back.W * (back.Y / back.W)) * 1920f;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rt = -1f; // not invertible at all
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP proj audit: fx={fx:0.####} fy={fy:0.####} ratio={(fy > 1e-9f ? fx / fy : 0f):0.####} " +
|
||||
$"n3={n3:0.#####} A={a:0.######} s={s:+0;-0} B={b:0.####} " +
|
||||
$"near={near:0.#####} far={far:0.##} tz={tz:0.####} " +
|
||||
$"campos=[{camX:0.##} {camY:0.##} {camZ:0.##}] camDist={camDist:0.##} wAtCam={wAtCam:0.######} " +
|
||||
$"rt={rt:0.####} px.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Render-target snapshot probe (RYUJINX_RT_DUMP=1, OFF by default).
|
||||
///
|
||||
/// WHY WE ARE HERE. The Xenoblade 2 artefact - large, SOFT rectangles of real-but-averaged
|
||||
/// content, when the camera turns, worse indoors - has survived nine eliminations, each one
|
||||
/// measured with its switch verified live in the log, never assumed:
|
||||
/// shader cache · DLSS/MV++/FG (mode=0 proven) · runtime mipmaps · recycled device memory
|
||||
/// (2000+ allocations zeroed) · history-reset storm · the fork's gobBlocksInZ clamp (zero
|
||||
/// firings) · sampling a bound render target (0 hits in 16731 draws) · the 1080p scene mod
|
||||
/// (patch proven applied) · dynamic resolution (depth AND colour sizes rock-steady).
|
||||
///
|
||||
/// Counters have run out of road, so we stop guessing at mechanisms and LOOK AT THE PIXELS.
|
||||
///
|
||||
/// WHAT THE LAST MEASUREMENT REVEALED. XC2 renders through several LOW-RESOLUTION buffers that
|
||||
/// are enlarged and composited back over the frame - 640x360 R11G11B10Float and 512x288
|
||||
/// R16G16B16A16Float, ~300 draws each per 5 s window, permanently. That matches the LOOK of the
|
||||
/// artefact: the blocks are big and soft, i.e. averaged content stretched up, not the crisp
|
||||
/// blocks a full-resolution buffer would give. It would also explain "worse indoors", where
|
||||
/// more lights and volumetrics are in play.
|
||||
///
|
||||
/// WHAT THIS PROBE DOES. On a trigger it captures ONE image per distinct colour render-target
|
||||
/// shape (plus the depth), straight off the GPU, to raw files named with their dimensions and
|
||||
/// format. Then we look at them one by one and SEE which buffer carries the blocks. No
|
||||
/// hypothesis survives contact with the actual pixels.
|
||||
///
|
||||
/// COST. GetData() is a full GPU-to-CPU readback and it stalls the pipeline, so this is
|
||||
/// strictly one short burst per session, capped, and off by default.
|
||||
/// </summary>
|
||||
static class MvppRtDumpProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Seconds to wait after boot before the FIRST capture (RYUJINX_RT_DUMP_START, default 90).
|
||||
/// Measured the hard way 21/07: with captures starting 10 s after boot, all four bursts
|
||||
/// landed during loading and the intro, so the "clean" buffer they showed said nothing
|
||||
/// about the moment the artefact is on screen. The captures have to happen when the player
|
||||
/// is IN the scene, looking at the defect.
|
||||
/// </summary>
|
||||
private static readonly int _startSeconds =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_START"), out int st) && st > 0 ? st : 90;
|
||||
|
||||
/// <summary>Seconds between capture bursts (RYUJINX_RT_DUMP_EVERY, default 10).</summary>
|
||||
private static readonly int _everySeconds =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_EVERY"), out int s) && s > 0 ? s : 10;
|
||||
|
||||
/// <summary>How many bursts before the probe stops for good (RYUJINX_RT_DUMP_MAX, default 4).</summary>
|
||||
private static readonly int _maxBursts =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_MAX"), out int m) && m > 0 ? m : 4;
|
||||
|
||||
/// <summary>RYUJINX_RT_DUMP_ALL=1: capture every bound target, not just the scene buffer.</summary>
|
||||
private static readonly bool _dumpAll =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_ALL") == "1";
|
||||
|
||||
/// <summary>RYUJINX_RT_DUMP_INPUTS=1: also capture the textures the draws SAMPLE.</summary>
|
||||
private static readonly bool _dumpInputs =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_INPUTS") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// RYUJINX_RT_DUMP_FRAME=1 : une rafale == UNE IMAGE EXACTEMENT, bornée par les presents,
|
||||
/// au lieu d'une fenêtre en millisecondes.
|
||||
///
|
||||
/// Pourquoi (mesuré deux fois le 21/07) : avec un découpage temporel, les cibles intermédiaires
|
||||
/// et l'image présentée tombent sur des images DIFFÉRENTES. Pendant une rotation caméra la vue
|
||||
/// change complètement en une seconde — un burst montrait une grue pendant que l'image finale
|
||||
/// montrait des caisses. On comparait deux scènes sans rapport, et aucune conclusion n'était
|
||||
/// possible sur « à quel étage les premiers pixels faux apparaissent ».
|
||||
///
|
||||
/// Ici la rafale est ouverte par un present et fermée par le SUIVANT : tous les draws capturés
|
||||
/// entre les deux appartiennent à l'image dont on capture ensuite le résultat présenté. Les deux
|
||||
/// sondes vivent du même côté du code (couche Gpu), donc ce découpage suffit — aucune
|
||||
/// synchronisation inter-assembly n'est nécessaire.
|
||||
/// </summary>
|
||||
private static readonly bool _frameMode =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_FRAME") == "1";
|
||||
|
||||
private const int MaxShapesPerBurst = 26;
|
||||
|
||||
private static long _nextBurstMs;
|
||||
private static bool _bursting;
|
||||
private static long _burstEndsMs;
|
||||
private static int _burstIndex;
|
||||
private static string _burstDir;
|
||||
private static bool _frameArmLogged;
|
||||
private static readonly HashSet<string> _doneThisBurst = new();
|
||||
|
||||
/// <summary>
|
||||
/// Called from Window.Present with the texture that is about to reach the screen. Captured
|
||||
/// only while a burst is running, so the presented frame lands in the SAME folder as the
|
||||
/// intermediate buffers of that moment and the two can be compared directly.
|
||||
/// </summary>
|
||||
public static void NotePresented(Image.Texture texture)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_frameMode)
|
||||
{
|
||||
if (!_bursting || texture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TryDump(texture, "PRESENTED");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"RTDUMP: presented illisible: {e.GetType().Name} {e.Message}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Mode IMAGE : ce present est la frontière entre deux images. ---
|
||||
try
|
||||
{
|
||||
if (_bursting)
|
||||
{
|
||||
// Fin de l'image capturée : son résultat présenté ferme le dossier, à côté des
|
||||
// cibles intermédiaires des draws de CETTE image, et d'aucune autre.
|
||||
if (texture != null)
|
||||
{
|
||||
TryDump(texture, "PRESENTED");
|
||||
}
|
||||
|
||||
_bursting = false;
|
||||
_nextBurstMs = Environment.TickCount64 + _everySeconds * 1000L;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"RTDUMP: image {_burstIndex}/{_maxBursts} complete, {_doneThisBurst.Count} tampons.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_burstIndex >= _maxBursts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (_nextBurstMs == 0)
|
||||
{
|
||||
_nextBurstMs = now + _startSeconds * 1000L;
|
||||
}
|
||||
|
||||
if (!_frameArmLogged)
|
||||
{
|
||||
_frameArmLogged = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"RTDUMP: armed en MODE IMAGE -- premiere image dans {_startSeconds}s, puis toutes les " +
|
||||
$"{_everySeconds}s, {_maxBursts} images. Une rafale == UNE image : cibles et resultat presente " +
|
||||
"appartiennent a la MEME image.");
|
||||
}
|
||||
|
||||
if (now < _nextBurstMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ouverture : les draws de l'image SUIVANTE seront capturés, puis le present suivant
|
||||
// fermera le dossier avec le résultat de cette même image.
|
||||
_burstIndex++;
|
||||
_doneThisBurst.Clear();
|
||||
_burstDir = Path.Combine("rtdump", $"image{_burstIndex:D2}");
|
||||
Directory.CreateDirectory(_burstDir);
|
||||
_bursting = true;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"RTDUMP: image {_burstIndex}/{_maxBursts} ouverte -> {_burstDir}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"RTDUMP: mode image desactive apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnDraw(GpuChannel channel)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OnDrawImpl(channel);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"RTDUMP: disabled after unexpected error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDrawImpl(GpuChannel channel)
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
// En mode IMAGE, l'ouverture et la fermeture de la rafale appartiennent à NotePresented :
|
||||
// ici on ne fait que capturer les cibles des draws de l'image en cours, sans toucher au
|
||||
// découpage. Toute logique de temps ici casserait l'appariement.
|
||||
if (_frameMode)
|
||||
{
|
||||
if (!_bursting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!_bursting)
|
||||
{
|
||||
if (_burstIndex >= _maxBursts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_nextBurstMs == 0)
|
||||
{
|
||||
_nextBurstMs = now + _startSeconds * 1000L;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"RTDUMP: armed -- first capture in {_startSeconds}s, then every {_everySeconds}s, " +
|
||||
$"{_maxBursts} bursts. Be in the scene with the defect visible by then.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (now < _nextBurstMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A burst spans a full second: the low-resolution buffers are bound on their own
|
||||
// draws, so capturing a single draw would only ever catch the main target.
|
||||
_bursting = true;
|
||||
_burstEndsMs = now + 1000;
|
||||
_burstIndex++;
|
||||
_doneThisBurst.Clear();
|
||||
_burstDir = Path.Combine("rtdump", $"burst{_burstIndex:D2}");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_burstDir);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"RTDUMP: cannot create {_burstDir}: {e.Message}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"RTDUMP: burst {_burstIndex}/{_maxBursts} started -> {_burstDir}");
|
||||
}
|
||||
|
||||
if (!_frameMode && now > _burstEndsMs)
|
||||
{
|
||||
_bursting = false;
|
||||
_nextBurstMs = now + _everySeconds * 1000L;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"RTDUMP: burst {_burstIndex} done, {_doneThisBurst.Count} buffers captured.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
channel.TextureManager.MvppEnumerateRenderTargets((index, tex) => TryDump(tex, $"color{index}"));
|
||||
|
||||
Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil;
|
||||
|
||||
if (ds != null)
|
||||
{
|
||||
TryDump(ds, "depth");
|
||||
}
|
||||
|
||||
// Also capture what the draws READ. Measured 21/07: the 640x360 R32G32Uint buffer that
|
||||
// the glow draws sample never showed up among the render targets, because nothing
|
||||
// DRAWS into it -- it is written by a COMPUTE shader. That is exactly how tiled/
|
||||
// clustered lighting builds its per-tile light lists, so the buffer has to be captured
|
||||
// from the read side or not at all.
|
||||
if (_dumpInputs)
|
||||
{
|
||||
channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) => TryDump(tex, $"input{stage}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDump(Image.Texture tex, string role)
|
||||
{
|
||||
if (tex == null || (role != "PRESENTED" && _doneThisBurst.Count >= MaxShapesPerBurst))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GAL.ITexture host = tex.HostTexture;
|
||||
|
||||
if (host == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// One capture per distinct SHAPE, not per texture: the same few buffers are rebound
|
||||
// thousands of times a second and what we need is one picture of each kind.
|
||||
string name = $"{role}_{tex.Info.Width}x{tex.Info.Height}_{tex.Info.FormatInfo.Format}";
|
||||
|
||||
// The artefact is INTERMITTENT and only shows while the camera moves, so a handful of
|
||||
// snapshots will miss it. We need many captures over a long window -- which means the
|
||||
// disk budget has to be spent where the answer is. The 21/07 dumps showed the composed
|
||||
// scene lives in the 1280x720 R11G11B10Float target; that is the one that answers the
|
||||
// bisection question "do the blocks exist in the game's own image?". Everything else
|
||||
// is skipped unless RYUJINX_RT_DUMP_ALL=1.
|
||||
if (role != "PRESENTED" && !_dumpAll && !_dumpInputs &&
|
||||
!tex.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// [28/07] L'ADRESSE GPU DANS LE NOM DU FICHIER. Sans elle, les dumps sont nommes par
|
||||
// SLOT (color0, color5...) alors que MvppUiProbe raisonne en ADRESSES : impossible de
|
||||
// croiser les deux, et un meme buffer lie a trois slots ressort en triple exemplaire
|
||||
// (color0/5/6 du 28/07 avaient un decor identique a +0,957 -- c'etait le meme).
|
||||
// Deduplication par ADRESSE et plus par nom : un buffer par contenu reel.
|
||||
ulong gpuAddr = 0;
|
||||
|
||||
try
|
||||
{
|
||||
gpuAddr = tex.Range.GetSubRange(0).Address;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// pas d'adresse exploitable : on garde 0, le nom reste unique par le slot
|
||||
}
|
||||
|
||||
if (!_doneThisBurst.Add(gpuAddr != 0 ? $"@{gpuAddr:X}" : name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using GAL.PinnedSpan<byte> data = host.GetData();
|
||||
ReadOnlySpan<byte> bytes = data.Get();
|
||||
|
||||
string path = Path.Combine(_burstDir, $"{name}_at{gpuAddr:X}" + ".bin");
|
||||
File.WriteAllBytes(path, bytes.ToArray());
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"RTDUMP: {name}.bin ({bytes.Length} octets, {tex.Info.Width}x{tex.Info.Height} " +
|
||||
$"{tex.Info.FormatInfo.Format}, {tex.Info.Target}).");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"RTDUMP: {name} illisible: {e.GetType().Name} {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,204 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// ⛔⛔ MORT PAR LA MESURE, 29/07 AU SOIR -- NE PAS RE-ARMER, NE PAS RE-REGLER. Sonde `HDRTRY` :
|
||||
/// la passe de scene HDR existe dans **98,9 %** des images 3D, mais la camera n'y est **lisible
|
||||
/// que dans 36,5 %** des cas. N'autoriser QUE cette passe ferait donc tomber la publication de
|
||||
/// 83 % a ~37 % : la camera serait AFFAMEE, et une camera manquee fait ecrire zero sur toute
|
||||
/// l'image. Ce n'est pas un probleme de reglage du filet -- le filet mal dimensionne (voir plus
|
||||
/// bas) n'etait que le symptome. Le remplacant est <see cref="MvppLdrSkip"/>, qui ECARTE la pire
|
||||
/// passe au lieu d'en ELIRE une. Ce fichier est conserve pour la trace, et parce que
|
||||
/// <see cref="IsSceneColorPass"/> sert encore a la sonde.
|
||||
///
|
||||
/// [SCENEPASS 29/07 SOIR] Capturer la camera sur LA PASSE DE LA SCENE, et non sur la premiere
|
||||
/// passe 3D venue (RYUJINX_MVPP_SCENEPASS=1, coupe par defaut).
|
||||
///
|
||||
/// POURQUOI, ET SUR QUELLE MESURE. <see cref="MvppCameraCapture.OnDraw"/> capture au PREMIER
|
||||
/// dessin de l'image qui passe le pre-filtre, puis se tait. Ce pre-filtre reconnait "une passe
|
||||
/// 3D", pas "LA passe de la scene". Recensement CTXPROBE du 29/07 au soir, run etalon de 3 min,
|
||||
/// 3 633 lectures et 177 intruses -- du GROS volume, pas trois evenements tires au sort :
|
||||
///
|
||||
/// cible couleur branchee a la lecture lectures intruses taux
|
||||
/// R8G8B8A8Unorm (8 bits, LDR) 1 340 119 8,9 % <- 67 % des intruses
|
||||
/// aucune (passe profondeur seule) 2 044 52 2,5 %
|
||||
/// R11G11B10Float (couleur HDR de scene) 230 3 1,3 % <- la vraie passe
|
||||
/// R32Float 19 3 15,8 %
|
||||
///
|
||||
/// La passe 8 bits donne SEPT FOIS plus d'intruses que la couleur HDR et fournit deux tiers du
|
||||
/// total. Le discriminant que le dossier croyait inexistant n'est pas dans la MATRICE (fermé le
|
||||
/// 27/07 : vue-projection structurellement parfaite, rien ne l'identifie) -- il est dans la
|
||||
/// PASSE. Et il y a de quoi choisir : 125 passes qualifiantes par image en moyenne, dont ~87
|
||||
/// sur la couleur HDR.
|
||||
///
|
||||
/// LE CRITERE EST GENERIQUE -- forme de pipeline, rien d'autre : cible couleur attachee, NON
|
||||
/// CARREE (une cible carree est un atlas d'ombres), format flottant HDR. C'est mot pour mot
|
||||
/// celui que <see cref="MvppColorSnapshot"/> utilise deja pour epingler la couleur de scene.
|
||||
/// Aucune adresse, aucun hash, aucune resolution, aucun nom de jeu.
|
||||
///
|
||||
/// ⛔ CE QUE CE FICHIER NE PROMET PAS. Attendu : 4,9 % d'intruses -> 1,3 %, soit environ 73 %
|
||||
/// de moins. PAS zero : la passe de scene elle-meme en porte 3 sur 230. C'est une attenuation,
|
||||
/// obtenue a la source au lieu d'un filtre pose apres coup -- ce n'est pas une execution.
|
||||
///
|
||||
/// ⚠️ LE RISQUE EST LA PUBLICATION, PAS L'INTRUSE, et c'est la borne INFERIEURE qui protege
|
||||
/// (lecon du 29/07 : pour un composant dont le role est de refuser, prouver "il ne refuse
|
||||
/// jamais plus" est la moitie inutile de la question). Une camera manquee fait ecrire ZERO sur
|
||||
/// toute l'image. Donc la restriction s'auto-desarme, en deux etages :
|
||||
/// 1. RATTRAPAGE -- deux images d'affilee avec des passes qualifiantes mais AUCUNE capture,
|
||||
/// et la restriction se leve pour 300 images (comportement d'avant, a l'identique). Perte
|
||||
/// bornee : 2 images sur 300, soit 0,7 %.
|
||||
/// 2. DESARMEMENT -- cinq rattrapages, et la restriction se coupe pour la session, avec une
|
||||
/// ligne de journal explicite. Si la passe HDR n'est pas fiable sur ce jeu, on le DIT au
|
||||
/// lieu de saigner 2 images sur 3.
|
||||
/// Le taux de publication se lit dans l'instrument qui existe deja ("MVPP capture window:
|
||||
/// enq/pub") : mesure de reference du 29/07 au soir = 6 761 / 8 127 = 83,2 %. En dessous, le
|
||||
/// correctif est mauvais quel que soit son gain sur les intruses.
|
||||
///
|
||||
/// Les compteurs sont approximatifs : OnDraw tourne sur le fil GPU, OnFrame sur celui de la
|
||||
/// presentation. On ne batit aucune conclusion fine dessus -- seulement des ordres de grandeur.
|
||||
/// </summary>
|
||||
static class MvppScenePass
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_SCENEPASS") == "1";
|
||||
|
||||
private const int MissesBeforeCatchUp = 2;
|
||||
private const int CatchUpFrames = 300;
|
||||
private const int CatchUpsBeforeDisarm = 5;
|
||||
private const int ReportMs = 5000;
|
||||
|
||||
private static bool _disarmed;
|
||||
private static int _missStreak;
|
||||
private static int _catchUpLeft;
|
||||
private static int _catchUps;
|
||||
|
||||
private static bool _qualThisFrame;
|
||||
|
||||
private static int _frames;
|
||||
private static int _captured;
|
||||
private static int _skips;
|
||||
private static int _catchUpFramesSeen;
|
||||
private static long _lastReportMs;
|
||||
|
||||
/// <summary>
|
||||
/// Appelee sur un dessin qualifiant tant que l'image n'a pas encore sa camera. Rend false
|
||||
/// pour SAUTER la tentative de capture sur ce dessin -- ce qui laisse la capture se faire
|
||||
/// plus loin dans la MEME image, sur la passe de la scene.
|
||||
/// </summary>
|
||||
public static bool Allows(GpuChannel channel)
|
||||
{
|
||||
_qualThisFrame = true;
|
||||
|
||||
if (_disarmed || _catchUpLeft > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsSceneColorPass(channel))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_skips++;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconnait la passe de la scene a la FORME de sa cible couleur : attachee, non carree,
|
||||
/// format flottant HDR. Meme critere que <see cref="MvppColorSnapshot"/>. Une cible 8 bits
|
||||
/// (interface, composition, passe auxiliaire) et une passe sans couleur du tout echouent
|
||||
/// ici -- ce sont exactement les deux contextes qui portaient 96 % des intruses.
|
||||
/// </summary>
|
||||
internal static bool IsSceneColorPass(GpuChannel channel)
|
||||
{
|
||||
Image.Texture col0 = channel.TextureManager.RenderTargetColor0;
|
||||
|
||||
if (col0 == null || col0.Info.Width == col0.Info.Height)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GAL.Format f = col0.Info.FormatInfo.Format;
|
||||
|
||||
return f == GAL.Format.R11G11B10Float || f == GAL.Format.R16G16B16A16Float;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Une fois par image presentee, AVANT que le drapeau de capture ne soit re-arme. C'est ici
|
||||
/// que vit le filet de securite : une image qui avait des passes qualifiantes et n'a rien
|
||||
/// capture est une image potentiellement perdue, et on ne l'accepte pas deux fois de suite.
|
||||
/// </summary>
|
||||
public static void OnFrame(bool captured)
|
||||
{
|
||||
if (!_qualThisFrame)
|
||||
{
|
||||
// Image sans passe 3D (menu, chargement) : elle n'a jamais eu de camera a prendre,
|
||||
// la compter comme un manque ferait declencher le rattrapage pour rien.
|
||||
return;
|
||||
}
|
||||
|
||||
_qualThisFrame = false;
|
||||
_frames++;
|
||||
|
||||
if (_catchUpLeft > 0)
|
||||
{
|
||||
_catchUpLeft--;
|
||||
_catchUpFramesSeen++;
|
||||
}
|
||||
|
||||
if (captured)
|
||||
{
|
||||
_captured++;
|
||||
_missStreak = 0;
|
||||
}
|
||||
else if (!_disarmed && _catchUpLeft == 0)
|
||||
{
|
||||
_missStreak++;
|
||||
|
||||
if (_missStreak >= MissesBeforeCatchUp)
|
||||
{
|
||||
_missStreak = 0;
|
||||
_catchUps++;
|
||||
_catchUpLeft = CatchUpFrames;
|
||||
|
||||
if (_catchUps >= CatchUpsBeforeDisarm)
|
||||
{
|
||||
_disarmed = true;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"MVPP SCENEPASS DESARME : {_catchUps} rattrapages, la passe de scene HDR " +
|
||||
"n'est pas assez fiable sur ce jeu pour porter la capture. Retour au " +
|
||||
"comportement d'avant pour le reste de la session.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP SCENEPASS rattrapage #{_catchUps} : {MissesBeforeCatchUp} images " +
|
||||
$"d'affilee sans capture, restriction levee pour {CatchUpFrames} images.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastReportMs >= ReportMs)
|
||||
{
|
||||
_lastReportMs = now;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP SCENEPASS : {_frames} images 3D, {_captured} avec camera " +
|
||||
$"({(_frames > 0 ? 100f * _captured / _frames : 0f):0.#} %), " +
|
||||
$"{_skips} dessins sautes, {_catchUps} rattrapages " +
|
||||
$"({_catchUpFramesSeen} images en rattrapage)" +
|
||||
$"{(_disarmed ? " · DESARME" : "")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
public bool AnyDepth;
|
||||
public bool AnyBlend;
|
||||
public int Draws;
|
||||
public int NoDepthDraws; // [HUDSPLIT] dessins sans test de profondeur = candidats HUD
|
||||
public int FirstNoDepthAt; // rang du premier d'entre eux dans l'epoque (1 = des le debut)
|
||||
}
|
||||
|
||||
private static readonly List<Epoch> _epochs = new();
|
||||
@@ -51,9 +53,84 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private static int _frameNo;
|
||||
private static int _framesLoggedFull;
|
||||
private static long _lastVerdictMs;
|
||||
private static long _lastDetailMs; // [28/07] cadence du dump detaille en regime de jeu
|
||||
|
||||
// [EPOCHDUMP 28/07] RYUJINX_MVPP_UIPROBE_DUMP=<secondes avant armement>, 0 = OFF.
|
||||
//
|
||||
// POURQUOI CE MODE EXISTE. Les captures de render targets photographient soit un instant
|
||||
// arbitraire, soit la PRESENTATION -- et a ce moment-la le buffer LDR pleine resolution
|
||||
// qui recoit 95 a 99 dessins par image est deja RECYCLE : mesure du 28/07, il ressort
|
||||
// VIDE alors que la sonde d'epoques le voit travailler. C'est exactement pour ca qu'on
|
||||
// n'a jamais pu regarder ce que le jeu y peint.
|
||||
//
|
||||
// Ici on capture le render target AU MOMENT OU SON EPOQUE SE FERME, c'est-a-dire juste
|
||||
// apres son dernier dessin et avant qu'il serve a autre chose. Une seule image capturee,
|
||||
// toutes ses epoques, puis la sonde se desarme d'elle-meme.
|
||||
private static readonly int _dumpAfterSeconds =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE_DUMP"), out int ds) && ds > 0 ? ds : 0;
|
||||
|
||||
private static long _dumpArmedAtMs;
|
||||
private static bool _dumpDone;
|
||||
private static int _dumpSeq;
|
||||
private static Image.Texture _epochTex; // le RT de l'epoque en cours, garde pour la capture
|
||||
|
||||
// [HUDLESS 28/07] RYUJINX_MVPP_HUDLESS=1 : DETECTION SEULE, ne capture rien, ne change rien.
|
||||
//
|
||||
// Reconnait, PAR SA FORME, le render target qui porte l'image tonemappee SANS interface --
|
||||
// celui que la Frame Generation reclame et qu'on n'a jamais pu lui donner. Signature
|
||||
// mesuree le 28/07 sur XC2 : format 8 bits par canal (donc MEME espace colorimetrique que
|
||||
// l'image presentee, ce qui rend la soustraction UI = backbuffer - hudless valide),
|
||||
// PLEINE largeur de rendu, et une epoque nourrie (~95-99 dessins) -- une passe d'interface
|
||||
// n'en aurait que quelques-uns, une passe d'effet serait plus petite.
|
||||
//
|
||||
// JAMAIS par adresse : 0x2280250000 change d'un lancement a l'autre.
|
||||
//
|
||||
// Ce mode existe pour verifier que la regle attrape le bon buffer A TOUS LES COUPS avant
|
||||
// qu'on branche quoi que ce soit dessus. Le dossier a deja paye une regle non verifiee.
|
||||
private static readonly bool _hudless =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS") == "1";
|
||||
|
||||
private const int HudlessMinDraws = 20;
|
||||
|
||||
// [HUDLESSFEED 28/07] RYUJINX_MVPP_HUDLESS_FEED=1 : COPIE le buffer sans interface vers une
|
||||
// texture persistante, et la publie pour la couche Vulkan. Desarme par defaut.
|
||||
//
|
||||
// POURQUOI UNE COPIE ET PAS UNE REFERENCE. Le render target est RECYCLE juste apres son
|
||||
// epoque -- c'est demontre : toutes les captures faites au present le trouvaient VIDE.
|
||||
// Publier une simple reference donnerait donc a la Frame Generation un contenu deja
|
||||
// ecrase. La copie est faite a l'instant exact de la fermeture, seul moment ou le buffer
|
||||
// contient l'image.
|
||||
//
|
||||
// POURQUOI ON COPIE CHAQUE CANDIDAT PLUTOT QUE LE SEUL ELU. L'election ne peut se faire
|
||||
// qu'a la fin de l'image, quand le buffer n'existe plus. On copie donc tout candidat au
|
||||
// moins aussi nourri que le meilleur vu dans cette image : le dernier ecrase les
|
||||
// precedents, et c'est justement lui le bon (29 dessins puis 81 -- mesure du 28/07).
|
||||
//
|
||||
// Cout : une copie GPU pleine resolution par candidat, 1 a 2 par image.
|
||||
private static readonly bool _hudlessFeed =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS_FEED") == "1";
|
||||
|
||||
private static ITexture _hudlessCopy;
|
||||
private static int _hudlessCopyW;
|
||||
private static int _hudlessCopyH;
|
||||
private static Format _hudlessCopyFmt;
|
||||
private static long _hudlessFeedId;
|
||||
private static int _hudlessCopyFails;
|
||||
|
||||
private static int _renderWidthSeen;
|
||||
private static int _hudlessHits;
|
||||
private static int _hudlessFrames;
|
||||
private static int _hudlessMultiple; // images ou PLUSIEURS epoques matchent = regle ambigue
|
||||
private static long _hudlessLogMs;
|
||||
private static int _hudlessInFrame;
|
||||
private static int _hudlessBestDraws; // [v2] meilleur candidat de l'image en cours
|
||||
private static ulong _hudlessBestAddr;
|
||||
private static int _hudlessBestRank;
|
||||
private static int _hudlessRetenus; // images ou un candidat a bien ete elu
|
||||
private static int _hudlessEchecsLogues;
|
||||
private static string _lastVerdictSig = "";
|
||||
|
||||
public static void OnDraw(GpuChannel channel, ref ThreedClassState state)
|
||||
public static void OnDraw(GpuChannel channel, ref ThreedClassState state, GpuContext context)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
@@ -64,7 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
// fatal and unlogged. On any error: disable and say why (same hardening as the other probes).
|
||||
try
|
||||
{
|
||||
OnDrawImpl(channel, ref state);
|
||||
OnDrawImpl(channel, ref state, context);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -73,12 +150,44 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state)
|
||||
/// <summary>
|
||||
/// Vraie frontière d'image, appelée depuis Gpu/Window.Present. Indépendante de ResScale, donc
|
||||
/// fonctionne sur les jeux rendus à l'échelle native (XC2) contrairement à la détection par
|
||||
/// montée du facteur de résolution, qui n'a de sens que si la scène 3D tourne au-dessus de 1x.
|
||||
/// </summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_epochs.Count > 0)
|
||||
{
|
||||
FlushFrame();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP UIPROBE: disabled after unexpected error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state, GpuContext context)
|
||||
{
|
||||
float scale = channel.TextureManager.RenderTargetScale;
|
||||
|
||||
// Frame boundary = rising edge of the render scale (1x UI/native -> >1x scaled 3D scene = a
|
||||
// new frame started). Flush what we accumulated for the previous frame.
|
||||
//
|
||||
// [21/07] /!\ CETTE DETECTION NE MARCHE QUE SI ResScale > 1. Sur XC2 (ResScale = 1) le facteur
|
||||
// ne bouge jamais, la condition n'est JAMAIS vraie et la sonde reste MUETTE -- un run entier
|
||||
// perdu à cause de ça. C'est la 3e sonde héritée du chantier TOTK inutilisable telle quelle sur
|
||||
// XC2 (avec MvppGlowProbe, filtrée R11G11B10, et MvppHdrCensusProbe, filtrée width >= 1500).
|
||||
// La vraie fin d'image est maintenant donnée par OnPresent(), appelé depuis Gpu/Window.Present.
|
||||
if (_prevScale == 1f && scale > 1f && _epochs.Count > 0)
|
||||
{
|
||||
FlushFrame();
|
||||
@@ -111,10 +220,68 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
last.Draws++;
|
||||
last.AnyDepth |= depth;
|
||||
last.AnyBlend |= blend;
|
||||
|
||||
// [HUDSPLIT 28/07] Granularite PAR DESSIN. AnyDepth agrege toute l'epoque :
|
||||
// une epoque qui melange scene et interface ressort "depth=True" et cache
|
||||
// exactement la frontiere qu'on cherche. Le HUD ne teste jamais la profondeur
|
||||
// -- compter les dessins SANS test, et retenir le rang du PREMIER, donne le
|
||||
// point ou l'interface commence a etre posee sur l'image.
|
||||
if (!depth)
|
||||
{
|
||||
if (last.NoDepthDraws == 0)
|
||||
{
|
||||
last.FirstNoDepthAt = last.Draws;
|
||||
}
|
||||
|
||||
last.NoDepthDraws++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// [HUDLESS] L'epoque qui vient de se fermer correspond-elle a la signature ?
|
||||
if (_hudless && _epochs.Count > 0)
|
||||
{
|
||||
Epoch fini = _epochs[^1];
|
||||
|
||||
if (fini.W > _renderWidthSeen)
|
||||
{
|
||||
_renderWidthSeen = fini.W;
|
||||
}
|
||||
|
||||
bool ldr8 = fini.Fmt == Format.R8G8B8A8Unorm || fini.Fmt == Format.B8G8R8A8Unorm;
|
||||
|
||||
if (ldr8 && fini.W == _renderWidthSeen && fini.Draws >= HudlessMinDraws)
|
||||
{
|
||||
_hudlessInFrame++;
|
||||
|
||||
// [v2 28/07] LE DERNIER, LE PLUS NOURRI. Mesure : dans certaines situations le
|
||||
// jeu ecrit DEUX FOIS dans ce buffer par image (29 dessins puis 81) -- la v1
|
||||
// comptait les deux (149 % de detections, 74 images ambigues sur 151). L'image
|
||||
// se construit progressivement : le bon candidat est le dernier ET le plus
|
||||
// nourri. On ne tranche donc qu'a la FIN de l'image, jamais a la volee.
|
||||
if (fini.Draws >= _hudlessBestDraws)
|
||||
{
|
||||
_hudlessBestDraws = fini.Draws;
|
||||
_hudlessBestAddr = fini.Addr;
|
||||
_hudlessBestRank = _epochs.Count;
|
||||
|
||||
// [HUDLESSFEED] C'est ICI, et nulle part ailleurs, que le buffer contient
|
||||
// encore l'image : son epoque vient de se fermer, il n'a pas encore servi.
|
||||
if (_hudlessFeed && _epochTex != null && context != null)
|
||||
{
|
||||
CopyHudless(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [EPOCHDUMP] L'epoque precedente vient de se fermer : son render target contient
|
||||
// encore ce que le jeu vient d'y peindre. C'est le SEUL instant ou on peut le voir.
|
||||
TryDumpEpoch();
|
||||
_epochTex = col0;
|
||||
|
||||
if (_epochs.Count < MaxEpochsPerFrame)
|
||||
{
|
||||
_epochs.Add(new Epoch
|
||||
@@ -127,10 +294,128 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
AnyDepth = depth,
|
||||
AnyBlend = blend,
|
||||
Draws = 1,
|
||||
NoDepthDraws = depth ? 0 : 1,
|
||||
FirstNoDepthAt = depth ? 0 : 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [HUDLESSFEED] Copie le render target sans interface vers une texture persistante, puis
|
||||
/// la publie pour la couche Vulkan via le pont GAL existant (MvppColorSnapshot). Aucune
|
||||
/// modification du GAL n'est necessaire -- ce pont date du 12/07 et vit deja dans le
|
||||
/// binaire deploye, contrairement a DlssCameraState qui, lui, a bouge le 28/07 a 11h46.
|
||||
/// </summary>
|
||||
private static void CopyHudless(GpuContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
Image.Texture src = _epochTex;
|
||||
ITexture host = src.HostTexture;
|
||||
|
||||
if (host == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int w = src.Info.Width;
|
||||
int h = src.Info.Height;
|
||||
Format fmt = src.Info.FormatInfo.Format;
|
||||
|
||||
// Recreation seulement si la FORME change (resolution dynamique, changement de
|
||||
// cible) : une texture par forme, pas une par image.
|
||||
if (_hudlessCopy == null || _hudlessCopyW != w || _hudlessCopyH != h || _hudlessCopyFmt != fmt)
|
||||
{
|
||||
_hudlessCopy?.Release();
|
||||
_hudlessCopy = context.Renderer.CreateTexture(new TextureCreateInfo(
|
||||
w, h, 1, 1, 1, 1, 1, 1,
|
||||
fmt,
|
||||
DepthStencilMode.Depth,
|
||||
Target.Texture2D,
|
||||
SwizzleComponent.Red,
|
||||
SwizzleComponent.Green,
|
||||
SwizzleComponent.Blue,
|
||||
SwizzleComponent.Alpha));
|
||||
_hudlessCopyW = w;
|
||||
_hudlessCopyH = h;
|
||||
_hudlessCopyFmt = fmt;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP-HUDLESSFEED: texture de destination creee {w}x{h} {fmt}.");
|
||||
}
|
||||
|
||||
host.CopyTo(_hudlessCopy, 0, 0);
|
||||
|
||||
GAL.MvppColorSnapshot.SceneColorHost = _hudlessCopy;
|
||||
GAL.MvppColorSnapshot.FrameId = ++_hudlessFeedId;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (++_hudlessCopyFails <= 3)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP-HUDLESSFEED: copie echouee: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [EPOCHDUMP] Capture le render target de l'epoque qui vient de se fermer. Lecture seule :
|
||||
/// aucune ecriture GPU, aucun changement de rendu. Une seule image, puis desarmement.
|
||||
/// </summary>
|
||||
private static void TryDumpEpoch()
|
||||
{
|
||||
if (_dumpAfterSeconds == 0 || _dumpDone || _epochTex == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (_dumpArmedAtMs == 0)
|
||||
{
|
||||
_dumpArmedAtMs = now;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - _dumpArmedAtMs < _dumpAfterSeconds * 1000L)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GAL.ITexture host = _epochTex.HostTexture;
|
||||
|
||||
if (host == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong addr = _epochTex.Range.GetSubRange(0).Address;
|
||||
string dir = System.IO.Path.Combine("rtdump", "epochs");
|
||||
System.IO.Directory.CreateDirectory(dir);
|
||||
|
||||
using GAL.PinnedSpan<byte> data = host.GetData();
|
||||
string nom = $"ep{_dumpSeq:D2}_{_epochTex.Info.Width}x{_epochTex.Info.Height}_" +
|
||||
$"{_epochTex.Info.FormatInfo.Format}_at{addr:X}.bin";
|
||||
System.IO.File.WriteAllBytes(System.IO.Path.Combine(dir, nom), data.Get().ToArray());
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP-EPOCHDUMP: {nom} ecrit a la FERMETURE de son epoque.");
|
||||
|
||||
if (++_dumpSeq >= 12)
|
||||
{
|
||||
_dumpDone = true;
|
||||
Logger.Info?.Print(LogClass.Gpu, "MVPP-EPOCHDUMP: termine (12 epoques capturees), sonde desarmee.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_dumpDone = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP-EPOCHDUMP: abandonne apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlushFrame()
|
||||
{
|
||||
_frameNo++;
|
||||
@@ -215,15 +500,98 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
}
|
||||
|
||||
// First frames: dump the full epoch list so the verdict is auditable from raw data.
|
||||
if (_framesLoggedFull < FullDetailFrames)
|
||||
//
|
||||
// [28/07] ET AUSSI UNE FOIS TOUTES LES 5 s ENSUITE. Motif : les 8 premieres images
|
||||
// tombent sur le logo/l'ecran de chargement (mesure du 28/07 17h51 : une seule epoque,
|
||||
// 768x432), donc le detail n'a JAMAIS decrit une image de jeu reelle. Or c'est
|
||||
// exactement ce detail qu'il faut pour savoir comment le HUD est compose avant de
|
||||
// pouvoir le sortir de l'image donnee a la Frame Generation.
|
||||
bool periodic = now - _lastDetailMs >= 5000;
|
||||
|
||||
if (_framesLoggedFull < FullDetailFrames || periodic)
|
||||
{
|
||||
if (periodic)
|
||||
{
|
||||
_lastDetailMs = now;
|
||||
}
|
||||
|
||||
_framesLoggedFull++;
|
||||
for (int i = 0; i < _epochs.Count; i++)
|
||||
{
|
||||
Epoch e = _epochs[i];
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP-UIPROBE f{_frameNo} epoch{i}: RT@0x{e.Addr:X} {e.Fmt} {e.W}x{e.H} " +
|
||||
$"scale={e.Scale:0.##} depth={e.AnyDepth} blend={e.AnyBlend} draws={e.Draws}");
|
||||
$"scale={e.Scale:0.##} depth={e.AnyDepth} blend={e.AnyBlend} draws={e.Draws}" +
|
||||
(e.NoDepthDraws > 0 ? $" >>> SANS-DEPTH {e.NoDepthDraws} (des le n°{e.FirstNoDepthAt}) = CANDIDAT HUD" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
// [HUDLESS] Bilan par image, puis resume a 1 Hz. Ce qu'on veut lire : UNE seule
|
||||
// epoque candidate par image (regle non ambigue) et UNE detection sur CHAQUE image
|
||||
// (regle qui ne decroche pas). Tout ecart se voit dans ces trois chiffres.
|
||||
if (_hudless)
|
||||
{
|
||||
_hudlessFrames++;
|
||||
|
||||
if (_hudlessInFrame > 1)
|
||||
{
|
||||
_hudlessMultiple++;
|
||||
}
|
||||
|
||||
// [v2] Election de fin d'image : UN candidat retenu, et un seul.
|
||||
if (_hudlessBestDraws > 0)
|
||||
{
|
||||
_hudlessRetenus++;
|
||||
_hudlessHits++;
|
||||
|
||||
if (_hudlessRetenus <= 6)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP-HUDLESS elu: @0x{_hudlessBestAddr:X} {_hudlessBestDraws} dessins " +
|
||||
$"(epoque n°{_hudlessBestRank}), {_hudlessInFrame} candidat(s) dans l'image.");
|
||||
}
|
||||
}
|
||||
|
||||
// [v2] ECHEC D'ELECTION : dire CE QU'ON A VU plutot que de le contourner par un
|
||||
// repli. Mesure du 28/07 : la regle tient a 100 % dans la plupart des situations
|
||||
// mais tombe a 83,9 % par moments -- 24 images sur 149 sans aucun candidat. Un
|
||||
// HUD-less manquant sur une image = la FG travaille sur du perime, donc exactement
|
||||
// l'artefact intermittent qu'on veut supprimer. Il faut savoir ce que sont ces
|
||||
// images avant de decider quoi faire.
|
||||
else if (_hudlessEchecsLogues < 8)
|
||||
{
|
||||
_hudlessEchecsLogues++;
|
||||
|
||||
var vu = new System.Text.StringBuilder();
|
||||
|
||||
for (int i = 0; i < _epochs.Count; i++)
|
||||
{
|
||||
vu.Append($"[{_epochs[i].Fmt} {_epochs[i].W}x{_epochs[i].H} {_epochs[i].Draws}d] ");
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP-HUDLESS AUCUN CANDIDAT sur cette image ({_epochs.Count} epoques) : " +
|
||||
(vu.Length > 0 ? vu.ToString().TrimEnd() : "aucune epoque"));
|
||||
}
|
||||
|
||||
_hudlessInFrame = 0;
|
||||
_hudlessBestDraws = 0;
|
||||
_hudlessBestAddr = 0;
|
||||
_hudlessBestRank = 0;
|
||||
|
||||
if (now - _hudlessLogMs >= 5000)
|
||||
{
|
||||
_hudlessLogMs = now;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP-HUDLESS: {_hudlessHits} elus sur {_hudlessFrames} images " +
|
||||
$"({(_hudlessFrames > 0 ? 100f * _hudlessHits / _hudlessFrames : 0f):0.0}% - doit valoir 100), " +
|
||||
$"images ou plusieurs candidats se presentaient={_hudlessMultiple} " +
|
||||
$"(l'election en garde UN, c'est desormais informatif), " +
|
||||
$"largeur de rendu retenue={_renderWidthSeen}.");
|
||||
_hudlessHits = 0;
|
||||
_hudlessFrames = 0;
|
||||
_hudlessMultiple = 0;
|
||||
_hudlessRetenus = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
/// <summary>
|
||||
/// Viewport-vs-render-target census (RYUJINX_VP_PROBE=1). READ-ONLY, default off.
|
||||
///
|
||||
/// XC2 lateral-band artefact, 21/07. What is already MEASURED: the presented guest image is 1280x720
|
||||
/// and the present blits the FULL rectangle (`src[0,0,1280,720]`, constant over a whole capture run),
|
||||
/// so the corrupt side bands are already inside the guest image -- it is not a crop or a stretch.
|
||||
/// What is NOT measured, on this game, ever: the VIEWPORT. Pixels of a render target lying OUTSIDE the
|
||||
/// current viewport are neither cleared nor defined (the host texture is always allocated at the FULL
|
||||
/// guest size, and nothing wipes the remainder), so a game that shrinks its viewport -- dynamic
|
||||
/// resolution, which XC2 is known for -- leaves stale content on the sides of an otherwise fresh frame.
|
||||
/// The earlier "dynamic resolution eliminated" verdict only ever compared SURFACE sizes, which is
|
||||
/// precisely what dyn-res keeps constant; it never looked at the viewport.
|
||||
///
|
||||
/// This probe answers one question: does XC2's viewport cover its render target, and if not, is it
|
||||
/// CENTRED (two bands, matching the observed artefact) or anchored to one edge (a single band)?
|
||||
/// Stats are kept PER RENDER-TARGET SHAPE -- mixing the scene pass with shadow and low-resolution
|
||||
/// passes is what produced empty verdicts on the earlier probes.
|
||||
/// </summary>
|
||||
static class MvppViewportProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_VP_PROBE") == "1";
|
||||
|
||||
private static bool _announced;
|
||||
private static long _windowMs;
|
||||
|
||||
private sealed class Stats
|
||||
{
|
||||
public int Draws;
|
||||
public int Narrow; // viewport does not cover the target horizontally
|
||||
public int Short; // ... vertically
|
||||
public int TransformOff; // ViewportTransformEnable == 0 (screen-scissor path)
|
||||
public float MinX, MaxX, MinW, MaxW, MinY, MaxY, MinH, MaxH;
|
||||
public bool Seeded;
|
||||
|
||||
// [v2, 21/07] The viewport came back covering the target on 91 398 draws, so the OTHER half of
|
||||
// the same mechanism is what matters now: the SCISSOR, and the SCREEN scissor that feeds the
|
||||
// texture-width heuristic. Neither has ever been measured on this game's scene passes -- the one
|
||||
// scissor test on record only ever looked at draws targeting the HDR buffer.
|
||||
// [v3, 21/07] The three values NOTHING in this emulator ever reads, and that no probe in this
|
||||
// dossier has ever looked at. All three are structurally symmetric in X, which is the one
|
||||
// property the artefact demands and that every mechanism eliminated so far fails to provide.
|
||||
// - ViewportExtents.X/Width : the guest's viewport CLIP rectangle. The struct is documented
|
||||
// "viewport extents for viewport clipping", but a grep over StateUpdater shows only
|
||||
// DepthNear/DepthFar are consumed (l.1240-1241, 2117-2119). Vulkan does not clip to the
|
||||
// viewport -- only scissor and framebuffer do -- so an ignored clip means we rasterise
|
||||
// fragments the console discards. X=m, Width=W-2m cuts m pixels off BOTH sides.
|
||||
// - sign of ScaleX : a negative ScaleX is a horizontal MIRROR. StateUpdater.cs:1204 destroys
|
||||
// it with MathF.Abs, and so did v1/v2 of this very probe -- the measurement copied the bug.
|
||||
// A mirror leaves the centre invariant and grows outward symmetrically: the exact geography.
|
||||
// - swizzle X : NegativeX and the X<->Y permutations are never consumed by the Vulkan backend.
|
||||
public int ClipZero; // register never written (X==0 && Width==0)
|
||||
public int ClipFull; // clip covers the target
|
||||
public int ClipCuts; // clip leaves pixels out on the left or the right
|
||||
public int ClipMinX, ClipMaxX, ClipMinW, ClipMaxW;
|
||||
public bool ClipSeeded;
|
||||
public int A2c; // alpha-to-coverage actif sur ce draw
|
||||
public int A2cDither; // ... ET dither actif => chemin du discard en damier
|
||||
public int NegScaleX; // ScaleX < 0 -> horizontal mirror the backend cannot express
|
||||
public int SwizzleXOdd; // swizzle X is not PositiveX
|
||||
|
||||
public void AddClip(int x, int w)
|
||||
{
|
||||
if (!ClipSeeded)
|
||||
{
|
||||
ClipSeeded = true;
|
||||
ClipMinX = ClipMaxX = x;
|
||||
ClipMinW = ClipMaxW = w;
|
||||
return;
|
||||
}
|
||||
|
||||
if (x < ClipMinX) { ClipMinX = x; }
|
||||
if (x > ClipMaxX) { ClipMaxX = x; }
|
||||
if (w < ClipMinW) { ClipMinW = w; }
|
||||
if (w > ClipMaxW) { ClipMaxW = w; }
|
||||
}
|
||||
|
||||
public int ScissorOn; // scissor enabled at all
|
||||
public int ScissorCuts; // scissor leaves uncovered pixels left or right of the target
|
||||
public int ScissorSeen;
|
||||
public int SciMinX1, SciMaxX1, SciMinX2, SciMaxX2;
|
||||
public int ScrMinW, ScrMaxW, ScrMinH, ScrMaxH;
|
||||
public bool ScrSeeded;
|
||||
|
||||
public void AddScissor(int x1, int x2)
|
||||
{
|
||||
if (ScissorSeen++ == 0)
|
||||
{
|
||||
SciMinX1 = SciMaxX1 = x1;
|
||||
SciMinX2 = SciMaxX2 = x2;
|
||||
return;
|
||||
}
|
||||
|
||||
if (x1 < SciMinX1) { SciMinX1 = x1; }
|
||||
if (x1 > SciMaxX1) { SciMaxX1 = x1; }
|
||||
if (x2 < SciMinX2) { SciMinX2 = x2; }
|
||||
if (x2 > SciMaxX2) { SciMaxX2 = x2; }
|
||||
}
|
||||
|
||||
public void AddScreen(int w, int h)
|
||||
{
|
||||
if (!ScrSeeded)
|
||||
{
|
||||
ScrSeeded = true;
|
||||
ScrMinW = ScrMaxW = w;
|
||||
ScrMinH = ScrMaxH = h;
|
||||
return;
|
||||
}
|
||||
|
||||
if (w < ScrMinW) { ScrMinW = w; }
|
||||
if (w > ScrMaxW) { ScrMaxW = w; }
|
||||
if (h < ScrMinH) { ScrMinH = h; }
|
||||
if (h > ScrMaxH) { ScrMaxH = h; }
|
||||
}
|
||||
|
||||
public void Add(float x, float y, float w, float h)
|
||||
{
|
||||
if (!Seeded)
|
||||
{
|
||||
Seeded = true;
|
||||
MinX = MaxX = x;
|
||||
MinY = MaxY = y;
|
||||
MinW = MaxW = w;
|
||||
MinH = MaxH = h;
|
||||
return;
|
||||
}
|
||||
|
||||
if (x < MinX) { MinX = x; }
|
||||
if (x > MaxX) { MaxX = x; }
|
||||
if (y < MinY) { MinY = y; }
|
||||
if (y > MaxY) { MaxY = y; }
|
||||
if (w < MinW) { MinW = w; }
|
||||
if (w > MaxW) { MaxW = w; }
|
||||
if (h < MinH) { MinH = h; }
|
||||
if (h > MaxH) { MaxH = h; }
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<(int W, int H), Stats> _byShape = new();
|
||||
|
||||
// [ADDR, 21/07] Modes d'adressage réellement demandés par le jeu, TOUTES passes confondues.
|
||||
// Ryujinx substitue quatre modes (EnumConversion.cs:97-110), chacun marqué "TODO: Should be ..." :
|
||||
// Clamp -> ClampToEdge · MirrorClamp -> ClampToEdge · MirrorClampToBorder -> ClampToBorder
|
||||
// + tout mode inconnu -> ClampToEdge
|
||||
// Le vrai Clamp renvoie la COULEUR DE BORDURE hors [0,1] ; ClampToEdge RÉPLIQUE le dernier texel.
|
||||
// Demander le premier et recevoir le second ne donne pas une bordure : ça donne une TRAÎNÉE --
|
||||
// exactement l'étirement vertical photographié par Alex quand il monte/descend la caméra.
|
||||
// L'élimination du journal ("modes d'adressage cohérents") venait de MvppGlowProbe, filtrée sur les
|
||||
// seules cibles R11G11B10 : elle ne couvrait ni le 512x288, ni le buffer d'ids, ni le compute.
|
||||
// Ici AUCUN filtre. Échantillonné 1 draw sur 16 pour que l'énumération ne coûte pas la frame.
|
||||
private static readonly int[] _addrU = new int[16];
|
||||
private static readonly int[] _addrV = new int[16];
|
||||
private static int _addrBindings;
|
||||
private static int _addrSampled;
|
||||
|
||||
public static void OnDraw(GpuChannel channel, ref ThreedClassState state)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OnDrawImpl(channel, ref state);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// A bench instrument must never take the emulator down with it.
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP VPPROBE: disabled after unexpected error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state)
|
||||
{
|
||||
if (!_announced)
|
||||
{
|
||||
// Armed proof before any measurement: a switch you cannot SEE in the log is worth nothing.
|
||||
_announced = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
"MVPP VPPROBE: ON -- viewport vs render target, read-only, one line per shape per second.");
|
||||
}
|
||||
|
||||
int rtW = 0, rtH = 0;
|
||||
|
||||
channel.TextureManager.MvppEnumerateRenderTargets((slot, rt) =>
|
||||
{
|
||||
if (rtW == 0 && slot == 0 && rt != null)
|
||||
{
|
||||
rtW = rt.Info.Width;
|
||||
rtH = rt.Info.Height;
|
||||
}
|
||||
});
|
||||
|
||||
if (rtW <= 0 || rtH <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Same arithmetic as StateUpdater.UpdateViewportTransform (l.1204-1221), minus the render-target
|
||||
// scale (inert at ResScale=1) and the swizzle handling, which do not move the rectangle.
|
||||
ref ViewportTransform transform = ref state.ViewportTransform[0];
|
||||
|
||||
float scaleX = MathF.Abs(transform.ScaleX);
|
||||
float scaleY = MathF.Abs(transform.ScaleY);
|
||||
float vpX = transform.TranslateX - scaleX;
|
||||
float vpY = transform.TranslateY - scaleY;
|
||||
float vpW = scaleX * 2f;
|
||||
float vpH = scaleY * 2f;
|
||||
|
||||
(int, int) key = (rtW, rtH);
|
||||
|
||||
if (!_byShape.TryGetValue(key, out Stats st))
|
||||
{
|
||||
st = new Stats();
|
||||
_byShape[key] = st;
|
||||
}
|
||||
|
||||
st.Draws++;
|
||||
st.Add(vpX, vpY, vpW, vpH);
|
||||
|
||||
if (state.ViewportTransformEnable == 0)
|
||||
{
|
||||
st.TransformOff++;
|
||||
}
|
||||
|
||||
// "Narrow" = the viewport leaves uncovered pixels on the left or the right of the target.
|
||||
// One pixel of slack absorbs the float arithmetic; real dyn-res steps are tens of pixels.
|
||||
if (vpX > 1f || vpX + vpW < rtW - 1f)
|
||||
{
|
||||
st.Narrow++;
|
||||
}
|
||||
|
||||
if (vpY > 1f || vpY + vpH < rtH - 1f)
|
||||
{
|
||||
st.Short++;
|
||||
}
|
||||
|
||||
// Scissor: same question as the viewport, other register. A scissor narrower than the target
|
||||
// leaves its sides unwritten, which is exactly the observed geography. Disabled or maxed out,
|
||||
// it becomes (0,0,0xffff,0xffff) and protects nothing -- that case counts as "no cut".
|
||||
ScissorState sc = state.ScissorState[0];
|
||||
bool scEnabled = sc.Enable;
|
||||
|
||||
if (scEnabled)
|
||||
{
|
||||
st.ScissorOn++;
|
||||
st.AddScissor(sc.X1, sc.X2);
|
||||
|
||||
if (sc.X1 > 0 || sc.X2 < rtW)
|
||||
{
|
||||
st.ScissorCuts++;
|
||||
}
|
||||
}
|
||||
|
||||
// Screen scissor: it is the size hint that decides the created texture WIDTH
|
||||
// (TextureCache.GetMinimumWidthInGob), so a moving screen scissor can hand back the same guest
|
||||
// surface at slightly different widths. Worth a number before anyone theorises about it.
|
||||
ScreenScissorState scr = state.ScreenScissorState;
|
||||
st.AddScreen(scr.Width, scr.Height);
|
||||
|
||||
// [v3] The clip rectangle the emulator decodes and throws away.
|
||||
ViewportExtents ext = state.ViewportExtents[0];
|
||||
int clipX = ext.X;
|
||||
int clipW = ext.Width;
|
||||
|
||||
if (clipX == 0 && clipW == 0)
|
||||
{
|
||||
st.ClipZero++;
|
||||
}
|
||||
else
|
||||
{
|
||||
st.AddClip(clipX, clipW);
|
||||
|
||||
if (clipX > 0 || clipX + clipW < rtW)
|
||||
{
|
||||
st.ClipCuts++;
|
||||
}
|
||||
else
|
||||
{
|
||||
st.ClipFull++;
|
||||
}
|
||||
}
|
||||
|
||||
// [v3] Raw sign, NOT MathF.Abs -- this is the whole point of the v3 pass.
|
||||
if (transform.ScaleX < 0f)
|
||||
{
|
||||
st.NegScaleX++;
|
||||
}
|
||||
|
||||
if (transform.UnpackSwizzleX() != ViewportSwizzle.PositiveX)
|
||||
{
|
||||
st.SwizzleXOdd++;
|
||||
}
|
||||
|
||||
// [A2C, 21/07] LA mesure qui décide du dossier. Ryujinx émule le fondu par transparence en
|
||||
// REJETANT des pixels selon un damier 2x2 (EmitterContext.GenerateAlphaToCoverageDitherDiscard,
|
||||
// masque 0xfbb99110, discard si le bit est à 0). Sur la console ce fondu est lissé par le
|
||||
// multi-échantillonnage ; ici les pixels sont jetés et il reste une grille de trous dans la
|
||||
// géométrie -- exactement le motif régulier qu'Alex photographie. Ce chemin n'est actif que si
|
||||
// le jeu pose les DEUX drapeaux. Si a2c=0 sur XC2, l'hypothèse meurt en un run.
|
||||
if ((state.MultisampleControl & 1) != 0)
|
||||
{
|
||||
st.A2c++;
|
||||
|
||||
if (state.AlphaToCoverageDitherEnable)
|
||||
{
|
||||
st.A2cDither++;
|
||||
}
|
||||
}
|
||||
|
||||
// Modes d'adressage demandés, toutes passes. 1 draw sur 16 : l'énumération des bindings est
|
||||
// le seul point coûteux de cette sonde.
|
||||
if ((++_addrSampled & 15) == 0)
|
||||
{
|
||||
channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) =>
|
||||
{
|
||||
if (smp == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int u = (int)smp.ProbeAddressU;
|
||||
int v = (int)smp.ProbeAddressV;
|
||||
|
||||
if ((uint)u < 16) { _addrU[u]++; }
|
||||
if ((uint)v < 16) { _addrV[v]++; }
|
||||
|
||||
_addrBindings++;
|
||||
});
|
||||
}
|
||||
|
||||
long nowMs = Environment.TickCount64;
|
||||
|
||||
if (nowMs < _windowMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_windowMs = nowMs + 1000;
|
||||
|
||||
foreach (KeyValuePair<(int W, int H), Stats> entry in _byShape)
|
||||
{
|
||||
Stats s = entry.Value;
|
||||
|
||||
if (s.Draws == 0 || !s.Seeded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Left/right margins of the widest and narrowest viewport seen on this shape. Two roughly
|
||||
// equal margins = a centred viewport = two lateral bands. One-sided = a single band.
|
||||
float leftMin = s.MinX;
|
||||
float rightMin = entry.Key.W - (s.MinX + s.MinW);
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP VPPROBE: rt {entry.Key.W}x{entry.Key.H} draws={s.Draws} narrow={s.Narrow} short={s.Short} " +
|
||||
$"tfOff={s.TransformOff} vpW=[{s.MinW:F1}..{s.MaxW:F1}] vpH=[{s.MinH:F1}..{s.MaxH:F1}] " +
|
||||
$"vpX=[{s.MinX:F1}..{s.MaxX:F1}] vpY=[{s.MinY:F1}..{s.MaxY:F1}] " +
|
||||
$"margeG={leftMin:F1} margeD={rightMin:F1} " +
|
||||
$"| sciOn={s.ScissorOn} sciCuts={s.ScissorCuts} sciX=[{s.SciMinX1}..{s.SciMaxX1}][{s.SciMinX2}..{s.SciMaxX2}] " +
|
||||
$"scrW=[{s.ScrMinW}..{s.ScrMaxW}] scrH=[{s.ScrMinH}..{s.ScrMaxH}] " +
|
||||
$"|| CLIP zero={s.ClipZero} full={s.ClipFull} CUTS={s.ClipCuts} " +
|
||||
$"clipX=[{s.ClipMinX}..{s.ClipMaxX}] clipW=[{s.ClipMinW}..{s.ClipMaxW}] " +
|
||||
$"MIRROR negScaleX={s.NegScaleX} swizzleXodd={s.SwizzleXOdd} || A2C={s.A2c} A2C_DITHER={s.A2cDither}");
|
||||
s.Draws = 0;
|
||||
s.Narrow = 0;
|
||||
s.Short = 0;
|
||||
s.TransformOff = 0;
|
||||
s.Seeded = false;
|
||||
s.ScissorOn = 0;
|
||||
s.ScissorCuts = 0;
|
||||
s.ScissorSeen = 0;
|
||||
s.ScrSeeded = false;
|
||||
s.ClipZero = 0;
|
||||
s.ClipFull = 0;
|
||||
s.ClipCuts = 0;
|
||||
s.ClipSeeded = false;
|
||||
s.NegScaleX = 0;
|
||||
s.SwizzleXOdd = 0;
|
||||
s.A2c = 0;
|
||||
s.A2cDither = 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,
|
||||
$"MVPP ADDRPROBE: {_addrBindings} bindings echantillonnes | U: {DescribeModes(_addrU)} | V: {DescribeModes(_addrV)}");
|
||||
|
||||
Array.Clear(_addrU);
|
||||
Array.Clear(_addrV);
|
||||
_addrBindings = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static string DescribeModes(int[] counts)
|
||||
{
|
||||
System.Text.StringBuilder sb = new();
|
||||
|
||||
for (int i = 0; i < counts.Length; i++)
|
||||
{
|
||||
if (counts[i] == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddressMode m = (AddressMode)i;
|
||||
bool substitue = m is AddressMode.Clamp or AddressMode.MirrorClamp or AddressMode.MirrorClampToBorder;
|
||||
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(' ');
|
||||
}
|
||||
|
||||
sb.Append(m).Append('=').Append(counts[i]);
|
||||
|
||||
if (substitue)
|
||||
{
|
||||
sb.Append("(SUBSTITUE)");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.Length > 0 ? sb.ToString() : "aucun";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
// 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.
|
||||
if (scale != System.MathF.Floor(scale) && (_fractProbeCount++ & 31) == 0)
|
||||
// [31/07] GATE AJOUTE. Elle etait en release SANS aucun interrupteur : la
|
||||
// condition « echelle fractionnaire » est remplie par TOUT utilisateur en DLSS
|
||||
// quality ou performance (DlssIntegration pilote GraphicsConfig.ResScale ;
|
||||
// 4K + quality = 1440/1080 = 1,334). Mesure du 31/07 sur GYLT : 21 471 lignes
|
||||
// en 3 minutes -- soit ~690 000 requetes de compteur -- chez quelqu'un qui
|
||||
// n'avait aucun probleme a diagnostiquer. La sonde rend son service UNE fois
|
||||
// par rapport de bug ; elle ne doit pas ecrire le reste du temps.
|
||||
if (GAL.MvppDev.Enabled && scale != System.MathF.Floor(scale) && (_fractProbeCount++ & 31) == 0)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"MVPP fract-probe: samples query #{_fractProbeCount - 1}, divisor {divisor} (scale {scale}), gpuVa 0x{gpuVa:X}.");
|
||||
|
||||
@@ -58,6 +58,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private static readonly bool _worldResJitter =
|
||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES") == "1";
|
||||
|
||||
// [WORLDRES-LOW, 20/07 — natif] At native+4K the July selection window ([In/2, In*0.95],
|
||||
// designed at 2x) leaves the HALF-render-res world passes (1600x900 for a 3200x1800 render:
|
||||
// cloth/tents/particles/far LOD) un-jittered inside a de-jittered image => residual world
|
||||
// trembling in motion (run #7, PASSPROBE proof in docs/JITTER-BANC.md). This knob widens the
|
||||
// low bound to In/3 for 16:9 passes, depth-bound or not (the camera pass needs >= In/2, so no
|
||||
// overlap). Same NDC path as WORLDRES. Gated; default OFF => byte-identical.
|
||||
private static readonly bool _worldResLow =
|
||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES_LOW") == "1";
|
||||
|
||||
// [FULLJIT, 09/07 — kill the inject blur] The camera-pass jitter amplitude denominator is the
|
||||
// present-time InputWidth (published at present, one frame stale = the FINAL DLSS-input width 3200)
|
||||
// instead of THIS frame's real render viewport (2666). So the geometry only receives
|
||||
@@ -98,6 +107,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
private readonly ShaderProgramInfo[] _currentProgramInfo;
|
||||
|
||||
// [SCENEPROBE] guest shader VAs of the current draw (read-only, for the scene-pass probe).
|
||||
private ulong _probeSceneFsAddr;
|
||||
private ulong _probeSceneVsAddr;
|
||||
|
||||
// [MVPP CUTOUT PROBE] Read-only per-draw tally: draws whose active FRAGMENT shader emits Discard
|
||||
// (alpha cutout), + their host viewport, to PROVE where the cutout foliage draws are. Gated by
|
||||
// RYUJINX_MVPP_CUTOUT_PROBE; default OFF => zero cost, no state touched, no render/DLSS/jitter change.
|
||||
@@ -422,6 +435,134 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
CutoutDrawProbeTick();
|
||||
}
|
||||
|
||||
// [CONSTDIFF] Read-only per-draw: trace the builder's prev-frame matrix (journal 221).
|
||||
if (Image.MvppConstDiffProbe.Enabled)
|
||||
{
|
||||
Image.MvppConstDiffProbe.OnDraw(_channel, _probeSceneFsAddr);
|
||||
}
|
||||
|
||||
// [BUILDERIN] Read-only per-draw: which surfaces are actually bound to the builder's
|
||||
// samplers and the exact constants it consumes (journal 226). Self-gated; always
|
||||
// called so its per-draw input list is cleared even on non-builder draws.
|
||||
Image.MvppBuilderInProbe.OnDraw(_channel, _channel.TextureManager, _probeSceneFsAddr);
|
||||
|
||||
// [MVHASH] Read-only: content occupancy of the MV twin at builder-draw time (242).
|
||||
Image.MvppContentProbe.OnBuilderDraw(_channel.TextureManager, _probeSceneFsAddr);
|
||||
|
||||
// [SCENEPROBE] Read-only per-draw: identify the pass writing the 720p R11G11B10 scene.
|
||||
// Same point as the cutout probe (after CommitBindings): RT, inputs and shader VAs are all
|
||||
// current for this draw. Touches no state.
|
||||
if (Image.MvppScenePassProbe.Enabled)
|
||||
{
|
||||
Image.MvppScenePassProbe.OnDraw(
|
||||
_channel.TextureManager,
|
||||
_probeSceneFsAddr,
|
||||
_probeSceneVsAddr,
|
||||
Image.MvppFeedbackProbe.Frame);
|
||||
}
|
||||
|
||||
// [MAP64] Read-only per-draw: who writes the small DoF maps, with the draw's viewport
|
||||
// extents (a partial viewport leaves stale texels = the staleness suspect). Touches no state.
|
||||
if (Image.MvppMap64Probe.Enabled)
|
||||
{
|
||||
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
|
||||
ref ViewportTransform vp0 = ref vpSpan[0];
|
||||
float vpScale = _channel.TextureManager.RenderTargetScale;
|
||||
|
||||
Image.MvppMap64Probe.OnDraw(
|
||||
_channel.TextureManager,
|
||||
_probeSceneFsAddr,
|
||||
_probeSceneVsAddr,
|
||||
(int)(MathF.Abs(vp0.ScaleX) * 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)
|
||||
{
|
||||
Span<ViewportTransform> vpSpan = _state.State.ViewportTransform.AsSpan();
|
||||
ref ViewportTransform vp0 = ref vpSpan[0];
|
||||
float vpScale = _channel.TextureManager.RenderTargetScale;
|
||||
|
||||
Span<bool> probeBlend = stackalloc bool[Constants.TotalRenderTargets];
|
||||
Span<uint> probeMask = stackalloc uint[Constants.TotalRenderTargets];
|
||||
bool maskShared = _state.State.RtColorMaskShared;
|
||||
Span<Boolean32> blendSpan = _state.State.BlendEnable.AsSpan();
|
||||
Span<RtColorMask> maskSpan = _state.State.RtColorMask.AsSpan();
|
||||
|
||||
for (int i = 0; i < Constants.TotalRenderTargets; i++)
|
||||
{
|
||||
probeBlend[i] = blendSpan[i];
|
||||
RtColorMask cm = maskSpan[maskShared ? 0 : i];
|
||||
probeMask[i] = (cm.UnpackRed() ? 1u : 0u) |
|
||||
(cm.UnpackGreen() ? 2u : 0u) |
|
||||
(cm.UnpackBlue() ? 4u : 0u) |
|
||||
(cm.UnpackAlpha() ? 8u : 0u);
|
||||
}
|
||||
|
||||
Image.MvppMvBufProbe.OnDraw(
|
||||
_channel.TextureManager,
|
||||
_probeSceneFsAddr,
|
||||
(int)(MathF.Abs(vp0.ScaleX) * 2f * vpScale),
|
||||
(int)(MathF.Abs(vp0.ScaleY) * 2f * vpScale),
|
||||
probeBlend,
|
||||
probeMask);
|
||||
}
|
||||
|
||||
// [CENSUS] Read-only per-draw: MV-buffer writers vs the fingerprint-armed registry --
|
||||
// flags the writers every value scrub missed. v2: write masks passed so bound-but-
|
||||
// masked-off draws are counted apart. v3: classification by PROGRAM identity via
|
||||
// ShaderProgramInfo (VA census defeated by code dedup). Touches no state.
|
||||
if (Image.MvppWriterCensusProbe.Enabled)
|
||||
{
|
||||
Span<uint> censusMask = stackalloc uint[Constants.TotalRenderTargets];
|
||||
bool censusMaskShared = _state.State.RtColorMaskShared;
|
||||
Span<RtColorMask> censusMaskSpan = _state.State.RtColorMask.AsSpan();
|
||||
|
||||
for (int i = 0; i < Constants.TotalRenderTargets; i++)
|
||||
{
|
||||
RtColorMask cm = censusMaskSpan[censusMaskShared ? 0 : i];
|
||||
censusMask[i] = (cm.UnpackRed() ? 1u : 0u) |
|
||||
(cm.UnpackGreen() ? 2u : 0u) |
|
||||
(cm.UnpackBlue() ? 4u : 0u) |
|
||||
(cm.UnpackAlpha() ? 8u : 0u);
|
||||
}
|
||||
|
||||
ShaderProgramInfo censusFragInfo = null;
|
||||
for (int stage = 0; stage < Constants.ShaderStages; stage++)
|
||||
{
|
||||
ShaderProgramInfo info = _currentProgramInfo[stage];
|
||||
if (info != null && info.Stage == ShaderStage.Fragment)
|
||||
{
|
||||
censusFragInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Image.MvppWriterCensusProbe.OnDraw(_channel.TextureManager, _probeSceneFsAddr, censusMask, censusFragInfo);
|
||||
}
|
||||
|
||||
// [TWINMAP] Read-only per-draw: writers per MV twin (by guest VA). v2: + live GMMU
|
||||
// re-translation. v5: masks passed, all slots scanned (no early return). Touches no state.
|
||||
if (Image.MvppTwinMapProbe.Enabled)
|
||||
{
|
||||
Span<uint> twinMask = stackalloc uint[Constants.TotalRenderTargets];
|
||||
bool twinMaskShared = _state.State.RtColorMaskShared;
|
||||
Span<RtColorMask> twinMaskSpan = _state.State.RtColorMask.AsSpan();
|
||||
|
||||
for (int i = 0; i < Constants.TotalRenderTargets; i++)
|
||||
{
|
||||
RtColorMask cm = twinMaskSpan[twinMaskShared ? 0 : i];
|
||||
twinMask[i] = (cm.UnpackRed() ? 1u : 0u) |
|
||||
(cm.UnpackGreen() ? 2u : 0u) |
|
||||
(cm.UnpackBlue() ? 4u : 0u) |
|
||||
(cm.UnpackAlpha() ? 8u : 0u);
|
||||
}
|
||||
|
||||
Image.MvppTwinMapProbe.OnDraw(_channel.TextureManager, _probeSceneFsAddr, _channel.MemoryManager, twinMask);
|
||||
}
|
||||
}
|
||||
|
||||
// [MVPP CUTOUT PROBE] Count this draw + (if its fragment shader is a cutout shader) tally it and its
|
||||
@@ -1051,6 +1192,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
vpW >= DlssJitterState.InputWidth / 2f &&
|
||||
vpW <= DlssJitterState.InputWidth * 0.95f;
|
||||
|
||||
// [WORLDRES-LOW] The half-render-res world passes, below the July window.
|
||||
bool worldResLowPass = _worldResLow && !cameraPass &&
|
||||
aspect > 1.5f && aspect < 1.95f &&
|
||||
DlssJitterState.InputWidth > 0 &&
|
||||
vpW >= DlssJitterState.InputWidth / 3f &&
|
||||
vpW < DlssJitterState.InputWidth / 2f;
|
||||
worldResPass |= worldResLowPass;
|
||||
|
||||
if (cameraPass || worldResPass)
|
||||
{
|
||||
// Amplitude denominator: default = the final DLSS-input size (present, 1-frame stale).
|
||||
@@ -1058,6 +1207,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
// sub-pixel jitter (kills the under-coverage blur). OFF => byte-identical.
|
||||
float denomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth;
|
||||
float denomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight;
|
||||
|
||||
// [WORLDRES-LOW v2, 20/07] Sub-window passes are composited with a STRETCH to the
|
||||
// final image (run #8 proof: vpW denominator => world tremble x2, matching the 2.4x
|
||||
// overshoot the stretch model predicts). A stretched pass must receive its jitter as
|
||||
// a fraction of the FINAL input, whatever FULLJIT says, so the on-screen shift equals
|
||||
// the declared offset exactly.
|
||||
if (worldResLowPass)
|
||||
{
|
||||
denomW = DlssJitterState.InputWidth;
|
||||
denomH = DlssJitterState.InputHeight;
|
||||
}
|
||||
|
||||
jitterNdcX = DlssJitterState.OffsetX * 2f / denomW;
|
||||
jitterNdcY = DlssJitterState.OffsetY * 2f / denomH;
|
||||
}
|
||||
@@ -1074,12 +1235,22 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
float appliedPxX = inW > 0 ? offX * vpW / inW : 0f;
|
||||
float appliedPxY = inH > 0 ? offY * vpH / inH : 0f;
|
||||
var sc = _state.State.ScreenScissorState;
|
||||
// [JITTERVAL v2, 20/07] The 07/07 fields above assume the LEGACY denominator (inW) and
|
||||
// are blind to FULLJIT. truePx = offset scaled by the denominator ACTUALLY used for the
|
||||
// NDC this frame (same expression as the inject block above), so the applied-vs-declared
|
||||
// proof measures the real consumption point whatever the knobs. Log-only, same gate.
|
||||
float jvDenomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth;
|
||||
float jvDenomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight;
|
||||
float truePxX = jvDenomW > 0f ? offX * vpW / jvDenomW : 0f;
|
||||
float truePxY = jvDenomH > 0f ? offY * vpH / jvDenomH : 0f;
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"JITTERVAL-GEO frame={DlssJitterState.FrameId} vpW={(int)vpW} vpH={(int)vpH} " +
|
||||
$"inW={inW} inH={inH} offX={offX:0.0000} offY={offY:0.0000} " +
|
||||
$"ndcX={ndcX:0.00000} ndcY={ndcY:0.00000} appliedPxX={appliedPxX:0.0000} appliedPxY={appliedPxY:0.0000} " +
|
||||
$"rtColW={_jvColorW} rtColH={_jvColorH} rtDepthW={_jvDepthW} rtDepthH={_jvDepthH} " +
|
||||
$"scX={sc.X} scY={sc.Y} scW={sc.Width} scH={sc.Height}");
|
||||
$"scX={sc.X} scY={sc.Y} scW={sc.Width} scH={sc.Height} " +
|
||||
$"denomW={(int)jvDenomW} denomH={(int)jvDenomH} fulljit={(_injectFullJit ? 1 : 0)} " +
|
||||
$"truePxX={truePxX:0.0000} truePxY={truePxY:0.0000}");
|
||||
}
|
||||
|
||||
// [JITTER-PASSPROBE, 07/07] One line per unique (width, height, depth-bound) pass geometry.
|
||||
@@ -1879,6 +2050,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
addressesSpan[index] = baseAddress + shader.Offset;
|
||||
}
|
||||
|
||||
// [SCENEPROBE] stable per-pass identity for the current draw (guest shader VAs).
|
||||
_probeSceneFsAddr = addressesSpan[5]; // fragment
|
||||
_probeSceneVsAddr = addressesSpan[1]; // vertex (main)
|
||||
|
||||
int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex
|
||||
? _state.State.TexturePoolState.MaximumId
|
||||
: _state.State.SamplerPoolState.MaximumId;
|
||||
@@ -1979,6 +2154,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
_currentProgramInfo[stageIndex] = info;
|
||||
}
|
||||
|
||||
|
||||
if (gs.Shaders[5]?.Info.UsesFragCoord == true)
|
||||
{
|
||||
// Make sure we update the viewport size on the support buffer if it will be consumed on the new shader.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
||||
{
|
||||
/// <summary>
|
||||
/// Recensement des blits du MOTEUR 2D (RYUJINX_TWOD_PROBE=1). READ-ONLY, off par défaut.
|
||||
///
|
||||
/// POURQUOI ICI, ET PAS AILLEURS. Au 21/07, l'artefact XC2 a été poursuivi étage par étage et chacun
|
||||
/// est tombé sur mesure :
|
||||
/// - présentation / blit final / HDR / swapchain : hors de cause (paire même-image guest+swapchain) ;
|
||||
/// - états de rastérisation : viewport, scissor, screen scissor, clip, miroir, swizzle = 6 mesures,
|
||||
/// 0 anomalie sur ~90 000 draws ;
|
||||
/// - appels de dessin : TOUS capturés sur des images entières, la scène est propre au dernier draw
|
||||
/// alors que l'image présentée est détruite ;
|
||||
/// - compute : 0 dispatch par image, compteur visible dans le log.
|
||||
/// Il ne reste que les COPIES. Le moteur 2D est un moteur SÉPARÉ du 3D : aucune sonde de ce dossier ne
|
||||
/// le regarde, puisqu'elles sont toutes accrochées aux draws.
|
||||
///
|
||||
/// CE QU'ON MESURE. Les rectangles source et destination de chaque blit, et surtout les cas anormaux :
|
||||
/// coordonnée négative, région qui dépasse la texture, source et destination de tailles différentes.
|
||||
/// L'analyse forensique avait désigné TextureCopy.Blit comme le seul mécanisme de copie capable de
|
||||
/// mordre les DEUX bords à la fois (clamps indépendants sur la source et sur la destination, avec des
|
||||
/// coordonnées qui peuvent devenir négatives) -- c'est exactement la géométrie de l'artefact.
|
||||
/// </summary>
|
||||
static class MvppTwodProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TWOD_PROBE") == "1";
|
||||
|
||||
private static bool _announced;
|
||||
private static long _nextLogMs;
|
||||
private static int _blits;
|
||||
private static int _negatives; // une coordonnée source ou destination est négative
|
||||
private static int _resized; // la région source n'a pas la même taille que la destination
|
||||
private static readonly Dictionary<string, int> _shapes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Battement branché sur la fin d'image, appelé depuis Gpu/Window.Present. Indispensable : la
|
||||
/// synthèse ci-dessous ne s'écrivait qu'au premier blit, donc "aucune ligne dans le log" ne
|
||||
/// distinguait pas "aucun blit 2D" de "sonde jamais armée". Ici la ligne sort même à zéro.
|
||||
/// </summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now < _nextLogMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_nextLogMs = now + 2000;
|
||||
_announced = true;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP TWOD: {_blits} blits, {_negatives} hors bornes, {_resized} redimensionnes, {_shapes.Count} formes distinctes.");
|
||||
|
||||
int shown = 0;
|
||||
|
||||
foreach (KeyValuePair<string, int> kv in _shapes)
|
||||
{
|
||||
if (shown++ >= 8)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP TWOD: x{kv.Value} {kv.Key}");
|
||||
}
|
||||
|
||||
_blits = 0;
|
||||
_negatives = 0;
|
||||
_resized = 0;
|
||||
_shapes.Clear();
|
||||
}
|
||||
|
||||
public static void OnBlit(
|
||||
int srcX1, int srcY1, int srcX2, int srcY2,
|
||||
int dstX1, int dstY1, int dstX2, int dstY2,
|
||||
int srcW, int srcH, int dstW, int dstH)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_announced)
|
||||
{
|
||||
_announced = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
"MVPP TWOD: ON -- recensement des blits du moteur 2D, lecture seule, 1 ligne toutes les 2 s.");
|
||||
}
|
||||
|
||||
_blits++;
|
||||
|
||||
if (srcX1 < 0 || srcY1 < 0 || dstX1 < 0 || dstY1 < 0 || srcX2 > srcW || srcY2 > srcH)
|
||||
{
|
||||
_negatives++;
|
||||
}
|
||||
|
||||
if ((srcX2 - srcX1) != (dstX2 - dstX1) || (srcY2 - srcY1) != (dstY2 - dstY1))
|
||||
{
|
||||
_resized++;
|
||||
}
|
||||
|
||||
// Une forme = la géométrie complète du blit. C'est elle qui dira si XC2 recopie des bandes
|
||||
// latérales, et à quelles abscisses exactes.
|
||||
string shape = $"src[{srcX1},{srcY1}->{srcX2},{srcY2}]/{srcW}x{srcH} dst[{dstX1},{dstY1}->{dstX2},{dstY2}]/{dstW}x{dstH}";
|
||||
_shapes[shape] = _shapes.TryGetValue(shape, out int n) ? n + 1 : 1;
|
||||
|
||||
// Le log periodique vit dans OnPresent : il doit sortir meme quand il n'y a aucun blit.
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP TWOD: desactive apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
||||
|
||||
FormatInfo dstCopyTextureFormat = dstCopyTexture.Format.Convert();
|
||||
|
||||
// Dernier étage jamais instrumenté du dossier XC2 : le moteur 2D. Lecture seule, gated.
|
||||
MvppTwodProbe.OnBlit(
|
||||
srcX1, srcY1, srcX2, srcY2,
|
||||
dstX1, dstY1, dstX2, dstY2,
|
||||
srcCopyTexture.Width, srcCopyTexture.Height,
|
||||
dstCopyTexture.Width, dstCopyTexture.Height);
|
||||
|
||||
bool canDirectCopy = GraphicsConfig.Fast2DCopy &&
|
||||
srcX2 == dstX2 && srcY2 == dstY2 &&
|
||||
IsDataCompatible(srcCopyTexture, dstCopyTexture, srcCopyTextureFormat, dstCopyTextureFormat) &&
|
||||
@@ -396,6 +403,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
|
||||
dstRegion.X1, dstRegion.Y1, dstRegion.X2, dstRegion.Y2,
|
||||
linearFilter ? "linear" : "point");
|
||||
|
||||
Image.MvppTwinXferProbe.OnCopy("2D-BLIT", srcTexture, dstTexture); // [TWINXFER] read-only, self-gated
|
||||
|
||||
srcTexture.HostTexture.CopyTo(dstTexture.HostTexture, srcRegion, dstRegion, linearFilter);
|
||||
|
||||
dstTexture.SignalModified();
|
||||
|
||||
@@ -119,6 +119,28 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
_totalSize += texture.Size;
|
||||
|
||||
// Report the cache state on a timer, NOT only when an eviction happens. First version
|
||||
// of this probe logged from inside RemoveLeastUsedTexture, so a run with zero evictions
|
||||
// produced zero lines, and "no line" meant either "no eviction" or "probe not running".
|
||||
// Same trap as the birth-clear switch earlier today: a diagnostic you cannot SEE is
|
||||
// worth nothing. Now the line always comes, and the eviction count is a field in it.
|
||||
if (CacheProbeEnabled)
|
||||
{
|
||||
long tickMs = System.Environment.TickCount64;
|
||||
|
||||
if (tickMs - _probeLastLogMs >= 5000)
|
||||
{
|
||||
_probeLastLogMs = tickMs;
|
||||
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"CACHEPROBE: cache {_textures.Count}/{MaxCapacity} textures, " +
|
||||
$"{_totalSize / (1024 * 1024)} Mo / {_maxCacheMemoryUsage / (1024 * 1024)} Mo, " +
|
||||
$"{_probeEvictions} evictions depuis le dernier point.");
|
||||
|
||||
_probeEvictions = 0;
|
||||
}
|
||||
}
|
||||
|
||||
texture.IncrementReferenceCount();
|
||||
texture.CacheNode = _textures.AddLast(texture);
|
||||
|
||||
@@ -162,10 +184,30 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <summary>
|
||||
/// Removes the least used texture from the cache.
|
||||
/// </summary>
|
||||
// [CACHEPROBE, 21/07] Alex's decisive observation on the Xenoblade 2 artefact: "it is not
|
||||
// there at the start, it ACCUMULATES -- the more I turn the camera, the more it comes".
|
||||
// A defect that grows with time is a different family from the one-off race I had been
|
||||
// chasing all day, and the obvious thing that grows while turning the camera is the number
|
||||
// of loaded textures. This cache is bounded (2048 entries, or a memory budget) and starts
|
||||
// EVICTING the least recently used once full. Empty cache at first, no eviction, no
|
||||
// artefact; keep turning, cache fills, evictions begin, and if one of them takes a texture
|
||||
// that is still needed the game draws something real in the wrong place. Read-only: counts
|
||||
// evictions and reports the cache state, changes nothing.
|
||||
internal static readonly bool CacheProbeEnabled =
|
||||
System.Environment.GetEnvironmentVariable("RYUJINX_CACHE_PROBE") == "1";
|
||||
|
||||
private int _probeEvictions;
|
||||
private long _probeLastLogMs;
|
||||
|
||||
private void RemoveLeastUsedTexture()
|
||||
{
|
||||
Texture oldestTexture = _textures.First.Value;
|
||||
|
||||
if (CacheProbeEnabled)
|
||||
{
|
||||
_probeEvictions++;
|
||||
}
|
||||
|
||||
_totalSize -= oldestTexture.Size;
|
||||
|
||||
if (!oldestTexture.CheckModified(false))
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [BUILDERIN] (RYUJINX_BUILDERIN=1, inert unless set). Read-only, BACKEND-NEUTRAL.
|
||||
///
|
||||
/// Why this exists. The offline bench (journal 226) runs the builder's two translations
|
||||
/// outside the game on byte-identical inputs and finds them equivalent to well under one
|
||||
/// 10-bit code on the real target. That contradicts the elimination chain of (222), which
|
||||
/// concluded the divergence must live in the builder's translation because "inputs equal,
|
||||
/// constants equal, outputs divergent". One of those premises is therefore wrong -- and the
|
||||
/// two that were established indirectly are the input identity (measured on dump FILES whose
|
||||
/// mapping to the shader's samplers was inferred, never read) and the output identity.
|
||||
///
|
||||
/// So this probe stops inferring and reads it: for the builder draw only, which surface is
|
||||
/// actually bound to each sampler (guest VA, format, size, handle -> fp_t_tcb_<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
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_BUILDERIN") == "1";
|
||||
|
||||
// The whole DoF/motion chain, not just the builder: (229) showed the builder writes a
|
||||
// single 320x180 target and does NOT produce the 64x36 maps, so its consumers are now
|
||||
// the suspects - and they read their OWN constants, which is why the bench found them
|
||||
// dead when fed the builder's.
|
||||
private static readonly (ulong Addr, string Name)[] Passes =
|
||||
{
|
||||
(0x1000AD730UL, "builder"),
|
||||
(0x1000B4530UL, "down64"),
|
||||
(0x1000C2F30UL, "coc64"),
|
||||
(0x1000AE430UL, "bokeh_fs"),
|
||||
(0x100071F30UL, "pingpong"),
|
||||
};
|
||||
|
||||
private static readonly HashSet<ulong> _unknown = new();
|
||||
private static readonly HashSet<string> _census = new();
|
||||
private static int _censusLines;
|
||||
|
||||
/// <summary>Cheap identity of the draw's first colour target, for throttling.</summary>
|
||||
private static int TargetKey(TextureManager texMgr)
|
||||
{
|
||||
if (texMgr == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Texture t = texMgr.GetColorTarget(0);
|
||||
|
||||
return t == null ? 0 : (t.Info.Width * 8191) ^ t.Info.Height;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CensusTick(TextureManager texMgr, ulong fsAddr)
|
||||
{
|
||||
if (texMgr == null || _censusLines >= 400)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
Texture t;
|
||||
|
||||
try
|
||||
{
|
||||
t = texMgr.GetColorTarget(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (t == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = $"{fsAddr:X}|rt{i}|{t.Info.Width}x{t.Info.Height}|{t.Info.FormatInfo.Format}";
|
||||
|
||||
if (!_census.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_censusLines++;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[CENSUS] fs=0x{fsAddr:X} rt{i} {t.Info.Width}x{t.Info.Height} {t.Info.FormatInfo.Format}");
|
||||
}
|
||||
}
|
||||
private static string _shape = "?";
|
||||
|
||||
/// <summary>The 1280x720 twin the builder sampled on its last draw (journal 242).</summary>
|
||||
public static Texture LastTwin { get; private set; }
|
||||
|
||||
/// <summary>True when the draw writes one of the small DoF map shapes.</summary>
|
||||
private static bool MatchesUnknownTarget(TextureManager texMgr, out string shape)
|
||||
{
|
||||
shape = null;
|
||||
|
||||
if (texMgr == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
Texture t;
|
||||
|
||||
try
|
||||
{
|
||||
t = texMgr.GetColorTarget(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (t == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int w = t.Info.Width, h = t.Info.Height;
|
||||
|
||||
if ((w == 64 && (h == 36 || h == 180)) || (w == 320 && h == 36))
|
||||
{
|
||||
shape = $"{w}x{h} {t.Info.FormatInfo.Format}";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string PassName(ulong addr)
|
||||
{
|
||||
foreach ((ulong a, string n) in Passes)
|
||||
{
|
||||
if (a == addr)
|
||||
{
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order)
|
||||
private const int CbufSlot = 3; // fp_c3
|
||||
|
||||
// The vec4 indices the passes read, from its decompiled source:
|
||||
// [0] output scale / blend / strength / falloff, [5][6][7][8] the reprojection columns,
|
||||
// [11] the NDC->pixel scale. Anything else in the buffer is not consumed by this shader.
|
||||
private static readonly int[] WantedC3 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13 };
|
||||
|
||||
private struct Bound
|
||||
{
|
||||
public ulong Va;
|
||||
public string Fmt;
|
||||
public int W;
|
||||
public int H;
|
||||
public int Handle;
|
||||
public int Binding;
|
||||
public string Filter;
|
||||
public int Levels;
|
||||
public Texture Tex;
|
||||
}
|
||||
|
||||
private static readonly List<Bound> _inputs = new();
|
||||
private static bool _armed;
|
||||
private static bool _failLogged;
|
||||
|
||||
// v2. The first cut logged once and fired at ~8 s - the title screen, where the game
|
||||
// has motion blur off (fp_c3[0].z = 0 kills the whole camera branch). The bench fed
|
||||
// with those values is structurally dead, so a one-shot log is worthless. Now it
|
||||
// re-reports whenever the values actually CHANGE, capped so the log stays readable.
|
||||
private const int MaxReports = 40;
|
||||
private static int _reports;
|
||||
// v5. A single global throttle meant one sampled draw every 250 ms for the WHOLE
|
||||
// chain, so the busiest passes ate every slot and coc64/bokeh/pingpong were never
|
||||
// sampled at all (23rd trap). The throttle is per pass now.
|
||||
private static readonly Dictionary<ulong, long> _lastBeat = new();
|
||||
private static readonly Dictionary<string, string> _lastSig = new();
|
||||
private static readonly Dictionary<string, int> _perConfig = new();
|
||||
private static long _builderDraws;
|
||||
private static TextureManager _texMgr;
|
||||
|
||||
public static void OnInput(Texture texture, ShaderStage stage, int handle, int binding, Sampler sampler)
|
||||
{
|
||||
if (!Enabled || texture == null || stage != ShaderStage.Fragment)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_inputs)
|
||||
{
|
||||
if (_inputs.Count >= 64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextureInfo info = texture.Info;
|
||||
ulong va;
|
||||
|
||||
try
|
||||
{
|
||||
va = texture.Range.GetSubRange(0).Address;
|
||||
}
|
||||
catch
|
||||
{
|
||||
va = 0;
|
||||
}
|
||||
|
||||
_inputs.Add(new Bound
|
||||
{
|
||||
Va = va,
|
||||
Fmt = info.FormatInfo.Format.ToString(),
|
||||
W = info.Width,
|
||||
H = info.Height,
|
||||
Handle = handle,
|
||||
Binding = binding,
|
||||
// down64's second invocation reduces 320 -> 64 horizontally with NO
|
||||
// horizontal taps: that factor-5 reduction rests entirely on the sampler.
|
||||
// The address mode was checked long ago and matches the console; the FILTER
|
||||
// never was, on either backend.
|
||||
Levels = texture.Info.Levels,
|
||||
Tex = texture,
|
||||
Filter = sampler == null
|
||||
? "sampler=null"
|
||||
: $"min={sampler.ProbeMinFilter} mag={sampler.ProbeMagFilter} " +
|
||||
$"wrap={sampler.ProbeAddressU}/{sampler.ProbeAddressV}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once per draw after bindings are committed. Logs the builder's full input
|
||||
/// identity and constants once, then keeps quiet; always clears the per-draw list so a
|
||||
/// non-builder draw cannot leak its inputs into the next one.
|
||||
/// </summary>
|
||||
public static void OnDraw(GpuChannel channel, TextureManager texMgr, ulong fsAddr)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_inputs)
|
||||
{
|
||||
try
|
||||
{
|
||||
// CENSUS: every (shader, target shape) pair seen, deduplicated. After the
|
||||
// 2D engine reported ZERO blits, "no draw writes the 64x36" rests entirely
|
||||
// on my shape detection - so this stops trusting it and enumerates instead.
|
||||
CensusTick(texMgr, fsAddr);
|
||||
|
||||
string pass = PassName(fsAddr);
|
||||
|
||||
if (pass == null)
|
||||
{
|
||||
// The chain read in (232) has a HOLE: down64 emits 64x180 but coc64 and
|
||||
// the bokeh consume 64x36, so an unlisted pass performs the vertical
|
||||
// reduction - and that pass produces the very map whose z-channel
|
||||
// variation Alex proved to be the trigger. Catch it by its target
|
||||
// shape rather than by an address we do not have yet.
|
||||
if (!MatchesUnknownTarget(texMgr, out string shape))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_unknown.Add(fsAddr))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[BUILDERIN] *** UNLISTED PASS writing {shape} : guest fs = 0x{fsAddr:X} ***");
|
||||
}
|
||||
|
||||
pass = $"unlisted_{fsAddr:X}";
|
||||
}
|
||||
|
||||
_builderDraws++;
|
||||
|
||||
// Latch the twin from THIS draw's inputs, and only when this draw is the
|
||||
// builder - otherwise the last unrelated pass wins and the content probe
|
||||
// ends up measuring a different texture entirely (29th trap).
|
||||
if (fsAddr == Passes[0].Addr)
|
||||
{
|
||||
foreach (Bound bb in _inputs)
|
||||
{
|
||||
if (bb.W == 1280 && bb.H == 720)
|
||||
{
|
||||
LastTwin = bb.Tex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_armed)
|
||||
{
|
||||
_armed = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[BUILDERIN] armed: tracking the DoF/motion chain (builder, down64, coc64, bokeh_fs, pingpong)");
|
||||
}
|
||||
|
||||
// Sample at most ~4x/s: reading guest memory on every builder draw would
|
||||
// cost far more than the answer is worth.
|
||||
// The throttle was keyed on the shader alone, so down64's two invocations
|
||||
// (64x180 then 64x36) shared one 250 ms slot and the vertical one was never
|
||||
// sampled. Key it on shader + target shape: same shader, two targets, two
|
||||
// independent slots.
|
||||
long now = Environment.TickCount64;
|
||||
ulong beatKey = fsAddr ^ ((ulong)TargetKey(texMgr) << 40);
|
||||
_lastBeat.TryGetValue(beatKey, out long beat);
|
||||
|
||||
if (beat != 0 && now - beat < 250)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastBeat[beatKey] = now;
|
||||
|
||||
_texMgr = texMgr;
|
||||
string signature = BuildSignature(channel, out string[] lines);
|
||||
|
||||
// 24th trap: budgeting per PASS let constant churn eat all eight slots, so a
|
||||
// second invocation of the same shader with a DIFFERENT target could never be
|
||||
// reported. The key is the pass plus its I/O shape, so every distinct
|
||||
// configuration gets its own budget.
|
||||
string config = pass + "|" + _shape;
|
||||
|
||||
_lastSig.TryGetValue(config, out string prev);
|
||||
_perConfig.TryGetValue(config, out int seen);
|
||||
|
||||
if (signature == prev || seen >= 4 || _reports >= MaxReports)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool first = prev == null;
|
||||
_lastSig[config] = signature;
|
||||
_perConfig[config] = seen + 1;
|
||||
_reports++;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
first
|
||||
? $"[BUILDERIN] --- {pass} [{_shape}] state #1 ---"
|
||||
: $"[BUILDERIN] --- {pass} [{_shape}] state #{seen + 1} CHANGED ---");
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, line);
|
||||
}
|
||||
|
||||
if (_reports == MaxReports)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[BUILDERIN] report cap reached - further changes are no longer printed");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_failLogged)
|
||||
{
|
||||
_failLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[BUILDERIN] read FAILED: {ex.Message}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_inputs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the draw's full input state as log lines, and returns a signature that
|
||||
/// changes exactly when any of those values changes.
|
||||
/// </summary>
|
||||
private static string BuildSignature(GpuChannel channel, out string[] lines)
|
||||
{
|
||||
List<string> outLines = new();
|
||||
StringBuilder sig = new();
|
||||
StringBuilder shape = new();
|
||||
|
||||
foreach (Bound b in _inputs)
|
||||
{
|
||||
outLines.Add(
|
||||
$"[BUILDERIN] sampler fp_t_tcb_{b.Handle:X} (binding {b.Binding}) " +
|
||||
$"<- VA 0x{b.Va:X} {b.W}x{b.H} {b.Fmt} mips={b.Levels} [{b.Filter}]");
|
||||
sig.Append($"{b.Handle:X}:{b.Va:X}:{b.W}x{b.H}:{b.Levels}:{b.Filter};");
|
||||
shape.Append($"{b.W}x{b.H},");
|
||||
}
|
||||
|
||||
if (_inputs.Count == 0)
|
||||
{
|
||||
outLines.Add("[BUILDERIN] no sampled inputs recorded for this draw");
|
||||
sig.Append("noinputs;");
|
||||
}
|
||||
|
||||
AppendTargets(_texMgr, outLines, sig, shape);
|
||||
AppendCbuf(channel, CbufSlot, "fp_c3", WantedC3, outLines, sig);
|
||||
AppendCbuf(channel, 1, "fp_c1", new[] { 0 }, outLines, sig);
|
||||
|
||||
lines = outLines.ToArray();
|
||||
_shape = shape.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)
|
||||
{
|
||||
if (texMgr == null)
|
||||
{
|
||||
lines.Add("[BUILDERIN] no texture manager for this draw");
|
||||
sig.Append("notexmgr;");
|
||||
return;
|
||||
}
|
||||
|
||||
int found = 0;
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
Texture t;
|
||||
|
||||
try
|
||||
{
|
||||
t = texMgr.GetColorTarget(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (t == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
found++;
|
||||
TextureInfo info = t.Info;
|
||||
ulong va;
|
||||
|
||||
try
|
||||
{
|
||||
va = t.Range.GetSubRange(0).Address;
|
||||
}
|
||||
catch
|
||||
{
|
||||
va = 0;
|
||||
}
|
||||
|
||||
lines.Add($"[BUILDERIN] OUTPUT rt{i} -> VA 0x{va:X} {info.Width}x{info.Height} {info.FormatInfo.Format}");
|
||||
sig.Append($"rt{i}:{va:X}:{info.Width}x{info.Height}:{info.FormatInfo.Format};");
|
||||
shape.Append($"{info.Width}x{info.Height}>");
|
||||
}
|
||||
|
||||
if (found == 0)
|
||||
{
|
||||
lines.Add("[BUILDERIN] OUTPUT: no colour target reported");
|
||||
sig.Append("nort;");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendCbuf(
|
||||
GpuChannel channel,
|
||||
int slot,
|
||||
string name,
|
||||
int[] indices,
|
||||
List<string> lines,
|
||||
StringBuilder sig)
|
||||
{
|
||||
uint useMask = channel.BufferManager.GetGraphicsUniformBufferUseMask(FragmentStageIndex);
|
||||
|
||||
if ((useMask & (1u << slot)) == 0)
|
||||
{
|
||||
lines.Add($"[BUILDERIN] {name} (slot {slot}) not bound for this draw");
|
||||
sig.Append($"{name}:unbound;");
|
||||
return;
|
||||
}
|
||||
|
||||
ulong addr = channel.BufferManager.GetGraphicsUniformBufferAddress(FragmentStageIndex, slot);
|
||||
|
||||
if (addr == 0 || addr == ulong.MaxValue)
|
||||
{
|
||||
lines.Add($"[BUILDERIN] {name} (slot {slot}) has no address");
|
||||
sig.Append($"{name}:noaddr;");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (int i in indices)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append($"[BUILDERIN] {name}.data[{i}] =");
|
||||
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
float f = BitConverter.Int32BitsToSingle(
|
||||
channel.MemoryManager.Physical.Read<int>(addr + (ulong)(i * 16 + c * 4)));
|
||||
|
||||
sb.Append($" {f:G9}");
|
||||
sig.Append($"{f:G9},");
|
||||
}
|
||||
|
||||
lines.Add(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Recensement des opérations INTERNES du cache de textures (RYUJINX_CACHEOPS=1). READ-ONLY, off par défaut.
|
||||
///
|
||||
/// DERNIER ÉTAGE DEBOUT du dossier XC2 au 21/07. Tous les autres sont tombés sur mesure :
|
||||
/// - présentation / blit final / HDR / swapchain : paire MÊME-IMAGE guest+swapchain ;
|
||||
/// - états de rastérisation : 6 mesures, ~90 000 draws, 0 anomalie ;
|
||||
/// - appels de dessin : images entières capturées draw par draw, scène PROPRE au dernier draw
|
||||
/// alors que l'image présentée est détruite ;
|
||||
/// - compute : 0 dispatch par image ;
|
||||
/// - moteur 2D : 0 blit sur 61 relevés.
|
||||
/// Le jeu produit donc une image propre et ne demande AUCUNE copie. Ce qui reste ne peut être que ce
|
||||
/// que l'émulateur fait de lui-même : recopier entre textures qui se chevauchent en mémoire,
|
||||
/// resynchroniser depuis la mémoire invitée, créer des vues partielles.
|
||||
///
|
||||
/// /!\ L'ancien verdict du journal « cache de textures : 363/2048 entrées, 0 éviction » ne mesurait QUE
|
||||
/// les évictions. Les copies de dépendance et SynchronizeMemory n'ont JAMAIS été comptées.
|
||||
///
|
||||
/// Le battement est branché sur la FIN D'IMAGE, jamais sur l'événement mesuré : deux fois ce soir un
|
||||
/// compteur qui ne s'annonçait qu'en cas de trouvaille a rendu un run inexploitable, parce que
|
||||
/// « aucune ligne » ne distinguait pas « rien trouvé » de « jamais armé ».
|
||||
/// </summary>
|
||||
static class MvppCacheProbe
|
||||
{
|
||||
private static bool _enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_CACHEOPS") == "1";
|
||||
|
||||
private static long _nextLogMs;
|
||||
private static int _copyDeps; // copies de dépendance créées entre textures qui se chevauchent
|
||||
private static int _syncs; // APPELS à SynchronizeMemory (la plupart ressortent sans rien faire)
|
||||
private static int _mismatch; // ... dont les deux textures n'ont PAS la même largeur
|
||||
private static int _realSyncs; // appels qui rechargent VRAIMENT des données (texture sale)
|
||||
private static int _realNoData; // ... dont la texture n'avait pas encore de données (SynchronizeFull)
|
||||
private static readonly Dictionary<string, int> _realShapes = new(); // formes qui rechargent, par fréquence
|
||||
private static readonly Dictionary<string, int> _pairs = new();
|
||||
|
||||
/// <summary>Un appel qui recharge RÉELLEMENT des données depuis la mémoire invitée.</summary>
|
||||
public static void OnRealSync(int w, int h, Format fmt, bool hadData)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_realSyncs++;
|
||||
|
||||
if (!hadData)
|
||||
{
|
||||
_realNoData++;
|
||||
}
|
||||
|
||||
string key = $"{w}x{h} {fmt}";
|
||||
_realShapes[key] = _realShapes.TryGetValue(key, out int n) ? n + 1 : 1;
|
||||
}
|
||||
|
||||
/// <summary>Une copie de dépendance vient d'être créée entre deux textures qui se chevauchent.</summary>
|
||||
public static void OnCopyDependency(Texture a, Texture b)
|
||||
{
|
||||
if (!_enabled || a == null || b == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_copyDeps++;
|
||||
|
||||
bool diff = a.Info.Width != b.Info.Width || a.Info.Height != b.Info.Height ||
|
||||
a.Info.FormatInfo.Format != b.Info.FormatInfo.Format;
|
||||
|
||||
if (diff)
|
||||
{
|
||||
_mismatch++;
|
||||
}
|
||||
|
||||
// Une largeur ou un format différent entre deux textures qui partagent la MÊME mémoire,
|
||||
// c'est le mécanisme canonique qui recopie du contenu valide au mauvais endroit : une ligne
|
||||
// de l'une tombe au milieu d'une ligne de l'autre.
|
||||
string key = $"{a.Info.Width}x{a.Info.Height} {a.Info.FormatInfo.Format} <-> {b.Info.Width}x{b.Info.Height} {b.Info.FormatInfo.Format}{(diff ? " <<< DIFFERENT" : "")}";
|
||||
_pairs[key] = _pairs.TryGetValue(key, out int n) ? n + 1 : 1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_enabled = false;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"MVPP CACHEOPS: desactive apres erreur: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Une texture vient d'être resynchronisée depuis la mémoire invitée.</summary>
|
||||
public static void OnSynchronize()
|
||||
{
|
||||
if (_enabled)
|
||||
{
|
||||
_syncs++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Battement, branché sur la fin d'image. Sort même quand tous les compteurs sont à zéro.</summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now < _nextLogMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_nextLogMs = now + 2000;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"MVPP CACHEOPS: {_copyDeps} copies dep ({_mismatch} DIFFERENTES) | {_syncs} appels sync dont " +
|
||||
$"{_realSyncs} RECHARGENT ({_realNoData} sans donnees prealables) sur {_realShapes.Count} formes.");
|
||||
|
||||
int shown = 0;
|
||||
|
||||
foreach (KeyValuePair<string, int> kv in _realShapes)
|
||||
{
|
||||
if (shown++ >= 10)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, $"MVPP CACHEOPS: recharge x{kv.Value} {kv.Key}");
|
||||
}
|
||||
|
||||
_copyDeps = 0;
|
||||
_syncs = 0;
|
||||
_mismatch = 0;
|
||||
_realSyncs = 0;
|
||||
_realNoData = 0;
|
||||
_pairs.Clear();
|
||||
_realShapes.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [CONSTDIFF] (RYUJINX_CONSTDIFF=1, inert unless set). Journal 221, read-only,
|
||||
/// BACKEND-NEUTRAL (lives in the Gpu layer: the same probe serves GL and Vulkan).
|
||||
///
|
||||
/// 219 proved the builder transforms comparable texture inputs differently per backend;
|
||||
/// 220-221 proved its MUFU precision is a lever but the calibration sweep dead-ends
|
||||
/// (only full quantization is clean). The last ingredient never compared: the builder's
|
||||
/// CONSTANTS at pass time -- fp_c3[5..8] is the PREVIOUS-FRAME reprojection matrix. A
|
||||
/// stale matrix is invisible at rest (prev==cur) and wrong exactly in motion: the
|
||||
/// artifact's oldest signature. Logs, once per builder draw, one row of that matrix:
|
||||
/// a value REPEATING across consecutive frames during motion = staleness caught.
|
||||
/// </summary>
|
||||
static class MvppConstDiffProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_CONSTDIFF") == "1";
|
||||
|
||||
private const ulong BuilderFsAddress = 0x1000AD730UL;
|
||||
private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order)
|
||||
private const int CbufSlot = 3; // fp_c3
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static bool _failLogged;
|
||||
private static float _lastA;
|
||||
private static float _lastB;
|
||||
private static int _holdCount;
|
||||
private static long _lines;
|
||||
private static long _draws;
|
||||
private static long _changes;
|
||||
private static int _maxHold;
|
||||
private static long _heartbeatMs;
|
||||
private static readonly float[] _slotLast = new float[8];
|
||||
private static readonly long[] _slotChanges = new long[8];
|
||||
private static readonly int[] _slotHold = new int[8];
|
||||
private static readonly int[] _slotMaxHold = new int[8];
|
||||
|
||||
public static void OnDraw(GpuChannel channel, ulong fsAddr)
|
||||
{
|
||||
if (!Enabled || fsAddr != BuilderFsAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[CONSTDIFF] armed: tracing the builder's fp_c3 prev-frame matrix per draw (repeat across frames in motion = stale)");
|
||||
|
||||
for (int st = 0; st < 5; st++)
|
||||
{
|
||||
uint mask;
|
||||
|
||||
try
|
||||
{
|
||||
mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(st);
|
||||
}
|
||||
catch
|
||||
{
|
||||
mask = 0xDEAD;
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[CONSTDIFF] stage {st}: cbuf use mask = 0x{mask:X}");
|
||||
}
|
||||
}
|
||||
|
||||
_draws++;
|
||||
|
||||
try
|
||||
{
|
||||
uint useMask = channel.BufferManager.GetGraphicsUniformBufferUseMask(FragmentStageIndex);
|
||||
|
||||
for (int slot = 0; slot < 8; slot++)
|
||||
{
|
||||
if ((useMask & (1u << slot)) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// GetGraphicsUniformBufferAddress renvoie une adresse DEJA physique
|
||||
// (Range traduite) : lecture directe, PAS de Translate.
|
||||
ulong addr = channel.BufferManager.GetGraphicsUniformBufferAddress(FragmentStageIndex, slot);
|
||||
|
||||
if (addr == 0 || addr == ulong.MaxValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float a = BitConverter.Int32BitsToSingle(channel.MemoryManager.Physical.Read<int>(addr + 5 * 16));
|
||||
|
||||
if (_slotLast[slot] != a)
|
||||
{
|
||||
_slotChanges[slot]++;
|
||||
_slotLast[slot] = a;
|
||||
_slotHold[slot] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_slotHold[slot]++;
|
||||
if (_slotHold[slot] > _slotMaxHold[slot])
|
||||
{
|
||||
_slotMaxHold[slot] = _slotHold[slot];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_failLogged)
|
||||
{
|
||||
_failLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[CONSTDIFF] read FAILED: {ex.Message}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _heartbeatMs >= 5000)
|
||||
{
|
||||
_heartbeatMs = now;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[CONSTDIFF/HB ~5s] builderDraws={_draws}");
|
||||
|
||||
for (int slot = 0; slot < 8; slot++)
|
||||
{
|
||||
if (_slotChanges[slot] > 0 || _slotMaxHold[slot] > 0)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[CONSTDIFF/HB ~5s] slot={slot} draws={_draws} changes={_slotChanges[slot]} maxHold={_slotMaxHold[slot] + 1} last={_slotLast[slot]:G9}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [MVHASH] (RYUJINX_MVHASH=1, inert unless set). Read-only, BACKEND-NEUTRAL.
|
||||
///
|
||||
/// Journal 241 closed the shader family: every stage of the DoF chain - builder, down64,
|
||||
/// pingpong, bokeh fragment AND the bokeh geometry shader - is bit-identical between OpenGL
|
||||
/// and Vulkan when fed identical inputs, proven on an offline bench. And 229 proved the
|
||||
/// builder's inputs are identical in IDENTITY: same guest address, same format, same size,
|
||||
/// same constants. What was never compared is their CONTENT.
|
||||
///
|
||||
/// By elimination the content must differ, and it is produced upstream by the ~730 draws
|
||||
/// that write the motion-vector twin. This measures that content directly, at the moment
|
||||
/// the builder reads it.
|
||||
///
|
||||
/// Comparing two runs pose-for-pose is impossible by hand, so this does NOT rely on a hash
|
||||
/// matching. It reports pose-robust occupancy statistics - what fraction of the twin carries
|
||||
/// a written motion vector rather than its clear value - sampled over the whole run. A
|
||||
/// backend where a class of geometry fails to export its vectors shows a systematically
|
||||
/// different occupancy, whatever the camera is doing. That is the (217) observation, this
|
||||
/// time measured over a distribution instead of a single snapshot (the mistake 218 caught).
|
||||
/// </summary>
|
||||
static class MvppContentProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVHASH") == "1";
|
||||
|
||||
private const ulong BuilderFsAddress = 0x1000AD730UL;
|
||||
private const int SampleIntervalMs = 500;
|
||||
private const int MaxSamples = 60;
|
||||
|
||||
private static long _lastMs;
|
||||
private static int _samples;
|
||||
private static bool _failed;
|
||||
private static readonly List<double> _occupancy = new();
|
||||
private static readonly List<double> _meanAbs = new();
|
||||
|
||||
public static void OnBuilderDraw(TextureManager texMgr, ulong fsAddr)
|
||||
{
|
||||
if (!Enabled || fsAddr != BuilderFsAddress || _failed || _samples >= MaxSamples)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastMs < SampleIntervalMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastMs = now;
|
||||
|
||||
try
|
||||
{
|
||||
Sample(texMgr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_failed = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[MVHASH] read FAILED: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Sample(TextureManager texMgr)
|
||||
{
|
||||
Texture twin = MvppBuilderInProbe.LastTwin;
|
||||
|
||||
if (twin == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> bytes = twin.HostTexture.GetData().Get();
|
||||
|
||||
if (bytes.Length < 4)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int words = bytes.Length / 4;
|
||||
long written = 0;
|
||||
double sumAbs = 0;
|
||||
ulong hash = 14695981039346656037UL;
|
||||
|
||||
// The twin is A2B10G10R10: R and G carry sqrt-encoded magnitude, the 2-bit alpha the
|
||||
// signs. A texel still at its clear value has R and G at zero, so "occupancy" is the
|
||||
// share of texels some draw actually wrote a vector into.
|
||||
for (int i = 0; i < words; i++)
|
||||
{
|
||||
uint w = (uint)(bytes[i * 4] |
|
||||
(bytes[i * 4 + 1] << 8) |
|
||||
(bytes[i * 4 + 2] << 16) |
|
||||
(bytes[i * 4 + 3] << 24));
|
||||
|
||||
uint r = w & 1023;
|
||||
uint g = (w >> 10) & 1023;
|
||||
|
||||
if (r != 0 || g != 0)
|
||||
{
|
||||
written++;
|
||||
sumAbs += (r + g) / 1023.0;
|
||||
}
|
||||
|
||||
if ((i & 63) == 0)
|
||||
{
|
||||
hash = (hash ^ w) * 1099511628211UL;
|
||||
}
|
||||
}
|
||||
|
||||
double occ = 100.0 * written / words;
|
||||
double mean = written > 0 ? sumAbs / written : 0.0;
|
||||
|
||||
_occupancy.Add(occ);
|
||||
_meanAbs.Add(mean);
|
||||
_samples++;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVHASH] sample {_samples,2}: twin {twin.Info.Width}x{twin.Info.Height} " +
|
||||
$"occupancy={occ:F3}% meanMag={mean:F5} hash=0x{hash:X16}");
|
||||
|
||||
if (_samples % 10 == 0)
|
||||
{
|
||||
Report();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Report()
|
||||
{
|
||||
if (_occupancy.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<double> o = new(_occupancy);
|
||||
o.Sort();
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVHASH/SUMMARY] n={o.Count} occupancy min={o[0]:F3}% " +
|
||||
$"median={o[o.Count / 2]:F3}% max={o[^1]:F3}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [DOFPROBE, READ-ONLY, default OFF] Grounding probe for the XC2 depth-of-field bokeh
|
||||
/// artifact (a temporal history buffer that diverges under camera motion and accumulates).
|
||||
/// Nsight located the bokeh pass writing a low-res 512x288 target (event 11872) and reading
|
||||
/// R8G8B8A8 inputs (candidates Image_4615 / Image_4601). This probe reproduces that mapping
|
||||
/// emulator-side: when a draw's colour render target is the low-res DoF buffer, it logs each
|
||||
/// distinct input texture (format, size, guest VA) plus how many DoF draws it has fed. The
|
||||
/// history/feedback buffer is the input that persists on (nearly) every DoF draw; its
|
||||
/// format+size+VA signature is what the later 8-bit -> 16f experiment must target.
|
||||
/// A separate census lists every small render-target size seen, so if the real DoF size
|
||||
/// differs from 512x288 we still learn it in a single run instead of logging nothing.
|
||||
/// Enable with RYUJINX_DOF_PROBE=1. Never modifies bindings, textures, samplers or targets.
|
||||
/// </summary>
|
||||
static class MvppDofProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_DOF_PROBE") == "1";
|
||||
|
||||
// Nsight-confirmed low-res DoF target on XC2 with res-scale 1 (Image_4637, event 11872).
|
||||
private const int DofWidth = 512;
|
||||
private const int DofHeight = 288;
|
||||
|
||||
// Anything this small is a candidate small pass (DoF / bloom / blur chain). Census only.
|
||||
private const int SmallRtMax = 640;
|
||||
|
||||
private static readonly HashSet<string> _smallRtSizes = new();
|
||||
private static readonly Dictionary<string, long> _dofInputs = new();
|
||||
|
||||
/// <summary>Guest VAs of every texture seen as an input to the low-res DoF pass. Read by
|
||||
/// <see cref="MvppFeedbackProbe"/> to flag which temporal-history buffers actually feed the DoF.</summary>
|
||||
public static readonly HashSet<ulong> DofInputVas = new();
|
||||
private static long _lastSummaryMs;
|
||||
private static long _lastHeartbeatMs;
|
||||
private static long _totalBinds;
|
||||
private static bool _armed;
|
||||
private static int _announced;
|
||||
|
||||
/// <summary>
|
||||
/// Logs the gate state exactly once per process, regardless of <see cref="Enabled"/>, so
|
||||
/// every run makes it visible whether the probe is armed. Removes the "no lines" ambiguity
|
||||
/// (probe off vs. probe on but never reached the DoF pass). Cheap: a single interlocked
|
||||
/// exchange after the first call. Thread-safe (this path runs on multiple GPU threads).
|
||||
/// </summary>
|
||||
public static void AnnounceOnce()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string v = Environment.GetEnvironmentVariable("RYUJINX_DOF_PROBE");
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"DOFPROBE gate check: RYUJINX_DOF_PROBE={(v ?? "<unset>")} -> enabled={Enabled}. " +
|
||||
(Enabled ? "Probe ACTIVE." : "Probe OFF -- launch via XC2_DOF_PROBE.bat to enable."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspects one input texture bound during a draw, with the draw's active colour target
|
||||
/// (call site is gated on <see cref="Enabled"/>). Purely observational.
|
||||
/// </summary>
|
||||
/// <param name="texture">The input texture being bound</param>
|
||||
/// <param name="renderTarget">The draw's active colour render target (may be null)</param>
|
||||
/// <param name="stage">The shader stage this texture is bound to (DoF reads are Fragment)</param>
|
||||
public static void OnInput(Texture texture, Texture renderTarget, ShaderStage stage)
|
||||
{
|
||||
if (texture == null || renderTarget == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextureInfo rt = renderTarget.Info;
|
||||
|
||||
lock (_dofInputs)
|
||||
{
|
||||
_totalBinds++;
|
||||
|
||||
// Heartbeat so "no DoF lines" is never ambiguous (a gated switch you cannot SEE
|
||||
// in the log is worthless -- lesson from this dossier). If OnInput ever runs, we
|
||||
// log "armed" once; then a periodic "alive" line proves the hook is live and how
|
||||
// many draws/small-RTs/DoF-inputs it has counted -- distinguishing "probe off"
|
||||
// from "probe on but never reached the DoF pass (still at a 2D menu/loading)".
|
||||
if (!_armed)
|
||||
{
|
||||
_armed = true;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
"DOFPROBE armed: hook is live (RYUJINX_DOF_PROBE=1). Waiting for the low-res DoF pass -- get IN-GAME and move the camera.");
|
||||
}
|
||||
|
||||
long hb = Environment.TickCount64;
|
||||
if (hb - _lastHeartbeatMs > 5000)
|
||||
{
|
||||
_lastHeartbeatMs = hb;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"DOFPROBE alive: binds={_totalBinds} smallRTsizes={_smallRtSizes.Count} dofInputs={_dofInputs.Count}");
|
||||
}
|
||||
|
||||
// Census: record every distinct small render-target size once. Confirms the real
|
||||
// DoF resolution without hard-assuming 512x288.
|
||||
if (rt.Width <= SmallRtMax)
|
||||
{
|
||||
string rtSize = $"{rt.Width}x{rt.Height} {rt.FormatInfo.Format}";
|
||||
if (_smallRtSizes.Add(rtSize))
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu, $"DOFPROBE small-RT seen: {rtSize}");
|
||||
}
|
||||
}
|
||||
|
||||
// Grounding gate: only draws that render INTO the low-res DoF buffer.
|
||||
if (rt.Width != DofWidth || rt.Height != DofHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextureInfo info = texture.Info;
|
||||
|
||||
ulong va;
|
||||
try
|
||||
{
|
||||
va = texture.Range.GetSubRange(0).Address;
|
||||
}
|
||||
catch
|
||||
{
|
||||
va = 0;
|
||||
}
|
||||
|
||||
DofInputVas.Add(va); // cross-reference key for the feedback probe
|
||||
|
||||
string key =
|
||||
$"stage={stage} @0x{va:X}/{info.FormatInfo.Format}/{info.Width}x{info.Height}/lv{info.Levels}/{info.Target}";
|
||||
|
||||
if (_dofInputs.TryGetValue(key, out long n))
|
||||
{
|
||||
_dofInputs[key] = n + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dofInputs[key] = 1;
|
||||
Logger.Info?.Print(LogClass.Gpu, $"DOFPROBE new DoF input: {key}");
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _lastSummaryMs > 5000)
|
||||
{
|
||||
_lastSummaryMs = now;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"DOFPROBE summary: {_dofInputs.Count} distinct DoF inputs (most persistent = history candidate) =====");
|
||||
foreach (KeyValuePair<string, long> kv in _dofInputs)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu, $"DOFPROBE hits={kv.Value,7} {kv.Key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [FEEDBACKPROBE, READ-ONLY, default OFF] Frame-to-frame feedback detector for the XC2
|
||||
/// temporal artifact. For every texture ever used as a colour render target, it classifies
|
||||
/// the transition across each guest frame boundary (Window.Present):
|
||||
///
|
||||
/// Case A = written in frame N, and its FIRST access in frame N+1 is a READ
|
||||
/// (read before any write) ==> TRUE temporal history buffer.
|
||||
/// Case B = written in frame N, and its FIRST access in frame N+1 is a WRITE
|
||||
/// (write before any read) ==> transient render target reused every frame.
|
||||
///
|
||||
/// Writes are observed at SetRenderTargetColor (a texture bound as a colour target =
|
||||
/// SignalModifying(true)); reads at CommitTextureBindings (a texture bound as a sampled input).
|
||||
/// All three call sites (write, read, frame boundary) run on the GPU command thread, so the
|
||||
/// first-access ordering within a frame is the true GPU order. Only render-target textures are
|
||||
/// tracked: a read of a texture never seen as a target is a single dictionary miss, no insert.
|
||||
/// Enable with RYUJINX_FEEDBACK_PROBE=1. Never modifies textures, targets or bindings.
|
||||
/// </summary>
|
||||
static class MvppFeedbackProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_FEEDBACK_PROBE") == "1";
|
||||
|
||||
private sealed class Rec
|
||||
{
|
||||
public long LastWriteFrame = long.MinValue;
|
||||
public long FirstAccessFrame = long.MinValue;
|
||||
public long CaseA; // write(N) -> read-first(N+1): temporal history
|
||||
public long CaseB; // write(N) -> write-first(N+1): transient reused
|
||||
public string Format;
|
||||
public int Width;
|
||||
public int Height;
|
||||
public ulong Va;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<ulong, Rec> _rts = new();
|
||||
private static long _frame;
|
||||
private static long _lastReportMs;
|
||||
private static int _announced;
|
||||
|
||||
/// <summary>Logs the gate state once per process, even when OFF (removes the "no lines" ambiguity).</summary>
|
||||
public static void AnnounceOnce()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string v = Environment.GetEnvironmentVariable("RYUJINX_FEEDBACK_PROBE");
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"FEEDBACKPROBE gate check: RYUJINX_FEEDBACK_PROBE={(v ?? "<unset>")} -> enabled={Enabled}. " +
|
||||
(Enabled ? "Probe ACTIVE." : "Probe OFF -- launch via XC2_FEEDBACK_PROBE.bat to enable."));
|
||||
}
|
||||
|
||||
/// <summary>Current guest frame counter (for other probes' log lines).</summary>
|
||||
public static long Frame => System.Threading.Interlocked.Read(ref _frame);
|
||||
|
||||
/// <summary>Read the frame-to-frame classification of a texture VA (for the trace probe).</summary>
|
||||
public static (long caseA, long caseB) Classify(ulong va)
|
||||
{
|
||||
lock (_rts)
|
||||
{
|
||||
return _rts.TryGetValue(va, out Rec r) ? (r.CaseA, r.CaseB) : (0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong Va(Texture t)
|
||||
{
|
||||
try
|
||||
{
|
||||
return t.Range.GetSubRange(0).Address;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A texture is bound as a colour render target this frame (a write).</summary>
|
||||
public static void OnWrite(Texture texture)
|
||||
{
|
||||
if (texture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong va = Va(texture);
|
||||
|
||||
lock (_rts)
|
||||
{
|
||||
if (!_rts.TryGetValue(va, out Rec rec))
|
||||
{
|
||||
rec = new Rec();
|
||||
_rts[va] = rec;
|
||||
}
|
||||
|
||||
if (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)
|
||||
{
|
||||
rec.CaseB++;
|
||||
}
|
||||
}
|
||||
|
||||
rec.LastWriteFrame = _frame;
|
||||
|
||||
TextureInfo info = texture.Info;
|
||||
rec.Format = info.FormatInfo.Format.ToString();
|
||||
rec.Width = info.Width;
|
||||
rec.Height = info.Height;
|
||||
rec.Va = va;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A texture is bound as a sampled input this frame (a read). Only render targets are tracked.</summary>
|
||||
public static void OnRead(Texture texture)
|
||||
{
|
||||
if (texture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong va = Va(texture);
|
||||
|
||||
lock (_rts)
|
||||
{
|
||||
if (!_rts.TryGetValue(va, out Rec rec))
|
||||
{
|
||||
return; // never a render target -> cannot be a history buffer
|
||||
}
|
||||
|
||||
if (rec.FirstAccessFrame != _frame)
|
||||
{
|
||||
rec.FirstAccessFrame = _frame;
|
||||
|
||||
// First access of this frame is a READ. If it was written the previous frame,
|
||||
// it carries content across the frame boundary = TRUE temporal history (Case A).
|
||||
if (rec.LastWriteFrame == _frame - 1)
|
||||
{
|
||||
rec.CaseA++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called at the guest frame boundary (Window.Present). Advances the frame and reports periodically.</summary>
|
||||
public static void OnFrameBoundary()
|
||||
{
|
||||
lock (_rts)
|
||||
{
|
||||
_frame++;
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _lastReportMs < 2000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastReportMs = now;
|
||||
|
||||
var caseA = new List<Rec>();
|
||||
foreach (Rec r in _rts.Values)
|
||||
{
|
||||
if (r.CaseA > 0)
|
||||
{
|
||||
caseA.Add(r);
|
||||
}
|
||||
}
|
||||
|
||||
caseA.Sort((a, b) => b.CaseA.CompareTo(a.CaseA));
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"FEEDBACKPROBE report: frame={_frame} trackedRTs={_rts.Count} caseA(history)={caseA.Count} " +
|
||||
"(*** = also a DoF input; run with RYUJINX_DOF_PROBE=1 to populate) =====");
|
||||
|
||||
foreach (Rec r in caseA)
|
||||
{
|
||||
bool feedsDof = MvppDofProbe.DofInputVas.Contains(r.Va);
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"FEEDBACKPROBE CASE A history: caseA={r.CaseA,6} caseB={r.CaseB,6} {r.Format}/{r.Width}x{r.Height} " +
|
||||
$"va=0x{r.Va:X}{(feedsDof ? " *** FEEDS DoF" : "")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [FULLSYNC] Read-only probe (RYUJINX_FULLSYNC=1), inert unless set. Changes nothing that is
|
||||
/// rendered; it only reports which textures get a FULL upload from guest memory, and whether that
|
||||
/// guest memory is actually empty.
|
||||
///
|
||||
/// Why this is the current suspect. Journal (111) localised the XC2 block corruption to the pass
|
||||
/// filling slot4, found the guest memory of that surface EMPTY, and measured a period of 8 lines =
|
||||
/// one GOB. Two facts sat badly together: a desktop GPU has no GOB, so an 8-line period means the
|
||||
/// data went through a guest block-linear layout at some point -- yet the surface was said to be a
|
||||
/// pure GPU target that never round-trips through guest memory.
|
||||
///
|
||||
/// Texture.SynchronizeMemory takes `SynchronizeFull()` whenever `_hasData` is false, which is the
|
||||
/// state of a freshly created texture -- including a view created over an existing overlap, where
|
||||
/// TextureCache calls SynchronizeMemory immediately after CreateView. SynchronizeFull reads
|
||||
/// `_physicalMemory.GetSpan(Range)` and uploads it through the block-linear conversion. Uploading
|
||||
/// empty or stale guest memory over an already-rendered surface would produce exactly what the
|
||||
/// captures show: flat colour blocks where the memory is zero, displaced valid content where it is
|
||||
/// stale, at GOB granularity. Nothing else proposed so far explains the flat blocks.
|
||||
///
|
||||
/// This probe does not decide that this is the bug. It answers: does slot4 take this path, and is
|
||||
/// the data it uploads empty?
|
||||
/// </summary>
|
||||
static class MvppFullSyncProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_FULLSYNC") == "1";
|
||||
|
||||
private static readonly HashSet<string> _reported = new();
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Positive control. Without it, "no [FULLSYNC] line" cannot be told apart from "the flag never
|
||||
/// took", and a silence that cannot be read is not a measurement.
|
||||
/// </summary>
|
||||
public static void ReportArmed()
|
||||
{
|
||||
if (!Enabled || _armedLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_armedLogged = true;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu, "[FULLSYNC] ARMED (RYUJINX_FULLSYNC=1). Expect one line per distinct texture shape taking the full-upload path.");
|
||||
}
|
||||
|
||||
// Per-shape counters. The first run showed that BOTH the corrupted surface (R8G8B8A8 720p) and
|
||||
// the clean one (R11G11B10 720p) take this path with empty guest memory, so "takes the path" is
|
||||
// not the discriminator. What can still separate them is HOW OFTEN, and whether the texture is
|
||||
// a VIEW over a parent that already holds rendered pixels -- an empty upload onto a fresh
|
||||
// texture is harmless, the same upload onto a parent's memory wipes what was drawn.
|
||||
private static readonly Dictionary<string, int> _shapeCounts = new();
|
||||
|
||||
private static int _fullResLogged;
|
||||
|
||||
private const int FullResLogCap = 60;
|
||||
|
||||
public static void OnFullSync(TextureInfo info, ReadOnlySpan<byte> data, bool isView)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_count++;
|
||||
|
||||
string key = $"{info.FormatInfo.Format}|{info.Width}x{info.Height}|{info.Target}";
|
||||
|
||||
lock (_shapeCounts)
|
||||
{
|
||||
_shapeCounts.TryGetValue(key, out int n);
|
||||
_shapeCounts[key] = n + 1;
|
||||
|
||||
// Full-screen surfaces are the ones journal (111) is about, and they are rare enough to
|
||||
// log individually. Capped so a runaway loop cannot fill the log and slow the run.
|
||||
if (info.Width == 1280 && info.Height == 720 && _fullResLogged < FullResLogCap)
|
||||
{
|
||||
_fullResLogged++;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[FULLSYNC/720p] #{n + 1} {info.FormatInfo.Format} isView={isView} " +
|
||||
$"| sync {_count}");
|
||||
}
|
||||
}
|
||||
|
||||
lock (_reported)
|
||||
{
|
||||
if (!_reported.Add(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Sampled rather than exhaustive: these spans reach several megabytes and this runs on the
|
||||
// render thread. A stride of 4093 (prime) avoids aligning with any power-of-two structure
|
||||
// in the data, which a round stride could alias with and mistake for "all zero".
|
||||
int nonZero = 0;
|
||||
int sampled = 0;
|
||||
|
||||
for (int i = 0; i < data.Length; i += 4093)
|
||||
{
|
||||
sampled++;
|
||||
|
||||
if (data[i] != 0)
|
||||
{
|
||||
nonZero++;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[FULLSYNC] {info.FormatInfo.Format} {info.Width}x{info.Height} {info.Target} " +
|
||||
$"({info.FormatInfo.BytesPerPixel} bpp) uploaded FROM GUEST | guest bytes {data.Length} | " +
|
||||
$"sampled {sampled}, non-zero {nonZero} => {(nonZero == 0 ? "GUEST MEMORY IS EMPTY" : "guest has content")} | " +
|
||||
$"total full-syncs so far {_count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Probe for the fork's gobBlocksInZ clamp (RYUJINX_GOBZ_PROBE=1, off by default).
|
||||
///
|
||||
/// WHY. Xenoblade 2 shows rectangles of displaced-but-real content, in rows, appearing as the
|
||||
/// camera turns and reportedly worse indoors where more objects are on screen. Measured and
|
||||
/// ELIMINATED so far, each with the switch verified live in the log: shader cache (purged),
|
||||
/// DLSS/MV++/FG (log proved mode=0, zero "using mode"), runtime mipmaps (never applied when
|
||||
/// DLSS is off - checked in the code path), recycled device memory (2000+ fresh allocations
|
||||
/// zeroed, artefact unchanged), history-reset storm (4 resets in a minute).
|
||||
///
|
||||
/// What is left that can displace real content in rectangles is the BLOCK-LINEAR layout. Switch
|
||||
/// textures are tiled, and decoding them needs the exact gob parameters the game declared. The
|
||||
/// fork overrides one of them: for any non-3D target with more than one layer it forces
|
||||
/// gobBlocksInZ to 1, a change added for the BOTW/TOTK 4K packs that upstream does not have.
|
||||
/// A wrong stride there detiles the texture with the wrong pitch, which looks like blocks of
|
||||
/// content moved around - exactly the reported shape.
|
||||
///
|
||||
/// This probe does NOT change behaviour: the clamp still applies. It counts how often it fires
|
||||
/// and describes what it hit, so the lead is settled by measurement:
|
||||
/// never fires on XC2 -> dead lead, drop it;
|
||||
/// fires constantly -> suspect confirmed, and RYUJINX_NO_GOBZ_CLAMP=1 tests it for real.
|
||||
/// </summary>
|
||||
static class MvppGobProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_GOBZ_PROBE") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// RYUJINX_NO_GOBZ_CLAMP=1: honour the descriptor instead of forcing gobBlocksInZ to 1.
|
||||
/// The actual A/B, to be run only once the probe has shown the clamp fires at all.
|
||||
/// ⚠️ The clamp exists for the BOTW/TOTK 4K packs -- disabling it may bring their original
|
||||
/// problem back, so this is a diagnostic, never a default.
|
||||
/// </summary>
|
||||
public static readonly bool ClampDisabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_NO_GOBZ_CLAMP") == "1";
|
||||
|
||||
private static readonly object _lock = new();
|
||||
private static readonly Dictionary<string, int> _shapes = new();
|
||||
private static int _total;
|
||||
private static long _lastLogMs;
|
||||
|
||||
public static void NoteClamp(int width, int height, int layers, int gobZ, string target, string format)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_total++;
|
||||
|
||||
// Distinct shapes rather than a line per texture: a game binds the same few
|
||||
// hundred textures over and over, and what we need is WHICH KINDS get clamped.
|
||||
string key = $"{width}x{height}x{layers} {target} {format} gobZ={gobZ}";
|
||||
|
||||
_shapes.TryGetValue(key, out int n);
|
||||
_shapes[key] = n + 1;
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
if (now - _lastLogMs < 5000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastLogMs = now;
|
||||
|
||||
System.Text.StringBuilder sb = new();
|
||||
sb.Append($"GOBPROBE: clamp gobBlocksInZ->1 fired {_total} times, {_shapes.Count} distinct shapes");
|
||||
sb.Append(ClampDisabled ? " [CLAMP DESACTIVE]" : "");
|
||||
sb.Append(':');
|
||||
|
||||
int shown = 0;
|
||||
|
||||
foreach (KeyValuePair<string, int> kv in _shapes)
|
||||
{
|
||||
if (shown++ >= 8)
|
||||
{
|
||||
sb.Append($" (+{_shapes.Count - 8} autres)");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append($" [{kv.Key} x{kv.Value}]");
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [MAP64] Read-only probe (RYUJINX_MAP64_PROBE=1), inert unless set. Changes nothing that is
|
||||
/// rendered; it reports who PRODUCES the small DoF maps and what the host cache does to them.
|
||||
///
|
||||
/// Why. EXP 4 (journal 122) proved the XC2 form-A rectangles require the data of the 64x36 DoF
|
||||
/// tile map (fp_t_tcb_E): forcing its samples to a constant removes them, and the downstream
|
||||
/// arithmetic was already exonerated. So the map's CONTENT arrives corrupted -- and every host
|
||||
/// probe so far (STORAGEID/FULLSYNC/VIEWALIAS) filtered on 1280x720, so this surface was never
|
||||
/// watched. This probe watches the small shapes (64x36, 320x180, 160x90) on four channels:
|
||||
///
|
||||
/// 1. WRITER passes: every draw whose colour target is one of these shapes -- fragment/vertex
|
||||
/// guest addresses, target VA and instance, and the VIEWPORT extents of the draw. A viewport
|
||||
/// smaller than the map would leave the rest of the texels STALE, which is invisible while
|
||||
/// the camera is still and visible in motion -- exactly the artifact's behaviour.
|
||||
/// 2. Guest uploads onto them (SynchronizeFull), with a FULL emptiness scan (the texture is
|
||||
/// tiny): an upload of empty guest memory over a rendered map mid-game would inject garbage.
|
||||
/// 3. Partial group syncs onto them (the branch never instrumented before).
|
||||
/// 4. Instance churn: distinct object identities seen per shape (STORAGEID's question, but on
|
||||
/// the right surface this time).
|
||||
///
|
||||
/// Reading grid, written before coding: writer draws with FULL 64x36 viewport every frame and
|
||||
/// zero syncs => corruption comes from the writer's INPUTS, move up the chain. In-game uploads
|
||||
/// or partial viewports or heavy churn => host-side mechanism found, instrument that path next.
|
||||
/// No [MAP64] lines at all => instrument mute (VOID), never "nothing happens".
|
||||
/// </summary>
|
||||
static class MvppMap64Probe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MAP64_PROBE") == "1";
|
||||
|
||||
private static bool _armedLogged;
|
||||
|
||||
/// <summary>Positive control (TextureCache ctor). Silence without it is unreadable.</summary>
|
||||
public static void ReportArmed()
|
||||
{
|
||||
if (!Enabled || _armedLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_armedLogged = true;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[MAP64] ARMED (RYUJINX_MAP64_PROBE=1). Watching 64x36 / 320x180 / 160x90: writer passes + viewports, guest uploads, group syncs, instance churn.");
|
||||
}
|
||||
|
||||
private static bool IsWatched(TextureInfo info)
|
||||
{
|
||||
return (info.Width == 64 && info.Height == 36) ||
|
||||
(info.Width == 320 && info.Height == 180) ||
|
||||
(info.Width == 160 && info.Height == 90);
|
||||
}
|
||||
|
||||
private struct In
|
||||
{
|
||||
public string Fmt;
|
||||
public int W;
|
||||
public int H;
|
||||
public ShaderStage Stage;
|
||||
public int Handle;
|
||||
}
|
||||
|
||||
private static readonly List<In> _inputs = new();
|
||||
|
||||
/// <summary>Per-draw sampled input (TextureBindingsManager, both bind paths). Self-gated.</summary>
|
||||
public static void OnInput(Texture texture, ShaderStage stage, int handle)
|
||||
{
|
||||
if (!Enabled || texture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_inputs)
|
||||
{
|
||||
if (_inputs.Count >= 64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextureInfo info = texture.Info;
|
||||
_inputs.Add(new In
|
||||
{
|
||||
Fmt = info.FormatInfo.Format.ToString(),
|
||||
W = info.Width,
|
||||
H = info.Height,
|
||||
Stage = stage,
|
||||
Handle = handle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// One detailed line per distinct (fragment shader, target shape, viewport) -- a NEW viewport
|
||||
// on a known writer is a finding on its own (partial write), so it re-triggers the log.
|
||||
private static readonly HashSet<(ulong, string, int, int)> _seenWriters = new();
|
||||
|
||||
// Instance churn per shape, WRITER side (RuntimeHelpers identity, no field added).
|
||||
private static readonly Dictionary<string, HashSet<int>> _instances = new();
|
||||
|
||||
private static long _totalDraws;
|
||||
private static long _mapDraws;
|
||||
private static long _fullSyncs;
|
||||
private static long _fullSyncsEmpty;
|
||||
private static long _groupSyncs;
|
||||
private static long _summaryMs;
|
||||
|
||||
/// <summary>
|
||||
/// Called once per draw AFTER bindings are committed (StateUpdater probe block, gated there).
|
||||
/// Viewport extents are the draw's host viewport 0, already multiplied by the RT scale.
|
||||
/// </summary>
|
||||
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, int vpW, int vpH)
|
||||
{
|
||||
_totalDraws++;
|
||||
|
||||
try
|
||||
{
|
||||
if (texMgr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
||||
{
|
||||
Texture ct = texMgr.GetColorTarget(slot);
|
||||
if (ct == null || !IsWatched(ct.Info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_mapDraws++;
|
||||
|
||||
TextureInfo info = ct.Info;
|
||||
string shape = $"{info.FormatInfo.Format}/{info.Width}x{info.Height}";
|
||||
int id = RuntimeHelpers.GetHashCode(ct);
|
||||
|
||||
lock (_instances)
|
||||
{
|
||||
if (!_instances.TryGetValue(shape, out HashSet<int> set))
|
||||
{
|
||||
_instances[shape] = set = new HashSet<int>();
|
||||
}
|
||||
|
||||
set.Add(id);
|
||||
}
|
||||
|
||||
bool isNew;
|
||||
lock (_seenWriters)
|
||||
{
|
||||
isNew = _seenWriters.Add((fsAddr, shape, vpW, vpH));
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
lock (_inputs)
|
||||
{
|
||||
foreach (In i in _inputs)
|
||||
{
|
||||
sb.Append($" <-tcb_{i.Handle:X} {i.Fmt}/{i.W}x{i.H} {i.Stage};");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MAP64/WRITER] out_attr{slot} {shape} inst=0x{id:X8} | fs=0x{fsAddr:X} vs=0x{vsAddr:X} " +
|
||||
$"| VIEWPORT {vpW}x{vpH}{(vpW < info.Width || vpH < info.Height ? " *** PARTIAL ***" : "")} " +
|
||||
$"| inputs:{sb}");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_inputs)
|
||||
{
|
||||
_inputs.Clear();
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _summaryMs >= 3000 && _mapDraws > 0)
|
||||
{
|
||||
_summaryMs = now;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
lock (_instances)
|
||||
{
|
||||
foreach (KeyValuePair<string, HashSet<int>> kv in _instances)
|
||||
{
|
||||
sb.Append($"{kv.Key}: {kv.Value.Count} instance(s); ");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MAP64/SUMMARY ~3s] mapDraws={_mapDraws} / totalDraws={_totalDraws} | " +
|
||||
$"fullSyncs={_fullSyncs} (empty {_fullSyncsEmpty}) | groupSyncs={_groupSyncs} | {sb}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long _fullSyncLogged;
|
||||
private static long _groupSyncLogged;
|
||||
private const long PerEventLogCap = 40;
|
||||
|
||||
/// <summary>Full guest-memory upload onto a watched shape (Texture.SynchronizeFull). Rare = log each, capped.</summary>
|
||||
public static void OnFullSync(Texture texture, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (!Enabled || texture == null || !IsWatched(texture.Info))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fullSyncs++;
|
||||
|
||||
// The texture is tiny (64x36x4 = 9 KiB guest range): scan EVERY byte, no sampling caveat.
|
||||
bool empty = true;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
if (data[i] != 0)
|
||||
{
|
||||
empty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty)
|
||||
{
|
||||
_fullSyncsEmpty++;
|
||||
}
|
||||
|
||||
if (_fullSyncLogged++ < PerEventLogCap)
|
||||
{
|
||||
TextureInfo info = texture.Info;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MAP64/FULLSYNC] {info.FormatInfo.Format}/{info.Width}x{info.Height} inst=0x{RuntimeHelpers.GetHashCode(texture):X8} " +
|
||||
$"isView={texture.IsView} | guest bytes {data.Length} => {(empty ? "GUEST MEMORY EMPTY" : "guest has content")} " +
|
||||
$"| fullSyncs so far {_fullSyncs}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Partial (per-handle) group sync onto a watched shape (Texture.SynchronizeMemory, _hasData branch).</summary>
|
||||
public static void OnGroupSync(Texture texture)
|
||||
{
|
||||
if (!Enabled || texture == null || !IsWatched(texture.Info))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_groupSyncs++;
|
||||
|
||||
if (_groupSyncLogged++ < PerEventLogCap)
|
||||
{
|
||||
TextureInfo info = texture.Info;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MAP64/GROUPSYNC] {info.FormatInfo.Format}/{info.Width}x{info.Height} inst=0x{RuntimeHelpers.GetHashCode(texture):X8} " +
|
||||
$"| groupSyncs so far {_groupSyncs}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [MVBUF] Read-only probe (RYUJINX_MVBUF_PROBE=1), inert unless set. Watches the XC2
|
||||
/// object-motion-vector buffer (R10G10B10A2Unorm 1280x720), the carrier of the form-A/B
|
||||
/// corruption (EXP 5, journal 126): with its samples nulled, every artifact form disappears.
|
||||
///
|
||||
/// Why clears matter. That buffer is written per-frame ONLY by moving/skinned geometry; the
|
||||
/// rest of the screen keeps whatever the buffer already contains, so the game depends on a
|
||||
/// RELIABLE per-frame clear to a neutral value. A clear that is skipped, scissored short, or
|
||||
/// reordered leaves RECTANGLES of stale motion -- which the motion-blur chain then renders as
|
||||
/// the measured smears. Upstream PR #4596 documents exactly this clear/read hazard family on
|
||||
/// RTX 3000+ and names Xenoblade explicitly; and a MISSING clear is not a timing bug, which is
|
||||
/// consistent with forced barriers and DeviceWaitIdle having had no effect (journal 112).
|
||||
///
|
||||
/// Reading grid, written before coding:
|
||||
/// - clears present EVERY frame, full scissor, neutral value -> the clear COMMAND is fine;
|
||||
/// the failure would be content/decode or host-side execution -> instrument the Vulkan
|
||||
/// clear path (v2) or the A2 sign-channel decode next;
|
||||
/// - frames with ZERO clear of this target (and no full-screen writer draw those frames) ->
|
||||
/// the game relies on a clear path this probe does not see (loadOp/fast clear) or the
|
||||
/// clear is genuinely skipped -> hook the Vulkan level next;
|
||||
/// - clears with PARTIAL scissor or component mask != 0xF -> partial-clear family;
|
||||
/// - no [MVBUF] lines at all with ARMED present -> the buffer is not engine-cleared and not
|
||||
/// drawn: copy path, v2.
|
||||
/// </summary>
|
||||
static class MvppMvBufProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVBUF_PROBE") == "1";
|
||||
|
||||
private static bool _armedLogged;
|
||||
|
||||
public static void ReportArmed()
|
||||
{
|
||||
if (!Enabled || _armedLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_armedLogged = true;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[MVBUF] ARMED (RYUJINX_MVBUF_PROBE=1). Watching R10G10B10A2 1280x720: engine clears (value/mask/scissor), writer draws, per-frame reliability.");
|
||||
}
|
||||
|
||||
private static bool IsMvBuffer(Texture t)
|
||||
{
|
||||
return t != null &&
|
||||
t.Info.Width == 1280 &&
|
||||
t.Info.Height == 720 &&
|
||||
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
|
||||
}
|
||||
|
||||
// Frame accounting (advanced from the present boundary, always on GPU thread).
|
||||
private static long _frames;
|
||||
private static int _clearsThisFrame;
|
||||
private static int _writesThisFrame;
|
||||
private static long _frames0Clear;
|
||||
private static long _frames1Clear;
|
||||
private static long _framesMultiClear;
|
||||
private static long _frames0Write;
|
||||
private static long _framesWithWrite;
|
||||
|
||||
private static long _clears;
|
||||
private static long _writerDraws;
|
||||
private static long _summaryMs;
|
||||
|
||||
private static readonly HashSet<string> _clearSignatures = new();
|
||||
private static readonly HashSet<(ulong, int, int)> _writers = new();
|
||||
|
||||
// v3: per-RT blend/write-mask census on the MV attachment. A transparent/additive draw
|
||||
// BLENDING into the MV target (instead of masked-off or opaque-write) would accumulate
|
||||
// garbage magnitudes exactly where transparency-heavy content sits -- the co-location the
|
||||
// captures show. Console intent for such draws is mask-off or no MV attachment at all.
|
||||
private static readonly HashSet<string> _writerStates = new();
|
||||
private static long _drawsBlendOnMv;
|
||||
private static long _drawsPartialMaskOnMv;
|
||||
|
||||
// Host-instance identity split: the object the engine CLEARS vs the object shaders SAMPLE.
|
||||
// Full overlap = one host texture, identities fine. Disjoint sets = the clear lands on one
|
||||
// host copy while consumers read another (stale) one -- the desync suspect.
|
||||
private static readonly HashSet<int> _clearedIds = new();
|
||||
private static readonly HashSet<int> _sampledIds = new();
|
||||
|
||||
/// <summary>Guest frame boundary (Window present). Folds the per-frame counters into histograms.</summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only count frames once the buffer exists (first clear or write seen), so the
|
||||
// pre-title-screen frames do not drown the histogram.
|
||||
if (_clears + _writerDraws > 0)
|
||||
{
|
||||
_frames++;
|
||||
|
||||
if (_clearsThisFrame == 0)
|
||||
{
|
||||
_frames0Clear++;
|
||||
}
|
||||
else if (_clearsThisFrame == 1)
|
||||
{
|
||||
_frames1Clear++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_framesMultiClear++;
|
||||
}
|
||||
|
||||
if (_writesThisFrame == 0)
|
||||
{
|
||||
_frames0Write++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_framesWithWrite++;
|
||||
}
|
||||
}
|
||||
|
||||
_clearsThisFrame = 0;
|
||||
_writesThisFrame = 0;
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _summaryMs >= 3000 && _frames > 0)
|
||||
{
|
||||
_summaryMs = now;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVBUF/SUMMARY ~3s] frames={_frames} | clears/frame: 0x{_frames0Clear} 1x{_frames1Clear} multi x{_framesMultiClear} | " +
|
||||
$"writes/frame: 0 x{_frames0Write} >=1 x{_framesWithWrite} | totals: clears={_clears} writerDraws={_writerDraws} | " +
|
||||
$"clearSigs={_clearSignatures.Count} writers={_writers.Count} | " +
|
||||
$"BLEND-on-MV draws={_drawsBlendOnMv} partial-mask draws={_drawsPartialMaskOnMv}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Engine clear about to hit a colour target (DrawManager.Clear). Logs each DISTINCT signature.</summary>
|
||||
public static void OnClear(Texture target, int index, uint componentMask, ColorF color, bool customScissor, int sx, int sy, int sw, int sh)
|
||||
{
|
||||
if (!Enabled || !IsMvBuffer(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_clears++;
|
||||
_clearsThisFrame++;
|
||||
|
||||
string sig = $"rgba=({color.Red:0.###},{color.Green:0.###},{color.Blue:0.###},{color.Alpha:0.###}) mask=0x{componentMask:X} " +
|
||||
(customScissor ? $"scissor={sx},{sy} {sw}x{sh}" : "scissor=full");
|
||||
|
||||
lock (_clearSignatures)
|
||||
{
|
||||
if (_clearSignatures.Add(sig))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVBUF/CLEAR] out_attr{index} NEW SIGNATURE: {sig} | clear #{_clears}");
|
||||
}
|
||||
}
|
||||
|
||||
int id = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(target);
|
||||
lock (_clearedIds)
|
||||
{
|
||||
if (_clearedIds.Add(id))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVBUF/ID] CLEAR target instance 0x{id:X8} (new; {_clearedIds.Count} cleared instance(s) so far)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shader sampling of the MV buffer (TextureBindingsManager, both bind paths). Self-gated.</summary>
|
||||
public static void OnSampled(Texture texture)
|
||||
{
|
||||
if (!Enabled || !IsMvBuffer(texture))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int id = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(texture);
|
||||
lock (_clearedIds)
|
||||
{
|
||||
if (_sampledIds.Add(id))
|
||||
{
|
||||
bool overlaps = _clearedIds.Contains(id);
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVBUF/ID] SAMPLED instance 0x{id:X8} isView={texture.IsView} " +
|
||||
$"=> {(overlaps ? "SAME as a cleared instance" : "*** NEVER CLEARED (desync suspect) ***")} " +
|
||||
$"| sampled {_sampledIds.Count}, cleared {_clearedIds.Count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Per-draw (StateUpdater probe block): draws whose colour target is the MV buffer.</summary>
|
||||
public static void OnDraw(TextureManager texMgr, ulong fsAddr, int vpW, int vpH, ReadOnlySpan<bool> blendEnable, ReadOnlySpan<uint> writeMasks)
|
||||
{
|
||||
if (texMgr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
||||
{
|
||||
Texture ct = texMgr.GetColorTarget(slot);
|
||||
if (!IsMvBuffer(ct))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_writerDraws++;
|
||||
_writesThisFrame++;
|
||||
|
||||
bool blend = slot < blendEnable.Length && blendEnable[slot];
|
||||
uint mask = slot < writeMasks.Length ? writeMasks[slot] : 0xFu;
|
||||
|
||||
if (blend && mask != 0)
|
||||
{
|
||||
_drawsBlendOnMv++;
|
||||
}
|
||||
|
||||
if (mask != 0xF && mask != 0)
|
||||
{
|
||||
_drawsPartialMaskOnMv++;
|
||||
}
|
||||
|
||||
string state = $"out_attr{slot} fs=0x{fsAddr:X} vp={vpW}x{vpH} blend={(blend ? "ON" : "off")} mask=0x{mask:X}";
|
||||
bool isNew;
|
||||
lock (_writerStates)
|
||||
{
|
||||
isNew = _writerStates.Add(state);
|
||||
_writers.Add((fsAddr, vpW, vpH));
|
||||
}
|
||||
|
||||
if (isNew && (blend || mask != 0xF))
|
||||
{
|
||||
// Only the suspicious combinations get their own line; clean opaque writers
|
||||
// stay in the summary counts (there are hundreds of them).
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVBUF/WRITER-STATE] {state}{(blend && mask != 0 ? " *** BLEND INTO MV ***" : "")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [MVSYNC] Read-only probe (RYUJINX_MVSYNC_PROBE=1), inert unless set. Journal 158.
|
||||
///
|
||||
/// Last family standing: the poison appears IN the XC2 object-MV buffer (R10G10B10A2
|
||||
/// 1280x720) between the writers' stores and the consumers' reads -- every write-side value
|
||||
/// scrub was negative (NaN, >=0.999, >=0.5), both read-side kills are positive, and the
|
||||
/// periscope shows saturated garbage in the buffer at rest. This probe watches the three
|
||||
/// host paths that can INJECT content into an existing texture behind the game's back,
|
||||
/// none of which were ever measured for this format:
|
||||
/// 1. TextureGroup.SynchronizePartial -- partial upload of "CPU-dirty" guest pages;
|
||||
/// 2. Texture.SynchronizeFull (re-sync branch) -- full guest upload over live content;
|
||||
/// 3. TextureGroupHandle.Copy -- copy-dependency pull from an overlapping texture.
|
||||
///
|
||||
/// Reading grid (written before coding):
|
||||
/// ARMED + heartbeats all zero -> no injection path fires; family dead -> next = Vulkan
|
||||
/// clear/loadOp execution level.
|
||||
/// partial/full syncs > 0 -> stale guest pages uploaded over rendered MV = smoking
|
||||
/// gun -> instrument WHY dirty (tracking/protection).
|
||||
/// copy-ins > 0 -> alias family: an overlapping texture overwrites the MV
|
||||
/// buffer via copy dependency -> identify the source.
|
||||
/// The 3s heartbeat prints even at zero so a negative is distinguishable from a mute probe.
|
||||
/// </summary>
|
||||
static class MvppMvSyncProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVSYNC_PROBE") == "1";
|
||||
|
||||
private static bool _armedLogged;
|
||||
|
||||
private static long _partialSyncs;
|
||||
private static long _fullSyncs;
|
||||
private static long _copyIns;
|
||||
private static long _events;
|
||||
private static long _summaryMs;
|
||||
|
||||
public static bool IsMvBuffer(Texture t)
|
||||
{
|
||||
return t != null &&
|
||||
t.Info.Width == 1280 &&
|
||||
t.Info.Height == 720 &&
|
||||
t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm;
|
||||
}
|
||||
|
||||
/// <summary>Frame boundary (Window present): arming witness + 3s heartbeat, zeros included.</summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[MVSYNC] ARMED (RYUJINX_MVSYNC_PROBE=1). Watching guest-data injection into R10G10B10A2 1280x720: partial sync / full re-sync / copy-dependency.");
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _summaryMs >= 3000)
|
||||
{
|
||||
_summaryMs = now;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVSYNC/HEARTBEAT ~3s] partialSyncs={_partialSyncs} fullSyncs={_fullSyncs} copyIns={_copyIns}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>SynchronizeMemory decided an upload is needed for this storage (dirty branch taken).</summary>
|
||||
public static void OnSyncDecision(Texture storage, bool partial, int regionCount, bool anyModified)
|
||||
{
|
||||
if (!Enabled || !IsMvBuffer(storage))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long n;
|
||||
if (partial)
|
||||
{
|
||||
n = ++_partialSyncs;
|
||||
}
|
||||
else
|
||||
{
|
||||
n = ++_fullSyncs;
|
||||
}
|
||||
|
||||
if (++_events <= 20 || _events % 100 == 0)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVSYNC] GUEST->TEXTURE {(partial ? "PARTIAL" : "FULL")} sync #{n} | regions={regionCount} anyModified={anyModified} | guest data uploaded OVER the MV buffer");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A copy dependency pulled data from an overlapping texture into this storage.</summary>
|
||||
public static void OnCopyIn(Texture storage)
|
||||
{
|
||||
if (!Enabled || !IsMvBuffer(storage))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long n = ++_copyIns;
|
||||
|
||||
if (++_events <= 20 || _events % 100 == 0)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[MVSYNC] COPY-IN #{n} | copy dependency wrote into the MV buffer from an overlapping texture");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [SCENEPROBE, READ-ONLY, default OFF] Per-draw identifier for the pass that produces the fresh
|
||||
/// 1280x720 R11G11B10 scene the DoF consumes. Fires only for a draw whose colour target is a
|
||||
/// 1280x720 R11G11B10Float texture, and only once per distinct fragment-shader guest address, so
|
||||
/// the output is a short list of the distinct passes writing that format. For each it prints the
|
||||
/// frame, the fragment + vertex shader guest addresses (stable pass identity, maps to Nsight via
|
||||
/// the shader), the framebuffer (colour-target) VA, and every sampled texture of that draw with
|
||||
/// its format/size and whether <see cref="MvppFeedbackProbe"/> classifies it as temporal history.
|
||||
/// The temporal-reconstruction pass is the one whose inputs include a 720p history [H] of its own
|
||||
/// format (it reads its previous output). Never modifies any GPU state.
|
||||
/// Enable with RYUJINX_SCENE_PROBE=1 (run alongside RYUJINX_FEEDBACK_PROBE=1 for the [H] flags).
|
||||
/// </summary>
|
||||
static class MvppScenePassProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_SCENE_PROBE") == "1";
|
||||
|
||||
private struct In
|
||||
{
|
||||
public ulong Va;
|
||||
public string Fmt;
|
||||
public int W;
|
||||
public int H;
|
||||
public ShaderStage Stage;
|
||||
public int Handle; // [E0] texture-CB handle -> GLSL sampler name fp_t_tcb_<Handle:X>
|
||||
public int Binding; // [E0] runtime binding slot -> GLSL layout(binding=N)
|
||||
public AddressMode WrapU; // [CLAMPPROBE] sampler address mode U (edge/border/wrap) of this input
|
||||
public AddressMode WrapV; // [CLAMPPROBE] sampler address mode V
|
||||
// [FILTERPROBE] The address mode was checked and matches the console; the FILTER never was.
|
||||
// It decides how the tiny 64x36 CoC map is interpolated, and that map sizes every bokeh
|
||||
// sprite -- nearest vs linear there changes sprite sizes in ~20-pixel blocks.
|
||||
public MinFilter MinF;
|
||||
public MagFilter MagF;
|
||||
}
|
||||
|
||||
private static readonly List<In> _inputs = new();
|
||||
private static readonly HashSet<ulong> _seenShaders = new();
|
||||
private static int _announced;
|
||||
|
||||
public static void AnnounceOnce()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string v = Environment.GetEnvironmentVariable("RYUJINX_SCENE_PROBE");
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"SCENEPROBE gate check: RYUJINX_SCENE_PROBE={(v ?? "<unset>")} -> enabled={Enabled}. " +
|
||||
(Enabled ? "Probe ACTIVE." : "Probe OFF."));
|
||||
}
|
||||
|
||||
private static ulong Va(Texture t)
|
||||
{
|
||||
try
|
||||
{
|
||||
return t.Range.GetSubRange(0).Address;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Appends one sampled input of the current draw (called from CommitTextureBindings, gated).</summary>
|
||||
public static void OnInput(Texture texture, ShaderStage stage, int handle, int binding, Sampler sampler)
|
||||
{
|
||||
if (texture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_inputs)
|
||||
{
|
||||
if (_inputs.Count >= 256)
|
||||
{
|
||||
return; // safety cap; a real draw never samples this many
|
||||
}
|
||||
|
||||
TextureInfo info = texture.Info;
|
||||
_inputs.Add(new In
|
||||
{
|
||||
Va = Va(texture),
|
||||
Fmt = info.FormatInfo.Format.ToString(),
|
||||
W = info.Width,
|
||||
H = info.Height,
|
||||
Stage = stage,
|
||||
Handle = handle,
|
||||
Binding = binding,
|
||||
WrapU = sampler != null ? sampler.ProbeAddressU : default,
|
||||
WrapV = sampler != null ? sampler.ProbeAddressV : default,
|
||||
MinF = sampler != null ? sampler.ProbeMinFilter : default,
|
||||
MagF = sampler != null ? sampler.ProbeMagFilter : default,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once per draw AFTER bindings are committed (StateUpdater, gated). If the draw writes a
|
||||
/// 1280x720 R11G11B10 colour target, logs the pass and its sampled inputs (once per shader).
|
||||
/// Always clears the per-draw input list.
|
||||
/// </summary>
|
||||
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, long frame)
|
||||
{
|
||||
lock (_inputs)
|
||||
{
|
||||
try
|
||||
{
|
||||
Texture colorRt = texMgr?.GetAnyRenderTarget();
|
||||
if (colorRt == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextureInfo rt = colorRt.Info;
|
||||
bool isScene = rt.Width == 1280 && rt.Height == 720 && rt.FormatInfo.Format == Format.R11G11B10Float;
|
||||
// [CLAMPPROBE] also catch the DoF bokeh scatter pass by its output signature (512x288 R16G16B16A16F),
|
||||
// so we can log the address mode of the textures IT gathers (the OOB-gather edge suspect).
|
||||
bool isDof = rt.Width == 512 && rt.Height == 288 && rt.FormatInfo.Format == Format.R16G16B16A16Float;
|
||||
if (!isScene && !isDof)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_seenShaders.Add(fsAddr))
|
||||
{
|
||||
return; // this pass already reported
|
||||
}
|
||||
|
||||
ulong fbVa = Va(colorRt);
|
||||
string kind = isDof ? "512x288 R16G16B16A16 (DoF/bokeh)" : "720p R11G11B10 (scene)";
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"SCENEPROBE pass writes {kind}: fs=0x{fsAddr:X} vs=0x{vsAddr:X} fb=0x{fbVa:X} frame={frame} inputs={_inputs.Count} =====");
|
||||
|
||||
// [E0] Every bound colour render target (out_attr_N) with its runtime format. out_attr1 is
|
||||
// the resolved-colour / temporal-history feedback target H1 cares about. Read-only.
|
||||
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
||||
{
|
||||
Texture ct = texMgr.GetColorTarget(slot);
|
||||
if (ct != null)
|
||||
{
|
||||
TextureInfo cti = ct.Info;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"SCENEPROBE -> out_attr{slot} RT 0x{Va(ct):X} {cti.FormatInfo.Format}/{cti.Width}x{cti.Height}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (In i in _inputs)
|
||||
{
|
||||
(long caseA, long caseB) = MvppFeedbackProbe.Classify(i.Va);
|
||||
bool history = caseA > 0 && caseA >= caseB;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"SCENEPROBE <- fp_t_tcb_{i.Handle:X} (bind={i.Binding}) 0x{i.Va:X} {i.Fmt}/{i.W}x{i.H} {i.Stage} wrap=[{i.WrapU}/{i.WrapV}] filter=[{i.MinF}/{i.MagF}] [A={caseA},B={caseB}]");
|
||||
if (history)
|
||||
{
|
||||
sb.Append(" [H HISTORY]");
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_inputs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [TAAPROBE 28/07, LECTURE SEULE, OFF par defaut] Trouve la PASSE DE RESOLUTION TEMPORELLE du
|
||||
/// jeu : le dessin qui LIT une cible couleur pleine resolution ecrite a l'image PRECEDENTE.
|
||||
///
|
||||
/// POURQUOI UNE DEUXIEME SONDE ALORS QUE FEEDBACKPROBE EXISTE. FEEDBACKPROBE prend un verrou
|
||||
/// sur un dictionnaire a CHAQUE liaison de texture, des milliers de fois par image. Il est
|
||||
/// "lecture seule" pour les pixels, pas pour le temps : Alex a vu un defaut revenir pendant le
|
||||
/// run qui l'utilisait. Celle-ci ne prend aucun verrou et ne fait, dans le cas courant, que
|
||||
/// deux comparaisons d'entiers -- le tableau n'est parcouru que pour les textures qui ont deja
|
||||
/// la bonne taille. Tous les appels arrivent sur le thread de commandes GPU.
|
||||
///
|
||||
/// CE QU'ELLE CHERCHE. FEEDBACKPROBE a montre 33 cibles qui survivent d'une image a l'autre,
|
||||
/// dont QUATRE en R11G11B10Float a la resolution de rendu pleine -- la forme d'un historique
|
||||
/// couleur HDR (un bloom serait en resolution reduite, et il y en a justement a cote en
|
||||
/// 960x540). Reste a savoir quel dessin les consomme : c'est lui, la passe temporelle.
|
||||
///
|
||||
/// Elle n'ecrit qu'une ligne par combinaison distincte, une poignee en tout, puis se tait.
|
||||
/// Aucune texture, aucune cible, aucune liaison n'est modifiee.
|
||||
/// </summary>
|
||||
static class MvppTaaProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_TAAPROBE") == "1";
|
||||
|
||||
// 24 emplacements : FEEDBACKPROBE en a compte 33 au total, dont une majorite en resolution
|
||||
// reduite que le pre-filtre elimine avant d'arriver ici.
|
||||
private const int Slots = 24;
|
||||
|
||||
private static readonly ulong[] _va = new ulong[Slots];
|
||||
private static readonly long[] _lastSeen = new long[Slots];
|
||||
private static int _count;
|
||||
|
||||
private static long _frame;
|
||||
private static int _mainW;
|
||||
private static int _mainH;
|
||||
private static long _mainArea;
|
||||
|
||||
private static readonly ulong[] _reported = new ulong[16];
|
||||
private static int _reportedN;
|
||||
private static int _announced;
|
||||
|
||||
public static void AnnounceOnce()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string v = Environment.GetEnvironmentVariable("RYUJINX_MVPP_TAAPROBE");
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"TAAPROBE gate check: RYUJINX_MVPP_TAAPROBE={(v ?? "<unset>")} -> enabled={Enabled}.");
|
||||
}
|
||||
|
||||
/// <summary>Frontiere d'image invitee.</summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
_frame++;
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong Va(Texture t) => t.Range.GetSubRange(0).Address;
|
||||
|
||||
private static int Find(ulong va)
|
||||
{
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (_va[i] == va)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appelee la ou la sonde du flou l'est deja : elle recoit l'entree ECHANTILLONNEE et la
|
||||
/// cible couleur du dessin en cours. Les deux ensemble suffisent -- pas besoin de se
|
||||
/// greffer aussi sur la pose des cibles.
|
||||
/// </summary>
|
||||
public static void OnInput(Texture input, Texture target, ShaderStage stage)
|
||||
{
|
||||
if (!Enabled || input == null || target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-filtre : la resolution principale est la plus grande cible couleur vue. Deux
|
||||
// comparaisons d'entiers eliminent tout le trafic (masques, bloom, ombres, interface)
|
||||
// avant le moindre parcours.
|
||||
long area = (long)target.Info.Width * target.Info.Height;
|
||||
|
||||
if (area > _mainArea)
|
||||
{
|
||||
_mainArea = area;
|
||||
_mainW = target.Info.Width;
|
||||
_mainH = target.Info.Height;
|
||||
}
|
||||
|
||||
if (target.Info.Width != _mainW || target.Info.Height != _mainH)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// La cible pleine resolution de ce dessin est notee comme "vue a cette image".
|
||||
ulong tva = Va(target);
|
||||
int ti = Find(tva);
|
||||
|
||||
if (ti >= 0)
|
||||
{
|
||||
_lastSeen[ti] = _frame;
|
||||
}
|
||||
else if (_count < Slots)
|
||||
{
|
||||
_va[_count] = tva;
|
||||
_lastSeen[_count] = _frame;
|
||||
_count++;
|
||||
}
|
||||
|
||||
// L'entree doit elle aussi etre pleine resolution : un historique temporel a forcement
|
||||
// la taille de ce qu'il reconstruit.
|
||||
if (input.Info.Width != _mainW || input.Info.Height != _mainH)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int ii = Find(Va(input));
|
||||
|
||||
// Ecrite a une image PRECEDENTE et lue maintenant = historique temporel.
|
||||
if (ii < 0 || _lastSeen[ii] >= _frame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong key = Va(input) ^ (tva << 1);
|
||||
|
||||
for (int i = 0; i < _reportedN; i++)
|
||||
{
|
||||
if (_reported[i] == key)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_reportedN < _reported.Length)
|
||||
{
|
||||
_reported[_reportedN++] = key;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"TAAPROBE PASSE TEMPORELLE #{_reportedN} (image {_frame}) : lit l'historique " +
|
||||
$"{input.Info.FormatInfo.Format}/{input.Info.Width}x{input.Info.Height} " +
|
||||
$"va=0x{Va(input):X} (ecrit a l'image {_lastSeen[ii]}) " +
|
||||
$"-> ecrit {target.Info.FormatInfo.Format}/{target.Info.Width}x{target.Info.Height} " +
|
||||
$"va=0x{tva:X} | etage {stage} | resolution principale {_mainW}x{_mainH}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [TRACEPROBE, READ-ONLY, default OFF] Rendering dependency-graph tracer for the XC2 temporal
|
||||
/// artifact. At every texture read it records a directed edge "current render target <- input
|
||||
/// texture" (both by guest VA). Over many draws this reconstructs the pass dependency graph.
|
||||
/// Periodically it walks the graph UP from each DoF input (see <see cref="MvppDofProbe"/>),
|
||||
/// printing the producer chain and flagging every node that <see cref="MvppFeedbackProbe"/>
|
||||
/// classifies as temporal history (Case A). This answers: which pass writes the fresh DoF scene,
|
||||
/// what it reads, and which of those reads is a temporal-history buffer -- recursively, until the
|
||||
/// first temporal producer feeding the DoF scene is found. Never modifies any GPU state.
|
||||
/// Enable with RYUJINX_TRACE_PROBE=1 (run alongside RYUJINX_DOF_PROBE=1 + RYUJINX_FEEDBACK_PROBE=1).
|
||||
/// </summary>
|
||||
static class MvppTraceProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TRACE_PROBE") == "1";
|
||||
|
||||
private const int MaxDepth = 5;
|
||||
|
||||
private sealed class Node
|
||||
{
|
||||
public string Fmt = "?";
|
||||
public int Width;
|
||||
public int Height;
|
||||
public readonly HashSet<ulong> Inputs = new();
|
||||
}
|
||||
|
||||
private static readonly Dictionary<ulong, Node> _graph = new();
|
||||
private static long _lastReportMs;
|
||||
private static int _announced;
|
||||
|
||||
public static void AnnounceOnce()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string v = Environment.GetEnvironmentVariable("RYUJINX_TRACE_PROBE");
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"TRACEPROBE gate check: RYUJINX_TRACE_PROBE={(v ?? "<unset>")} -> enabled={Enabled}. " +
|
||||
(Enabled ? "Probe ACTIVE." : "Probe OFF."));
|
||||
}
|
||||
|
||||
private static ulong Va(Texture t)
|
||||
{
|
||||
try
|
||||
{
|
||||
return t.Range.GetSubRange(0).Address;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static Node GetOrAdd(ulong va, TextureInfo info)
|
||||
{
|
||||
if (!_graph.TryGetValue(va, out Node n))
|
||||
{
|
||||
n = new Node();
|
||||
_graph[va] = n;
|
||||
}
|
||||
|
||||
n.Fmt = info.FormatInfo.Format.ToString();
|
||||
n.Width = info.Width;
|
||||
n.Height = info.Height;
|
||||
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)
|
||||
{
|
||||
if (renderTarget == null || input == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong rtVa = Va(renderTarget);
|
||||
ulong inVa = Va(input);
|
||||
|
||||
if (rtVa == inVa || inVa == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_graph)
|
||||
{
|
||||
Node rt = GetOrAdd(rtVa, renderTarget.Info);
|
||||
GetOrAdd(inVa, input.Info); // ensure the input has a node (its format), even as a leaf
|
||||
rt.Inputs.Add(inVa);
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnFrameBoundary()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
lock (_graph)
|
||||
{
|
||||
if (now - _lastReportMs < 3000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastReportMs = now;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"TRACEPROBE dependency walk from DoF inputs (nodes={_graph.Count}) [H]=CaseA history =====");
|
||||
|
||||
foreach (ulong root in MvppDofProbe.DofInputVas)
|
||||
{
|
||||
if (!_graph.TryGetValue(root, out Node rn))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"TRACEPROBE DoF-input 0x{root:X} {rn.Fmt}/{rn.Width}x{rn.Height}");
|
||||
|
||||
var visited = new HashSet<ulong>();
|
||||
Walk(root, 1, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Holds the _graph lock (called from OnFrameBoundary).
|
||||
private static void Walk(ulong va, int depth, HashSet<ulong> visited)
|
||||
{
|
||||
if (depth > MaxDepth || !visited.Add(va))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_graph.TryGetValue(va, out Node n) || n.Inputs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ulong inVa in n.Inputs)
|
||||
{
|
||||
_graph.TryGetValue(inVa, out Node inNode);
|
||||
(long caseA, long caseB) = MvppFeedbackProbe.Classify(inVa);
|
||||
bool history = caseA > 0 && caseA >= caseB;
|
||||
bool dofIn = MvppDofProbe.DofInputVas.Contains(inVa);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("TRACEPROBE ");
|
||||
sb.Append(' ', depth * 2);
|
||||
sb.Append($"<- 0x{inVa:X} {(inNode != null ? $"{inNode.Fmt}/{inNode.Width}x{inNode.Height}" : "?")}");
|
||||
sb.Append($" [A={caseA},B={caseB}]");
|
||||
if (history)
|
||||
{
|
||||
sb.Append(" [H]");
|
||||
}
|
||||
if (dofIn)
|
||||
{
|
||||
sb.Append(" [DoF-in]");
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu, sb.ToString());
|
||||
|
||||
Walk(inVa, depth + 1, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [TWINFIX] EXP 21 -- the tiebreaker (RYUJINX_TWINFIX=1 + RYUJINX_TWINMAP=1), journal 178.
|
||||
///
|
||||
/// Measured chain (journal 174-177): the resolve and the DoF chain sample twin X, whose only
|
||||
/// writer is the sky; the 800+ material shaders write twin Y; the guest pool descriptor
|
||||
/// REALLY points at X (28k raw checks, 0 mismatch); and no mechanism on the emulator ever
|
||||
/// transfers Y into X. The poison is X's never-written content.
|
||||
///
|
||||
/// This experiment redirects, READ SIDE ONLY, every sampled binding of the sky-only twin to
|
||||
/// the material twin. No feedback loop is possible (Y's writers do not read X). The sky
|
||||
/// keeps writing X; sky pixels lose their MV during the test (acceptable: Y's sky region
|
||||
/// holds the neutral clear).
|
||||
/// flat-block counter ~0 => the whole diagnosis is PROVEN experimentally and this redirect
|
||||
/// is the shape of the fix (X and Y must be one, as on console);
|
||||
/// artifact unchanged => a flaw exists in the diagnosis chain -- back to cold analysis
|
||||
/// with a decisive new fact.
|
||||
/// Twin classification is automatic and conservative: exactly two twins, one with <= 4
|
||||
/// writers (sky) and one with >= 50 (materials); no redirect until both are established.
|
||||
/// </summary>
|
||||
static class MvppTwinFixProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TWINFIX") == "1";
|
||||
|
||||
private static bool _classifiedLogged;
|
||||
private static long _redirects;
|
||||
|
||||
private static readonly HashSet<(ShaderStage, int)> _redirectTuples = new();
|
||||
|
||||
/// <summary>Fast-path gate: forces one slow-path re-resolution for bindings still cached
|
||||
/// on the sky twin once the classification (and thus the redirect) is available.</summary>
|
||||
public static bool NeedsRebind(Texture cached)
|
||||
{
|
||||
if (!Enabled || cached == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MvppMvSyncProbe.IsMvBuffer(cached))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MvppTwinMapProbe.TryClassify(out ulong skyVa, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return cached.Range.GetSubRange(0).Address == skyVa;
|
||||
}
|
||||
|
||||
/// <summary>Slow-path substitution: reads of the sky twin are served by the material twin.</summary>
|
||||
public static Texture MaybeRedirect(Texture texture, ShaderStage stage, int handle)
|
||||
{
|
||||
if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture))
|
||||
{
|
||||
return texture;
|
||||
}
|
||||
|
||||
if (!MvppTwinMapProbe.TryClassify(out ulong skyVa, out Texture materialTwin) || materialTwin == null)
|
||||
{
|
||||
return texture;
|
||||
}
|
||||
|
||||
if (texture.Range.GetSubRange(0).Address != skyVa)
|
||||
{
|
||||
return texture;
|
||||
}
|
||||
|
||||
if (!_classifiedLogged)
|
||||
{
|
||||
_classifiedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINFIX] twins classified; redirect ARMED: reads of sky-twin@0x{skyVa:X} will be served by material-twin@0x{materialTwin.Range.GetSubRange(0).Address:X}");
|
||||
}
|
||||
|
||||
_redirects++;
|
||||
|
||||
bool isNew;
|
||||
lock (_redirectTuples)
|
||||
{
|
||||
isNew = _redirectTuples.Add((stage, handle));
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINFIX] REDIRECT: stage={stage} handle=0x{handle:X} (tcb_{handle:X}) now reads the material twin | total redirected binds: {_redirects}");
|
||||
}
|
||||
|
||||
return materialTwin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [TWINMAP] Read-only probe (RYUJINX_TWINMAP=1), inert unless set. Journal 173.
|
||||
///
|
||||
/// VKIMG found the game double-buffers its MV target (two R10G10B10A2 1280x720 guest
|
||||
/// textures, both written and read every frame -- a two-stage intra-frame chain, VKPHASE).
|
||||
/// Before any twin-viewing experiment, MEASURE the roles instead of guessing them:
|
||||
/// - WRITE side: per twin (guest VA), which fragment shaders draw into it, and how many.
|
||||
/// Expected asymmetry: the raw twin has ~190 material writers; the processed twin has
|
||||
/// very few -- and those few NAME the processor.
|
||||
/// - READ side: per twin, which (stage, guest tcb handle) samples it. Cross-checked with
|
||||
/// the decompiled binding maps (resolve reads its MV at tcb_10, builder at tcb_8), this
|
||||
/// names which twin the periscope v3 run already showed poisoned.
|
||||
/// No swaps, no visual change, zero dataflow interference -- pure cartography.
|
||||
/// </summary>
|
||||
static class MvppTwinMapProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TWINMAP") == "1";
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static long _summaryMs;
|
||||
|
||||
private class TwinStats
|
||||
{
|
||||
public readonly HashSet<ulong> Writers = new();
|
||||
public long WriteDraws;
|
||||
public int WritersLogged;
|
||||
|
||||
// v2 (journal 176): GMMU aliasing check. On console two GPU VAs can alias the same
|
||||
// physical pages; a missed remap would leave the cached texture on stale backing.
|
||||
public ulong GpuVa;
|
||||
public ulong Backing;
|
||||
public ulong LastTranslation;
|
||||
|
||||
// v4 (journal 178): live Texture reference for TWINFIX's read-side redirect.
|
||||
public WeakReference<Texture> LastTexture;
|
||||
|
||||
// v5 (journal 179): per-twin clear attribution + masked-bind census.
|
||||
public long Clears;
|
||||
public int ClearsLogged;
|
||||
public long MaskedBinds;
|
||||
|
||||
// v6 (journal 201): clear-COLOUR census, the classification discriminant that
|
||||
// survives the v5 writer-count correction (both twins have ~730 writers, so the
|
||||
// old <=4 / >=50 rule never matches any more). Measured (180): X is cleared
|
||||
// BLACK (0,0,0,1), Y is cleared NEUTRAL (0.5,0.5,1,1) -- every frame.
|
||||
public long BlackClears;
|
||||
public long NeutralClears;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<ulong, TwinStats> _twins = new();
|
||||
private static readonly HashSet<(ulong, ShaderStage, int)> _reads = new();
|
||||
private static WeakReference<Memory.MemoryManager> _mm;
|
||||
|
||||
// v3 POOLTRUTH (journal 177): raw guest pool descriptor vs actually-bound texture.
|
||||
// The fast bind path skips descriptor re-reads unless the pool is flagged modified; if
|
||||
// that tracking misses the game's per-frame updates, a stale binding survives forever --
|
||||
// which would pin the resolve on twin X while the game re-points it at Y.
|
||||
private static long _poolMatches;
|
||||
private static long _poolMismatches;
|
||||
private static readonly HashSet<(ulong, ulong, int)> _mismatchTuples = new();
|
||||
|
||||
private static ulong GuestVa(Texture t)
|
||||
{
|
||||
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)
|
||||
{
|
||||
skyVa = 0;
|
||||
materialTwin = null;
|
||||
|
||||
lock (_twins)
|
||||
{
|
||||
if (_twins.Count != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TwinStats sky = null, mat = null;
|
||||
ulong skyKey = 0;
|
||||
|
||||
foreach (var kv in _twins)
|
||||
{
|
||||
if (kv.Value.Writers.Count <= 4)
|
||||
{
|
||||
sky = kv.Value;
|
||||
skyKey = kv.Key;
|
||||
}
|
||||
else if (kv.Value.Writers.Count >= 50)
|
||||
{
|
||||
mat = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// [v6] (journal 201) fallback: the writer-count rule died with the v5 counting
|
||||
// correction (both twins have ~730 material writers). The discriminant that
|
||||
// still holds, measured (180): X is cleared BLACK each frame, Y NEUTRAL.
|
||||
// Same redirect direction as the (179) worse-mode: reads of X served by Y.
|
||||
if (sky == null || mat == null)
|
||||
{
|
||||
TwinStats black = null, neutral = null;
|
||||
ulong blackKey = 0;
|
||||
|
||||
foreach (var kv in _twins)
|
||||
{
|
||||
if (kv.Value.BlackClears >= 10 && kv.Value.NeutralClears == 0)
|
||||
{
|
||||
black = kv.Value;
|
||||
blackKey = kv.Key;
|
||||
}
|
||||
else if (kv.Value.NeutralClears >= 10 && kv.Value.BlackClears == 0)
|
||||
{
|
||||
neutral = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (black != null && neutral != null)
|
||||
{
|
||||
sky = black;
|
||||
skyKey = blackKey;
|
||||
mat = neutral;
|
||||
}
|
||||
}
|
||||
|
||||
if (sky == null || mat == null || mat.LastTexture == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mat.LastTexture.TryGetTarget(out Texture matTex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
skyVa = skyKey;
|
||||
materialTwin = matTex;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Snapshot of the discovered twin VAs, for cross-probes (TWINXFER's DMA range check).</summary>
|
||||
public static ulong[] TwinVas()
|
||||
{
|
||||
lock (_twins)
|
||||
{
|
||||
ulong[] vas = new ulong[_twins.Count];
|
||||
_twins.Keys.CopyTo(vas, 0);
|
||||
return vas;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Per-draw (StateUpdater): record writers per MV-shaped colour target.</summary>
|
||||
public static void OnDraw(TextureManager texMgr, ulong fsAddr, Memory.MemoryManager memoryManager, ReadOnlySpan<uint> writeMasks)
|
||||
{
|
||||
if (!Enabled || texMgr == null || fsAddr == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (memoryManager != null)
|
||||
{
|
||||
_mm = new WeakReference<Memory.MemoryManager>(memoryManager);
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[TWINMAP] ARMED (RYUJINX_TWINMAP=1). Mapping writers and readers of each MV twin by guest VA.");
|
||||
}
|
||||
|
||||
// v5: NO early return -- a single draw can bind BOTH twins at different slots
|
||||
// (the early return hid that for a whole evening, journal 179). Masks passed in:
|
||||
// a bound-but-masked-off slot writes nothing and is counted apart.
|
||||
bool any = false;
|
||||
|
||||
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
||||
{
|
||||
Texture ct = texMgr.GetColorTarget(slot);
|
||||
if (!MvppMvSyncProbe.IsMvBuffer(ct))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
ulong va = GuestVa(ct);
|
||||
uint mask = (writeMasks.Length > slot) ? writeMasks[slot] : 0xFu;
|
||||
|
||||
lock (_twins)
|
||||
{
|
||||
TwinStats stats = GetOrAdd(va, ct, memoryManager);
|
||||
|
||||
stats.LastTexture = new WeakReference<Texture>(ct);
|
||||
|
||||
if (mask == 0)
|
||||
{
|
||||
stats.MaskedBinds++;
|
||||
continue;
|
||||
}
|
||||
|
||||
stats.WriteDraws++;
|
||||
|
||||
if (stats.Writers.Add(fsAddr) && stats.WritersLogged < 8)
|
||||
{
|
||||
stats.WritersLogged++;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP] WRITER of twin@0x{va:X}: fs=0x{fsAddr:X} slot={slot} mask=0x{mask:X} (writer #{stats.Writers.Count}; first 8 logged)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (any)
|
||||
{
|
||||
Heartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
private static TwinStats GetOrAdd(ulong va, Texture ct, Memory.MemoryManager memoryManager)
|
||||
{
|
||||
if (!_twins.TryGetValue(va, out TwinStats stats))
|
||||
{
|
||||
stats = new TwinStats
|
||||
{
|
||||
GpuVa = ct.Info.GpuAddress,
|
||||
Backing = va,
|
||||
};
|
||||
_twins[va] = stats;
|
||||
|
||||
ulong translated = memoryManager?.Translate(ct.Info.GpuAddress) ?? 0;
|
||||
stats.LastTranslation = translated;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP] NEW TWIN target: twin@0x{va:X} ({_twins.Count} twin(s)) | GpuVa=0x{ct.Info.GpuAddress:X} backing=0x{va:X} translatesTo=0x{translated:X}" +
|
||||
(translated != va ? " *** TRANSLATION != BACKING (aliasing/remap suspect) ***" : ""));
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <summary>v5: engine clear attribution per twin (DrawManager.Clear).</summary>
|
||||
public static void OnClear(Texture target, uint componentMask, float r, float g, float b, float a)
|
||||
{
|
||||
if (!Enabled || target == null || !MvppMvSyncProbe.IsMvBuffer(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong va = GuestVa(target);
|
||||
|
||||
lock (_twins)
|
||||
{
|
||||
TwinStats stats = GetOrAdd(va, target, null);
|
||||
stats.Clears++;
|
||||
|
||||
// [v6] clear-colour census for the fallback classification.
|
||||
if (r < 0.1f && g < 0.1f && b < 0.1f)
|
||||
{
|
||||
stats.BlackClears++;
|
||||
}
|
||||
else if (r > 0.4f && r < 0.6f && g > 0.4f && g < 0.6f)
|
||||
{
|
||||
stats.NeutralClears++;
|
||||
}
|
||||
|
||||
if (stats.ClearsLogged < 4)
|
||||
{
|
||||
stats.ClearsLogged++;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP] CLEAR of twin@0x{va:X}: rgba=({r:0.###},{g:0.###},{b:0.###},{a:0.###}) mask=0x{componentMask:X} (clear #{stats.Clears})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Per-bind (TextureBindingsManager, both paths): record readers per MV-shaped sampled
|
||||
/// texture. v3: also read the RAW guest pool descriptor and compare with what got bound.</summary>
|
||||
public static void OnRead(Texture texture, ShaderStage stage, int handle, TexturePool pool, int textureId, Memory.MemoryManager memoryManager)
|
||||
{
|
||||
if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong va = GuestVa(texture);
|
||||
bool isNew;
|
||||
|
||||
lock (_twins)
|
||||
{
|
||||
isNew = _reads.Add((va, stage, handle));
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP] READER of twin@0x{va:X}: stage={stage} handle=0x{handle:X} (tcb_{handle:X}) | distinct readers: {_reads.Count}");
|
||||
}
|
||||
|
||||
// v3 POOLTRUTH: guest truth vs emulator binding.
|
||||
if (pool != null && memoryManager != null && textureId >= 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
ulong descAddr = pool.Address + (ulong)textureId * 0x20;
|
||||
TextureDescriptor raw = System.Runtime.InteropServices.MemoryMarshal.Read<TextureDescriptor>(
|
||||
memoryManager.Physical.GetSpan(descAddr, 0x20));
|
||||
ulong rawVa = raw.UnpackAddress();
|
||||
ulong boundVa = texture.Info.GpuAddress;
|
||||
|
||||
if (rawVa != boundVa)
|
||||
{
|
||||
_poolMismatches++;
|
||||
|
||||
bool newMismatch;
|
||||
lock (_twins)
|
||||
{
|
||||
newMismatch = _mismatchTuples.Add((rawVa, boundVa, handle));
|
||||
}
|
||||
|
||||
if (newMismatch)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP] *** POOL DESCRIPTOR MISMATCH ***: raw guest VA=0x{rawVa:X} vs BOUND VA=0x{boundVa:X} (poolId={textureId} handle=0x{handle:X} stage={stage}) | the emulator is reading a STALE binding");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_poolMatches++;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Raw read outside mapped memory: report once via the heartbeat counters only.
|
||||
}
|
||||
}
|
||||
|
||||
Heartbeat();
|
||||
}
|
||||
|
||||
private static void Heartbeat()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _summaryMs >= 3000)
|
||||
{
|
||||
_summaryMs = now;
|
||||
|
||||
Memory.MemoryManager mm = null;
|
||||
_mm?.TryGetTarget(out mm);
|
||||
|
||||
lock (_twins)
|
||||
{
|
||||
string summary = "";
|
||||
foreach (var kv in _twins)
|
||||
{
|
||||
TwinStats stats = kv.Value;
|
||||
summary += $" twin@0x{kv.Key:X}: writers={stats.Writers.Count} draws={stats.WriteDraws} maskedBinds={stats.MaskedBinds} CLEARS={stats.Clears} |";
|
||||
|
||||
// v2: live GMMU re-translation of the twin's VA. A change or a mismatch
|
||||
// with the cached backing = the missed-remap smoking gun.
|
||||
if (mm != null && stats.GpuVa != 0)
|
||||
{
|
||||
ulong translated = mm.Translate(stats.GpuVa);
|
||||
|
||||
if (translated != stats.LastTranslation)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP] *** TRANSLATION CHANGED for twin@0x{kv.Key:X}: GpuVa=0x{stats.GpuVa:X} was->0x{stats.LastTranslation:X} now->0x{translated:X} (backing=0x{stats.Backing:X}) ***");
|
||||
stats.LastTranslation = translated;
|
||||
}
|
||||
else if (translated != stats.Backing)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP] *** TRANSLATION != BACKING for twin@0x{kv.Key:X}: GpuVa=0x{stats.GpuVa:X} ->0x{translated:X} vs backing 0x{stats.Backing:X} ***");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINMAP/HEARTBEAT ~3s]{summary} readers(distinct va,stage,handle)={_reads.Count} | poolTruth: match={_poolMatches} MISMATCH={_poolMismatches}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [TWINXFER] Read-only probe (RYUJINX_TWINXFER=1), inert unless set. Journal 174.
|
||||
///
|
||||
/// TWINMAP measured the twins' roles and it is damning: the resolve (tcb_10) and the DoF
|
||||
/// chain read twin X, whose ONLY fragment writer is the sky (1 draw/frame) -- while the 882
|
||||
/// material shaders write twin Y. On console this can only work if SOMETHING transfers the
|
||||
/// geometry MVs into X. Two mechanisms are invisible to every instrument so far:
|
||||
/// 1. engine copies (2D blit / DMA) -- the game may issue a per-frame Y->X copy;
|
||||
/// 2. a COMPUTE pass writing X via image store (storage-image bindings bypass both the
|
||||
/// draw census and the sampled-texture hooks).
|
||||
/// This probe watches both:
|
||||
/// - every 2D/DMA texture copy whose source or destination is MV-shaped (src/dst VAs);
|
||||
/// - every storage-IMAGE binding of an MV-shaped texture (stage, handle, isStore).
|
||||
/// Reading grid:
|
||||
/// Y->X copies present -> the transfer exists; next: is it executed correctly?
|
||||
/// image-store writes to X -> the missing writer is a compute pass; instrument it;
|
||||
/// NOTHING touches X -> X is genuinely sky-only + stale -> the game expects
|
||||
/// aliasing/offset semantics the emulator does not reproduce -> address decode next.
|
||||
/// </summary>
|
||||
static class MvppTwinXferProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TWINXFER") == "1";
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static long _copies;
|
||||
private static long _imageBinds;
|
||||
private static long _dmaHits;
|
||||
private static long _summaryMs;
|
||||
|
||||
private static readonly HashSet<(ulong, ulong, string)> _copyPairs = new();
|
||||
private static readonly HashSet<(ulong, ShaderStage, int, bool)> _imageTuples = new();
|
||||
private static readonly HashSet<(ulong, ulong, bool)> _dmaPairs = new();
|
||||
|
||||
// Generous per-twin window (real surface = 0x3C0000): a copy landing anywhere inside
|
||||
// still names the mechanism; sub-range starts (per-line copies) must not be missed.
|
||||
private const ulong TwinWindow = 0x400000;
|
||||
|
||||
private static bool IsMv(Texture t)
|
||||
{
|
||||
return MvppMvSyncProbe.IsMvBuffer(t);
|
||||
}
|
||||
|
||||
private static ulong Va(Texture t)
|
||||
{
|
||||
return t.Range.GetSubRange(0).Address;
|
||||
}
|
||||
|
||||
/// <summary>Frame boundary (Gpu Window present): arming witness + heartbeat, zeros included.</summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[TWINXFER] ARMED (RYUJINX_TWINXFER=1). Watching engine copies and storage-image bindings touching the MV twins.");
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _summaryMs >= 3000)
|
||||
{
|
||||
_summaryMs = now;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINXFER/HEARTBEAT ~3s] copies={_copies} distinctPairs={_copyPairs.Count} | imageBinds={_imageBinds} distinctTuples={_imageTuples.Count} | " +
|
||||
$"dmaHits={_dmaHits} distinctDma={_dmaPairs.Count} (twins connus: {MvppTwinMapProbe.TwinVas().Length})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Engine texture copy (2D blit or DMA). Logs pairs where either side is MV-shaped.</summary>
|
||||
public static void OnCopy(string path, Texture src, Texture dst)
|
||||
{
|
||||
if (!Enabled || (!IsMv(src) && !IsMv(dst)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_copies++;
|
||||
|
||||
ulong srcVa = src != null ? Va(src) : 0;
|
||||
ulong dstVa = dst != null ? Va(dst) : 0;
|
||||
|
||||
bool isNew;
|
||||
lock (_copyPairs)
|
||||
{
|
||||
isNew = _copyPairs.Add((srcVa, dstVa, path));
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINXFER] COPY via {path}: src@0x{srcVa:X} ({src?.Info.FormatInfo.Format}) -> dst@0x{dstVa:X} ({dst?.Info.FormatInfo.Format}) | distinct pairs: {_copyPairs.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Raw copy-engine launch (DmaClass.DmaCopy, BEFORE any branch): catches the
|
||||
/// buffer-domain path no texture-level hook sees. Twin VAs come live from TWINMAP
|
||||
/// (run both probes together).</summary>
|
||||
public static void OnDma(ulong srcVa, ulong dstVa, int xCount, int yCount, bool copy2D, bool srcLinear, bool dstLinear)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong[] twins = MvppTwinMapProbe.TwinVas();
|
||||
if (twins.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool hit = false;
|
||||
foreach (ulong twin in twins)
|
||||
{
|
||||
if ((srcVa >= twin && srcVa < twin + TwinWindow) ||
|
||||
(dstVa >= twin && dstVa < twin + TwinWindow))
|
||||
{
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dmaHits++;
|
||||
|
||||
bool isNew;
|
||||
lock (_copyPairs)
|
||||
{
|
||||
isNew = _dmaPairs.Add((srcVa, dstVa, copy2D));
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINXFER] *** DMA TOUCHING A TWIN ***: src=0x{srcVa:X} -> dst=0x{dstVa:X} x={xCount} y={yCount} copy2D={copy2D} srcLinear={srcLinear} dstLinear={dstLinear} | distinct: {_dmaPairs.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Storage-image binding of an MV-shaped texture (the compute-writer blind spot).</summary>
|
||||
public static void OnImageBind(Texture texture, ShaderStage stage, int handle, bool isStore)
|
||||
{
|
||||
if (!Enabled || !IsMv(texture))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_imageBinds++;
|
||||
|
||||
ulong va = Va(texture);
|
||||
bool isNew;
|
||||
lock (_copyPairs)
|
||||
{
|
||||
isNew = _imageTuples.Add((va, stage, handle, isStore));
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[TWINXFER] IMAGE BIND of twin@0x{va:X}: stage={stage} handle=0x{handle:X} store={(isStore ? "YES (writer!)" : "no")} | distinct: {_imageTuples.Count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [VIEWALIAS] Read-only probe (RYUJINX_VIEWPROBE=1), inert unless set. Changes nothing that is
|
||||
/// rendered: it only reports, once per distinct case, when a texture is created as a VIEW over an
|
||||
/// existing texture whose FORMAT differs.
|
||||
///
|
||||
/// Why: journal entry (111) localised the XC2 block corruption to the deferred composition pass
|
||||
/// that fills slot4 (4 bytes per pixel), measured x65 vs a clean buffer, with the signature
|
||||
/// "valid content displaced" at GOB granularity -- and the guest memory is empty, so nothing is
|
||||
/// being detiled. Meanwhile slot6 has identical dimensions AND identical bytes per pixel yet stays
|
||||
/// clean, which rules out the block-linear size/tiling math (it depends only on those two things).
|
||||
///
|
||||
/// What is left is format-specific host behaviour, and TextureCompatibility has exactly one rule
|
||||
/// wide enough to matter here: for non-sampler textures, two textures are considered FULLY view
|
||||
/// compatible whenever their bytes-per-pixel and compression status match, regardless of the actual
|
||||
/// format. 4 bytes per pixel is the most crowded class in the renderer, so slot4 can legitimately
|
||||
/// be born as a view over an unrelated 4-byte texture and read its memory.
|
||||
///
|
||||
/// This probe does NOT decide whether that is the bug -- it answers whether it happens at all, and
|
||||
/// on which pair of formats. Instruments before levers.
|
||||
/// </summary>
|
||||
static class MvppViewAliasProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_VIEWPROBE") == "1";
|
||||
|
||||
// One line per distinct (child format, parent format, size) case. Texture creation is frequent,
|
||||
// so an ungated log would drown the file and slow the run enough to change what we measure.
|
||||
private static readonly HashSet<string> _reported = new();
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static bool _firstViewLogged;
|
||||
private static int _viewCount;
|
||||
|
||||
/// <summary>
|
||||
/// Positive control, called once when the texture cache is built. WITHOUT it, "no [VIEWALIAS]
|
||||
/// line" is ambiguous between "the flag never took", "the view path is never reached" and the
|
||||
/// only meaningful reading, "views happen but never change format". A probe whose silence
|
||||
/// cannot be interpreted is not an instrument.
|
||||
/// </summary>
|
||||
public static void ReportArmed()
|
||||
{
|
||||
if (!Enabled || _armedLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_armedLogged = true;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu, "[VIEWALIAS] ARMED (RYUJINX_VIEWPROBE=1). Expect a FIRST VIEW line if the view-creation path is ever reached.");
|
||||
}
|
||||
|
||||
public static void OnViewCreated(TextureInfo child, Texture parent, int firstLayer, int firstLevel, string compatibility)
|
||||
{
|
||||
if (!Enabled || parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextureInfo p = parent.Info;
|
||||
|
||||
// Second control: proves the path is alive, whatever the formats are. Logged before the
|
||||
// same-format early-out below, otherwise a run with only ordinary views looks identical
|
||||
// to a run where this code never executed.
|
||||
_viewCount++;
|
||||
|
||||
if (!_firstViewLogged)
|
||||
{
|
||||
_firstViewLogged = true;
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[VIEWALIAS] FIRST VIEW reached: child {child.FormatInfo.Format} {child.Width}x{child.Height} " +
|
||||
$"over parent {p.FormatInfo.Format} {p.Width}x{p.Height} | compat {compatibility}");
|
||||
}
|
||||
|
||||
// Periodic tally so the end of the log states how many views happened in total, which turns
|
||||
// "no differing-format line" into a quantified negative instead of a silence.
|
||||
if ((_viewCount % 500) == 0)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[VIEWALIAS] tally: {_viewCount} views created so far, {_reported.Count} distinct cross-format cases.");
|
||||
}
|
||||
|
||||
// Same format is the ordinary, expected case: a plain view. Only report reinterpretation.
|
||||
if (child.FormatInfo.Format == p.FormatInfo.Format)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string key = $"{child.FormatInfo.Format}|{p.FormatInfo.Format}|{child.Width}x{child.Height}|{p.Width}x{p.Height}";
|
||||
|
||||
lock (_reported)
|
||||
{
|
||||
if (!_reported.Add(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[VIEWALIAS] view {child.FormatInfo.Format} {child.Width}x{child.Height} " +
|
||||
$"({child.FormatInfo.BytesPerPixel} bpp) created OVER parent {p.FormatInfo.Format} " +
|
||||
$"{p.Width}x{p.Height} ({p.FormatInfo.BytesPerPixel} bpp) | parent addr 0x{parent.Range.GetSubRange(0).Address:X} " +
|
||||
$"| firstLayer {firstLayer} firstLevel {firstLevel} | compat {compatibility}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// [CENSUS] Read-only probe (RYUJINX_MVWRITER_CENSUS=1 + RYUJINX_NANSCRUB=1), journal 159.
|
||||
///
|
||||
/// The value scrubs covered shaders selected by FINGERPRINT; the writer set is defined by
|
||||
/// RENDER TARGET. The intersection was never verified. This probe checks, per draw whose
|
||||
/// colour target is the XC2 object-MV buffer (R10G10B10A2 1280x720), whether the bound
|
||||
/// fragment shader was armed by the +0.01 encode fingerprint -- any writer that was NOT is a
|
||||
/// writer every scrub missed, free to deposit garbage since day one.
|
||||
///
|
||||
/// Requirements: NANSCRUB=1 fills the armed registry (its NaN wrap is visually inert, EXP 14),
|
||||
/// and the shader cache must be OFF -- cache-path translations carry address 0 (journal 148),
|
||||
/// which would make every writer look unarmed (void). The bat handles both.
|
||||
///
|
||||
/// Reading grid (written before coding):
|
||||
/// UNARMED distinct > 0 -> the escaped writers, by address -> dumpmap + decompile them next;
|
||||
/// UNARMED = 0, armed > 0 -> writers fully covered by the scrubs -> value family truly closed
|
||||
/// at the FS level -> next stage is the Vulkan object level
|
||||
/// (clear/loadOp execution, view identity below the Texture cache);
|
||||
/// registry = 0 in the heartbeat -> VOID (cache ON or NANSCRUB missing), fix the run.
|
||||
/// </summary>
|
||||
static class MvppWriterCensusProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVWRITER_CENSUS") == "1";
|
||||
|
||||
private static bool _armedLogged;
|
||||
|
||||
private static long _armedDraws;
|
||||
private static long _unarmedDraws;
|
||||
private static long _maskedOffDraws;
|
||||
private static long _summaryMs;
|
||||
|
||||
private static readonly HashSet<ulong> _armedFs = new();
|
||||
private static readonly HashSet<ulong> _unarmedFs = new();
|
||||
private static readonly HashSet<ulong> _maskedOffFs = new();
|
||||
|
||||
/// <summary>Frame boundary (Window present): arming witness + 3s heartbeat, zeros included.</summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[CENSUS] ARMED (RYUJINX_MVWRITER_CENSUS=1). Cross-checking MV-buffer writers against the fingerprint-armed registry.");
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _summaryMs >= 3000)
|
||||
{
|
||||
_summaryMs = now;
|
||||
|
||||
int registry = NanScrubProbe.ArmedAddressCount();
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[CENSUS/HEARTBEAT ~3s v3-program] registry={registry} | ARMED-program draws={_armedDraws} distinctVA={_armedFs.Count} | " +
|
||||
$"UNARMED-program WRITING (mask!=0): draws={_unarmedDraws} distinctVA={_unarmedFs.Count} | " +
|
||||
$"bound-but-masked-off: draws={_maskedOffDraws} distinctVA={_maskedOffFs.Count}" +
|
||||
(registry == 0 ? " *** REGISTRY EMPTY: cache ON or NANSCRUB missing = VOID ***" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Per-draw (StateUpdater probe block): classify draws whose colour target is the MV buffer.
|
||||
/// v2: a draw with the MV slot bound but its colour write mask OFF writes nothing -- counted
|
||||
/// apart, or the whole static-geometry MRT set shows up as false-positive "unarmed writers".
|
||||
/// v3: classification by PROGRAM identity (ShaderProgramInfo.MvppMvEncodeArmed), not by VA --
|
||||
/// the guest places the same code at many VAs and the cache dedups by code, so only the
|
||||
/// first VA ever reaches translation and the VA registry misses every mirror (journal 163).</summary>
|
||||
public static void OnDraw(TextureManager texMgr, ulong fsAddr, ReadOnlySpan<uint> writeMasks, Ryujinx.Graphics.Shader.ShaderProgramInfo fragInfo)
|
||||
{
|
||||
if (texMgr == null || fsAddr == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++)
|
||||
{
|
||||
if (!MvppMvSyncProbe.IsMvBuffer(texMgr.GetColorTarget(slot)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint mask = slot < writeMasks.Length ? writeMasks[slot] : 0xFu;
|
||||
|
||||
if (mask == 0)
|
||||
{
|
||||
_maskedOffDraws++;
|
||||
|
||||
bool newMasked;
|
||||
lock (_maskedOffFs)
|
||||
{
|
||||
newMasked = _maskedOffFs.Add(fsAddr);
|
||||
}
|
||||
|
||||
if (newMasked)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[CENSUS] bound-but-masked-off writer: fs=0x{fsAddr:X} slot={slot} | distinct: {_maskedOffFs.Count}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (fragInfo != null && fragInfo.MvppMvEncodeArmed)
|
||||
{
|
||||
_armedDraws++;
|
||||
lock (_armedFs)
|
||||
{
|
||||
_armedFs.Add(fsAddr);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_unarmedDraws++;
|
||||
|
||||
bool isNew;
|
||||
lock (_unarmedFs)
|
||||
{
|
||||
isNew = _unarmedFs.Add(fsAddr);
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[CENSUS] MV writer whose PROGRAM carries no fingerprint AND WRITING: fs=0x{fsAddr:X} slot={slot} mask=0x{mask:X} | escaped every value scrub | distinct unarmed VAs: {_unarmedFs.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
return; // one classification per draw, even if bound on several slots
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
public float ProbeMaxLod { get; }
|
||||
public float ProbeMipLodBias { get; }
|
||||
|
||||
/// <summary>
|
||||
/// [GLOWPROBE, 21/07, read-only] Texture addressing modes and border colour, kept for the
|
||||
/// Xenoblade 2 light-halo dossier. The halos are sliced off at the edge of their quad
|
||||
/// instead of fading out - proven abnormal against a TOTK control where the same buffer
|
||||
/// shows no hard edge at all - and the addressing mode is what decides what a sample
|
||||
/// beyond the texture edge returns. Never read by any rendering path.
|
||||
/// </summary>
|
||||
public AddressMode ProbeAddressU { get; }
|
||||
public AddressMode ProbeAddressV { get; }
|
||||
public float ProbeBorderA { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Host sampler object.
|
||||
/// </summary>
|
||||
@@ -99,6 +110,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
ProbeMinLod = minLod;
|
||||
ProbeMaxLod = maxLod;
|
||||
ProbeMipLodBias = mipLodBias;
|
||||
ProbeAddressU = addressU;
|
||||
ProbeAddressV = addressV;
|
||||
ProbeBorderA = descriptor.BorderColorA;
|
||||
|
||||
float maxRequestedAnisotropy = descriptor.UnpackMaxAnisotropy();
|
||||
float maxSupportedAnisotropy = context.Capabilities.MaximumSupportedAnisotropy;
|
||||
|
||||
@@ -246,6 +246,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public MultiRange Range { get; private set; }
|
||||
|
||||
/// <summary>[MVPP] Accès lecture seule à la mémoire physique invitée, pour la sonde PRESYNC GUEST.</summary>
|
||||
internal PhysicalMemory PhysicalMemory => _physicalMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Layer size in bytes.
|
||||
/// </summary>
|
||||
@@ -833,6 +836,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param>
|
||||
public void CreateCopyDependency(Texture contained, int layer, int level, bool copyTo)
|
||||
{
|
||||
MvppCacheProbe.OnCopyDependency(this, contained); // read-only (gated)
|
||||
|
||||
if (contained.Group == Group)
|
||||
{
|
||||
return;
|
||||
@@ -904,7 +909,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
// 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).
|
||||
if (copy && scale == 1f && ScaleFactor != MathF.Floor(ScaleFactor))
|
||||
// [31/07] GATE AJOUTE, meme raison que sa jumelle dans SemaphoreUpdater : elle etait
|
||||
// en release sans interrupteur et se declenche chez tout utilisateur en DLSS quality
|
||||
// ou performance, qui produit une echelle fractionnaire par construction.
|
||||
if (GAL.MvppDev.Enabled && copy && scale == 1f && ScaleFactor != MathF.Floor(ScaleFactor))
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"MVPP fract-probe: descale flush {Info.Width}x{Info.Height} {Info.FormatInfo.Format} target {Info.Target} from scale {ScaleFactor}.");
|
||||
@@ -1015,6 +1023,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public void SynchronizeMemory()
|
||||
{
|
||||
MvppCacheProbe.OnSynchronize(); // read-only (gated)
|
||||
|
||||
if (Target == Target.TextureBuffer)
|
||||
{
|
||||
return;
|
||||
@@ -1027,8 +1037,14 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
_dirty = false;
|
||||
|
||||
// Le VRAI travail : seul un appel qui trouve la texture SALE recharge des données. Les
|
||||
// ~367 000 "resynchronisations" comptées avant étaient surtout des appels qui ressortent ici
|
||||
// sans rien faire. C'est ce compteur-ci qui dit si le cache RECHARGE en boucle.
|
||||
MvppCacheProbe.OnRealSync(Info.Width, Info.Height, Info.FormatInfo.Format, _hasData); // read-only (gated)
|
||||
|
||||
if (_hasData)
|
||||
{
|
||||
MvppMap64Probe.OnGroupSync(this); // [MAP64] read-only (gated): partial syncs onto the small DoF maps
|
||||
Group.SynchronizeMemory(this);
|
||||
}
|
||||
else
|
||||
@@ -1062,6 +1078,13 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Range);
|
||||
|
||||
// [FULLSYNC, READ-ONLY] Report which surfaces get a full upload from guest memory, and
|
||||
// whether that memory is empty. Gated; does nothing to the data or the upload.
|
||||
MvppFullSyncProbe.OnFullSync(Info, data, IsView);
|
||||
|
||||
// [MAP64, READ-ONLY] Same question, filtered on the small DoF maps (gated).
|
||||
MvppMap64Probe.OnFullSync(this, data);
|
||||
|
||||
// [MIPPROBE, READ-ONLY] Measure mip alpha coverage once per texture. Gated; does nothing to data/upload.
|
||||
if (_mipProbe && !_mipProbed)
|
||||
{
|
||||
|
||||
@@ -138,6 +138,31 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
|
||||
/// <summary>[MV++ UI audit, read-only] Like MvppEnumerateInputs but also reports the shader stage.</summary>
|
||||
/// <summary>[GLOWPROBE, read-only] Same as MvppEnumerateInputsStage but also hands over the
|
||||
/// SAMPLER bound with each texture: the Xenoblade 2 halo dossier needs the addressing mode,
|
||||
/// which is what decides what a read past the texture edge returns.</summary>
|
||||
public void MvppEnumerateInputsWithSampler(System.Action<int, Texture, Sampler> report)
|
||||
{
|
||||
for (int stage = 0; stage < _textureBindings.Length; stage++)
|
||||
{
|
||||
TextureBindingInfo[] texs = _textureBindings[stage];
|
||||
for (int i = 0; i < texs.Length; i++)
|
||||
{
|
||||
TextureBindingInfo b = texs[i];
|
||||
if (b.ArrayLength > 1 || b.Binding < 0 || b.Binding >= _textureState.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Texture t = _textureState[b.Binding].CachedTexture;
|
||||
if (t != null)
|
||||
{
|
||||
report(stage, t, _textureState[b.Binding].CachedSampler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void MvppEnumerateInputsStage(System.Action<int, Texture> report)
|
||||
{
|
||||
for (int stage = 0; stage < _textureBindings.Length; stage++)
|
||||
@@ -608,6 +633,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
return true;
|
||||
}
|
||||
|
||||
MvppDofProbe.AnnounceOnce(); // [DOFPROBE] one-shot gate-state line on the real draw path, even when OFF
|
||||
MvppFeedbackProbe.AnnounceOnce(); // [FEEDBACKPROBE] one-shot gate-state line, even when OFF
|
||||
MvppTraceProbe.AnnounceOnce(); // [TRACEPROBE] one-shot gate-state line, even when OFF
|
||||
MvppScenePassProbe.AnnounceOnce(); // [SCENEPROBE] one-shot gate-state line, even when OFF
|
||||
MvppTaaProbe.AnnounceOnce(); // [TAAPROBE] one-shot gate-state line, even when OFF
|
||||
|
||||
bool specStateMatches = true;
|
||||
|
||||
int cachedTextureBufferIndex = -1;
|
||||
@@ -651,11 +682,45 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
state.SamplerHandle == samplerId &&
|
||||
state.CachedTexture != null &&
|
||||
state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence &&
|
||||
state.CachedSampler?.IsDisposed != true)
|
||||
state.CachedSampler?.IsDisposed != true &&
|
||||
!MvppTwinFixProbe.NeedsRebind(state.CachedTexture)) // [TWINFIX] one forced re-resolve once classified
|
||||
{
|
||||
// The texture is already bound.
|
||||
state.CachedTexture.SynchronizeMemory();
|
||||
|
||||
if (MvppDofProbe.Enabled)
|
||||
{
|
||||
// [DOFPROBE] cached (fast) path: a stable input re-bound this draw.
|
||||
MvppDofProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||
}
|
||||
|
||||
if (MvppTaaProbe.Enabled)
|
||||
{
|
||||
// [TAAPROBE] meme point d'observation, sans verrou : deux comparaisons
|
||||
// d'entiers eliminent tout le trafic qui n'est pas pleine resolution.
|
||||
MvppTaaProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||
}
|
||||
|
||||
if (MvppFeedbackProbe.Enabled)
|
||||
{
|
||||
MvppFeedbackProbe.OnRead(state.CachedTexture); // [FEEDBACKPROBE] input read (fast path)
|
||||
}
|
||||
|
||||
if (MvppTraceProbe.Enabled)
|
||||
{
|
||||
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), state.CachedTexture); // [TRACEPROBE] dep edge (fast path)
|
||||
}
|
||||
|
||||
if (MvppScenePassProbe.Enabled)
|
||||
{
|
||||
MvppScenePassProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); // [SCENEPROBE] per-draw input (fast path)
|
||||
}
|
||||
|
||||
MvppBuilderInProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); // [BUILDERIN] per-draw input (fast path, self-gated)
|
||||
MvppMap64Probe.OnInput(state.CachedTexture, stage, bindingInfo.Handle); // [MAP64] per-draw input (fast path, self-gated)
|
||||
MvppMvBufProbe.OnSampled(state.CachedTexture); // [MVBUF] identity of the sampled MV buffer (self-gated)
|
||||
MvppTwinMapProbe.OnRead(state.CachedTexture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated)
|
||||
|
||||
state.CachedTexture.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty
|
||||
|
||||
if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 &&
|
||||
@@ -683,6 +748,44 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
MvppPairProbe.OnPair(texture, sampler); // read-only (gated)
|
||||
}
|
||||
|
||||
if (MvppDofProbe.Enabled)
|
||||
{
|
||||
// [DOFPROBE] resolve (cache-miss) path: an input re-fetched this draw. The DoF
|
||||
// feedback/history buffer is re-invalidated every frame, so it lands here often.
|
||||
MvppDofProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||
}
|
||||
|
||||
if (MvppTaaProbe.Enabled)
|
||||
{
|
||||
// [TAAPROBE] chemin de resolution (defaut de cache) : c'est celui ou un
|
||||
// historique temporel, re-invalide a chaque image, atterrit le plus souvent.
|
||||
MvppTaaProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage);
|
||||
}
|
||||
|
||||
if (MvppFeedbackProbe.Enabled)
|
||||
{
|
||||
MvppFeedbackProbe.OnRead(texture); // [FEEDBACKPROBE] input read (resolve path)
|
||||
}
|
||||
|
||||
if (MvppTraceProbe.Enabled)
|
||||
{
|
||||
MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), texture); // [TRACEPROBE] dep edge (resolve path)
|
||||
}
|
||||
|
||||
if (MvppScenePassProbe.Enabled)
|
||||
{
|
||||
MvppScenePassProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); // [SCENEPROBE] per-draw input (resolve path)
|
||||
}
|
||||
|
||||
MvppBuilderInProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); // [BUILDERIN] per-draw input (resolve path, self-gated)
|
||||
MvppMap64Probe.OnInput(texture, stage, bindingInfo.Handle); // [MAP64] per-draw input (resolve path, self-gated)
|
||||
MvppMvBufProbe.OnSampled(texture); // [MVBUF] identity of the sampled MV buffer (self-gated)
|
||||
MvppTwinMapProbe.OnRead(texture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated)
|
||||
|
||||
// [TWINFIX] (EXP 21) read-side redirect: reads of the sky-only twin are served by
|
||||
// the material twin. Self-gated; returns the input texture unless armed+matched.
|
||||
texture = MvppTwinFixProbe.MaybeRedirect(texture, stage, bindingInfo.Handle);
|
||||
|
||||
texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty
|
||||
|
||||
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
|
||||
@@ -798,6 +901,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
// The texture is already bound.
|
||||
cachedTexture.SynchronizeMemory();
|
||||
|
||||
MvppTwinXferProbe.OnImageBind(cachedTexture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated
|
||||
|
||||
if (isStore)
|
||||
{
|
||||
cachedTexture.SignalModified();
|
||||
@@ -837,6 +942,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
else
|
||||
{
|
||||
MvppTwinXferProbe.OnImageBind(texture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated
|
||||
|
||||
if (isStore)
|
||||
{
|
||||
texture?.SignalModified();
|
||||
|
||||
@@ -66,6 +66,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
|
||||
_overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
|
||||
|
||||
// [VIEWALIAS] / [FULLSYNC] / [MAP64] Positive controls: make the probes' silence interpretable.
|
||||
MvppViewAliasProbe.ReportArmed();
|
||||
MvppFullSyncProbe.ReportArmed();
|
||||
MvppMap64Probe.ReportArmed();
|
||||
MvppMvBufProbe.ReportArmed();
|
||||
|
||||
_cache = [];
|
||||
}
|
||||
|
||||
@@ -872,6 +878,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
info = info.CreateInfoWithFormat(overlap.Info.FormatInfo);
|
||||
}
|
||||
|
||||
// [VIEWALIAS] Read-only: report views created over a parent of a DIFFERENT format.
|
||||
MvppViewAliasProbe.OnViewCreated(info, overlap, oInfo.FirstLayer, oInfo.FirstLevel, oInfo.Compatibility.ToString());
|
||||
|
||||
texture = overlap.CreateView(info, sizeInfo, range.Value, oInfo.FirstLayer, oInfo.FirstLevel);
|
||||
texture.SynchronizeMemory();
|
||||
}
|
||||
|
||||
@@ -363,6 +363,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
if (group.NeedsCopy && group.Copy(_context))
|
||||
{
|
||||
MvppMvSyncProbe.OnCopyIn(Storage); // [MVSYNC] read-only, self-gated
|
||||
anyModified |= true; // The copy target has been modified.
|
||||
handleDirty = false;
|
||||
}
|
||||
@@ -386,7 +387,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
if (dirty)
|
||||
{
|
||||
if (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
|
||||
|
||||
if (partialPath)
|
||||
{
|
||||
// Partial texture invalidation. Only update the layers/levels with dirty flags of the storage.
|
||||
|
||||
|
||||
@@ -114,6 +114,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_gpBindingsManager.MvppEnumerateInputsStage(report);
|
||||
}
|
||||
|
||||
/// <summary>[GLOWPROBE, read-only] Bound textures WITH their sampler.</summary>
|
||||
public void MvppEnumerateGraphicsInputsWithSampler(System.Action<int, Texture, Sampler> report)
|
||||
{
|
||||
_gpBindingsManager.MvppEnumerateInputsWithSampler(report);
|
||||
}
|
||||
|
||||
/// <summary>[MV++ UI audit, read-only] Enumerates the currently-bound colour render targets (all 8 slots).</summary>
|
||||
public void MvppEnumerateRenderTargets(System.Action<int, Texture> report)
|
||||
{
|
||||
@@ -253,6 +259,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
color.DemoteForcedMipsOnRenderTargetBind(); // [MIPS-RT fix] no-op hors boost (2 tests de champ)
|
||||
}
|
||||
|
||||
if (MvppFeedbackProbe.Enabled && color != null)
|
||||
{
|
||||
MvppFeedbackProbe.OnWrite(color); // [FEEDBACKPROBE] colour target bound = write (gated)
|
||||
}
|
||||
|
||||
_rtColors[index] = color;
|
||||
}
|
||||
|
||||
@@ -311,6 +322,18 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public Texture RenderTargetColor0 => _rtColors[0];
|
||||
|
||||
/// <summary>
|
||||
/// [E0/SCENEPROBE read-only] Number of colour render-target (MRT) slots. Lets the scene probe
|
||||
/// enumerate every out_attr_N attachment. No behaviour change.
|
||||
/// </summary>
|
||||
public int ColorTargetsLength => _rtColors.Length;
|
||||
|
||||
/// <summary>
|
||||
/// [E0/SCENEPROBE read-only] Bound colour render target at the given MRT slot, or null. Lets the
|
||||
/// scene probe log every out_attr_N attachment format (esp. out_attr1, the feedback target).
|
||||
/// </summary>
|
||||
public Texture GetColorTarget(int index) => (uint)index < (uint)_rtColors.Length ? _rtColors[index] : null;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the host clip region, which should be the intersection of all render target texture sizes.
|
||||
/// </summary>
|
||||
|
||||
@@ -598,7 +598,25 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
int gobBlocksInZ = descriptor.UnpackGobBlocksInZ();
|
||||
if (target != Target.Texture3D && gobBlocksInZ > 1 && depthOrLayers > 1)
|
||||
{
|
||||
gobBlocksInZ = 1;
|
||||
// [GOBPROBE, 21/07] This clamp is a FORK addition (it was put in for the BOTW/TOTK
|
||||
// 4K packs); upstream honours the descriptor. It overrides the block-linear stride
|
||||
// the game declared, and getting that stride wrong is precisely what produces
|
||||
// RECTANGLES OF DISPLACED-BUT-REAL CONTENT -- the shape of the Xenoblade 2 artefact
|
||||
// (blocky rows, worse indoors where more array textures are in flight, appearing as
|
||||
// the camera turns and brings new ones in).
|
||||
//
|
||||
// Read-only for now: the clamp still applies exactly as before, we only COUNT it and
|
||||
// describe what it hit. If it never fires on XC2 the lead dies here; if it fires
|
||||
// constantly we have the suspect, and RYUJINX_NO_GOBZ_CLAMP=1 then tests it for real.
|
||||
if (MvppGobProbe.Enabled)
|
||||
{
|
||||
MvppGobProbe.NoteClamp(width, height, depthOrLayers, gobBlocksInZ, target.ToString(), formatInfo.Format.ToString());
|
||||
}
|
||||
|
||||
if (!MvppGobProbe.ClampDisabled)
|
||||
{
|
||||
gobBlocksInZ = 1;
|
||||
}
|
||||
}
|
||||
int gobBlocksInTileX = descriptor.UnpackGobBlocksInTileX();
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
}
|
||||
|
||||
_externalFlushDelegate = ExternalFlush;
|
||||
_loadDelegate = LoadRegion;
|
||||
_loadDelegate = _bufCoalEnabled ? LoadRegionCoalesced : LoadRegion;
|
||||
_modifiedDelegate = RegionModified;
|
||||
|
||||
_virtualDependenciesLock = new ReaderWriterLockSlim();
|
||||
@@ -283,16 +283,128 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// </remarks>
|
||||
/// <param name="address">Start address of the range to synchronize</param>
|
||||
/// <param name="size">Size in bytes of the range to synchronize</param>
|
||||
// [Beast Roofer diag] RYUJINX_BUFSEQ=1 (EXP 13, gated OFF by default): defeat the
|
||||
// sequence-number short-circuit of buffer synchronization. Within one sequence, a buffer
|
||||
// region is checked for CPU writes at most once -- so a game writing constants MID-frame
|
||||
// (CPU running ahead, standard) has those writes served only NEXT sequence: late-frame
|
||||
// draws read one-generation-stale data. That is the LAST standing family for the XC2
|
||||
// stale-motion artifact (journal 145): patchy per draw, motion-only, barrier-insensitive.
|
||||
// With the flag on, every query runs with a fresh sequence number (full recheck; slower,
|
||||
// cannot be wrong). Artifact gone => root found; see the [BUFSEQ] witness counter.
|
||||
private static readonly bool _bufSeqForce =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_BUFSEQ") == "1";
|
||||
|
||||
private static int _bufSeqCounter = int.MaxValue / 2;
|
||||
private static long _bufSeqCalls;
|
||||
private static long _bufSeqLogMs;
|
||||
|
||||
// [BUFCOAL 02/08, journal (364)-(365)] Coalescence a trou tolere des uploads invite->hote
|
||||
// (RYUJINX_BUFCOAL=1, OFF par defaut = chemin stock a l'octet pres). Mesure (362)-(363) :
|
||||
// en rotation BOTW le fil GPU emet ~17 000 SetBufferData par vraie image (miettes de
|
||||
// 160 o a 4 Ko), et A ~50 000 appels/s la machinerie de la file threadee par commande
|
||||
// coute autant que les octets. Ce gate fusionne les regions sales d'un MEME buffer
|
||||
// distantes de moins de RYUJINX_BUFCOAL_GAP octets (defaut 4096, borne 64 Ko) en UN
|
||||
// LoadRegion, a l'interieur d'UN SEUL SynchronizeMemory (jamais de report inter-appel).
|
||||
// ⚠️ SURETE : re-uploader des octets PROPRES n'est correct que si l'hote n'a pas de
|
||||
// donnees que l'invite n'a pas — donc coalescence UNIQUEMENT quand _modifiedRanges est
|
||||
// null (aucune plage ecrite par le GPU) ; sinon repli immediat sur le chemin stock
|
||||
// (ExcludeModifiedRegions doit continuer de decouper autour des plages GPU).
|
||||
private static readonly bool _bufCoalEnabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_BUFCOAL") == "1";
|
||||
|
||||
private static readonly ulong _bufCoalGap =
|
||||
ulong.TryParse(Environment.GetEnvironmentVariable("RYUJINX_BUFCOAL_GAP"), out ulong g)
|
||||
? Math.Min(g, 0x10000)
|
||||
: 0x1000;
|
||||
|
||||
private static bool _bufCoalArmedLogged;
|
||||
|
||||
private ulong _coalStart = ulong.MaxValue;
|
||||
private ulong _coalEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Gated replacement for the load delegate: merges dirty regions of this buffer that are
|
||||
/// within the tolerated gap into one pending window, flushed by <see cref="FlushCoalesced"/>
|
||||
/// at the end of the synchronization. Falls back to the stock path the moment the buffer
|
||||
/// has GPU-modified ranges (see the [BUFCOAL] safety note above).
|
||||
/// </summary>
|
||||
private void LoadRegionCoalesced(ulong mAddress, ulong mSize)
|
||||
{
|
||||
if (!_bufCoalArmedLogged)
|
||||
{
|
||||
_bufCoalArmedLogged = true;
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"[BUFCOAL] ARME (RYUJINX_BUFCOAL=1, trou tolere {_bufCoalGap} o) - coalescence des uploads buffer.");
|
||||
}
|
||||
|
||||
if (_modifiedRanges != null)
|
||||
{
|
||||
FlushCoalesced();
|
||||
LoadRegion(mAddress, mSize);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ulong end = mAddress + mSize;
|
||||
|
||||
if (_coalStart == ulong.MaxValue)
|
||||
{
|
||||
_coalStart = mAddress;
|
||||
_coalEnd = end;
|
||||
}
|
||||
else if (mAddress <= _coalEnd + _bufCoalGap && end + _bufCoalGap >= _coalStart)
|
||||
{
|
||||
_coalStart = Math.Min(_coalStart, mAddress);
|
||||
_coalEnd = Math.Max(_coalEnd, end);
|
||||
}
|
||||
else
|
||||
{
|
||||
FlushCoalesced();
|
||||
_coalStart = mAddress;
|
||||
_coalEnd = end;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Uploads the pending coalesced window, if any. Must run before leaving the
|
||||
/// synchronization that produced it (the window never survives across calls).</summary>
|
||||
private void FlushCoalesced()
|
||||
{
|
||||
if (_coalStart != ulong.MaxValue)
|
||||
{
|
||||
ulong start = _coalStart;
|
||||
ulong size = _coalEnd - _coalStart;
|
||||
_coalStart = ulong.MaxValue;
|
||||
|
||||
LoadRegion(start, size);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SynchronizeMemory(ulong address, ulong size)
|
||||
{
|
||||
if (_useGranular)
|
||||
{
|
||||
_memoryTrackingGranular.QueryModified(address, size, _modifiedDelegate, _context.SequenceNumber);
|
||||
int seq = _context.SequenceNumber;
|
||||
|
||||
if (_bufSeqForce)
|
||||
{
|
||||
seq = System.Threading.Interlocked.Increment(ref _bufSeqCounter);
|
||||
|
||||
long calls = ++_bufSeqCalls;
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _bufSeqLogMs >= 3000)
|
||||
{
|
||||
_bufSeqLogMs = now;
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
|
||||
$"[BUFSEQ] forced full rechecks so far: {calls}");
|
||||
}
|
||||
}
|
||||
|
||||
_memoryTrackingGranular.QueryModified(address, size, _modifiedDelegate, seq);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_context.SequenceNumber != _sequenceNumber && _memoryTracking.DirtyOrVolatile())
|
||||
if ((_context.SequenceNumber != _sequenceNumber || _bufSeqForce) && _memoryTracking.DirtyOrVolatile())
|
||||
{
|
||||
_memoryTracking.Reprotect();
|
||||
|
||||
@@ -304,6 +416,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
BackingState.RecordSet();
|
||||
_context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size));
|
||||
MvppPalProbe.OnUpload(Address, Size, site: 0, _physicalMemory); // [PAL/UPVOL] read-only (gated): full guest->host upload
|
||||
CopyToDependantVirtualBuffers();
|
||||
}
|
||||
|
||||
@@ -324,12 +437,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadRegion(_dirtyStart, _dirtyEnd - _dirtyStart);
|
||||
_loadDelegate(_dirtyStart, _dirtyEnd - _dirtyStart);
|
||||
}
|
||||
|
||||
_dirtyStart = ulong.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (_bufCoalEnabled)
|
||||
{
|
||||
// [BUFCOAL] la fenetre en attente ne survit jamais a la synchro qui l'a produite.
|
||||
FlushCoalesced();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -574,7 +693,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadRegion(mAddress, mSize);
|
||||
_loadDelegate(mAddress, mSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,6 +710,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
|
||||
_context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize));
|
||||
|
||||
MvppPalProbe.OnUpload(mAddress, mSize, site: 1, _physicalMemory); // [PAL/UPVOL] read-only (gated): dirty-region upload (LoadRegion)
|
||||
|
||||
CopyToDependantVirtualBuffers(mAddress, mSize);
|
||||
}
|
||||
|
||||
|
||||
@@ -690,6 +690,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy);
|
||||
MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size, BufferStage.Copy);
|
||||
|
||||
// [PAL] Read-only (gated): does a GPU copy write into the tracked palettes?
|
||||
MvppPalProbe.OnGpuCopy(dstRange.GetSubRange(0).Address, size);
|
||||
|
||||
if (srcRange.Count == 1 && dstRange.Count == 1)
|
||||
{
|
||||
CopyBufferSingleRange(memoryManager, srcRange.GetSubRange(0).Address, dstRange.GetSubRange(0).Address, size);
|
||||
|
||||
@@ -215,6 +215,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStage.TransformFeedback);
|
||||
|
||||
// [PAL] Read-only (gated): does transform feedback write into the tracked palettes?
|
||||
MvppPalProbe.OnXfbBind(index, range);
|
||||
|
||||
_transformFeedbackBuffers[index] = new BufferBounds(range);
|
||||
_transformFeedbackBuffersDirty = true;
|
||||
}
|
||||
@@ -262,6 +265,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
|
||||
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags));
|
||||
|
||||
// [PAL] Read-only (gated): which compute dispatches touch the tracked palettes.
|
||||
MvppPalProbe.OnComputeBind(index, range, (int)flags);
|
||||
|
||||
_cpStorageBuffers.SetBounds(index, range, flags);
|
||||
}
|
||||
|
||||
@@ -286,6 +292,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
|
||||
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags));
|
||||
|
||||
// [PAL] Read-only (gated): guest-hash the XC2 velocity palettes (vertex SSBO slots 0/1).
|
||||
MvppPalProbe.OnBind(stage, index, range, _channel.MemoryManager.Physical);
|
||||
|
||||
if (!buffers.Buffers[index].Range.Equals(range))
|
||||
{
|
||||
_gpStorageBuffersDirty = true;
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Memory.Range;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// [PAL] Read-only probe (RYUJINX_PAL_PROBE=1), inert unless set. v2.
|
||||
/// Watches the XC2 velocity matrix palettes (vertex-stage SSBOs, slots 0/1).
|
||||
///
|
||||
/// v1 lessons (journal 137/138): slot 0 is shared by 7+ different SSBOs (first-bind-per-frame
|
||||
/// sampling compared DIFFERENT buffers across frames), and a 256-byte prefix hash misses
|
||||
/// palette changes past the first matrix. The v1 run still nailed the architecture: the two
|
||||
/// big palettes (0x5DC00 bytes) PING-PONG between slots 0 and 1 every frame.
|
||||
///
|
||||
/// v2 therefore tracks buffers BY ADDRESS, not by slot: every distinct (address, size) bound
|
||||
/// at vertex slots 0/1 with size >= 64 KiB (the big palettes only), with a STRIDED hash over
|
||||
/// the WHOLE range (1 byte every 4093 -- prime stride, ~94 samples for 384 KiB), once per
|
||||
/// frame per buffer. Uploads (Buffer.LoadRegion / full SetBufferData) are attributed per
|
||||
/// tracked buffer. Reading grid:
|
||||
/// - a tracked palette shows guest CHANGES with ZERO intersecting upload that frame, and
|
||||
/// the artifact correlates with motion of skinned objects => STALE DATA SERVED = root;
|
||||
/// - every guest change is matched by an upload => buffer sync innocent for these ranges;
|
||||
/// - palettes never change even with NPCs moving on screen => palettes are not per-frame
|
||||
/// data as assumed: revisit the VS reading.
|
||||
/// </summary>
|
||||
static class MvppPalProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_PAL_PROBE") == "1";
|
||||
|
||||
private const ulong MinTrackedSize = 0x1000; // v5: >=4KiB -- catch skinned-character palettes too
|
||||
private const int MaxTracked = 32;
|
||||
private const int HashStride = 4093;
|
||||
private const int AnomalyLogCap = 24;
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static long _frame;
|
||||
|
||||
private sealed class Rec
|
||||
{
|
||||
public ulong Address;
|
||||
public ulong Size;
|
||||
public uint LastHash;
|
||||
public bool HashValid;
|
||||
public bool SeenThisFrame;
|
||||
public bool ChangedThisFrame;
|
||||
public int UploadsThisFrame;
|
||||
public long FramesSeen;
|
||||
public long FramesChanged;
|
||||
public long FramesChangedWithUpload;
|
||||
public long FramesChangedNoUpload;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<ulong, Rec> _tracked = new();
|
||||
private static readonly object _lock = new();
|
||||
private static long _anomaliesLogged;
|
||||
private static long _summaryMs;
|
||||
|
||||
/// <summary>Vertex-stage storage buffer bound (BufferManager.SetGraphicsStorageBuffer). Self-gated.</summary>
|
||||
public static void OnBind(int stage, int index, MultiRange range, PhysicalMemory pm)
|
||||
{
|
||||
if (!Enabled || stage != 0 || index > 5 || pm == null) // v5: slots 0..5
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[PAL] ARMED v2 (RYUJINX_PAL_PROBE=1). Tracking big vertex SSBOs (slots 0/1, >=64KiB) BY ADDRESS: strided guest hash vs host uploads, per frame.");
|
||||
}
|
||||
|
||||
MemoryRange sub = range.GetSubRange(0);
|
||||
if (sub.Address == MemoryManager.PteUnmapped || sub.Size < MinTrackedSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_tracked.TryGetValue(sub.Address, out Rec rec))
|
||||
{
|
||||
if (_tracked.Count >= MaxTracked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tracked[sub.Address] = rec = new Rec { Address = sub.Address, Size = sub.Size };
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[PAL] TRACKING palette 0x{sub.Address:X} size 0x{sub.Size:X} (slot {index}, {_tracked.Count} tracked)");
|
||||
}
|
||||
|
||||
if (rec.SeenThisFrame)
|
||||
{
|
||||
return; // one hash per buffer per frame
|
||||
}
|
||||
|
||||
rec.SeenThisFrame = true;
|
||||
rec.FramesSeen++;
|
||||
|
||||
ReadOnlySpan<byte> data = pm.GetSpan(rec.Address, (int)rec.Size);
|
||||
|
||||
uint hash = 2166136261;
|
||||
for (int i = 0; i < data.Length; i += HashStride)
|
||||
{
|
||||
hash = (hash ^ data[i]) * 16777619;
|
||||
}
|
||||
|
||||
rec.ChangedThisFrame = rec.HashValid && hash != rec.LastHash;
|
||||
rec.LastHash = hash;
|
||||
rec.HashValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// v3: identify the PRODUCER. v2 measured that no tracked palette is ever CPU-written
|
||||
// (guest hash frozen over 3600 frames with NPCs moving) => they are GPU-written. This
|
||||
// hook logs which COMPUTE dispatches bind a tracked range as storage, with the usage
|
||||
// flags -- write-usage binds name the producer pass.
|
||||
private static readonly HashSet<(ulong, int, int)> _computeBinds = new();
|
||||
|
||||
/// <summary>Compute storage buffer bound (BufferManager.SetComputeStorageBuffer). Self-gated.</summary>
|
||||
public static void OnComputeBind(int index, MultiRange range, int flags)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MemoryRange sub = range.GetSubRange(0);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (Rec rec in _tracked.Values)
|
||||
{
|
||||
if (sub.Address < rec.Address + rec.Size &&
|
||||
rec.Address < sub.Address + sub.Size)
|
||||
{
|
||||
if (_computeBinds.Add((rec.Address, index, flags)))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[PAL] COMPUTE BINDS palette 0x{rec.Address:X} at c-slot {index} flags=0x{flags:X} (bind range 0x{sub.Address:X}+0x{sub.Size:X})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v4: v3 measured ZERO compute binds on the tracked palettes -- the producer is not a
|
||||
// compute SSBO write. Remaining GPU write paths: TRANSFORM FEEDBACK (vertex-shader
|
||||
// skinning into buffers, classic for this engine generation) and DMA buffer copies.
|
||||
private static readonly HashSet<(ulong, int)> _xfbBinds = new();
|
||||
private static readonly HashSet<(ulong, ulong)> _copyHits = new();
|
||||
|
||||
/// <summary>Transform feedback buffer bound (BufferManager.SetTransformFeedbackBuffer). Self-gated.</summary>
|
||||
public static void OnXfbBind(int index, MultiRange range)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MemoryRange sub = range.GetSubRange(0);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (Rec rec in _tracked.Values)
|
||||
{
|
||||
if (sub.Address < rec.Address + rec.Size &&
|
||||
rec.Address < sub.Address + sub.Size)
|
||||
{
|
||||
if (_xfbBinds.Add((rec.Address, index)))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[PAL] *** TRANSFORM FEEDBACK WRITES palette 0x{rec.Address:X} *** (xfb slot {index}, bind 0x{sub.Address:X}+0x{sub.Size:X})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>GPU buffer copy (BufferCache.CopyBuffer). Self-gated; physical addresses.</summary>
|
||||
public static void OnGpuCopy(ulong dstAddress, ulong size)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (Rec rec in _tracked.Values)
|
||||
{
|
||||
if (dstAddress < rec.Address + rec.Size &&
|
||||
rec.Address < dstAddress + size)
|
||||
{
|
||||
if (_copyHits.Add((rec.Address, dstAddress)))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[PAL] *** GPU COPY WRITES palette 0x{rec.Address:X} *** (dst 0x{dstAddress:X}+0x{size:X})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Guest-to-host buffer upload (Buffer.LoadRegion / full SetBufferData). Self-gated.
|
||||
/// site : 0 = SetBufferData COMPLET (creation/reset, Buffer.cs:~338), 1 = LoadRegion
|
||||
/// (region sale d'un buffer existant, Buffer.cs:~626). Sert au split UPVOL v2 (362).</summary>
|
||||
public static void OnUpload(ulong address, ulong size, int site = 0, PhysicalMemory pm = null)
|
||||
{
|
||||
// [UPVOL] compteur de volume independant (gate RYUJINX_UPVOL, inerte sinon) --
|
||||
// partage ces sites d'appel pour ne pas toucher une 2e fois au chemin chaud.
|
||||
MvppUpVolProbe.OnUpload(address, size, site, pm);
|
||||
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (Rec rec in _tracked.Values)
|
||||
{
|
||||
if (rec.SeenThisFrame &&
|
||||
address < rec.Address + rec.Size &&
|
||||
rec.Address < address + size)
|
||||
{
|
||||
rec.UploadsThisFrame++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Guest frame boundary (Window present). Folds per-frame flags into the verdict counters.</summary>
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_frame++;
|
||||
|
||||
foreach (Rec rec in _tracked.Values)
|
||||
{
|
||||
if (rec.SeenThisFrame && rec.ChangedThisFrame)
|
||||
{
|
||||
rec.FramesChanged++;
|
||||
|
||||
if (rec.UploadsThisFrame > 0)
|
||||
{
|
||||
rec.FramesChangedWithUpload++;
|
||||
}
|
||||
else
|
||||
{
|
||||
rec.FramesChangedNoUpload++;
|
||||
|
||||
if (_anomaliesLogged++ < AnomalyLogCap)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[PAL] frame {_frame}: palette 0x{rec.Address:X} guest CHANGED but ZERO host upload *** STALE DATA SERVED ***");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rec.SeenThisFrame = false;
|
||||
rec.ChangedThisFrame = false;
|
||||
rec.UploadsThisFrame = 0;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _summaryMs >= 3000 && _frame > 0)
|
||||
{
|
||||
_summaryMs = now;
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (Rec rec in _tracked.Values)
|
||||
{
|
||||
if (rec.FramesSeen > 0)
|
||||
{
|
||||
sb.Append($"0x{rec.Address:X}: seen {rec.FramesSeen}, changed {rec.FramesChanged} (ok {rec.FramesChangedWithUpload}, NOUP {rec.FramesChangedNoUpload}); ");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[PAL/SUMMARY ~3s] frames={_frame} | {sb}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// [UPVOL v2] Compteur de VOLUME des uploads invite->hote (RYUJINX_UPVOL=1, inerte sinon).
|
||||
/// Journal (361)-(362). v1 a mesure : rotation = ~45 000 uploads/s, 112-122 Mo/s, tout en
|
||||
/// 4-64 Ko (immobile 3 500/s, 8 Mo/s) => RE-UPLOADS, pas des donnees neuves.
|
||||
/// v2 SPLIT PAR PORTE pour viser le fix :
|
||||
/// site 0 = SetBufferData COMPLET (creation/reset de buffer, Buffer.cs:~338)
|
||||
/// site 1 = LoadRegion (region sale d'un buffer existant, Buffer.cs:~626)
|
||||
/// Rafale au site 1 => fix = politique de synchro (dedup/fusion/dissociation) ;
|
||||
/// rafale au site 0 => fix = politique du cache (retention, tempete de recreation).
|
||||
/// v2 corrige aussi le caveat (362) : DEUX fils appellent (GPU + present) => Interlocked.
|
||||
/// Fenetre ~5 s, histogramme de tailles, log Info une ligne.
|
||||
/// </summary>
|
||||
static class MvppUpVolProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_UPVOL") == "1";
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static long _windowMs;
|
||||
private static long _calls;
|
||||
private static long _bytes;
|
||||
private static long _maxSize;
|
||||
private static long _site0Calls;
|
||||
private static long _site0Bytes;
|
||||
private static long _site1Calls;
|
||||
private static long _site1Bytes;
|
||||
// Buckets: <4K, 4K-64K, 64K-1M, >=1M (nombre, octets)
|
||||
private static readonly long[] _bucketCalls = new long[4];
|
||||
private static readonly long[] _bucketBytes = new long[4];
|
||||
|
||||
// [MIRBENCH v3] micro-banc de vitesse de lecture du MIROIR invite (l'hypothese (366) :
|
||||
// la copie productrice est ~35x plus lente PAR OCTET que la meme copie depuis le pool).
|
||||
// A chaque fenetre : retenir le PLUS GROS upload ; au flush, relire CE range depuis le
|
||||
// miroir (pm.GetSpan -> copie vers un scratch) et copier la MEME taille depuis un tableau
|
||||
// gere -> deux debits Mo/s compares, meme taille, meme scratch. Lecture seule.
|
||||
private static ulong _benchAddr;
|
||||
private static long _benchSize;
|
||||
private static PhysicalMemory _benchPm;
|
||||
private static byte[] _benchScratch;
|
||||
private static byte[] _benchManaged;
|
||||
|
||||
public static void OnUpload(ulong address, ulong size, int site, PhysicalMemory pm)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((long)size > Volatile.Read(ref _benchSize) && pm != null)
|
||||
{
|
||||
_benchAddr = address;
|
||||
_benchPm = pm;
|
||||
Volatile.Write(ref _benchSize, (long)size);
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Info?.Print(LogClass.Gpu, "[UPVOL] ARME v2 (RYUJINX_UPVOL=1) - volume des uploads buffer par porte (0=complet, 1=region), fenetre ~5 s.");
|
||||
}
|
||||
|
||||
long s = (long)size;
|
||||
Interlocked.Increment(ref _calls);
|
||||
Interlocked.Add(ref _bytes, s);
|
||||
|
||||
long seenMax = Volatile.Read(ref _maxSize);
|
||||
while (s > seenMax && Interlocked.CompareExchange(ref _maxSize, s, seenMax) != seenMax)
|
||||
{
|
||||
seenMax = Volatile.Read(ref _maxSize);
|
||||
}
|
||||
|
||||
if (site == 0)
|
||||
{
|
||||
Interlocked.Increment(ref _site0Calls);
|
||||
Interlocked.Add(ref _site0Bytes, s);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref _site1Calls);
|
||||
Interlocked.Add(ref _site1Bytes, s);
|
||||
}
|
||||
|
||||
int b = s < 0x1000 ? 0 : s < 0x10000 ? 1 : s < 0x100000 ? 2 : 3;
|
||||
Interlocked.Increment(ref _bucketCalls[b]);
|
||||
Interlocked.Add(ref _bucketBytes[b], s);
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
long window = Volatile.Read(ref _windowMs);
|
||||
|
||||
if (window == 0)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _windowMs, now, 0);
|
||||
}
|
||||
else if (now - window >= 5000 && Interlocked.CompareExchange(ref _windowMs, now, window) == window)
|
||||
{
|
||||
double sec = (now - window) / 1000.0;
|
||||
long calls = Interlocked.Exchange(ref _calls, 0);
|
||||
long bytes = Interlocked.Exchange(ref _bytes, 0);
|
||||
long max = Interlocked.Exchange(ref _maxSize, 0);
|
||||
long s0c = Interlocked.Exchange(ref _site0Calls, 0);
|
||||
long s0b = Interlocked.Exchange(ref _site0Bytes, 0);
|
||||
long s1c = Interlocked.Exchange(ref _site1Calls, 0);
|
||||
long s1b = Interlocked.Exchange(ref _site1Bytes, 0);
|
||||
long b0c = Interlocked.Exchange(ref _bucketCalls[0], 0);
|
||||
long b0b = Interlocked.Exchange(ref _bucketBytes[0], 0);
|
||||
long b1c = Interlocked.Exchange(ref _bucketCalls[1], 0);
|
||||
long b1b = Interlocked.Exchange(ref _bucketBytes[1], 0);
|
||||
long b2c = Interlocked.Exchange(ref _bucketCalls[2], 0);
|
||||
long b2b = Interlocked.Exchange(ref _bucketBytes[2], 0);
|
||||
long b3c = Interlocked.Exchange(ref _bucketCalls[3], 0);
|
||||
long b3b = Interlocked.Exchange(ref _bucketBytes[3], 0);
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"[UPVOL ~5s] appels {calls} ({calls / sec:F0}/s), total {bytes / 1048576.0:F1} Mo ({bytes / 1048576.0 / sec:F1} Mo/s), max {max / 1024} Ko | " +
|
||||
$"COMPLET: {s0c} ({s0b / 1048576.0:F1} Mo) | REGION: {s1c} ({s1b / 1048576.0:F1} Mo) | " +
|
||||
$"<4K: {b0c} ({b0b / 1024} Ko) | 4-64K: {b1c} ({b1b / 1048576.0:F1} Mo) | 64K-1M: {b2c} ({b2b / 1048576.0:F1} Mo) | >=1M: {b3c} ({b3b / 1048576.0:F1} Mo)");
|
||||
|
||||
RunMirrorBench();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>[MIRBENCH] relit le plus gros upload de la fenetre depuis le miroir et copie
|
||||
/// la meme taille depuis un tableau gere : deux debits compares, une ligne de log.</summary>
|
||||
private static void RunMirrorBench()
|
||||
{
|
||||
long benchSize = Interlocked.Exchange(ref _benchSize, 0);
|
||||
ulong benchAddr = _benchAddr;
|
||||
PhysicalMemory pm = _benchPm;
|
||||
|
||||
if (pm == null || benchSize < 0x4000)
|
||||
{
|
||||
return; // rien d'assez gros dans la fenetre pour un chrono fiable
|
||||
}
|
||||
|
||||
int sz = (int)Math.Min(benchSize, 0x40000);
|
||||
|
||||
if (_benchScratch == null || _benchScratch.Length < sz)
|
||||
{
|
||||
_benchScratch = new byte[sz];
|
||||
_benchManaged = new byte[sz];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Lecture MIROIR, DEUX passes chronometrees : la 1re est FROIDE (TLB/defauts de
|
||||
// premiere lecture = exactement le cout que subit la copie de production, qui ne
|
||||
// lit chaque region qu'une fois), la 2e est chaude (reference haute). Le scratch
|
||||
// est reutilise entre fenetres => sa premiere-touche ne pollue que la fenetre 1.
|
||||
ReadOnlySpan<byte> src = pm.GetSpan(benchAddr, sz);
|
||||
long t0 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
src.CopyTo(_benchScratch);
|
||||
long t1 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
src.CopyTo(_benchScratch);
|
||||
long t2 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
|
||||
// Meme taille, source = tableau gere ordinaire (chaud)
|
||||
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
|
||||
long t3 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
_benchManaged.AsSpan(0, sz).CopyTo(_benchScratch);
|
||||
long t4 = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
|
||||
double freq = System.Diagnostics.Stopwatch.Frequency;
|
||||
double mirrorColdMBs = sz / ((t1 - t0) / freq) / 1048576.0;
|
||||
double mirrorWarmMBs = sz / ((t2 - t1) / freq) / 1048576.0;
|
||||
double managedMBs = sz / ((t4 - t3) / freq) / 1048576.0;
|
||||
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"[MIRBENCH] {sz / 1024} Ko @0x{benchAddr:X}: miroir FROID {mirrorColdMBs:F0} Mo/s, chaud {mirrorWarmMBs:F0} Mo/s, gere {managedMBs:F0} Mo/s (gere/froid {managedMBs / Math.Max(1, mirrorColdMBs):F1}x)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[MIRBENCH] echec lecture @0x{benchAddr:X}+{sz}: {e.Message} (banc ignore cette fenetre)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
Vector4<float> defaultScale = new() { X = 1f, Y = 0f, Z = 0f, W = 0f };
|
||||
_data.RenderScale.AsSpan().Fill(defaultScale);
|
||||
DirtyRenderScale(0, SupportBuffer.RenderScaleMaxCount);
|
||||
|
||||
// [JITTERINIT 02/08, journal (380)] Le bloc jitter est emis dans CHAQUE vertex shader
|
||||
// et LIT JitterOffset a chaque image — mais le champ n'etait marque dirty que quand sa
|
||||
// valeur CHANGEAIT : jitter jamais arme ⇒ zone jamais televersee ⇒ le shader lisait de
|
||||
// la memoire GPU non initialisee (suspect n°1 du dossier ecran-vert, 4 jeux). Marquer
|
||||
// le zero initial dirty garantit un (0,0) reellement present sur le GPU des la
|
||||
// premiere image, exactement comme RenderScale ci-dessus.
|
||||
_data.JitterOffset.X = 0f;
|
||||
_data.JitterOffset.Y = 0f;
|
||||
MarkDirty(SupportBuffer.JitterOffsetOffset, SupportBuffer.FieldSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -22,7 +22,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
private const ushort FileFormatVersionMajor = 1;
|
||||
private const ushort FileFormatVersionMinor = 2;
|
||||
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
|
||||
private const uint CodeGenVersion = 7354;
|
||||
// [JITTEREMIT 03/08] Le bloc de jitter clip-space n'est plus emis dans les vertex shaders
|
||||
// quand le jitter est eteint (il lisait Position en sortie avant de l'ecrire = indefini).
|
||||
// La TRADUCTION change => les caches de shaders existants doivent etre invalides, sinon
|
||||
// les utilisateurs continuent d'executer l'ancien code et le correctif ne les atteint pas.
|
||||
private const uint CodeGenVersion = 7355;
|
||||
|
||||
private const string SharedTocFileName = "shared.toc";
|
||||
private const string SharedDataFileName = "shared.data";
|
||||
@@ -573,7 +577,22 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.ProbeEnabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.RewriteEnabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.CaptureEnabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.LodBiasEnabled;
|
||||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.LodBiasEnabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.HashTestProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MufuPrecProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.RroReduceProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.CocConstProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.TileMapConstProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvKillProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvResolveMvProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvppTileCapProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvppTruncEpsProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvppForceLod0Probe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.MvSignProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.SkyMvProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.NanScrubProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.PeriscopeProbe.Enabled ||
|
||||
Ryujinx.Graphics.Shader.Translation.PeriscopeProbe.Stage2;
|
||||
|
||||
public void AddShader(GpuContext context, CachedShaderProgram program, ReadOnlySpan<byte> hostCode, DiskCacheOutputStreams streams = null)
|
||||
{
|
||||
|
||||
@@ -70,6 +70,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DUMPSPV");
|
||||
private static int _mvppDumpCount;
|
||||
|
||||
// [DUMPMAP] Read-only bridge: when RYUJINX_DUMP_MAP=1, log each dumped shader's guest
|
||||
// address <-> ShaderDumper file paths, so a guest VA (e.g. fs=0x100082C30) maps to its
|
||||
// ShaderNNNN.bin. Gated OFF by default; does not change dumping or translation.
|
||||
private static readonly bool _dumpMap =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_DUMP_MAP") == "1";
|
||||
|
||||
private readonly struct ProgramToSave
|
||||
{
|
||||
public readonly CachedShaderProgram CachedProgram;
|
||||
@@ -847,6 +853,15 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
||||
code ??= memoryManager.GetSpan(context.Address, context.Size).ToArray();
|
||||
|
||||
ShaderDumpPaths paths = dumper?.Dump(code, context.Stage == ShaderStage.Compute) ?? default;
|
||||
|
||||
// [DUMPMAP] Read-only VA<->file bridge (gated RYUJINX_DUMP_MAP=1). Only fires when a dump
|
||||
// actually happened (paths.HasPath). Does not alter dumping or translation.
|
||||
if (_dumpMap && paths.HasPath)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"[DUMPMAP] guest=0x{context.Address:X} stage={context.Stage} full=\"{paths.FullPath}\" code=\"{paths.CodePath}\"");
|
||||
}
|
||||
|
||||
ShaderProgram program = context.Translate(asCompute);
|
||||
|
||||
paths.Prepend(program);
|
||||
|
||||
@@ -119,6 +119,7 @@ namespace Ryujinx.Graphics.Gpu
|
||||
private int _framesAvailable;
|
||||
private long _mvppDepthLogMs;
|
||||
private long _mvppFifoLogMs;
|
||||
private long _jitTraceMs; // [JITTRACE] cadence de l'etape B
|
||||
private int _mvppTakes;
|
||||
private int _mvppTakeHits;
|
||||
private Image.Texture _mvppHeldSceneDepth;
|
||||
@@ -269,10 +270,57 @@ namespace Ryujinx.Graphics.Gpu
|
||||
|
||||
pt.Cache.Tick();
|
||||
|
||||
// [PRESYNC, 21/07] LE test décisif du dossier XC2. Tous les étages de rendu sont propres au
|
||||
// dernier draw, l'image affichée est détruite, et rien entre les deux ne montrait d'anomalie
|
||||
// -- SAUF cette resynchronisation, jamais instrumentée. Si le render target composé sur GPU
|
||||
// est aussi suivi comme mémoire invitée et marqué sale, SynchronizeMemory recharge la mémoire
|
||||
// invitée (périmée, car composée côté GPU) PAR-DESSUS le rendu propre. On capture avant/après.
|
||||
Engine.Threed.MvppPreSyncProbe.Before(texture);
|
||||
|
||||
texture.SynchronizeMemory();
|
||||
|
||||
Engine.Threed.MvppPreSyncProbe.After(texture);
|
||||
|
||||
// [LAYOUT, 21/07] La texture présentée arrive DÉJÀ corrompue (mosaïque en blocs = détuilage).
|
||||
// On compare ce que le JEU a demandé (pt.Info, issu de EnqueueFrameThreadSafe) à ce que le
|
||||
// cache a réellement fourni (texture.Info). Un désaccord de tuilage (isLinear / gobBlocksInY /
|
||||
// stride) est le suspect exact.
|
||||
Engine.Threed.MvppLayoutProbe.Compare(pt.Info, texture);
|
||||
Engine.Threed.MvppPreSyncProbe.DumpGuest(texture);
|
||||
|
||||
MvppDestProbe.RegisterPresented(texture); // read-only destination-anchored probe (gated)
|
||||
|
||||
// [RTDUMP, 21/07] The frame as it actually reaches the screen. Everything captured
|
||||
// so far was an INTERMEDIATE buffer, so "this one looks odd" could never be tied to
|
||||
// "that block, right there, is what I see". Capturing the presented image in the
|
||||
// same burst as the intermediates turns the whole investigation around: point at
|
||||
// the defect, then find which buffer already carries it.
|
||||
Engine.Threed.MvppRtDumpProbe.NotePresented(texture);
|
||||
|
||||
// Vraie frontière d'image pour la sonde UI : sa détection d'origine (montée du facteur de
|
||||
// résolution) est muette sur tout jeu rendu à l'échelle native, XC2 compris.
|
||||
Engine.Threed.MvppUiProbe.OnPresent();
|
||||
Engine.Threed.MvppDrawStepProbe.OnPresent(texture);
|
||||
Engine.Twod.MvppTwodProbe.OnPresent();
|
||||
Image.MvppCacheProbe.OnPresent();
|
||||
Image.MvppMvBufProbe.OnPresent(); // [MVBUF] frame boundary (self-gated)
|
||||
Image.MvppMvSyncProbe.OnPresent(); // [MVSYNC] arming witness + heartbeat (self-gated)
|
||||
Image.MvppWriterCensusProbe.OnPresent(); // [CENSUS] arming witness + heartbeat (self-gated)
|
||||
Image.MvppTwinXferProbe.OnPresent(); // [TWINXFER] arming witness + heartbeat (self-gated)
|
||||
Memory.MvppPalProbe.OnPresent(); // [PAL] frame boundary (self-gated)
|
||||
Image.MvppTaaProbe.OnPresent(); // [TAAPROBE] frontiere d'image (auto-gardee)
|
||||
// [JITTRACE] cadence de l'etape B, voir plus bas.
|
||||
|
||||
if (Image.MvppFeedbackProbe.Enabled)
|
||||
{
|
||||
Image.MvppFeedbackProbe.OnFrameBoundary(); // [FEEDBACKPROBE] guest frame boundary (gated)
|
||||
}
|
||||
|
||||
if (Image.MvppTraceProbe.Enabled)
|
||||
{
|
||||
Image.MvppTraceProbe.OnFrameBoundary(); // [TRACEPROBE] dependency-graph report (gated)
|
||||
}
|
||||
|
||||
float cropScaleX = texture.EffectiveScaleX;
|
||||
float cropScaleY = texture.EffectiveScaleY;
|
||||
|
||||
@@ -321,9 +369,71 @@ namespace Ryujinx.Graphics.Gpu
|
||||
// the queue with the frame.
|
||||
if (DlssCameraState.FifoEnabled)
|
||||
{
|
||||
// [GAMEJITTER 28/07] Le decalage sous-pixel du jeu sort de la file AVEC sa
|
||||
// matrice et entre dans l'anneau de presentation AVEC elle : apparie a la meme
|
||||
// image d'un bout a l'autre, sans champ statique qui pourrait deriver d'une
|
||||
// image. Sur un cycle de 8 phases, declarer la mauvaise phase serait aussi faux
|
||||
// que declarer zero.
|
||||
bool orderedValid = DlssCameraState.TryConsumeOrdered(out System.Numerics.Matrix4x4 orderedVp);
|
||||
|
||||
// [GAMEJITTER 28/07, v2] Le decalage ne passe PLUS par la file ordonnee -- y
|
||||
// ajouter deux flottants la faisait rendre du vide (voir le commentaire de
|
||||
// _fifo). Il entre ici, dans l'anneau de presentation, qui est une file
|
||||
// concurrente donc sure. La valeur est celle de la derniere lecture ACCEPTEE :
|
||||
// elle a ete prise pendant les dessins de cette image, c'est donc la bonne
|
||||
// phase pour l'image qu'on presente.
|
||||
float orderedJx = Engine.Threed.MvppSoloCamera.LastJitterX;
|
||||
float orderedJy = Engine.Threed.MvppSoloCamera.LastJitterY;
|
||||
|
||||
// ⛔ [28/07 17h35] APPEL A 4 PARAMETRES NEUTRALISE — DETTE DE GAMEJITTER.
|
||||
// La surcharge (vp, valid, jx, jy) a ete ajoutee a DlssCameraState le 28/07 a
|
||||
// 11:46, mais le GAL.dll de l'installation date de 10:07 et ne la contient pas.
|
||||
// Tant que le projet Gpu n'est pas recompile, personne ne s'en apercoit ; des
|
||||
// qu'il l'est, l'appel part dans le vide :
|
||||
// MissingMethodException: Method not found: 'Void
|
||||
// DlssCameraState.PublishPresent(Matrix4x4 ByRef, Boolean, Single, Single)'
|
||||
// (plantage constate 28/07 17h30 en deployant MULTIROT ; binaire restaure).
|
||||
//
|
||||
// Reconstruire le GAL reglerait la signature mais REINJECTERAIT GAMEJITTER et
|
||||
// 18 jours de changements dans l'etat valide par Alex -- c'est exactement la
|
||||
// casse du matin. Or GAMEJITTER est MORT : mesure du 28/07, 8 valeurs cote
|
||||
// camera et `brut (0,000;0,000)` cote DLSS, le canal ne transporte rien. On
|
||||
// perd donc zero fonctionnalite en appelant la surcharge a 2 parametres.
|
||||
//
|
||||
// A RETABLIR le jour ou le GAL sera reconstruit volontairement (une seule
|
||||
// ligne, les deux valeurs sont deja calculees juste au-dessus).
|
||||
_ = orderedJx;
|
||||
_ = orderedJy;
|
||||
DlssCameraState.PublishPresent(in orderedVp, orderedValid);
|
||||
|
||||
// [FAMHOLD 01/08] Sonde R1/R2 du mecanisme (290), lecture pure : le hold de
|
||||
// la file ordonnee (LastConsumeFresh == false) EST l'evenement mesure, et il
|
||||
// vient d'etre etabli trois lignes plus haut. Voir MvppFamHold.
|
||||
if (Engine.Threed.MvppFamHold.Enabled)
|
||||
{
|
||||
Engine.Threed.MvppFamHold.OnPresent(DlssCameraState.LastConsumeFresh, in orderedVp);
|
||||
}
|
||||
|
||||
// [EXTPROBE 01/08] Phase T du design E : prediction en memoire + comparaison
|
||||
// au reel suivant, lecture pure. Voir MvppExtProbe.
|
||||
if (Engine.Threed.MvppExtProbe.Enabled)
|
||||
{
|
||||
Engine.Threed.MvppExtProbe.OnPresent(
|
||||
DlssCameraState.LastConsumeFresh, in orderedVp, DlssCameraState.TeleportSeq);
|
||||
}
|
||||
|
||||
// [JITTRACE 28/07] ETAPE B : ce qui SORT de la file et entre dans l'anneau.
|
||||
// Si A est non nul et B vaut zero, la perte est dans la file ; si B est non nul
|
||||
// et C vaut zero, elle est dans l'anneau ou apres.
|
||||
if (Engine.Threed.MvppCameraCapture.JitTrace &&
|
||||
Environment.TickCount64 - _jitTraceMs >= 1000)
|
||||
{
|
||||
_jitTraceMs = Environment.TickCount64;
|
||||
Logger.Info?.Print(LogClass.Gpu,
|
||||
$"JITTRACE B (defile+publie) : jx={orderedJx:0.000000} jy={orderedJy:0.000000} " +
|
||||
$"valide={orderedValid} frais={DlssCameraState.LastConsumeFresh}");
|
||||
}
|
||||
|
||||
if (MvppDev.Enabled && Environment.TickCount64 - _mvppFifoLogMs >= 5000)
|
||||
{
|
||||
_mvppFifoLogMs = Environment.TickCount64;
|
||||
|
||||
@@ -35,6 +35,12 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
// [GLDUMP] (journal 216) zero-alloc accessors for the GL-side chain dump probe.
|
||||
public TextureView MvppGetColor(int index)
|
||||
{
|
||||
return (uint)index < (uint)_colors.Length ? _colors[index] : null;
|
||||
}
|
||||
|
||||
public void AttachColor(int index, TextureView color)
|
||||
{
|
||||
if (_colors[index] == color)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.OpenGL.Image;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Graphics.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// [GLDUMP] (RYUJINX_GLDUMP=1, inert unless set). Journal 216, read-only.
|
||||
///
|
||||
/// Mirror of the Vulkan-side MvppMvDumpProbe: dumps the XC2 DoF/MV chain buffers at the
|
||||
/// END of their pass, under the OPENGL backend -- the backend that renders CLEAN. The
|
||||
/// first buffer whose content diverges from the Vulkan dumps of the same scene names the
|
||||
/// faulty pass mechanically, with zero hypotheses.
|
||||
/// </summary>
|
||||
static class MvppGlDumpProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_GLDUMP") == "1";
|
||||
|
||||
private const long IntervalMs = 8000;
|
||||
private const long WindowMs = 400;
|
||||
|
||||
private static bool _armedLogged;
|
||||
private static long _lastDumpMs;
|
||||
private static long _windowUntilMs;
|
||||
private static int _dumpRound;
|
||||
private static string _dir;
|
||||
|
||||
private static bool IsWatched(TextureView view)
|
||||
{
|
||||
return (view.Info.Width, view.Info.Height, view.Info.Format) switch
|
||||
{
|
||||
(1280, 720, Format.R10G10B10A2Unorm) => true,
|
||||
(1280, 720, Format.R8G8B8A8Unorm) => true,
|
||||
(1280, 720, Format.R11G11B10Float) => true,
|
||||
(640, 360, Format.R32Float) => true,
|
||||
(512, 288, Format.R16G16B16A16Float) => true,
|
||||
(320, 180, Format.R8G8B8A8Unorm) => true,
|
||||
(64, 36, Format.R8G8B8A8Unorm) => true,
|
||||
(64, 36, Format.R8Unorm) => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
public static void OnRenderTargetsChange(Framebuffer fb)
|
||||
{
|
||||
if (!Enabled || fb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
bool inWindow = now < _windowUntilMs;
|
||||
|
||||
if (!inWindow && now - _lastDumpMs < IntervalMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_armedLogged)
|
||||
{
|
||||
_armedLogged = true;
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
"[GLDUMP] armed: OpenGL-side chain dumps at end of pass (mirror of the Vulkan probe)");
|
||||
}
|
||||
|
||||
bool any = false;
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
TextureView view = fb.MvppGetColor(i);
|
||||
|
||||
if (view == null || !IsWatched(view))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!any)
|
||||
{
|
||||
any = true;
|
||||
|
||||
if (!inWindow)
|
||||
{
|
||||
_lastDumpMs = now;
|
||||
_windowUntilMs = now + WindowMs;
|
||||
_dumpRound++;
|
||||
}
|
||||
|
||||
_dir ??= Directory.CreateDirectory("gldump").FullName;
|
||||
}
|
||||
|
||||
int id = RuntimeHelpers.GetHashCode(view);
|
||||
|
||||
try
|
||||
{
|
||||
using PinnedSpan<byte> pinned = view.GetData(0, 0);
|
||||
ReadOnlySpan<byte> data = pinned.Get();
|
||||
|
||||
long nonZero = 0;
|
||||
for (int b = 0; b < data.Length; b++)
|
||||
{
|
||||
if (data[b] != 0)
|
||||
{
|
||||
nonZero++;
|
||||
}
|
||||
}
|
||||
|
||||
string file = Path.Combine(_dir,
|
||||
$"round{_dumpRound:D3}_rt{i}_s{id:X8}_{view.Info.Width}x{view.Info.Height}_{view.Info.Format}_{now}.bin");
|
||||
File.WriteAllBytes(file, data.ToArray());
|
||||
|
||||
Logger.Warning?.Print(LogClass.Gpu,
|
||||
$"[GLDUMP] round={_dumpRound} rt={i} bytes={data.Length} nonZeroBytes={100.0 * nonZero / data.Length:F1}% file={Path.GetFileName(file)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Gpu, $"[GLDUMP] rt={i} FAILED: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1168,6 +1168,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
|
||||
public void SetRenderTargets(Span<ITexture> colors, ITexture depthStencil)
|
||||
{
|
||||
MvppGlDumpProbe.OnRenderTargetsChange(_framebuffer); // [GLDUMP] read-only, self-gated
|
||||
EnsureFramebuffer();
|
||||
|
||||
for (int index = 0; index < colors.Length; index++)
|
||||
|
||||
@@ -333,6 +333,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
private static OperationResult GenerateClamp(CodeGenContext context, AstOperation operation)
|
||||
{
|
||||
// [NCLAMP] (journal 196, gated OFF by default): the guest SAT modifier flushes
|
||||
// NaN to 0 in hardware (XC2's motion-blur builder computes 0*rsqrt(0) = NaN on
|
||||
// every below-threshold pixel and depends on it). FClamp is UNDEFINED on NaN per
|
||||
// the SPIR-V spec -- on NVIDIA Vulkan the NaN escapes into the temporal chain.
|
||||
// NClamp(NaN, lo, hi) == lo by construction: the console semantics.
|
||||
if (Translation.MvppNClampProbe.Enabled)
|
||||
{
|
||||
Translation.MvppNClampProbe.OnApplied(context.Logger);
|
||||
|
||||
return GenerateTernary(context, operation, context.Delegates.GlslNClamp, context.Delegates.GlslSClamp);
|
||||
}
|
||||
|
||||
return GenerateTernary(context, operation, context.Delegates.GlslFClamp, context.Delegates.GlslSClamp);
|
||||
}
|
||||
|
||||
@@ -953,6 +965,15 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
private static OperationResult GenerateMaximum(CodeGenContext context, AstOperation operation)
|
||||
{
|
||||
// [NMINMAX] (journal 199, gated OFF by default): hardware FMNMX returns the
|
||||
// non-NaN operand; FMax is undefined on NaN per spec. See MvppNMinMaxProbe.
|
||||
if (Translation.MvppNMinMaxProbe.Enabled)
|
||||
{
|
||||
Translation.MvppNMinMaxProbe.OnApplied(context.Logger);
|
||||
|
||||
return GenerateBinary(context, operation, context.Delegates.GlslNMax, context.Delegates.GlslSMax);
|
||||
}
|
||||
|
||||
return GenerateBinary(context, operation, context.Delegates.GlslFMax, context.Delegates.GlslSMax);
|
||||
}
|
||||
|
||||
@@ -969,6 +990,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
private static OperationResult GenerateMinimum(CodeGenContext context, AstOperation operation)
|
||||
{
|
||||
// [NMINMAX] see GenerateMaximum.
|
||||
if (Translation.MvppNMinMaxProbe.Enabled)
|
||||
{
|
||||
Translation.MvppNMinMaxProbe.OnApplied(context.Logger);
|
||||
|
||||
return GenerateBinary(context, operation, context.Delegates.GlslNMin, context.Delegates.GlslSMin);
|
||||
}
|
||||
|
||||
return GenerateBinary(context, operation, context.Delegates.GlslFMin, context.Delegates.GlslSMin);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
public readonly FuncBinaryInstruction FDiv;
|
||||
public readonly FuncBinaryInstruction SDiv;
|
||||
public readonly FuncBinaryInstruction GlslFMax;
|
||||
public readonly FuncBinaryInstruction GlslNMax; // [NMINMAX] NaN-aware max (journal 199)
|
||||
public readonly FuncBinaryInstruction GlslNMin; // [NMINMAX] NaN-aware min (journal 199)
|
||||
public readonly FuncBinaryInstruction GlslSMax;
|
||||
public readonly FuncBinaryInstruction GlslFMin;
|
||||
public readonly FuncBinaryInstruction GlslSMin;
|
||||
@@ -103,6 +105,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
// Ternary
|
||||
public readonly FuncTernaryInstruction GlslFClamp;
|
||||
public readonly FuncTernaryInstruction GlslNClamp; // [NCLAMP] NaN-aware clamp (journal 196)
|
||||
public readonly FuncTernaryInstruction GlslSClamp;
|
||||
public readonly FuncTernaryInstruction GlslFma;
|
||||
|
||||
@@ -172,6 +175,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
FDiv = context.FDiv;
|
||||
SDiv = context.SDiv;
|
||||
GlslFMax = context.GlslFMax;
|
||||
GlslNMax = context.GlslNMax; // [NMINMAX]
|
||||
GlslNMin = context.GlslNMin; // [NMINMAX]
|
||||
GlslSMax = context.GlslSMax;
|
||||
GlslFMin = context.GlslFMin;
|
||||
GlslSMin = context.GlslSMin;
|
||||
@@ -211,6 +216,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
// Ternary
|
||||
GlslFClamp = context.GlslFClamp;
|
||||
GlslNClamp = context.GlslNClamp; // [NCLAMP]
|
||||
GlslSClamp = context.GlslSClamp;
|
||||
GlslFma = context.GlslFma;
|
||||
|
||||
|
||||
@@ -184,6 +184,16 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
|
||||
Operand srcB = context.FPAbsNeg(src, absolute, negate, fpType);
|
||||
|
||||
// [TRUNCEPS] (journal 208, gated OFF by default): nudge the bokeh FS's F2I inputs
|
||||
// to probe the texel-snap precision sensitivity of its quantized gather taps.
|
||||
if (Translation.MvppTruncEpsProbe.Enabled &&
|
||||
srcType == DstFmt.F32 &&
|
||||
context.TranslatorContext.Address == 0x1000AE430UL)
|
||||
{
|
||||
Translation.MvppTruncEpsProbe.OnApplied(context.TranslatorContext.GpuAccessor);
|
||||
srcB = context.FPAdd(srcB, ConstF(Translation.MvppTruncEpsProbe.Value.Value));
|
||||
}
|
||||
|
||||
srcB = roundingMode switch
|
||||
{
|
||||
RoundMode2.Round => context.FPRound(srcB, fpType),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Ryujinx.Graphics.Shader.Decoders;
|
||||
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
@@ -143,10 +145,27 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
EmitFadd(context, Instruction.FP32, srcA, srcB, op.Dest, op.NegA, op.NegB, op.AbsA, op.AbsB, op.Sat, op.WriteCC);
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] NANSCRUB (EXP 14): the `x + 0.00999999978` add is the fingerprint of
|
||||
// the XC2 motion-vector encode epilogue (sign-code + 0.01, decoded downstream by *3.01 and
|
||||
// trunc) -- present in all ~199 MV-writing materials and the sky pass, and in nothing else
|
||||
// surveyed (850-shader census, journal 131/133). Seeing it arms the NaN scrub for THIS
|
||||
// translation's outputs (EmitterContext.PrepareForReturn). Fingerprint gate = works on the
|
||||
// disk-cache recompile path too (address is 0 there, no address needed).
|
||||
private const float NanScrubMagicImmediate = 0.00999999978f;
|
||||
|
||||
public static void Fadd32i(EmitterContext context)
|
||||
{
|
||||
InstFadd32i op = context.GetOp<InstFadd32i>();
|
||||
|
||||
if (NanScrubProbe.Enabled &&
|
||||
!context.TranslatorContext.MvppNanScrubArmed &&
|
||||
context.TranslatorContext.Definitions.Stage == ShaderStage.Fragment &&
|
||||
Math.Abs(BitConverter.Int32BitsToSingle(op.Imm32) - NanScrubMagicImmediate) < 1e-6f)
|
||||
{
|
||||
context.TranslatorContext.MvppNanScrubArmed = true;
|
||||
NanScrubProbe.OnArmed(context.TranslatorContext.Address, context.TranslatorContext.GpuAccessor);
|
||||
}
|
||||
|
||||
Operand srcA = GetSrcReg(context, op.SrcA);
|
||||
Operand srcB = GetSrcImm(context, op.Imm32);
|
||||
|
||||
@@ -238,10 +257,83 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
EmitFmul(context, Instruction.FP32, op.Scale, srcA, srcB, op.Dest, op.NegA, op.Sat, op.WriteCC);
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] HASH_TEST gate (env var RYUJINX_HASH_CONST). Temporary VALIDATION
|
||||
// experiment (NOT a fix): force the XC2 DoF bokeh dither hash to a constant, to test whether
|
||||
// the spatial hash is required for the border-streak artifact. The hash is
|
||||
// `fract(sin(...) * 43758.5469)`; we replace ONLY that multiply's result with a constant K,
|
||||
// so hash = fract(K) (K=0.5 -> 0.5, K=0.0 -> 0.0). Triple-gated below: shader address AND
|
||||
// magic immediate AND env var present. _hashConstValue == null => OFF, bit-identical.
|
||||
private const ulong HashTestFsAddress = 0x1000AE430UL; // guest FS 0x1000AE430 = Shader0053 (DoF bokeh)
|
||||
private const float HashMagicImmediate = 43758.5469f; // the fract(sin(...) * K) fingerprint
|
||||
private const float HashMagicTolerance = 0.1f;
|
||||
|
||||
// Single source of truth for the flag: HashTestProbe is also read by the disk-cache probe
|
||||
// interlock, so the instrumented shader can never be persisted to the host shader cache.
|
||||
private static readonly float? _hashConstValue = HashTestProbe.ConstValue;
|
||||
private static bool _hashTestLogged;
|
||||
|
||||
private static bool IsHashMagicImmediate(int imm32)
|
||||
{
|
||||
return MathF.Abs(BitConverter.Int32BitsToSingle(imm32) - HashMagicImmediate) < HashMagicTolerance;
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] SATSCRUB stage 2 (EXP 17): the `x * 3.00999999` multiply is the
|
||||
// MV sign-code DECODE fingerprint -- present in exactly the 6 MV-chain processors
|
||||
// (builder, ping-pong filter, downsamplers, temporal resolve; 850-shader census) whose
|
||||
// outputs also need the saturation scrub, INCLUDING rt0 (their map output is rt0).
|
||||
private const float SatScrubDecodeImmediate = 3.00999999f;
|
||||
|
||||
public static void Fmul32i(EmitterContext context)
|
||||
{
|
||||
InstFmul32i op = context.GetOp<InstFmul32i>();
|
||||
|
||||
if (NanScrubProbe.SatScrub &&
|
||||
!context.TranslatorContext.MvppSatScrubAllRts &&
|
||||
context.TranslatorContext.Definitions.Stage == ShaderStage.Fragment &&
|
||||
Math.Abs(BitConverter.Int32BitsToSingle(op.Imm32) - SatScrubDecodeImmediate) < 1e-5f)
|
||||
{
|
||||
context.TranslatorContext.MvppSatScrubAllRts = true;
|
||||
context.TranslatorContext.MvppNanScrubArmed = true;
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[SATSCRUB2] armed MV-chain shader (guest 0x{context.TranslatorContext.Address:X}) -- ALL rts scrubbed");
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] HASH_TEST: replace ONLY Shader0053's `sin(...) * 43758.5469` product
|
||||
// (the DoF dither hash) with a constant, so hash = fract(const). Nothing else is touched;
|
||||
// when the env var is absent this branch is skipped and behavior is bit-identical.
|
||||
if (_hashConstValue.HasValue && context.TranslatorContext.Address == HashTestFsAddress)
|
||||
{
|
||||
bool isHashMultiply = IsHashMagicImmediate(op.Imm32);
|
||||
|
||||
// Translation-time only (never per frame): log each Fmul32i of this shader up to and
|
||||
// including the one that matches, so the intercepted instruction can be verified.
|
||||
// Sat/WriteCC are printed because the override below skips them; both must be false.
|
||||
if (!_hashTestLogged)
|
||||
{
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[HASH_TEST] Guest FS: 0x{HashTestFsAddress:X} | " +
|
||||
$"Imm32: {BitConverter.Int32BitsToSingle(op.Imm32).ToString(CultureInfo.InvariantCulture)} | " +
|
||||
$"SrcA: R{op.SrcA} | Dest: R{op.Dest} | " +
|
||||
$"Sat: {op.Sat} | WriteCC: {op.WriteCC} | " +
|
||||
$"Override: {(isHashMultiply ? "YES" : "NO")}" +
|
||||
(isHashMultiply
|
||||
? $" | Replacement value: {_hashConstValue.Value.ToString(CultureInfo.InvariantCulture)}"
|
||||
: string.Empty));
|
||||
|
||||
if (isHashMultiply)
|
||||
{
|
||||
_hashTestLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHashMultiply)
|
||||
{
|
||||
context.Copy(GetDest(op.Dest), ConstF(_hashConstValue.Value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Operand srcA = GetSrcReg(context, op.SrcA);
|
||||
Operand srcB = GetSrcImm(context, op.Imm32);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Ryujinx.Graphics.Shader.Decoders;
|
||||
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
@@ -38,11 +39,11 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
switch (op.MufuOp)
|
||||
{
|
||||
case MufuOp.Cos:
|
||||
res = context.FPCosine(res);
|
||||
res = context.FPCosine(ReduceSinCosArg(context, res));
|
||||
break;
|
||||
|
||||
case MufuOp.Sin:
|
||||
res = context.FPSine(res);
|
||||
res = context.FPSine(ReduceSinCosArg(context, res));
|
||||
break;
|
||||
|
||||
case MufuOp.Ex2:
|
||||
@@ -54,11 +55,11 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
break;
|
||||
|
||||
case MufuOp.Rcp:
|
||||
res = context.FPReciprocal(res);
|
||||
res = ReduceMufuPrecision(context, context.FPReciprocal(res), MufuOp.Rcp);
|
||||
break;
|
||||
|
||||
case MufuOp.Rsq:
|
||||
res = context.FPReciprocalSquareRoot(res);
|
||||
res = ReduceMufuPrecision(context, context.FPReciprocalSquareRoot(res), MufuOp.Rsq);
|
||||
break;
|
||||
|
||||
case MufuOp.Rcp64h:
|
||||
@@ -72,7 +73,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
break;
|
||||
|
||||
case MufuOp.Sqrt:
|
||||
res = context.FPSquareRoot(res);
|
||||
res = ReduceMufuPrecision(context, context.FPSquareRoot(res), MufuOp.Sqrt);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -93,5 +94,90 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
|
||||
context.Copy(GetDest(rd), srcB);
|
||||
}
|
||||
|
||||
// [Beast Roofer diag/fix] RYUJINX_RRO_REDUCE=1 (OFF by default): Maxwell's RRO range-reduces
|
||||
// the argument before every MUFU.SIN/COS; the emulator skips it (EmitRro above is a plain
|
||||
// move), so large arguments reach the host sin/cos at full magnitude and lose precision.
|
||||
// That breaks fract(sin(dot(coord, magic)) * k) dither hashes -> visible banding (XC2 DoF).
|
||||
// Reducing x mod 2*pi is mathematically identical yet keeps a small, precise argument.
|
||||
// Sin/Cos only, so MUFU.EX2 (exponential) is left untouched. Zero effect while OFF.
|
||||
// The flag now lives in RroReduceProbe so the disk-cache interlock can see it too.
|
||||
private static bool _rroReduceLogged;
|
||||
|
||||
private static Operand ReduceSinCosArg(EmitterContext context, Operand x)
|
||||
{
|
||||
if (!RroReduceProbe.Enabled)
|
||||
{
|
||||
return x;
|
||||
}
|
||||
|
||||
if (!_rroReduceLogged)
|
||||
{
|
||||
_rroReduceLogged = true;
|
||||
context.TranslatorContext.GpuAccessor.Log("RROREDUCE: sin/cos argument range reduction ACTIVE (RYUJINX_RRO_REDUCE=1).");
|
||||
}
|
||||
|
||||
// x - 2*pi * floor(x / (2*pi))
|
||||
Operand k = context.FPFloor(context.FPMultiply(x, OperandHelper.ConstF(0.15915494309f)));
|
||||
return context.FPSubtract(x, context.FPMultiply(k, OperandHelper.ConstF(6.28318530718f)));
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_MUFU_PREC=<0-23> (absent by default): Maxwell's MUFU is an
|
||||
// APPROXIMATE unit -- MUFU.RCP/RSQ/SQRT are accurate to a couple of ulp -- while we translate
|
||||
// them to the host's exact IEEE ops, so the emulator is strictly MORE precise than the console.
|
||||
// XC2's DoF bokeh weights every tap with `w = clamp(bias - dist/CoC)` (~15 rcp + ~13 sqrt) and
|
||||
// normalises by `1/sum(w)`: a result landing on the other side of a clamp boundary flips a whole
|
||||
// tap in or out of the sum, which is a plausible mechanism for the border streaks. This gate
|
||||
// drops the low mantissa bits of those three results to test whether that precision is in the
|
||||
// causal chain. VALIDATION EXPERIMENT, NOT A FIX -- absent => not a single IR node is added.
|
||||
//
|
||||
// Truncation (a single BitwiseAnd) is used on purpose rather than round-to-nearest: Instruction.Add
|
||||
// is typed Scalar (polymorphic), so an IAdd on a float-typed operand would be inferred as a FLOAT
|
||||
// add instead of the intended add on the bit pattern. BitwiseAnd is declared S32/S32/S32, so the
|
||||
// codegen is forced to emit floatBitsToInt/intBitsToFloat around it. The resulting downward bias
|
||||
// is ~2^-N relative, i.e. invisible at N=12 and deliberately huge at N=0.
|
||||
//
|
||||
// N=0 is the CANARY: every rcp/rsq/sqrt result is flattened to a power of two, which wrecks the
|
||||
// blur in an unmistakable way. If the picture does NOT visibly break at N=0, this shader is not
|
||||
// on screen and any verdict from the finer runs would be worthless.
|
||||
// The flag itself lives in MufuPrecProbe: single source of truth, and the disk-cache
|
||||
// interlock reads the same property so an instrumented shader can never be persisted.
|
||||
private const ulong MufuPrecFsAddress = 0x1000AE430UL; // guest FS = Shader0053 (XC2 DoF bokeh)
|
||||
|
||||
private static readonly HashSet<MufuOp> _mufuPrecLoggedOps = new();
|
||||
|
||||
private static Operand ReduceMufuPrecision(EmitterContext context, Operand res, MufuOp mufuOp)
|
||||
{
|
||||
// Double gate: env var present AND this is the bokeh fragment shader. Keeping it scoped to
|
||||
// one shader means exactly one variable moves, and every other shader stays bit-identical.
|
||||
ulong target = MufuPrecProbe.AddressOverride ?? MufuPrecFsAddress; // [v2] journal 219
|
||||
|
||||
if (!MufuPrecProbe.Enabled || context.TranslatorContext.Address != target)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
int bits = MufuPrecProbe.BitsKept.Value;
|
||||
int dropped = MufuPrecProbe.MantissaBits - bits;
|
||||
|
||||
if (dropped <= 0)
|
||||
{
|
||||
return res; // 23 bits kept = the exact host result, nothing to drop.
|
||||
}
|
||||
|
||||
// One line per distinct MUFU op, at translation time only (never per frame), so the log
|
||||
// also tells us WHICH of the three actually occur in this shader.
|
||||
lock (_mufuPrecLoggedOps)
|
||||
{
|
||||
if (_mufuPrecLoggedOps.Add(mufuOp))
|
||||
{
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[MUFU_PREC] Guest FS: 0x{MufuPrecFsAddress:X} | Op: {mufuOp} | " +
|
||||
$"Mantissa bits kept: {bits}/{MufuPrecProbe.MantissaBits} | Dropped: {dropped}");
|
||||
}
|
||||
}
|
||||
|
||||
return context.BitwiseAnd(res, OperandHelper.Const(unchecked((int)(0xFFFFFFFFu << dropped))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
|
||||
|
||||
@@ -1196,6 +1197,25 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
Operand[] dests,
|
||||
Operand[] sources)
|
||||
{
|
||||
// [FORCELOD0] (journal 211, gated OFF by default): turn the bokeh gather's
|
||||
// implicit-LOD colour taps into textureLod(0) -- probing the mip-divergence
|
||||
// mechanism. Strictly gated to plain 2D samples of that one shader.
|
||||
if (Translation.MvppForceLod0Probe.Enabled &&
|
||||
context.TranslatorContext.Address == 0x1000AE430UL &&
|
||||
(handle == 0xC || handle == 0xA) &&
|
||||
type == SamplerType.Texture2D &&
|
||||
flags == TextureFlags.None)
|
||||
{
|
||||
Translation.MvppForceLod0Probe.OnApplied(context.TranslatorContext.GpuAccessor, handle);
|
||||
|
||||
flags = TextureFlags.LodLevel;
|
||||
|
||||
Operand[] withLod = new Operand[sources.Length + 1];
|
||||
sources.CopyTo(withLod, 0);
|
||||
withLod[^1] = ConstF(0);
|
||||
sources = withLod;
|
||||
}
|
||||
|
||||
SetBindingPair setAndBinding = flags.HasFlag(TextureFlags.Bindless) ? default : context.ResourceManager.GetTextureOrImageBinding(
|
||||
Instruction.TextureSample,
|
||||
type,
|
||||
@@ -1205,6 +1225,364 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
handle);
|
||||
|
||||
context.TextureSample(type, flags, setAndBinding, componentMask, dests, sources);
|
||||
|
||||
ForceConstantCoc(context, dests);
|
||||
ForceConstantTileMap(context, dests, handle);
|
||||
ForceConstantMvInput(context, dests, handle);
|
||||
ForceConstantMvSign(context, dests, handle);
|
||||
ForceConstantResolveMv(context, dests, handle);
|
||||
CapTileMap(context, dests, handle);
|
||||
CapturePeriscope2(context, dests, handle);
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] PERISCOPE2 (ladder stage 1): in the temporal resolve, capture the raw
|
||||
// 720p object-MV sample (tcb_10) so PrepareForReturn shows it as the scene -- one pixel per
|
||||
// texel, no readback. See PeriscopeProbe.Stage2.
|
||||
private const ulong Periscope2FsAddress = 0x100082C30UL; // temporal resolve (hash-confirmed, journal 132)
|
||||
private const int Periscope2Handle = 0x10; // fp_t_tcb_10 = R10G10B10A2 720p object MVs
|
||||
|
||||
private static void CapturePeriscope2(EmitterContext context, Operand[] dests, int handle)
|
||||
{
|
||||
if (!PeriscopeProbe.Stage2 ||
|
||||
dests == null ||
|
||||
dests.Length < 3 ||
|
||||
handle != Periscope2Handle ||
|
||||
context.TranslatorContext.Address != Periscope2FsAddress ||
|
||||
context.TranslatorContext.MvppPeriscopeSample != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The dests are GPRs the game's compiler reuses after the sample; the override at
|
||||
// PrepareForReturn reads their LAST value, not this one. Snapshot into fresh locals
|
||||
// here so the screen shows the sample (v1 silently showed the shader's normal output).
|
||||
context.TranslatorContext.MvppPeriscopeSample = new[]
|
||||
{
|
||||
context.Copy(dests[0]),
|
||||
context.Copy(dests[1]),
|
||||
context.Copy(dests[2]),
|
||||
};
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[PERISCOPE2] Guest FS: 0x{Periscope2FsAddress:X} | raw 720p MV sample captured (v3 all-rt) | screen will show the MV buffer");
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_COC_CONST=<float> (absent by default): make every DoF bokeh sprite
|
||||
// the same size, to split the last two suspects for the XC2 border streaks -- the circle-of-
|
||||
// confusion that arrives, versus where the quads land. See CocConstProbe for the reasoning.
|
||||
//
|
||||
// The sample is still emitted and then overwritten rather than skipped: dropping it would change
|
||||
// the shader's texture bindings and reflection, which is a second variable. The dead sample is
|
||||
// removed by the optimiser anyway. Verified surgical: this geometry shader samples EXACTLY ONE
|
||||
// texture (the CoC map at bokeh_gs.glsl:124), so the shader-address gate cannot hit anything else.
|
||||
private const ulong CocConstGsAddress = 0x1000ADF30UL; // guest GS 0x1000ADF30 = Shader0052 (bokeh scatter)
|
||||
|
||||
private static bool _cocConstLogged;
|
||||
|
||||
private static void ForceConstantCoc(EmitterContext context, Operand[] dests)
|
||||
{
|
||||
if (!CocConstProbe.Enabled || dests == null || context.TranslatorContext.Address != CocConstGsAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float value = CocConstProbe.Value.Value;
|
||||
|
||||
// Translation time only, never per frame. Its presence in the log is what makes the visual
|
||||
// verdict valid: no [COC_CONST] line means the experiment never ran, NOT that the CoC is
|
||||
// innocent -- the disk-cache recompile path passes address 0 and would silently miss here.
|
||||
if (!_cocConstLogged)
|
||||
{
|
||||
_cocConstLogged = true;
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[COC_CONST] Guest GS: 0x{CocConstGsAddress:X} | CoC forced to {value.ToString(CultureInfo.InvariantCulture)} | " +
|
||||
$"Overridden components: {dests.Length}");
|
||||
}
|
||||
|
||||
foreach (Operand dest in dests)
|
||||
{
|
||||
if (dest != null)
|
||||
{
|
||||
context.Copy(dest, ConstF(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_TILEMAP_CONST=<float> (absent by default): flatten the DoF tile
|
||||
// map, to test whether it drives the XC2 form-A corruption rectangles -- their edges snap to
|
||||
// the map's 20-px texel grid on every clean capture. See TileMapConstProbe for the reasoning.
|
||||
//
|
||||
// Same rules as the CoC probe above: the sample is still emitted and then overwritten rather
|
||||
// than skipped (dropping it would change bindings/reflection, a second variable), and unlike
|
||||
// that probe this shader samples SEVERAL textures, so the gate also matches the sampler
|
||||
// handle -- fp_t_tcb_E is handle 0xE (naming scheme: ResourceManager.cs, "{prefix}_tcb_{X}").
|
||||
private const ulong TileMapFsAddress = 0x1000AE430UL; // guest FS 0x1000AE430 = Shader0053 (bokeh shade/accumulate)
|
||||
private const int TileMapHandle = 0xE; // fp_t_tcb_E = 64x36 RGBA8 DoF tile map
|
||||
|
||||
private static readonly HashSet<int> _tileMapLoggedHandles = new();
|
||||
|
||||
private static void ForceConstantTileMap(EmitterContext context, Operand[] dests, int handle)
|
||||
{
|
||||
// PERISCOPE piggybacks on this gate's address+handle discrimination, so either flag
|
||||
// opens the method; the constant override below still requires its own flag.
|
||||
if ((!TileMapConstProbe.Enabled && !PeriscopeProbe.Enabled) || dests == null || context.TranslatorContext.Address != TileMapFsAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool match = handle == TileMapHandle;
|
||||
|
||||
// [PERISCOPE] (EXP 15) capture the FIRST tile-map sample's operands for the output
|
||||
// override in PrepareForReturn. Shares this gate's address+handle discrimination.
|
||||
if (match && PeriscopeProbe.Enabled && context.TranslatorContext.MvppPeriscopeSample == null && dests.Length >= 3)
|
||||
{
|
||||
// Same register-reuse trap as CapturePeriscope2: snapshot to locals at the sample.
|
||||
context.TranslatorContext.MvppPeriscopeSample = new[]
|
||||
{
|
||||
context.Copy(dests[0]),
|
||||
context.Copy(dests[1]),
|
||||
context.Copy(dests[2]),
|
||||
};
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[PERISCOPE] Guest FS: 0x{TileMapFsAddress:X} | tile-map sample captured (v3 all-rt) | screen will show the raw 64x36 map");
|
||||
}
|
||||
|
||||
// Translation time only, never per frame. One line per distinct sampler handle of this
|
||||
// shader: the Override: NO lines (0xA, 0xC, ...) prove the filter discriminates, the 0xE
|
||||
// line with Override: YES proves the right texture is hit. No [TILEMAP] line at all means
|
||||
// the experiment never ran (void), NOT that the tile map is innocent -- the disk-cache
|
||||
// recompile path passes address 0 and would silently miss here.
|
||||
if (!TileMapConstProbe.Enabled)
|
||||
{
|
||||
return; // PERISCOPE-only run: capture done above, no constant override
|
||||
}
|
||||
|
||||
lock (_tileMapLoggedHandles)
|
||||
{
|
||||
if (_tileMapLoggedHandles.Add(handle))
|
||||
{
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[TILEMAP] Guest FS: 0x{TileMapFsAddress:X} | Sampler handle: 0x{handle:X} | " +
|
||||
(match
|
||||
? $"Override: YES | Replacement value: {TileMapConstProbe.Value.Value.ToString(CultureInfo.InvariantCulture)} | Components: {dests.Length}"
|
||||
: "Override: NO"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!match)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float value = TileMapConstProbe.Value.Value;
|
||||
|
||||
foreach (Operand dest in dests)
|
||||
{
|
||||
if (dest != null)
|
||||
{
|
||||
context.Copy(dest, ConstF(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_MVKILL_CONST=<float> (absent by default): null the OBJECT-motion
|
||||
// input of the motion-map builder, to decide which of its two inputs carries the form-A
|
||||
// corruption (object MVs vs camera/depth reprojection). See MvKillProbe for the reasoning.
|
||||
// Same rules as the probes above: sample emitted then overwritten, handle-gated because this
|
||||
// shader samples several textures (0x8 = the R10G10B10A2 720p object-MV buffer).
|
||||
private const ulong MvKillFsAddress = 0x1000AD730UL; // guest FS 0x1000AD730 = motion-map builder (MAP64 writer)
|
||||
private const int MvKillHandle = 0x8; // fp_t_tcb_8 = R10G10B10A2 1280x720 object MVs
|
||||
|
||||
private static readonly HashSet<int> _mvKillLoggedHandles = new();
|
||||
|
||||
private static void ForceConstantMvInput(EmitterContext context, Operand[] dests, int handle)
|
||||
{
|
||||
if (!MvKillProbe.Enabled || dests == null || context.TranslatorContext.Address != MvKillFsAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool match = handle == MvKillHandle;
|
||||
|
||||
// Translation time only. One line per distinct sampler handle of this shader; the
|
||||
// Override: NO lines prove the filter discriminates. No [MVKILL] line at all = void.
|
||||
lock (_mvKillLoggedHandles)
|
||||
{
|
||||
if (_mvKillLoggedHandles.Add(handle))
|
||||
{
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[MVKILL] Guest FS: 0x{MvKillFsAddress:X} | Sampler handle: 0x{handle:X} | " +
|
||||
(match
|
||||
? $"Override: YES | Replacement value: {MvKillProbe.Value.Value.ToString(CultureInfo.InvariantCulture)} | Components: {dests.Length}"
|
||||
: "Override: NO"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!match)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float value = MvKillProbe.Value.Value;
|
||||
|
||||
foreach (Operand dest in dests)
|
||||
{
|
||||
if (dest != null)
|
||||
{
|
||||
context.Copy(dest, ConstF(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_MVSIGN_CONST=<float> (absent by default): override ONLY the sign
|
||||
// channel (w = dests[2]) of the motion-map builder's single .xyw sample of the object-MV
|
||||
// buffer, leaving the 10-bit magnitudes live. Splits "garbage in the A2 sign channel" from
|
||||
// "garbage in the magnitudes". See MvSignProbe for the reasoning.
|
||||
// [Beast Roofer diag] RYUJINX_RESOLVEMV_CONST=<float> (absent by default): null the motion
|
||||
// vector the TAA RESOLVE samples for its history reprojection (journal 203/204: the poison
|
||||
// enters at the resolve; displaced-block artifact => the displacement vector is suspect #1).
|
||||
private const ulong ResolveMvFsAddress = 0x100082C30UL; // guest FS = TAA temporal resolve (Shader0086)
|
||||
private const int ResolveMvHandle = 0x10; // fp_t_tcb_10 = R10G10B10A2 720p MV (X)
|
||||
|
||||
private static readonly HashSet<int> _resolveMvLoggedHandles = new();
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_TILECAP=<float> (absent by default): soft-cap the bokeh's
|
||||
// 64x36 tile-map samples -- x/y pulled toward their 0.5 bias by at most K, z clamped to K.
|
||||
// Small blur survives, giant blur becomes impossible. See MvppTileCapProbe.
|
||||
private static readonly HashSet<int> _tileCapLoggedHandles = new();
|
||||
|
||||
private static void CapTileMap(EmitterContext context, Operand[] dests, int handle)
|
||||
{
|
||||
if (!MvppTileCapProbe.Enabled || dests == null || context.TranslatorContext.Address != TileMapFsAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool match = handle == TileMapHandle;
|
||||
|
||||
lock (_tileCapLoggedHandles)
|
||||
{
|
||||
if (_tileCapLoggedHandles.Add(handle))
|
||||
{
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[TILECAP] Guest FS: 0x{TileMapFsAddress:X} | Sampler handle: 0x{handle:X} | " +
|
||||
(match
|
||||
? $"Cap: YES | K = {MvppTileCapProbe.Value.Value.ToString(CultureInfo.InvariantCulture)} | Channels: {MvppTileCapProbe.Channel ?? "xyz"} | Components: {dests.Length}"
|
||||
: "Cap: NO"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!match)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float k = MvppTileCapProbe.Value.Value;
|
||||
|
||||
for (int i = 0; i < dests.Length && i < 3; i++)
|
||||
{
|
||||
Operand dest = dests[i];
|
||||
|
||||
if (dest == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// [v2] channel bisection (journal 209).
|
||||
if (MvppTileCapProbe.Channel == "xy" && i == 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MvppTileCapProbe.Channel == "z" && i < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i < 2)
|
||||
{
|
||||
// x/y: 0.5-biased -> 0.5 + clamp(v - 0.5, -K, +K).
|
||||
Operand centered = context.FPSubtract(dest, ConstF(0.5f));
|
||||
Operand capped = context.FPMaximum(context.FPMinimum(centered, ConstF(k)), ConstF(-k));
|
||||
context.Copy(dest, context.FPAdd(capped, ConstF(0.5f)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// z: plain magnitude -> min(v, K).
|
||||
context.Copy(dest, context.FPMinimum(dest, ConstF(k)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ForceConstantResolveMv(EmitterContext context, Operand[] dests, int handle)
|
||||
{
|
||||
if (!MvResolveMvProbe.Enabled || dests == null || context.TranslatorContext.Address != ResolveMvFsAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool match = handle == ResolveMvHandle;
|
||||
|
||||
lock (_resolveMvLoggedHandles)
|
||||
{
|
||||
if (_resolveMvLoggedHandles.Add(handle))
|
||||
{
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[RESOLVEMV] Guest FS: 0x{ResolveMvFsAddress:X} | Sampler handle: 0x{handle:X} | " +
|
||||
(match
|
||||
? $"Override: YES | Replacement value: {MvResolveMvProbe.Value.Value.ToString(CultureInfo.InvariantCulture)} | Components: {dests.Length}"
|
||||
: "Override: NO"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!match)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float value = MvResolveMvProbe.Value.Value;
|
||||
|
||||
foreach (Operand dest in dests)
|
||||
{
|
||||
if (dest != null)
|
||||
{
|
||||
context.Copy(dest, ConstF(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<int> _mvSignLoggedHandles = new();
|
||||
|
||||
private static void ForceConstantMvSign(EmitterContext context, Operand[] dests, int handle)
|
||||
{
|
||||
if (!MvSignProbe.Enabled || dests == null || context.TranslatorContext.Address != MvKillFsAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool match = handle == MvKillHandle && dests.Length == 3;
|
||||
|
||||
lock (_mvSignLoggedHandles)
|
||||
{
|
||||
if (_mvSignLoggedHandles.Add(handle))
|
||||
{
|
||||
context.TranslatorContext.GpuAccessor.Log(
|
||||
$"[MVSIGN] Guest FS: 0x{MvKillFsAddress:X} | Sampler handle: 0x{handle:X} | Components: {dests.Length} | " +
|
||||
(match
|
||||
? $"Override: YES (w only) | Replacement value: {MvSignProbe.Value.Value.ToString(CultureInfo.InvariantCulture)}"
|
||||
: "Override: NO"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!match)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// dests follow the component mask order of the .xyw sample: [0]=x, [1]=y, [2]=w.
|
||||
if (dests[2] != null)
|
||||
{
|
||||
context.Copy(dests[2], ConstF(MvSignProbe.Value.Value));
|
||||
}
|
||||
}
|
||||
|
||||
private static SamplerType ConvertSamplerType(TexDim dimensions)
|
||||
|
||||
@@ -22,6 +22,11 @@ namespace Ryujinx.Graphics.Shader
|
||||
// Computed by a gated IR scan at translation; NOT serialized in the disk cache (format unchanged).
|
||||
// Diagnostic metadata only -- never read by codegen or the render path.
|
||||
public bool UsesDiscard { get; }
|
||||
// [CENSUS v3] True if this translation carried either MV-encode fingerprint (+0.01 sign add
|
||||
// or *3.01 sign decode) -- i.e. the value scrubs cover every draw bound to this program,
|
||||
// whatever guest VA it is bound at (the VA-based census was defeated by code dedup,
|
||||
// journal 163). Like UsesDiscard: diagnostic metadata, NOT serialized in the disk cache.
|
||||
public bool MvppMvEncodeArmed { get; }
|
||||
public byte ClipDistancesWritten { get; }
|
||||
public int FragmentOutputMap { get; }
|
||||
|
||||
@@ -40,7 +45,8 @@ namespace Ryujinx.Graphics.Shader
|
||||
bool usesRtLayer,
|
||||
bool usesDiscard,
|
||||
byte clipDistancesWritten,
|
||||
int fragmentOutputMap)
|
||||
int fragmentOutputMap,
|
||||
bool mvppMvEncodeArmed = false)
|
||||
{
|
||||
CBuffers = Array.AsReadOnly(cBuffers);
|
||||
SBuffers = Array.AsReadOnly(sBuffers);
|
||||
@@ -56,6 +62,7 @@ namespace Ryujinx.Graphics.Shader
|
||||
UsesDrawParameters = usesDrawParameters;
|
||||
UsesRtLayer = usesRtLayer;
|
||||
UsesDiscard = usesDiscard;
|
||||
MvppMvEncodeArmed = mvppMvEncodeArmed;
|
||||
ClipDistancesWritten = clipDistancesWritten;
|
||||
FragmentOutputMap = fragmentOutputMap;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [COC_CONST] Temporary VALIDATION experiment (RYUJINX_COC_CONST=<float>), inert unless set.
|
||||
/// It splits the last two suspects for the XC2 border streaks in a single run.
|
||||
///
|
||||
/// The DoF bokeh is a scatter: the geometry shader (guest GS 0x1000ADF30) receives one point per
|
||||
/// scene pixel, reads the circle-of-confusion in a 64x36 R8 map, and expands the point into a
|
||||
/// quad-sprite whose SIZE is that CoC times a game constant. Corrupting the fragment shader's
|
||||
/// arithmetic outright (see MufuPrecProbe) did NOT move the streaks, so they are not produced by
|
||||
/// the shading -- they come either from the CoC that arrives, or from where the quads land.
|
||||
///
|
||||
/// While this flag is set, that shader's single texture read (verified: the GS samples exactly one
|
||||
/// texture, the CoC map) returns the constant instead, so EVERY sprite gets the same size:
|
||||
/// streaks GONE -> the CoC map, or the way we sample it, drives them;
|
||||
/// streaks STAY -> it is the placement/rasterisation of the quads, independent of size.
|
||||
///
|
||||
/// The value is also its own canary, which the previous experiment lacked: a mid value such as 0.5
|
||||
/// blurs the WHOLE screen uniformly regardless of depth, and 0 removes the blur entirely. Both are
|
||||
/// impossible to miss, so "the picture looks normal" becomes a meaningful answer rather than an
|
||||
/// ambiguous one -- unlike perturbing the weights of an average, which stays a plausible blur.
|
||||
///
|
||||
/// This is a codegen probe: it alters generated host code, so it MUST be listed in
|
||||
/// DiskCacheHostStorage's probe interlock, otherwise the instrumented shader would be persisted
|
||||
/// and served to a later probe-less launch. It is also address-gated, and the disk-cache
|
||||
/// recompile path passes address 0 (ParallelDiskCacheLoader), so the experiment only actually
|
||||
/// runs with the shader cache disabled -- the [COC_CONST] log line is the proof it did.
|
||||
/// </summary>
|
||||
public static class CocConstProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_COC_CONST");
|
||||
|
||||
// Invariant culture on purpose: "0.5" parses, "0,5" does not and leaves the probe off.
|
||||
// A silently-off probe reads as "innocent" and would invert the conclusion, so the
|
||||
// [COC_CONST] log line is what proves the experiment actually ran.
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,6 +346,15 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
// scaled by w so it stays a constant pixel shift after the perspective divide -- the correct,
|
||||
// native-DLSS way to jitter, unlike a viewport shift. The offset is 0 unless jitter is enabled,
|
||||
// so the default path is byte-identical.
|
||||
//
|
||||
// [NOJITTEREMIT 02/08, journal (380)] Suspect du dossier ECRAN VERT (4 jeux, 3 UE) : ce bloc
|
||||
// est emis dans CHAQUE vertex shader, sans gate — seule sa VALEUR est nulle au repos. Deux
|
||||
// facons dont il peut nuire meme jitter eteint : (a) le champ JitterOffset n'est televerse
|
||||
// que quand il CHANGE ⇒ jamais ecrit sur le GPU si le jitter n'a jamais servi ⇒ le shader lit
|
||||
// de la memoire non initialisee (corrige en parallele cote SupportBufferUpdater) ; (b) le
|
||||
// load/store supplementaire de Position peut perturber des programmes qui n'ecrivent pas
|
||||
// Position comme prevu. RYUJINX_NOJITTER_EMIT=1 retire le bloc = epilogue STOCK exact.
|
||||
if (!MvppJitterEmit.Disabled)
|
||||
{
|
||||
Operand jpx = this.Load(StorageKind.Output, IoVariable.Position, null, Const(0));
|
||||
Operand jpy = this.Load(StorageKind.Output, IoVariable.Position, null, Const(1));
|
||||
@@ -406,6 +415,9 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
PrepareForVertexReturn();
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] one [SKYMV] log line per process, at translation time only.
|
||||
private static bool _skyMvLogged;
|
||||
|
||||
public bool PrepareForReturn()
|
||||
{
|
||||
if (IsNonMain)
|
||||
@@ -557,6 +569,55 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
|
||||
Operand src = Register(regIndexBase + component, RegisterType.Gpr);
|
||||
|
||||
// [Beast Roofer diag] RYUJINX_SKYMV_CONST=<float> (absent by default): replace the
|
||||
// sky pass's motion output (render target 1 of guest FS 0x100068730) with a
|
||||
// constant at the store point, colour output untouched. Splits "garbage magnitudes
|
||||
// born in the sky writer" from "born in the material writers". See SkyMvProbe.
|
||||
if (SkyMvProbe.Enabled && TranslatorContext.Address == 0x100068730UL && rtIndex == 1)
|
||||
{
|
||||
if (!_skyMvLogged)
|
||||
{
|
||||
_skyMvLogged = true;
|
||||
TranslatorContext.GpuAccessor.Log(
|
||||
$"[SKYMV] Guest FS: 0x100068730 | Override: YES (rt1) | Replacement value: " +
|
||||
SkyMvProbe.Value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
src = ConstF(SkyMvProbe.Value.Value);
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] PERISCOPE (EXP 15, gated OFF by default): replace the
|
||||
// captured shader's colour outputs with the raw sample -- the screen becomes
|
||||
// the probed texture. ALL rts, not just rt0: the XC2 temporal resolve writes
|
||||
// 4 targets and the displayed colour is NOT rt0 (journal 156), so overriding
|
||||
// every target is the only way to hit the screen without knowing which one
|
||||
// it is. Diagnostic-only shader, loop metadata poisoning is acceptable.
|
||||
if (TranslatorContext.MvppPeriscopeSample != null && component < 3)
|
||||
{
|
||||
src = TranslatorContext.MvppPeriscopeSample[component];
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] NANSCRUB (EXP 14, gated OFF by default): in shaders
|
||||
// carrying the MV-encode fingerprint, wrap every output component with
|
||||
// `x == x ? x : 0` -- NaN becomes 0 (the console's presumed conversion),
|
||||
// legitimate values pass through bit-identical. See NanScrubProbe.
|
||||
if (TranslatorContext.MvppNanScrubArmed)
|
||||
{
|
||||
Operand isNumber = this.FPCompareEqual(src, src);
|
||||
src = this.ConditionalSelect(isNumber, src, ConstF(0));
|
||||
}
|
||||
|
||||
// [Beast Roofer diag] SATSCRUB (EXP 16, gated OFF by default): in the same
|
||||
// fingerprinted shaders, saturated (>= 0.999) values on non-rt0 outputs
|
||||
// become 0 -- the host-side face of the degenerate-rcp garbage. See
|
||||
// NanScrubProbe.SatScrub. Requires RYUJINX_NANSCRUB=1 too (arming).
|
||||
if (TranslatorContext.MvppNanScrubArmed && NanScrubProbe.SatScrub &&
|
||||
(rtIndex != 0 || TranslatorContext.MvppSatScrubAllRts))
|
||||
{
|
||||
Operand isSat = this.FPCompareLess(src, ConstF(NanScrubProbe.SatScrubThreshold));
|
||||
src = this.ConditionalSelect(isSat, src, ConstF(0));
|
||||
}
|
||||
|
||||
// Perform B <-> R swap if needed, for BGRA formats (not supported on OpenGL).
|
||||
if (!supportsBgra && (component == 0 || component == 2))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [HASH_TEST] Temporary VALIDATION experiment (RYUJINX_HASH_CONST=<float>), inert unless set.
|
||||
/// It answers one question and is meant to be deleted afterwards: is the per-pixel dither hash
|
||||
/// of the XC2 DoF bokeh shader required for the border-streak artifact?
|
||||
///
|
||||
/// That shader (guest FS 0x1000AE430) computes hash = fract(sin(...) * 43758.5469). While this
|
||||
/// flag is set, the translator replaces ONLY that one multiply's result with the constant, so
|
||||
/// hash = fract(const) and every other part of the shader (CoC, gathers, weights, accumulation,
|
||||
/// normalization, sprite generation) is untouched. See InstEmit.Fmul32i for the injection site.
|
||||
///
|
||||
/// This is a codegen probe: it alters generated host code, so it MUST be listed in
|
||||
/// DiskCacheHostStorage's probe interlock, otherwise the instrumented shader would be persisted
|
||||
/// and served to a later probe-less launch. It is also address-gated, and the disk-cache
|
||||
/// recompile path passes address 0 (ParallelDiskCacheLoader), so the experiment only actually
|
||||
/// runs with the shader cache disabled -- the [HASH_TEST] log line is the proof it did.
|
||||
/// </summary>
|
||||
public static class HashTestProbe
|
||||
{
|
||||
public static readonly float? ConstValue = Parse();
|
||||
|
||||
public static bool Enabled => ConstValue.HasValue;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_HASH_CONST");
|
||||
|
||||
// Invariant culture on purpose: "0.5" parses, "0,5" does not and leaves the probe off.
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [MUFU_PREC] Temporary VALIDATION experiment (RYUJINX_MUFU_PREC=<0-23>), inert unless set.
|
||||
/// It answers one question and is meant to be deleted afterwards: is the precision of the
|
||||
/// approximate reciprocal/square-root unit in the causal chain of the XC2 border streaks?
|
||||
///
|
||||
/// Maxwell's MUFU is deliberately approximate (MUFU.RCP/RSQ/SQRT are good to a couple of ulp),
|
||||
/// while we translate those to the host's exact IEEE operations -- so the emulator is strictly
|
||||
/// MORE precise than the console it emulates. The DoF bokeh shader (guest FS 0x1000AE430) weighs
|
||||
/// every tap with a clamped `bias - dist/CoC` (~15 rcp + ~13 sqrt) and normalises by 1/sum(w),
|
||||
/// so a value landing on the other side of a clamp boundary flips a whole tap in or out of the
|
||||
/// sum. While this flag is set, the translator drops the low mantissa bits of those three
|
||||
/// results in that one shader; everything else is untouched. See InstEmit.Mufu.
|
||||
///
|
||||
/// Value = mantissa bits KEPT. 23 = no change. 0 = the canary: every result collapses to a power
|
||||
/// of two, which must visibly wreck the blur -- if it does not, that shader is not on screen and
|
||||
/// no verdict from the finer runs can be trusted.
|
||||
///
|
||||
/// This is a codegen probe: it alters generated host code, so it MUST be listed in
|
||||
/// DiskCacheHostStorage's probe interlock, otherwise the instrumented shader would be persisted
|
||||
/// and served to a later probe-less launch. It is also address-gated, and the disk-cache
|
||||
/// recompile path passes address 0 (ParallelDiskCacheLoader), so the experiment only actually
|
||||
/// runs with the shader cache disabled -- the [MUFU_PREC] log line is the proof it did.
|
||||
/// </summary>
|
||||
public static class MufuPrecProbe
|
||||
{
|
||||
/// <summary>Widest mantissa a binary32 result can carry.</summary>
|
||||
public const int MantissaBits = 23;
|
||||
|
||||
/// <summary>Mantissa bits to keep, or null when the probe is off.</summary>
|
||||
public static readonly int? BitsKept = Parse();
|
||||
|
||||
public static bool Enabled => BitsKept.HasValue;
|
||||
|
||||
/// <summary>[v2] (journal 219) optional target override: RYUJINX_MUFU_ADDR=<hex guest FS
|
||||
/// address> retargets the precision reduction (default stays the bokeh FS). The 219 diff
|
||||
/// proved the BUILDER (0x1000AD730) transforms identical inputs differently per backend;
|
||||
/// its MUFU ops (2x rcp, rsq, sqrt) were never probed.</summary>
|
||||
public static readonly ulong? AddressOverride = ParseAddr();
|
||||
|
||||
private static ulong? ParseAddr()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_MUFU_ADDR");
|
||||
|
||||
if (raw != null && ulong.TryParse(raw.Trim().Replace("0x", ""), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong addr))
|
||||
{
|
||||
return addr;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_MUFU_PREC");
|
||||
|
||||
// Invariant culture on purpose, and blank/malformed leaves the probe off rather than
|
||||
// defaulting to something: a silently-off probe reads as "innocent" and would invert
|
||||
// the conclusion. The [MUFU_PREC] log line is what proves it actually ran.
|
||||
if (!string.IsNullOrWhiteSpace(raw) &&
|
||||
int.TryParse(raw.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int bits))
|
||||
{
|
||||
return Math.Clamp(bits, 0, MantissaBits);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [MVKILL] Temporary VALIDATION experiment (RYUJINX_MVKILL_CONST=<float>), inert unless set.
|
||||
/// Splits the two inputs of the XC2 motion-map builder pass.
|
||||
///
|
||||
/// Chain established by the MAP64 probe + decompilation (journal 124/125): the 64x36 map that
|
||||
/// drives form A (EXP 4) is a per-tile MOTION map. Its builder (guest FS 0x1000AD730) combines
|
||||
/// OBJECT motion -- the 1280x720 R10G10B10A2 buffer, x/y encoded as sign*v^2 with the sign bits
|
||||
/// in the 2-bit alpha -- with CAMERA motion reprojected from depth (R32F 640x360) and the
|
||||
/// matrices in fp_c3[5..8]. While this flag is set, the builder's samples of the OBJECT-motion
|
||||
/// buffer (sampler handle 0x8) return the constant instead. With 0.0, v^2 = 0, so object motion
|
||||
/// is nulled regardless of the sign bits -- 0.0 is the only true zero of this encoding (0.5 is
|
||||
/// NOT neutral here, unlike the biased encoding used one pass later).
|
||||
/// flat-block counter reads ~0 on new captures -> form A needs OBJECT-MV data: the corruption
|
||||
/// rides the R10G10B10A2 720p buffer (never watched host-side); watch that surface next;
|
||||
/// flat blocks remain -> the camera/depth path carries it: instrument the 640x360 R32F next.
|
||||
/// Canary caveat: camera-induced blur SURVIVES this override by design, so the picture can look
|
||||
/// near-normal; the verdict comes from the flat-block counter, never from the eye.
|
||||
///
|
||||
/// This is a codegen probe: it alters generated host code, so it MUST be listed in
|
||||
/// DiskCacheHostStorage's probe interlock, and the shader cache must be OFF for the address
|
||||
/// gate to match (the disk-cache recompile path passes address 0). The [MVKILL] log lines are
|
||||
/// the proof the experiment ran.
|
||||
/// </summary>
|
||||
public static class MvKillProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_MVKILL_CONST");
|
||||
|
||||
// Invariant culture on purpose: "0.0" parses, "0,0" does not and leaves the probe off.
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [RESOLVEMV] Surgical A/B (RYUJINX_RESOLVEMV_CONST=<float>), inert unless set. Journal 204.
|
||||
///
|
||||
/// 203 bisected the chain: fresh frame CLEAN, TAA-resolve output TOUCHED, final full-blown --
|
||||
/// the poison enters AT the resolve (guest FS 0x100082C30, read line by line in 199) and
|
||||
/// accumulates in its history loop. The artifact is DISPLACED content in whole blocks, and the
|
||||
/// only displacement vector in that shader is the motion vector it samples from tcb_10 (X) to
|
||||
/// reproject its history (sign*v^2 decode, closest-depth dilation). While this flag is set, the
|
||||
/// resolve's tcb_10 samples return the constant instead. With 0.0: v^2 = 0 -> reprojection
|
||||
/// offset 0 -> history sampled IN PLACE.
|
||||
/// artifact DEAD (eye) -> the displacement path (MV decode/dilation/reprojection) carries
|
||||
/// the poison -> next: split decode vs dilation vs UV math;
|
||||
/// blocks remain -> displacement innocent -> the blend weight / clip box side.
|
||||
/// Codegen probe: MUST be in DiskCacheHostStorage's interlock; cache OFF (address gate).
|
||||
/// </summary>
|
||||
public static class MvResolveMvProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_RESOLVEMV_CONST");
|
||||
|
||||
// Invariant culture on purpose: "0.0" parses, "0,0" does not and leaves the probe off.
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [MVSIGN] Temporary VALIDATION experiment (RYUJINX_MVSIGN_CONST=<float>), inert unless set.
|
||||
/// Splits the object-MV data itself: sign channel vs magnitude channels.
|
||||
///
|
||||
/// Where we stand (journal 128/129): the motion-map builder's samples of the object-MV buffer
|
||||
/// carry the corruption (EXP 5), and the ENTIRE clear family is dead -- command, area, host
|
||||
/// identity and even execution ordering (EXP 6 draw-based clears changed nothing). So the
|
||||
/// garbage is WRITTEN into the buffer, or made at the read. The builder decodes motion as
|
||||
/// sign(w) * (xy)^2, where w is the 2-bit A2 alpha holding the x/y sign bits. EXP 5 nulled all
|
||||
/// three components at once; this experiment overrides ONLY the w component (dests[2] of the
|
||||
/// single .xyw sample, bokeh builder FS 0x1000AD730, handle 0x8), leaving magnitudes live:
|
||||
/// artifact GONE -> the garbage lives in the SIGN channel: the emulator's handling of the
|
||||
/// A2 component (write, blend or decode) is the narrow root surface to instrument next;
|
||||
/// artifact STAYS -> the garbage lives in the 10-bit magnitudes: the material writes.
|
||||
/// With 0.0 both sign bits read 0 (signs forced to -,-): legit object blur keeps its size but
|
||||
/// collapses to one diagonal -- a subtle visual change; the verdict is the flat-block counter.
|
||||
///
|
||||
/// Codegen probe: MUST be in DiskCacheHostStorage's interlock; shader cache OFF required for
|
||||
/// the address gate (recompile path passes address 0). The [MVSIGN] log lines are the proof.
|
||||
/// </summary>
|
||||
public static class MvSignProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_MVSIGN_CONST");
|
||||
|
||||
// Invariant culture on purpose: "0.0" parses, "0,0" does not and leaves the probe off.
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [FORCELOD0] Surgical A/B (RYUJINX_FORCELOD0=1), inert unless set. Journal 211.
|
||||
///
|
||||
/// The bokeh gather samples its colour inputs with IMPLICIT-LOD texture() while adjacent
|
||||
/// pixels can hold wildly different gather parameters (the z-variation trigger, 210):
|
||||
/// divergent UV derivatives push the sampler into HIGH MIPS -- and mip 3-4 of the 512x288
|
||||
/// half-res buffer is 64x36: 20-px texels on screen, the EXACT form-A grid. Unrendered
|
||||
/// mips hold stale/garbage content = displaced blocks. This flag turns the gather's
|
||||
/// implicit-LOD samples (handles 0xC / 0xA of that one shader) into textureLod(0).
|
||||
/// blocks DEAD -> mechanism = LOD/mip divergence; real fix = sampler/view LOD state
|
||||
/// (generic, upstream-able);
|
||||
/// blocks ALIVE -> mips innocent; back to the selection lines with data in hand.
|
||||
/// Codegen probe: MUST be in DiskCacheHostStorage's interlock; cache OFF (address gate).
|
||||
/// </summary>
|
||||
public static class MvppForceLod0Probe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_FORCELOD0") == "1";
|
||||
|
||||
private static readonly HashSet<int> _logged = new();
|
||||
|
||||
public static void OnApplied(IGpuAccessor gpuAccessor, int handle)
|
||||
{
|
||||
lock (_logged)
|
||||
{
|
||||
if (_logged.Add(handle))
|
||||
{
|
||||
gpuAccessor.Log($"[FORCELOD0] bokeh FS sampler handle 0x{handle:X}: implicit-LOD sample forced to textureLod(0)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [JITTEREMIT 03/08, journal (386)] Le bloc de jitter clip-space n'est plus emis QUE si le
|
||||
/// jitter est reellement actif. Racine trouvee par Alex : le bloc LIT `Position` en SORTIE
|
||||
/// avant de la reecrire, et lire une sortie jamais ecrite est INDEFINI — donc meme avec un
|
||||
/// offset a zero, `0 * w + valeur_indefinie` reste indefini. Zeroter la donnee (fix
|
||||
/// JITTERINIT du 02/08) etait necessaire mais PAS suffisant : il fallait ne pas emettre.
|
||||
///
|
||||
/// Par defaut (profil livre = sans jitter) l'epilogue est desormais STOCK a l'octet pres.
|
||||
/// RYUJINX_NOJITTER_EMIT=1 force le retrait meme si le jitter est arme (garde de diagnostic).
|
||||
/// ⚠️ Change la TRADUCTION : `CodeGenVersion` doit etre bumpee, sinon les caches de shaders
|
||||
/// existants servent l'ancien code et le correctif n'atteint pas les utilisateurs.
|
||||
/// </summary>
|
||||
public static class MvppJitterEmit
|
||||
{
|
||||
private static readonly bool _forceOff =
|
||||
System.Environment.GetEnvironmentVariable("RYUJINX_NOJITTER_EMIT") == "1";
|
||||
|
||||
private static readonly bool _jitterOn =
|
||||
System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER") is "1" or "true" or "on";
|
||||
|
||||
/// <summary>True quand le bloc ne doit PAS etre emis (defaut : jitter eteint).</summary>
|
||||
public static bool Disabled => _forceOff || !_jitterOn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Graphics.Shader.CodeGen;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [NCLAMP] EXP (RYUJINX_NCLAMP=1, inert unless set). Journal 196.
|
||||
///
|
||||
/// The XC2 motion-blur builder computes 0 * inversesqrt(0) = NaN on every below-threshold
|
||||
/// pixel and relies on the hardware SAT modifier to flush it to 0. The guest SAT becomes
|
||||
/// Instruction.Clamp; the GLSL backend's clamp() compiles on NVIDIA GL to the hardware
|
||||
/// saturate (NaN -> 0, matching the console), but the SPIR-V backend emits FClamp, whose
|
||||
/// result is UNDEFINED on NaN per spec -- on NVIDIA Vulkan the NaN propagates into the
|
||||
/// temporal ping-pong and becomes the saturated masses. NClamp is the NaN-aware variant:
|
||||
/// NClamp(NaN, 0, 1) == 0 by construction (NMax(NaN,0)=0 then NMin(0,1)=0) -- the exact
|
||||
/// console semantics.
|
||||
///
|
||||
/// Gated swap of FClamp -> NClamp for FLOAT clamps in the SPIR-V backend. Generic: no
|
||||
/// game data, capability-level semantics fix. Requires shader re-translation (cache OFF
|
||||
/// pre-flight in the bat) -- the armed line doubles as the translation witness.
|
||||
/// </summary>
|
||||
public static class MvppNClampProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_NCLAMP") == "1";
|
||||
|
||||
private static long _applied;
|
||||
|
||||
/// <summary>Called once per float clamp translated under the gate. The first call
|
||||
/// prints the armed witness: no line in the log = the gate is off OR nothing was
|
||||
/// re-translated (shader cache still on) = VOID.</summary>
|
||||
public static void OnApplied(ILogger logger)
|
||||
{
|
||||
if (Interlocked.Increment(ref _applied) == 1)
|
||||
{
|
||||
logger?.Log("[NCLAMP] armed: SPIR-V float clamps emitted as NClamp (NaN -> bound, console SAT semantics)");
|
||||
}
|
||||
}
|
||||
|
||||
public static long Applied => Interlocked.Read(ref _applied);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using Ryujinx.Graphics.Shader.CodeGen;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [NMINMAX] EXP (RYUJINX_NMINMAX=1, inert unless set). Journal 199.
|
||||
///
|
||||
/// Companion of [NCLAMP] (196): the hardware FMNMX instruction returns the NON-NaN
|
||||
/// operand when one input is NaN -- the TAA resolve (Shader0086) builds its history
|
||||
/// clip box from 116 min/max ops fed by 17 reciprocals; on hardware/GL a NaN entering
|
||||
/// the chain is flushed out, while our SPIR-V GlslFMin/GlslFMax are UNDEFINED on NaN
|
||||
/// per spec and may propagate it into the clamped history (temporal accumulation =>
|
||||
/// the masses). NMin/NMax have the exact hardware semantics.
|
||||
///
|
||||
/// Gated swap of FMin/FMax -> NMin/NMax for FLOAT min/max in the SPIR-V backend.
|
||||
/// Run TOGETHER with NCLAMP: the two halves are ONE semantic family (hardware NaN
|
||||
/// behavior) -- the (184) lesson about testing halves jointly applies.
|
||||
/// </summary>
|
||||
public static class MvppNMinMaxProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_NMINMAX") == "1";
|
||||
|
||||
private static long _applied;
|
||||
|
||||
/// <summary>First call prints the armed witness (translation-time): no line in the
|
||||
/// log = gate off OR nothing re-translated (shader cache still on) = VOID.</summary>
|
||||
public static void OnApplied(ILogger logger)
|
||||
{
|
||||
if (Interlocked.Increment(ref _applied) == 1)
|
||||
{
|
||||
logger?.Log("[NMINMAX] armed: SPIR-V float min/max emitted as NMin/NMax (NaN -> other operand, hardware FMNMX semantics)");
|
||||
}
|
||||
}
|
||||
|
||||
public static long Applied => Interlocked.Read(ref _applied);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [TILECAP] Surgical A/B (RYUJINX_TILECAP=<float>), inert unless set. Journal 205.
|
||||
///
|
||||
/// 203/204: the poison paints DISPLACED BLOCKS via the bokeh gather, driven by the 64x36
|
||||
/// tile map (EXP 4 proved the map's content causal: tcb_E const kills form A); the bokeh
|
||||
/// FS picks the MAX z among the 3x3 neighbour tiles and blurs along that tile's (x,y).
|
||||
/// One giant tile value contaminates its whole neighbourhood = the block clusters.
|
||||
///
|
||||
/// Unlike EXP 4 (constant = kills ALL DoF), this caps the sampled tile values SOFTLY:
|
||||
/// x/y pulled toward their 0.5 bias by at most K, z clamped to K. Small blur lives,
|
||||
/// giant blur becomes impossible.
|
||||
/// blocks DEAD, DoF alive -> the map VALUES are excessive -> upstream (builder math /
|
||||
/// its constant buffers) makes them; the artifact is the gather honestly obeying them;
|
||||
/// blocks REMAIN -> even small values misdrive the gather -> the defect is in
|
||||
/// the bokeh FS itself (translation of its addressing/weights).
|
||||
/// Codegen probe: MUST be in DiskCacheHostStorage's interlock; cache OFF (address gate).
|
||||
/// </summary>
|
||||
public static class MvppTileCapProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
/// <summary>[v2] (journal 209, Alex's bisection): RYUJINX_TILECAP_CH = "xy" caps only
|
||||
/// the direction channels, "z" caps only the selector/magnitude channel; absent = all
|
||||
/// three. Whichever channel's uniformity kills the blocks ALONE names the trigger.</summary>
|
||||
public static readonly string Channel =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TILECAP_CH");
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_TILECAP");
|
||||
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [TRUNCEPS] Surgical A/B (RYUJINX_TRUNCEPS=<float>), inert unless set. Journal 208.
|
||||
///
|
||||
/// 207 locked the perimeter: form A needs inter-tile VARIATION and lives in the bokeh
|
||||
/// FS's variation-triggered gather. That gather addresses its taps through MANUAL texel
|
||||
/// snapping -- trunc(x + 0.5) -- so a HAIR of upstream precision difference (fma
|
||||
/// contraction, GL vs SPIR-V) flips the snap by one whole texel: block-shaped
|
||||
/// displacement, exactly the artifact's form. This probe nudges every FP32 F2I input of
|
||||
/// that one shader by a tiny epsilon.
|
||||
/// blocks MOVE or DIE -> the snap is precision-critical: the defect is upstream
|
||||
/// rounding/contraction feeding trunc -> real fix = match hardware contraction
|
||||
/// (NoContraction / precise) or bias the snap;
|
||||
/// blocks IDENTICAL -> snap innocent -> next suspect in the gather.
|
||||
/// Codegen probe: MUST be in DiskCacheHostStorage's interlock; cache OFF (address gate).
|
||||
/// </summary>
|
||||
public static class MvppTruncEpsProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
private static long _applied;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_TRUNCEPS");
|
||||
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void OnApplied(IGpuAccessor gpuAccessor)
|
||||
{
|
||||
if (Interlocked.Increment(ref _applied) == 1)
|
||||
{
|
||||
gpuAccessor.Log(
|
||||
$"[TRUNCEPS] armed: bokeh FS F2I inputs nudged by {Value.Value.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [NANSCRUB] Temporary VALIDATION experiment (RYUJINX_NANSCRUB=1), inert unless set.
|
||||
/// Tests the value-divergence hypothesis left standing after the staleness/ordering tree was
|
||||
/// emptied (journal 146): the material velocity math can produce NaN in degenerate cases
|
||||
/// (inf * 0 chains are present in the encode), and writing NaN to a UNORM channel is
|
||||
/// implementation-defined -- likely 0 on the console (no motion, invisible), possibly 1.0 on
|
||||
/// the host GPU (maximum magnitude: giant smears). Patchy per object, motion-only, "delivered
|
||||
/// but wrong", immune to every freshness/order guarantee -- all fourteen measurements fit.
|
||||
///
|
||||
/// While this flag is set, fragment shaders carrying the MV-encode fingerprint (the
|
||||
/// `Fadd32i +0.00999999978` sign-code add -- covers all ~199 material writers plus the sky,
|
||||
/// with no address list, exactly like HASH_TEST's immediate gate) get every colour output
|
||||
/// component wrapped at the store point with `x == x ? x : 0` (NaN becomes 0, the console's
|
||||
/// presumed behavior; legitimate values are untouched, so colour outputs are safe to wrap too).
|
||||
/// flat-block counter ~0 -> ROOT: NaN-to-UNORM conversion divergence; the generic fix is
|
||||
/// this scrub gated per-target (or upstream-grade clamping at the encode);
|
||||
/// flat blocks remain -> hypothesis dead; the value divergence is elsewhere (denormals,
|
||||
/// precision of the encode chain) -- next split from the same fingerprint gate.
|
||||
/// The [NANSCRUB] armed-shader count is the witness: zero armed = void, never "innocent".
|
||||
/// Codegen probe: MUST be in DiskCacheHostStorage's interlock. NOTE: the gate is a code
|
||||
/// FINGERPRINT, not an address, so it also fires on the disk-cache recompile path (address 0)
|
||||
/// -- no config pre-flight needed; the interlock alone forces guest retranslation.
|
||||
/// </summary>
|
||||
public static class NanScrubProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_NANSCRUB") == "1";
|
||||
|
||||
// [SATSCRUB] (EXP 16) The periscope decode (journal 150) showed the garbage is EXACTLY 1.0
|
||||
// magnitudes written by the materials: on the host the degenerate 1/w path yields a huge
|
||||
// FINITE value, and huge * (1/max(huge,1)) ~= 1.0 -- while Maxwell's denormal flush turns
|
||||
// the same path into Inf*0 = NaN, written as 0 = invisible on console. So the divergence is
|
||||
// saturated-but-legal 1.0, which is why the NaN scrub (EXP 14) changed nothing. This flag
|
||||
// scrubs >= 0.999 to 0 on the NON-rt0 outputs of fingerprinted shaders (rt0 = main colour,
|
||||
// never the MV target in any surveyed layout; aux targets may lose rare legit 1.0s -- fine
|
||||
// for a TEST). Artifact gone => root = degenerate-rcp saturation; real fix = epsilon guard.
|
||||
public static readonly bool SatScrub =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_SATSCRUB") == "1";
|
||||
|
||||
// [SATSCRUB2] (EXP 18) Periscope rung 1 (journal 157): the 720p MV buffer already holds the
|
||||
// saturated masses at rest, and the builder's 1/max normalisation turns ANY large writer
|
||||
// output into an exact 1.0 in the map -- so the 0.999 cut can miss poison leaving the
|
||||
// writers at 0.6-0.99 (why EXP 16/17 changed nothing while MVKILL killed everything).
|
||||
// Optional scrub-threshold override, e.g. RYUJINX_SATSCRUB_T=0.5. Absent = 0.999 (EXP 16/17).
|
||||
public static readonly float SatScrubThreshold = ParseThreshold();
|
||||
|
||||
private static float ParseThreshold()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_SATSCRUB_T");
|
||||
|
||||
return float.TryParse(raw, System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out float value)
|
||||
? value
|
||||
: 0.999f;
|
||||
}
|
||||
|
||||
private static long _armed;
|
||||
|
||||
// [CENSUS] (sonde 20, journal 159) Registry of the guest FS addresses the fingerprint armed,
|
||||
// so the Gpu-side writer census can flag MV-buffer writers the scrubs never covered.
|
||||
// Cache-path translations report address 0 (journal 148) and are skipped: the census run
|
||||
// must disable the shader cache or every writer would look unarmed (void).
|
||||
private static readonly System.Collections.Generic.HashSet<ulong> _armedAddresses = new();
|
||||
|
||||
public static bool WasArmed(ulong address)
|
||||
{
|
||||
lock (_armedAddresses)
|
||||
{
|
||||
return _armedAddresses.Contains(address);
|
||||
}
|
||||
}
|
||||
|
||||
public static int ArmedAddressCount()
|
||||
{
|
||||
lock (_armedAddresses)
|
||||
{
|
||||
return _armedAddresses.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnArmed(ulong address, IGpuAccessor gpuAccessor)
|
||||
{
|
||||
if (address != 0)
|
||||
{
|
||||
lock (_armedAddresses)
|
||||
{
|
||||
_armedAddresses.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
long n = Interlocked.Increment(ref _armed);
|
||||
|
||||
if (n <= 3 || n % 50 == 0)
|
||||
{
|
||||
gpuAccessor.Log($"[NANSCRUB] armed shader #{n} (guest 0x{address:X}) -- outputs will be NaN-scrubbed"
|
||||
+ (SatScrub ? $" + sat-scrubbed at >= {SatScrubThreshold.ToString(System.Globalization.CultureInfo.InvariantCulture)}" : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
|
||||
{
|
||||
class BindlessElimination
|
||||
{
|
||||
// [NOBINDLESS 01/08] Voir GenerateBindlessAccess. OFF par defaut.
|
||||
private static readonly bool _noBindless =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_SHADER_NOBINDLESS") == "1";
|
||||
|
||||
public static void RunPass(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor)
|
||||
{
|
||||
// We can turn a bindless into regular access by recognizing the pattern
|
||||
@@ -61,6 +65,19 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
|
||||
TextureOperation texOp,
|
||||
LinkedListNode<INode> node)
|
||||
{
|
||||
// [NOBINDLESS 01/08] Test de DISCRIMINATION, gate OFF par defaut (variable absente =>
|
||||
// comportement a l'octet pres). RYUJINX_SHADER_NOBINDLESS=1 fait echouer la generation
|
||||
// d'acces bindless exactement comme sur un hote sans separate-sampler (OpenGL) : la
|
||||
// lecture de texture est supprimee et le resultat force a ZERO (voir le site appelant).
|
||||
// BUT : sur un jeu ou les couleurs sont fausses en Vulkan et justes en OpenGL, si les
|
||||
// couleurs redeviennent justes avec ce gate, c'est que le chemin bindless genere
|
||||
// echantillonne les MAUVAISES textures (poignee rabattue dans les bornes du pool
|
||||
// ligne ~99). Diagnostic seulement : ce n'est PAS un correctif candidat.
|
||||
if (_noBindless)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!gpuAccessor.QueryHostSupportsSeparateSampler())
|
||||
{
|
||||
// We depend on combining samplers and textures in the shader being supported for this.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [PERISCOPE] Temporary VISUALIZATION experiment (RYUJINX_PERISCOPE=1), inert unless set.
|
||||
/// Makes the screen show the RAW 64x36 motion tile map instead of the motion-blur result.
|
||||
///
|
||||
/// After ten cause families died by measurement (journal 121-148), the missing piece is a
|
||||
/// direct LOOK at the corrupted data itself: readbacks deform it ((112)(3)), Nsight is out.
|
||||
/// This probe turns the display into the debugger: in the blur shader (guest FS 0x1000AE430),
|
||||
/// the first sample of the tile map (handle 0xE) is captured at translation and REPLACES the
|
||||
/// shader's colour output (rt0.xyz), so the final image IS the map, one texel = 20 screen px,
|
||||
/// through the normal render path -- no readback, no deformation. Alex's captures become
|
||||
/// memory dumps I can analyse offline: replicated rows would say scale/clamp mismatch,
|
||||
/// static blocks would say uninitialized regions, flicker would say generation mixing.
|
||||
///
|
||||
/// Address-gated like TILEMAP_CONST (the blur FS lacks the 0.01 fingerprint), so the shader
|
||||
/// cache must be OFF for the run -- the TILEMAP-style pre-flight bat handles it. Codegen
|
||||
/// probe: in the DiskCacheHostStorage interlock. Witness: the screen looks obviously wrong
|
||||
/// (a coarse mosaic instead of the scene's blur) + the [PERISCOPE] log line.
|
||||
/// </summary>
|
||||
public static class PeriscopeProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_PERISCOPE") == "1";
|
||||
|
||||
// [PERISCOPE2] Ladder stage 1 (journal 153): display the RAW 720p object-MV buffer,
|
||||
// one pixel per texel, by capturing the temporal resolve's tcb_10 sample (guest FS
|
||||
// 0x100082C30) and overriding its rt0 scene output. Same field, same return-site override.
|
||||
public static readonly bool Stage2 =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_PERISCOPE2") == "1";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [RROREDUCE] Range reduction of the sin/cos argument before MUFU (RYUJINX_RRO_REDUCE=1),
|
||||
/// inert unless set. Maxwell's RRO instruction reduces the argument ahead of every MUFU.SIN/COS;
|
||||
/// we translate RRO as a plain move, so large arguments reach the host sin/cos at full magnitude.
|
||||
/// See InstEmit.Mufu / ReduceSinCosArg for the injection site.
|
||||
///
|
||||
/// Single source of truth for the flag. This is a codegen probe -- it changes the generated host
|
||||
/// code -- so it belongs in DiskCacheHostStorage's probe interlock exactly like the other probes:
|
||||
/// without it, a run with the flag on would persist the modified shader into the shared host
|
||||
/// cache and keep serving it to later launches that have the flag off, with the DLL restored and
|
||||
/// nothing on screen to explain it. That hole was latent for as long as this flag existed; it is
|
||||
/// closed here rather than left as a trap for the next experiment.
|
||||
/// </summary>
|
||||
public static class RroReduceProbe
|
||||
{
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_RRO_REDUCE") == "1";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [SKYMV] Temporary VALIDATION experiment (RYUJINX_SKYMV_CONST=<float>), inert unless set.
|
||||
/// Splits the WRITERS of the corrupted object-MV magnitudes: sky pass vs scene materials.
|
||||
///
|
||||
/// Where we stand (journal 130/131): the corruption lives in the 10-bit magnitude channels of
|
||||
/// the MV buffer, written after a proven-good clear. The buffer has two writer classes: ~763
|
||||
/// per-material fragment shaders, and ONE full-screen pass at guest FS 0x100068730 -- the
|
||||
/// SKY/background: it reprojects the view direction through the previous frame's matrices
|
||||
/// (fp_c3[10..18]) and encodes camera-rotation motion as out_attr1 = (sqrt|mx|, sqrt|my|, 0,
|
||||
/// signCode + 0.01), the sqrt built from an Rsq-then-Rcp MUFU chain never covered by any
|
||||
/// precision test (EXP 2 only touched the blur shader). The sky covers exactly the regions
|
||||
/// where the artifact is worst, and fills the screen when looking up/down -- the worst case.
|
||||
///
|
||||
/// While this flag is set, every component the sky pass writes to render target 1 (its MV
|
||||
/// output; its colour output at target 0 is untouched) is replaced by the constant at the
|
||||
/// output-store point (EmitterContext.PrepareForReturn). With 0.0 the sky writes zero motion:
|
||||
/// flat-block counter ~0 -> the garbage magnitudes are BORN IN THE SKY PASS's writes; the
|
||||
/// root is inside its encode chain (Rsq/Rcp MUFU, normalize, matrix path) -- narrow next;
|
||||
/// flat blocks remain -> sky innocent; the garbage comes from the material writers
|
||||
/// (bisection or material-encode reading next).
|
||||
/// Legit visual cost: the sky loses its motion blur only -- subtle; the verdict is the counter.
|
||||
///
|
||||
/// Codegen probe: MUST be in DiskCacheHostStorage's interlock; shader cache OFF required for
|
||||
/// the address gate (recompile path passes address 0). The [SKYMV] log line is the proof.
|
||||
/// </summary>
|
||||
public static class SkyMvProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_SKYMV_CONST");
|
||||
|
||||
// Invariant culture on purpose: "0.0" parses, "0,0" does not and leaves the probe off.
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
|
||||
// Built on Ryujinx (MIT).
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation
|
||||
{
|
||||
/// <summary>
|
||||
/// [TILEMAP] Temporary VALIDATION experiment (RYUJINX_TILEMAP_CONST=<float>), inert unless set.
|
||||
/// Tests whether the DoF tile map drives the XC2 "form A" corruption rectangles.
|
||||
///
|
||||
/// Form-A characterisation (journal entry 121) measured, on clean-run captures, that every
|
||||
/// locatable edge of the corruption rectangles snaps to a 20-px grid at 720p (12 of 12 edges
|
||||
/// across two captures, within 1 px) -- and 20 px is exactly one texel of the 64x36 DoF maps
|
||||
/// (1280/64 = 720/36 = 20). The bokeh fragment shader (guest FS 0x1000AE430) reads one such map,
|
||||
/// fp_t_tcb_E (64x36 RGBA8, sampler handle 0xE), as a 3x3 neighbourhood (bokeh_fs.glsl:866-906):
|
||||
/// the shape of a per-tile classification map, and the one bokeh input no instrument ever
|
||||
/// examined. While this flag is set, every sample of that map in that shader returns the
|
||||
/// constant instead:
|
||||
/// flat-block counter reads 0 on new captures -> the tile map (its content or our sampling of
|
||||
/// it) is in form A's causal chain; next step is watching the 64x36 surface host-side;
|
||||
/// flat blocks remain -> the map is innocent; next suspects are fp_t_tcb_C (320x180), then
|
||||
/// fp_t_tcb_A.
|
||||
/// The value is its own canary: 1.0 saturates every tile of an RGBA8 map (bounded, no perf
|
||||
/// hazard) and visibly changes the DoF everywhere, so "picture unchanged" means the override is
|
||||
/// not being consumed (void result), never "innocent".
|
||||
///
|
||||
/// This is a codegen probe: it alters generated host code, so it MUST be listed in
|
||||
/// DiskCacheHostStorage's probe interlock, otherwise the instrumented shader would be persisted
|
||||
/// and served to a later probe-less launch. It is also address-gated, and the disk-cache
|
||||
/// recompile path passes address 0 (ParallelDiskCacheLoader), so the experiment only actually
|
||||
/// runs with the shader cache disabled -- the [TILEMAP] log lines are the proof it did.
|
||||
/// </summary>
|
||||
public static class TileMapConstProbe
|
||||
{
|
||||
public static readonly float? Value = Parse();
|
||||
|
||||
public static bool Enabled => Value.HasValue;
|
||||
|
||||
private static float? Parse()
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable("RYUJINX_TILEMAP_CONST");
|
||||
|
||||
// Invariant culture on purpose: "1.0" parses, "1,0" does not and leaves the probe off.
|
||||
// A silently-off probe reads as "innocent" and would invert the conclusion, so the
|
||||
// [TILEMAP] log lines are what prove the experiment actually ran.
|
||||
if (raw != null &&
|
||||
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,21 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
Environment.GetEnvironmentVariable("RYUJINX_MVPP_CUTOUT_PROBE") == "1";
|
||||
|
||||
public ulong Address { get; }
|
||||
|
||||
// [Beast Roofer diag] NANSCRUB (EXP 14): set by Fadd32i when the MV-encode fingerprint
|
||||
// (+0.00999999978 sign-code add) is seen in this translation; PrepareForReturn then wraps
|
||||
// every colour output with a NaN scrub. Per-translation state, parallel-safe by ownership.
|
||||
public bool MvppNanScrubArmed;
|
||||
|
||||
// [Beast Roofer diag] PERISCOPE (EXP 15): the blur shader's first tile-map sample operands,
|
||||
// captured at EmitTextureSample; PrepareForReturn replaces rt0.xyz with them so the screen
|
||||
// shows the raw map. Per-translation state, parallel-safe by ownership.
|
||||
internal IntermediateRepresentation.Operand[] MvppPeriscopeSample;
|
||||
|
||||
// [Beast Roofer diag] SATSCRUB stage 2 (EXP 17): armed via the *3.00999999 decode
|
||||
// fingerprint (the 6 MV-chain processors incl. the builder); their MV output IS rt0,
|
||||
// so the saturation scrub must include it for these shaders.
|
||||
public bool MvppSatScrubAllRts;
|
||||
public int Size { get; }
|
||||
public int Cb1DataSize => _program.Cb1DataSize;
|
||||
|
||||
@@ -433,7 +448,8 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
usedFeatures.HasFlag(FeatureFlags.RtLayer),
|
||||
usesDiscard,
|
||||
clipDistancesWritten,
|
||||
originalDefinitions.OmapTargets);
|
||||
originalDefinitions.OmapTargets,
|
||||
MvppNanScrubArmed || MvppSatScrubAllRts);
|
||||
|
||||
HostCapabilities hostCapabilities = new HostCapabilities(
|
||||
GpuAccessor.QueryHostReducedPrecision(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user