From a4fb32b727d47d5fbbb925c20be78df6843f102a Mon Sep 17 00:00:00 2001 From: The Roofer Dev Date: Wed, 5 Aug 2026 20:56:37 -0400 Subject: [PATCH] The Beast Roofer Edition v1.2.5 --- .gitattributes | 3 + docs/RELEASE-NOTES-v1.2.4.md | 62 +- src/Ryujinx.Common/Configuration/VSyncMode.cs | 2 +- src/Ryujinx.Common/Utilities/JsonHelper.cs | 5 - src/Ryujinx.Graphics.GAL/DlssCameraState.cs | 128 -- src/Ryujinx.Graphics.GAL/DlssJitterState.cs | 45 - src/Ryujinx.Graphics.GAL/IPipeline.cs | 4 - .../Multithreading/CommandHelper.cs | 2 +- .../Buffer/BufferSetDataBatchCommand.cs | 16 - .../Commands/MvppInjectDlssCommand.cs | 11 - .../Multithreading/ThreadedRenderer.cs | 16 - src/Ryujinx.Graphics.GAL/MvppColorSnapshot.cs | 24 - src/Ryujinx.Graphics.GAL/MvppDev.cs | 9 - src/Ryujinx.Graphics.GAL/MvppInjectState.cs | 11 - src/Ryujinx.Graphics.GAL/MvppVelState.cs | 41 - src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs | 5 - .../Engine/Compute/ComputeClass.cs | 4 +- .../Engine/Dma/DmaClass.cs | 3 +- .../Engine/MvppBlitProbe.cs | 33 +- .../Engine/Threed/ConstantBufferUpdater.cs | 10 - .../Engine/Threed/DrawManager.cs | 31 +- .../Engine/Threed/MvppCamTrace.cs | 54 - .../Engine/Threed/MvppCameraCapture.cs | 357 +--- .../Engine/Threed/MvppCompInputsProbe.cs | 15 - .../Engine/Threed/MvppCompositeProbe.cs | 21 +- .../Engine/Threed/MvppCtxProbe.cs | 183 -- .../Engine/Threed/MvppDlssUpscale.cs | 67 +- .../Engine/Threed/MvppDofSkip.cs | 68 +- .../Engine/Threed/MvppDrawStepProbe.cs | 78 - .../Engine/Threed/MvppExtProbe.cs | 216 ++ .../Engine/Threed/MvppFamHold.cs | 312 +++ .../Engine/Threed/MvppFinalDrawProbe.cs | 26 +- .../Engine/Threed/MvppGlowProbe.cs | 36 - .../Engine/Threed/MvppHangWatch.cs | 47 - .../Engine/Threed/MvppHazardProbe.cs | 30 - .../Engine/Threed/MvppHdrCensusProbe.cs | 22 +- .../Engine/Threed/MvppInjectGate.cs | 41 +- .../Engine/Threed/MvppLayoutProbe.cs | 14 - .../Engine/Threed/MvppLdrSkip.cs | 149 -- .../Engine/Threed/MvppP2BProbe.cs | 26 +- .../Engine/Threed/MvppP2Probe.cs | 36 +- .../Engine/Threed/MvppPreSyncProbe.cs | 23 - .../Engine/Threed/MvppProbe.cs | 27 +- .../Engine/Threed/MvppProjAudit.cs | 81 +- .../Engine/Threed/MvppRtDumpProbe.cs | 90 - .../Engine/Threed/MvppScanProbe.cs | 294 +-- .../Engine/Threed/MvppScenePass.cs | 77 - .../Engine/Threed/MvppSoloCamera.cs | 1153 +---------- .../Engine/Threed/MvppSurviveProbeBuffer.cs | 46 - .../Engine/Threed/MvppUiProbe.cs | 149 +- .../Engine/Threed/MvppVelocityPass.cs | 509 +---- .../Engine/Threed/MvppViewportProbe.cs | 97 +- .../Engine/Threed/SemaphoreUpdater.cs | 12 - .../Engine/Threed/StateUpdater.cs | 188 +- .../Engine/Twod/MvppTwodProbe.cs | 35 +- .../Engine/Twod/TwodClass.cs | 9 +- src/Ryujinx.Graphics.Gpu/GpuContext.cs | 6 +- .../Image/AutoDeleteCache.cs | 16 +- .../Image/MvppBuilderInProbe.cs | 82 +- .../Image/MvppCacheProbe.cs | 43 +- .../Image/MvppConstDiffProbe.cs | 21 +- .../Image/MvppContentProbe.cs | 26 - .../Image/MvppDofProbe.cs | 43 +- .../Image/MvppFeedbackProbe.cs | 36 +- .../Image/MvppFullSyncProbe.cs | 39 - .../Image/MvppGobProbe.cs | 32 - .../Image/MvppMap64Probe.cs | 40 - .../Image/MvppMvBufProbe.cs | 43 - .../Image/MvppMvSyncProbe.cs | 28 - .../Image/MvppPairProbe.cs | 22 - .../Image/MvppScenePassProbe.cs | 40 +- .../Image/MvppSeamProbe.cs | 30 +- .../Image/MvppTaaProbe.cs | 37 - .../Image/MvppTraceProbe.cs | 18 +- .../Image/MvppTwinFixProbe.cs | 25 - .../Image/MvppTwinMapProbe.cs | 48 - .../Image/MvppTwinXferProbe.cs | 30 - .../Image/MvppViewAliasProbe.cs | 37 - .../Image/MvppWriterCensusProbe.cs | 32 +- src/Ryujinx.Graphics.Gpu/Image/Sampler.cs | 20 +- src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 149 +- .../Image/TextureBindingsArrayCache.cs | 4 +- .../Image/TextureBindingsManager.cs | 87 +- .../Image/TextureCache.cs | 23 - .../Image/TextureCompatibility.cs | 12 +- .../Image/TextureGroup.cs | 21 +- .../Image/TextureManager.cs | 55 +- src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs | 10 - src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs | 40 +- .../Memory/BufferCache.cs | 23 +- .../Memory/BufferManager.cs | 3 - .../Memory/MvppPalProbe.cs | 46 +- .../Memory/MvppUpVolProbe.cs | 30 +- .../Memory/SupportBufferUpdater.cs | 14 +- src/Ryujinx.Graphics.Gpu/MvppDestProbe.cs | 31 +- .../Shader/DiskCache/DiskCacheHostStorage.cs | 16 - .../Shader/ShaderCache.cs | 23 +- .../Shader/ShaderInfoBuilder.cs | 11 - src/Ryujinx.Graphics.Gpu/Window.cs | 112 +- .../FFmpegContext.cs | 51 + src/Ryujinx.Graphics.OpenGL/Framebuffer.cs | 1 - .../MvppGlDumpProbe.cs | 11 - src/Ryujinx.Graphics.OpenGL/Pipeline.cs | 4 +- src/Ryujinx.Graphics.OpenGL/Window.cs | 4 +- .../CodeGen/Spirv/Instructions.cs | 8 - .../CodeGen/Spirv/SpirvDelegates.cs | 12 +- .../Instructions/InstEmitConversion.cs | 2 - .../Instructions/InstEmitFloatArithmetic.cs | 28 +- .../Instructions/InstEmitMultifunction.cs | 38 +- .../Instructions/InstEmitTexture.cs | 81 +- .../TextureOperation.cs | 3 - .../ShaderProgramInfo.cs | 7 - .../Translation/AttributeUsage.cs | 9 - .../Translation/CocConstProbe.cs | 32 - .../Translation/EmitterContext.cs | 55 - .../Translation/HashTestProbe.cs | 20 - .../Translation/MufuPrecProbe.cs | 35 - .../Translation/MvKillProbe.cs | 27 - .../Translation/MvResolveMvProbe.cs | 19 - .../Translation/MvSignProbe.cs | 24 - .../Translation/MvppForceLod0Probe.cs | 17 - .../Translation/MvppHashedAlpha.cs | 165 +- .../Translation/MvppJitterEmit.cs | 16 - .../Translation/MvppNClampProbe.cs | 22 - .../Translation/MvppNMinMaxProbe.cs | 19 - .../Translation/MvppPositionProbe.cs | 30 - .../Translation/MvppSurviveProbe.cs | 38 - .../Translation/MvppTileCapProbe.cs | 23 - .../Translation/MvppTruncEpsProbe.cs | 18 - .../Translation/MvppVariantInjector.cs | 87 +- .../Translation/NanScrubProbe.cs | 43 - .../Optimizations/BindlessElimination.cs | 9 - .../Translation/PeriscopeProbe.cs | 24 - .../Translation/ResourceManager.cs | 9 +- .../Translation/RroReduceProbe.cs | 16 - .../Translation/SkyMvProbe.cs | 29 - .../Translation/TileMapConstProbe.cs | 32 - .../Translation/TranslationFlags.cs | 5 - .../Translation/TranslatorContext.cs | 33 - .../Astc/AstcDecoder.cs | 5 - .../LayoutConverter.cs | 15 - src/Ryujinx.Graphics.Vulkan/Constants.cs | 11 - .../DescriptorSetUpdater.cs | 19 +- .../Dlss/DlssBinaries.cs | 74 +- .../Dlss/DlssFrameGenHooks.cs | 52 - .../Dlss/DlssGpuTimer.cs | 24 +- .../Dlss/DlssIntegration.cs | 149 +- .../Dlss/DlssJitter.cs | 37 +- .../Dlss/DlssSharpenPass.cs | 19 - .../Dlss/DlssUpscaler.cs | 1744 +++-------------- .../Dlss/MvppSurviveMaskState.cs | 11 - .../Dlss/NvOpticalFlow.cs | 98 +- .../Dlss/Streamline.cs | 211 +- .../Dlss/StreamlineDlss.cs | 121 +- .../Dlss/StreamlineFrameGen.cs | 252 +-- .../Effects/FsrScalingFilter.cs | 4 - .../Effects/NisScalingFilter.cs | 55 +- .../Effects/Shaders/BorderClamp.glsl | 20 +- .../Effects/Shaders/FsrSharpening.glsl | 100 +- .../Effects/Shaders/MotionFilter.glsl | 22 +- .../Effects/Shaders/MotionVectors.glsl | 58 +- .../Effects/Shaders/MvppCutoutMask.glsl | 5 - .../Effects/Shaders/MvppReproject.glsl | 477 +---- .../Effects/Shaders/MvppReproject.spv | Bin 49896 -> 51000 bytes .../MvppReproject.spv.bak_avant_depthsonde | Bin 41920 -> 0 bytes .../MvppReproject.spv.bak_avant_dynsonde | Bin 38064 -> 0 bytes .../MvppReproject.spv.bak_avant_flowgate | Bin 29412 -> 0 bytes .../MvppReproject.spv.bak_avant_skydepth | Bin 44832 -> 0 bytes .../MvppReproject.spv.bak_avant_skyflow | Bin 26776 -> 0 bytes .../MvppReproject.spv.bak_avant_subgroup | Bin 35104 -> 0 bytes .../Effects/Shaders/MvppVelDebug.glsl | 34 +- .../Effects/Shaders/NisScaler.h | 213 +- .../Effects/Shaders/NisScaling.glsl | 26 - .../Effects/Shaders/NisScalingHdr.glsl | 30 - .../Effects/Shaders/TaaDownsample.glsl | 10 +- .../Effects/Shaders/Temporal.glsl | 56 +- .../Effects/Shaders/nis_coef.glsl | 23 - .../Effects/TemporalFilter.cs | 53 +- .../FramebufferParams.cs | 31 +- src/Ryujinx.Graphics.Vulkan/HelperShader.cs | 14 +- .../MemoryAllocation.cs | 6 +- .../MvppClearRectProbe.cs | 20 - .../MvppDecompProbe.cs | 21 - .../MvppDescTruthProbe.cs | 16 - .../MvppDofStateProbe.cs | 14 - .../MvppDrawClearProbe.cs | 26 - .../MvppHistWatchProbe.cs | 14 - .../MvppMemAliasProbe.cs | 20 - .../MvppMvDumpProbe.cs | 46 +- src/Ryujinx.Graphics.Vulkan/MvppNoFblProbe.cs | 21 - .../MvppNoFragMask0Probe.cs | 25 - src/Ryujinx.Graphics.Vulkan/MvppOmapMaskVk.cs | 18 - .../MvppOmapVentProbe.cs | 24 - .../MvppReadBarProbe.cs | 22 - .../MvppSampStoreProbe.cs | 17 - .../MvppStorageBarrierProbe.cs | 47 - .../MvppVkDropProbe.cs | 25 - src/Ryujinx.Graphics.Vulkan/MvppVkImgProbe.cs | 39 +- src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 92 +- src/Ryujinx.Graphics.Vulkan/PipelineFull.cs | 4 - src/Ryujinx.Graphics.Vulkan/PipelineState.cs | 23 +- .../ProvokingVertexOverride.cs | 14 + .../RenderSyncSwitch.cs | 35 - src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs | 14 +- src/Ryujinx.Graphics.Vulkan/Shader.cs | 4 - .../ShaderCollection.cs | 5 - .../ColorBlitHdrFragmentShaderSource.frag | 25 - src/Ryujinx.Graphics.Vulkan/TextureStorage.cs | 20 - src/Ryujinx.Graphics.Vulkan/TextureView.cs | 14 +- .../UnsafeBlitProbe.cs | 46 - .../VulkanInitialization.cs | 59 - src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 44 +- src/Ryujinx.Graphics.Vulkan/Window.cs | 281 +-- .../Debugger/Debugger.MainThread.cs | 6 +- src/Ryujinx.HLE/Debugger/Debugger.cs | 76 +- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 7 - .../HOS/Kernel/Process/KProcess.cs | 290 ++- .../Services/Fs/FileSystemProxy/IStorage.cs | 17 +- .../HOS/Services/Fs/IFileSystemProxy.cs | 4 - src/Ryujinx.HLE/HOS/Services/IpcService.cs | 42 +- .../Services/SurfaceFlinger/SurfaceFlinger.cs | 4 - src/Ryujinx.HLE/HleConfiguration.cs | 1 - .../Loaders/Processes/ProcessLoaderHelper.cs | 40 + src/Ryujinx.HLE/Switch.cs | 4 +- .../Sdk/Sf/Cmif/ServiceDispatchTableBase.cs | 9 + src/Ryujinx/Program.cs | 26 +- src/Ryujinx/Systems/AppHost.cs | 40 +- .../Systems/AppLibrary/ApplicationLibrary.cs | 8 +- .../Configuration/ConfigurationFileFormat.cs | 11 +- .../Configuration/ConfigurationState.Model.cs | 15 +- src/Ryujinx/Systems/DlssRestart.cs | 51 - src/Ryujinx/Systems/DlssUiSettings.cs | 115 +- src/Ryujinx/Systems/Updater/Updater.GitLab.cs | 9 - src/Ryujinx/Systems/Updater/Updater.cs | 41 +- src/Ryujinx/UI/Helpers/ContentDialogHelper.cs | 11 +- .../UI/ViewModels/MainWindowViewModel.cs | 33 +- .../UI/ViewModels/SettingsViewModel.cs | 64 +- .../GameSpecificSettingsWindow.axaml.cs | 8 +- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 10 +- .../UI/Windows/SettingsWindow.axaml.cs | 8 - 240 files changed, 2279 insertions(+), 11508 deletions(-) create mode 100644 src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppExtProbe.cs create mode 100644 src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFamHold.cs delete mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_depthsonde delete mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_dynsonde delete mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_flowgate delete mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_skydepth delete mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_skyflow delete mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_subgroup create mode 100644 src/Ryujinx.Graphics.Vulkan/ProvokingVertexOverride.cs diff --git a/.gitattributes b/.gitattributes index 5b44a82e0..de95a7c30 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,6 @@ ############################################################################### * text=auto eol=lf *.json text eol=lf + +# Les bats du coffre doivent garder leurs fins de ligne Windows (CRLF) +docs/bats-valides/*.bat text eol=crlf diff --git a/docs/RELEASE-NOTES-v1.2.4.md b/docs/RELEASE-NOTES-v1.2.4.md index 30d367651..ce0fb942f 100644 --- a/docs/RELEASE-NOTES-v1.2.4.md +++ b/docs/RELEASE-NOTES-v1.2.4.md @@ -10,12 +10,12 @@ I am shipping this update because I caught a bug I had introduced myself in v1.2 one that turns the screen green in some games. It could in principle affect any game, so please take this update even if yours looked fine. -One correction while I am at it: the black block artifacts in Xenoblade Chronicles 2, the -ones I had described as a long-standing emulation defect and worked around by skipping the +One correction while I am at it: the black block artifacts some of you saw in +motion-blur scenes, the ones I had described as a long-standing emulation defect and worked around by skipping the motion-blur pass, were caused by that same bug of mine. They were never an old emulator problem, they were mine. They are gone now, and the motion blur stays on. -### Fixed: green screen in several games (GYLT, Ghostbusters: Spirits Unleashed, and others) +### Fixed: green screen in several games Some games — several Unreal Engine titles among them — rendered their menus and videos correctly but showed a green (or corrupted) screen the moment 3D gameplay started. The @@ -47,9 +47,26 @@ can make halos and shimmer more visible in motion. Applied on the next launch, l other DLSS settings. An experimental `Advanced motion stack` checkbox is also present, **off by default**. It -is validated on one engine family only and can add trembling in motion elsewhere — leave +improves camera tracking: it reads the camera even when a game stores it in an unusual place, +it rejects misleading matrices such as sky or reflections, and it stops mistaking the game's +own sampling jitter for real motion. It removes nothing from the image. It has only been +tested on Xenoblade Chronicles 2, and it can add trembling in motion on other games, so leave it off unless you want to experiment. +### Requirements + +DLSS in all its modes — DLAA, Quality, Balanced and Performance — needs an NVIDIA RTX card, +RTX 20 series or newer. Frame Generation needs an RTX 40 or 50 series. If you do not have an +RTX card, NIS is included in this fork and runs on any GPU, and the green screen fix above +matters to you either way. + +The DLSS engine files are not in the package, because they are NVIDIA's proprietary DLLs and I +will not redistribute them. What the package does ship is the `dlss` folder next to the +executable, with NVIDIA's six Streamline files, which are MIT licensed. In that same folder you +add your own, taken from a DLSS game you own: `nvngx_dlss.dll` for upscaling, `nvngx_dlssg.dll` +for Frame Generation, which the NVIDIA driver usually provides already, and `NvLowLatencyVk.dll` +if your driver does not. + ### Known issue: Frame Generation still produces ghost images With Frame Generation enabled, ghost or "temporal" images can still appear — doubled @@ -59,9 +76,9 @@ motion vectors we reconstruct, and those describe the camera better than they de individual moving objects. If ghosting bothers you more than the extra smoothness helps, set Frame Generation to Off in the graphics settings. -### Known issue: Xenoblade Chronicles 2 can hang while loading +### Known issue: loading can hang on some games -On Xenoblade Chronicles 2, loading (or the menu) occasionally stops progressing — the +On some games, loading (or the menu) occasionally stops progressing — the picture keeps drawing, but the game stops advancing. Closing and reopening the emulator clears it. We captured this live several times and it is **not solved yet**: the freeze happens on the game's own side, and it predates the changes in this release. If you hit @@ -75,12 +92,12 @@ J'envoie cette mise à jour parce que j'ai attrapé un bug que j'avais moi-même v1.2.3, celui qui rend l'écran vert dans certains jeux. Il pouvait en principe toucher n'importe quel jeu, alors prenez la mise à jour même si le vôtre semblait correct. -Une correction au passage : les blocs noirs dans Xenoblade Chronicles 2, ceux que j'avais -décrits comme un vieux défaut d'émulation et que je contournais en désactivant la passe de +Une correction au passage : les blocs noirs que certains ont vus dans les scènes avec +flou de mouvement, ceux que j'avais décrits comme un vieux défaut d'émulation et que je contournais en désactivant la passe de flou de mouvement, venaient du même bug — le mien. Ça n'a jamais été un problème ancien de l'émulateur, c'était moi. Ils sont partis, et le flou de mouvement reste actif. -### Corrigé : écran vert dans plusieurs jeux (GYLT, Ghostbusters: Spirits Unleashed, et d'autres) +### Corrigé : écran vert dans plusieurs jeux Certains jeux — plusieurs titres Unreal Engine notamment — affichaient correctement leurs menus et vidéos, mais montraient un écran vert (ou corrompu) dès que la 3D commençait. @@ -116,8 +133,27 @@ mouvement deviennent plus visibles. Appliqué au prochain lancement, comme les a réglages DLSS. Une case `Advanced motion stack` expérimentale est également présente, **désactivée par -défaut**. Elle n'est validée que sur une famille de moteur et peut au contraire ajouter du -tremblement en mouvement ailleurs — laissez-la éteinte sauf si vous voulez expérimenter. +défaut**. Elle améliore le suivi de la caméra : elle lit la caméra même quand un jeu la range +dans un endroit inhabituel, elle rejette les matrices trompeuses comme le ciel ou les reflets, +et elle ne confond plus le tremblement d'échantillonnage du jeu avec du mouvement réel. Elle ne +retire rien de l'image. Elle n'a été testée que sur Xenoblade Chronicles 2, et elle peut au +contraire ajouter du tremblement en mouvement sur d'autres jeux, alors laissez-la éteinte sauf +si vous voulez expérimenter. + +### Prérequis + +Le DLSS dans tous ses modes — DLAA, Quality, Balanced et Performance — exige une carte NVIDIA +RTX, série 20 ou plus récente. La Frame Generation exige une RTX série 40 ou 50. Si vous n'avez +pas de carte RTX, le NIS est inclus dans ce fork et fonctionne sur n'importe quel GPU, et de +toute façon le correctif de l'écran vert ci-dessus vous concerne. + +Les fichiers du moteur DLSS ne sont pas dans le paquet, parce que ce sont des DLL propriétaires +de NVIDIA et que je ne les redistribue pas. Ce que le paquet contient, c'est le dossier `dlss` +à côté de l'exécutable, avec les six fichiers Streamline de NVIDIA, sous licence MIT. Dans ce +même dossier vous ajoutez les vôtres, pris dans un jeu DLSS que vous possédez : +`nvngx_dlss.dll` pour la mise à l'échelle, `nvngx_dlssg.dll` pour la Frame Generation, que le +pilote NVIDIA fournit généralement déjà, et `NvLowLatencyVk.dll` si votre pilote ne le fournit +pas. ### Limite connue : la Frame Generation produit encore des images fantômes @@ -129,9 +165,9 @@ ceux-ci décrivent mieux la caméra que les objets qui bougent individuellement. fantômes vous gênent plus que la fluidité supplémentaire ne vous apporte, mettez la Frame Generation sur Off dans les paramètres graphiques. -### Limite connue : Xenoblade Chronicles 2 peut se bloquer au chargement +### Limite connue : le chargement peut se bloquer sur certains jeux -Sur Xenoblade Chronicles 2, le chargement (ou le menu) cesse parfois d'avancer — l'image +Sur certains jeux, le chargement (ou le menu) cesse parfois d'avancer — l'image continue de s'afficher, mais le jeu n'avance plus. Fermer et rouvrir l'émulateur règle le problème. Nous l'avons capturé en direct à plusieurs reprises et il **n'est pas encore résolu** : le blocage se produit du côté du jeu lui-même, et il est antérieur aux diff --git a/src/Ryujinx.Common/Configuration/VSyncMode.cs b/src/Ryujinx.Common/Configuration/VSyncMode.cs index 3cfc9549e..f2380ee68 100644 --- a/src/Ryujinx.Common/Configuration/VSyncMode.cs +++ b/src/Ryujinx.Common/Configuration/VSyncMode.cs @@ -16,7 +16,7 @@ namespace Ryujinx.Common.Configuration VSyncMode.Switch => VSyncMode.Unbounded, VSyncMode.Unbounded => VSyncMode.Vrr, VSyncMode.Vrr => VSyncMode.Switch, - VSyncMode.Custom => VSyncMode.Switch, // legacy mode, no longer in the cycle + VSyncMode.Custom => VSyncMode.Switch, _ => VSyncMode.Switch }; } diff --git a/src/Ryujinx.Common/Utilities/JsonHelper.cs b/src/Ryujinx.Common/Utilities/JsonHelper.cs index dff605854..b5c5a4af0 100644 --- a/src/Ryujinx.Common/Utilities/JsonHelper.cs +++ b/src/Ryujinx.Common/Utilities/JsonHelper.cs @@ -39,11 +39,6 @@ namespace Ryujinx.Common.Utilities public static void SerializeToFile(string filePath, T value, JsonTypeInfo typeInfo) { - // [1.2.3] Écriture ATOMIQUE : File.Create tronquait le fichier AVANT d'écrire — un - // crash/kill pendant la sérialisation laissait un JSON tronqué (Config.json vidé, - // liste de jeux perdue : 2 occurrences le 18/07 pendant les bancs d'essai, et tout - // user dont l'émulateur meurt pendant une sauvegarde de config y est exposé). - // Le vrai fichier n'est remplacé qu'une fois le nouveau contenu ÉCRIT EN ENTIER. string tmpPath = filePath + ".tmp"; using (FileStream file = File.Create(tmpPath, DefaultFileWriteBufferSize, FileOptions.WriteThrough)) diff --git a/src/Ryujinx.Graphics.GAL/DlssCameraState.cs b/src/Ryujinx.Graphics.GAL/DlssCameraState.cs index 02165c79f..af98eb9c1 100644 --- a/src/Ryujinx.Graphics.GAL/DlssCameraState.cs +++ b/src/Ryujinx.Graphics.GAL/DlssCameraState.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using System.Collections.Concurrent; using System.Collections.Generic; @@ -8,20 +5,6 @@ using System.Numerics; namespace Ryujinx.Graphics.GAL { - /// - /// Cross-layer hand-off for MV++ camera matrices (Phase 1: camera-reprojection motion - /// vectors). The GPU layer detects the game's camera block in the bound constant buffers - /// (validated structurally: orthonormal view x perspective proj == the view-proj next to - /// them) and publishes the view-projection of the frame it is RENDERING. The value is - /// snapshotted onto each frame at presentation enqueue and republished at dequeue - the - /// same carrier pattern as , because the frame being presented - /// is not the frame being rendered - so the DLSS backend always reads the matrix matching - /// the frame on screen. - /// - /// Matrices are stored exactly as the game stores them: row-major rows, column-vector - /// convention (clip = M * world). Matrix4x4.M11..M14 = the matrix's first ROW. - /// Lives in GAL because the GPU layer cannot reference the Vulkan backend. - /// public static class DlssCameraState { public static bool Enabled; @@ -30,58 +13,26 @@ namespace Ryujinx.Graphics.GAL private static Matrix4x4 _currentVp; private static bool _currentValid; - /// - /// View-projection of the frame currently being presented, republished at dequeue. - /// Written by and read on the render thread only; the - /// GPU thread hands values across through the present ring below, never directly. - /// public static Matrix4x4 PresentVp; public static bool PresentVpValid; - /// - /// [GAMEJITTER 28/07] Le decalage sous-pixel que LE JEU appliquait a l'image presentee, en - /// NDC (x_ndc += Jx). Republie au dequeue, comme , donc apparie a la - /// meme image par le meme ordre. En pixels : Jx * largeurRendu / 2. - /// public static float PresentJitterX; public static float PresentJitterY; - // Present hand-off (GPU thread -> render thread). TryConsumeOrdered below pairs the game - // camera to the presented frame ON the GPU thread; this ring carries that paired result - // across to the render thread. A single static slot raced here: the GPU thread overwrote - // it while the render thread read it mid-evaluate (torn 64-byte matrix, or frame N - // evaluated with frame N+1's camera whenever the render thread lagged a present behind -- - // exactly the heavy scenes where DLSS matters most). Order does the pairing instead: one - // entry enqueued per GAL present (GPU thread), one dequeued per executed present (render - // thread), so a lagging consumer still reads the camera of the frame it is presenting. private static readonly ConcurrentQueue<(Matrix4x4 Vp, bool Valid, float Jx, float Jy)> _presentRing = new(); public static void PublishPresent(in Matrix4x4 vp, bool valid) => PublishPresent(in vp, valid, 0f, 0f); - /// - /// 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. - /// public static void PublishPresent(in Matrix4x4 vp, bool valid, float jx, float jy) { _presentRing.Enqueue((vp, valid, jx, jy)); - // A consumer-less backend (OpenGL, or presents executed before the consumer exists) - // must not grow the ring unbounded: keep the same short resync window as the ordered - // fifo below. while (_presentRing.Count > 4) { _presentRing.TryDequeue(out _); } } - /// - /// Dequeues the camera paired with the present being executed and republishes it on - /// /. Render thread, exactly once per - /// executed present. A re-present without a new frame (swapchain recreation) finds the - /// ring empty and keeps the previous values: same camera for the same content. - /// public static void ConsumePresent() { if (_presentRing.TryDequeue(out (Matrix4x4 Vp, bool Valid, float Jx, float Jy) entry)) @@ -93,9 +44,6 @@ namespace Ryujinx.Graphics.GAL } } - /// - /// Publishes the view-projection captured for the frame being rendered (GPU thread). - /// public static void PublishCurrent(in Matrix4x4 vp) { lock (_lock) @@ -105,9 +53,6 @@ namespace Ryujinx.Graphics.GAL } } - /// - /// Snapshots the current view-projection at frame enqueue (any thread). - /// public static bool SnapshotCurrent(out Matrix4x4 vp) { lock (_lock) @@ -118,91 +63,26 @@ namespace Ryujinx.Graphics.GAL } } - // Ordered hand-off (RYUJINX_MVPP_VPFIFO=1). The enqueue-time snapshot above races the - // GPU thread (measured on TOTK: 5-20% of frames mis-window their camera -> the brief - // image slide on a hard camera stop). Instead, the GPU thread pushes each DISTINCT - // camera it sees, in order, and the present dequeue - same thread, after the frame's - // fence - consumes the oldest: order does the pairing, timing no longer matters. - // Producer and consumer are both the GPU main thread, so a plain queue suffices. - public static bool FifoEnabled; - /// - /// Window-depth convention of the guest's main pass (from its viewport transform): - /// true = GL-style, the buffer stores (ndcZ + 1) / 2 and the reprojection shader must - /// undo it. Written by the GPU thread's state updater, read at reprojection time. - /// public static bool GuestDepthMinusOneToOne; - /// - /// [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. - /// public static int TeleportSeq; - /// - /// [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). - /// 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` 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 _fifo = new(); private static Matrix4x4 _fifoLast; private static bool _fifoHasLast; - // Health stats, logged by the consumer ~1/5s: a healthy 1:1 pairing consumes fresh - // nearly every present with depth <= 2 and never drops. Frequent holds WHILE pushes - // flow, or drops, mean the one-camera-per-frame assumption does not hold in order. public static int StatPushes; public static int StatFresh; public static int StatHolds; public static int StatDrops; public static int StatMaxDepth; - // [28/07] NE RIEN AJOUTER ICI SANS Y PENSER A DEUX FOIS. Ce fichier vit dans - // Ryujinx.Graphics.GAL, et la copie de test tourne sur un GAL.dll du 10/07 : y ajouter un - // champ oblige a redeployer cette DLL, donc a injecter d'un coup tous les changements de - // GAL accumules depuis. Fait le 28/07 a 10h07 pour une simple sonde de journal, suivi d'un - // defaut neuf en jeu. Faire transiter les sondes autrement. - - /// [JITTERVAL temporal probe, 10/07] Result of the most recent - /// : 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. public static bool LastConsumeFresh; - /// - /// Pushes a newly observed distinct camera view-projection, in draw order (GPU thread). - /// - // [28/07] La surcharge a decalage est SUPPRIMEE : voir le commentaire de _fifo. Elle reste - // acceptee pour ne pas casser les appelants, mais elle ignore le decalage. public static void PushOrdered(in Matrix4x4 vp, float jx, float jy) => PushOrdered(in vp); public static void PushOrdered(in Matrix4x4 vp) @@ -215,8 +95,6 @@ namespace Ryujinx.Graphics.GAL StatMaxDepth = _fifo.Count; } - // A backlog deeper than the game's own pipelining means presents were missed - // (loading hitch): resync by dropping the oldest entries. while (_fifo.Count > 4) { _fifo.Dequeue(); @@ -224,12 +102,6 @@ namespace Ryujinx.Graphics.GAL } } - /// - /// Consumes the camera paired with the frame being presented (GPU thread). With a - /// static camera no new value was written, so the last consumed one still describes - /// this frame. - /// - // [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)) diff --git a/src/Ryujinx.Graphics.GAL/DlssJitterState.cs b/src/Ryujinx.Graphics.GAL/DlssJitterState.cs index 1f338a6e9..0edf222d0 100644 --- a/src/Ryujinx.Graphics.GAL/DlssJitterState.cs +++ b/src/Ryujinx.Graphics.GAL/DlssJitterState.cs @@ -1,79 +1,34 @@ namespace Ryujinx.Graphics.GAL { - /// - /// Cross-layer hand-off for DLSS "Mode B" sub-pixel jitter. The DLSS backend generates the Halton - /// offset and publishes the NEXT frame's value in /; the GPU - /// layer (StateUpdater) reads it to shift the resolution-scaled viewport of the frame it is rendering. - /// - /// The hard part is handing DLSS the offset the PRESENTED frame was actually rendered with. Matching by - /// texture reference failed (the presented framebuffer is a different texture than the jittered 3D - /// target), so instead the offset is carried THROUGH the present frame queue: it is snapshotted onto - /// each frame when it is enqueued for presentation, then written to / when that exact frame is dequeued and presented. The DLSS backend reads those, so - /// the offset always matches the frame on screen regardless of frame-queue depth or frames-in-flight. - /// - /// When is false the GPU layer skips all of this, so the path is byte-identical. - /// This lives in GAL because the GPU layer cannot reference the Vulkan backend. - /// public static class DlssJitterState { public static bool Enabled; - /// Diagnostic split (jitter dossier 05/07): when false, the GPU layer does NOT shift the - /// viewport (no image jitter) while the declaration path keeps running -- isolates "does the DLL - /// consume our jitterOffset at all". True = normal Mode B. public static bool Inject = true; - /// [JITTER-UI] Also jitter the full-screen scale-1 color passes (UI/compositing) so DLSS - /// stabilizes the UI like the world instead of shaking it (see StateUpdater). public static bool JitterUi; - /// [JITTER-NOSHADOW] Exclude square scaled passes (shadow atlases, light space) from the - /// camera jitter injection (see StateUpdater). public static bool NoShadowJitter; - /// [JITTER-CAMPASS] By-the-book mode: inject ONLY the main camera geometry pass (16:9 + - /// depth bound), amplitude in final DLSS-input pixels. See StateUpdater. public static bool CamPassOnly; - /// The DLSS input (final composited frame) dimensions, published by the upscaler each - /// frame; the CAMPASS amplitude denominator. public static int InputWidth; public static int InputHeight; - /// [SUBRECT measurement] The most recent CAMERA-pass host viewport seen by the jitter - /// injector (StateUpdater cameraPass branch) -- i.e. the game's CURRENT dynamic-resolution step. - /// Diagnostic only: read by the inject dump gate to classify low/full-res frames. Written on the - /// GPU thread, read at replay -- a 1-frame skew is tolerable for this one-shot measurement (the - /// dynres steps are stable for seconds at a time). public static float CamVpW; public static float CamVpH; - /// The next frame's jitter offset, in internal-render pixels, published by the DLSS backend - /// and read by the GPU layer to jitter the scaled viewport. public static float OffsetX; public static float OffsetY; - /// The jitter offset the frame currently being presented was rendered with. Set by the GPU - /// present from the value carried alongside that frame through the queue; read by the DLSS backend. public static float PresentX; public static float PresentY; - /// [JITTERVAL probe, 07/07] Monotonic frame id, bumped once per DlssJitter.Advance in - /// lockstep with OffsetX/Y. Snapshotted at enqueue and delivered as PresentFrameId at present, so - /// the geometry-side probe (StateUpdater) and the DLSS-side probe (DlssUpscaler) can be joined per - /// frame offline. Diagnostic only; nothing in the render path reads these. public static long FrameId; public static long PresentFrameId; - /// [ALIGN0 probe] Frame id (= FrameId) at the moment MvppCameraCapture pinned the scene - /// depth. Read at the DLSS eval site to compare with the color/present frame id. Diagnostic only; - /// nothing in the render path reads it. public static long SceneDepthFrameId; - /// [ALIGN0 fix] Frame id (= FrameId) at the moment MvppCameraCapture pinned the scene - /// COLOR, stamped in the SAME StashSceneDepth with the SAME counter as SceneDepthFrameId — so - /// nativeVsDepthSameFrame is a valid same-counter comparison. Diagnostic only. public static long SceneColorFrameId; } } diff --git a/src/Ryujinx.Graphics.GAL/IPipeline.cs b/src/Ryujinx.Graphics.GAL/IPipeline.cs index ba8daad88..1c516a590 100644 --- a/src/Ryujinx.Graphics.GAL/IPipeline.cs +++ b/src/Ryujinx.Graphics.GAL/IPipeline.cs @@ -7,10 +7,6 @@ namespace Ryujinx.Graphics.GAL { void Barrier(); - // [INJECT étape C] Mid-frame DLSS upscale injection hook. The source + destination textures are carried - // through the command (captured at detection); host dimensions are read from the TextureViews at - // execution. Default no-op so only the Vulkan backend implements it; other backends (and the flag-off - // path) are unaffected. void MvppInjectDlssUpscale(ITexture src, ITexture dst, ITexture depth) { } void BeginTransformFeedback(PrimitiveTopology topology); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs index 2b3ade145..5611285cd 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs @@ -62,7 +62,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading Register(CommandType.BufferDispose); Register(CommandType.BufferGetData); Register(CommandType.BufferSetData); - Register(CommandType.BufferSetDataBatch); // [BUFBATCH] + Register(CommandType.BufferSetDataBatch); Register(CommandType.CounterEventDispose); Register(CommandType.CounterEventFlush); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Buffer/BufferSetDataBatchCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Buffer/BufferSetDataBatchCommand.cs index 23a93647c..4581e828a 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Buffer/BufferSetDataBatchCommand.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Buffer/BufferSetDataBatchCommand.cs @@ -1,16 +1,9 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Graphics.GAL.Multithreading.Model; using System; namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer { - /// - /// [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). - /// struct BufferSetDataBatchEntry { public BufferHandle Buffer; @@ -18,13 +11,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer public int Size; } - /// - /// [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). - /// struct BufferSetDataBatchCommand : IGALCommand, IGALCommand { public readonly CommandType CommandType => CommandType.BufferSetDataBatch; @@ -41,8 +27,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer public static void Run(ref BufferSetDataBatchCommand command, ThreadedRenderer threaded, IRenderer renderer) { - // Copie des entrees AVANT Dispose : le pool est un anneau, Dispose libere la zone - // pour le producteur — on ne garde jamais un Span pool au-dela de son Dispose. Span entries = command._count <= 64 ? stackalloc BufferSetDataBatchEntry[64] : new BufferSetDataBatchEntry[command._count]; diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/MvppInjectDlssCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/MvppInjectDlssCommand.cs index 72ca506f6..539a899dd 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/MvppInjectDlssCommand.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/MvppInjectDlssCommand.cs @@ -1,20 +1,9 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; namespace Ryujinx.Graphics.GAL.Multithreading.Commands { - /// - /// [INJECT étape C] Threaded-GAL passthrough for the mid-frame DLSS upscale injection. The GPU layer - /// captures the detected pass's SOURCE (scene colour) and DESTINATION (render target) textures at - /// detection time and enqueues this command right before the guest upscale draw. On the render thread it - /// runs IN ORDER at exactly that point, carrying the frame's OWN handles (via TableRef, like - /// SetImageCommand) -- no mutable side-channel, no data from a later frame. Host dimensions are read from - /// the TextureViews at execution (host space). Backends without an override fall back to the empty default. - /// struct MvppInjectDlssCommand : IGALCommand, IGALCommand { public readonly CommandType CommandType => CommandType.MvppInjectDlss; diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs index 5e3bc9d92..57a90ea20 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs @@ -162,16 +162,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading return _spanPool.Insert(data); } - // [BUFBATCH 02/08, journal (368)-(369)] Lots de SetBufferData consecutifs - // (RYUJINX_BUFBATCH=1, OFF par defaut = chemin stock). Mesure (362)-(368) : le jeu ecrit - // ses buffers au tick invite (~40-50 000 uploads/s CONSTANTS) ; a ~6 us de machinerie par - // commande, le cout par IMAGE RENDUE explose quand la cadence chute (boucle de - // retroaction, marches 12-20 img/s). Ce gate accumule les SetBufferData CONSECUTIFS dans - // une arene productrice privee et n'emet qu'UNE commande par lot. L'ordre global est - // preserve : TOUTE commande d'un autre type flushe d'abord le lot en attente (crochet - // unique dans New). Le lot ne detient AUCUNE ressource du pool avant son flush. - // Serialisation : memes hypotheses que le producteur stock (New/_producerPtr non - // verrouilles) — quiconque a le droit d'enqueue a le droit d'accumuler. private static readonly bool _bufBatchEnabled = Environment.GetEnvironmentVariable("RYUJINX_BUFBATCH") == "1"; @@ -195,7 +185,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading $"[BUFBATCH] ARME (RYUJINX_BUFBATCH=1) - lots de SetBufferData (max {BufBatchMaxEntries} entrees / {BufBatchArenaBytes / 1024} Ko)."); } - // Trop gros pour l'arene : flush du lot puis chemin stock (l'ordre reste correct). if (data.Length > BufBatchArenaBytes) { FlushBufferBatch(); @@ -220,8 +209,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading _bufBatchArenaUsed += data.Length; } - /// [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. private unsafe void FlushBufferBatch() { if (_bufBatchCount == 0) @@ -248,9 +235,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading internal unsafe T* New() 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(); diff --git a/src/Ryujinx.Graphics.GAL/MvppColorSnapshot.cs b/src/Ryujinx.Graphics.GAL/MvppColorSnapshot.cs index 95fe278cc..501f6c6bd 100644 --- a/src/Ryujinx.Graphics.GAL/MvppColorSnapshot.cs +++ b/src/Ryujinx.Graphics.GAL/MvppColorSnapshot.cs @@ -1,45 +1,21 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. namespace Ryujinx.Graphics.GAL { - /// - /// [COLORCAP snapshot prototype, 07/07] Cross-layer bridge that carries the pinned NATIVE scene - /// COLOR host texture from the GPU layer (MvppCameraCapture) to the Vulkan present, parallel to the - /// depth held path. The Vulkan present makes a GPU copy into a held texture and logs. Gated on - /// RYUJINX_MVPP_COLORCAP_SNAP=1; NOTHING is wired to DLSS and no default behavior changes. - /// public static class MvppColorSnapshot { public static readonly bool Enabled = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORCAP_SNAP") == "1"; - /// Host texture of this frame's pinned HDR scene color (same framebuffer as the pinned - /// scene depth). Published at present; read by the Vulkan snapshot. Null on gap frames. public static ITexture SceneColorHost; public static long FrameId; - /// [FLASHCAP gate] Wall-clock ms (Environment.TickCount64) of the last successful mid-frame - /// DLSS inject = we are rendering the real 3D scene. Loading/map/menu screens do NOT run the inject, - /// so this goes stale there -> the present-side flash detector uses it to skip non-gameplay frames. public static long LastInjectMs; - /// [DUMPCOLOR sync] Set true by the mid-frame dump ONLY once it has captured a real, bright - /// gameplay frame (not a black/loading frame). The present-side COLORDUMP waits for this so both - /// dumps come from the same confirmed-gameplay moment. Diagnostic only. public static bool DumpArmed; - /// The pinned scene color UNWRAPPED to its backend texture (ThreadedTexture.Base), so the - /// Vulkan present can cast it to the real TextureView. The GPU layer publishes a ThreadedTexture - /// wrapper; mirrors . Null-safe. public static ITexture SceneColorBackend => SceneColorHost is Multithreading.Resources.ThreadedTexture t ? t.Base : SceneColorHost; - /// [HELDCOLOR] The GPU held COPY of the PRESENT-time (tonemapped) native scene colour, - /// published by the Vulkan present after its copy. Measured 09/07: the present colour is tonemapped - /// (balanced) vs the mid-frame raw (crushed HDR). The mid-frame inject can feed THIS instead of the - /// live raw scene (1-frame stale). Already a backend TextureView. Null until the first present copy. public static ITexture SceneColorHeld; } } diff --git a/src/Ryujinx.Graphics.GAL/MvppDev.cs b/src/Ryujinx.Graphics.GAL/MvppDev.cs index 56bcf9cb3..705f4d5d0 100644 --- a/src/Ryujinx.Graphics.GAL/MvppDev.cs +++ b/src/Ryujinx.Graphics.GAL/MvppDev.cs @@ -1,15 +1,6 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. namespace Ryujinx.Graphics.GAL { - /// - /// Single switch for the MV++ development diagnostics: heartbeats, per-second stats - /// readbacks and counter log lines. Release default = OFF, where the pipelines run - /// identically and only the instruments go quiet (anything feeding the UI, like the FG - /// counter poll, keeps running silently). RYUJINX_MVPP_DEV=1 brings every gauge back. - /// public static class MvppDev { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.GAL/MvppInjectState.cs b/src/Ryujinx.Graphics.GAL/MvppInjectState.cs index b0a1bb795..3cee32290 100644 --- a/src/Ryujinx.Graphics.GAL/MvppInjectState.cs +++ b/src/Ryujinx.Graphics.GAL/MvppInjectState.cs @@ -1,17 +1,6 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. namespace Ryujinx.Graphics.GAL { - /// - /// [INJECT étape C] Feature flag for the "inject DLSS before the game composite" chantier. - /// This holds ONLY the immutable enable flag. The per-frame resources (scene source, destination dims) - /// are NOT stored here: they are captured at the exact moment of detection on the GPU thread and carried - /// THROUGH the GAL command (MvppInjectDlssCommand), so the render thread receives exactly the frame's own - /// handles, in command order, with no cross-thread mutable side-channel and no data from a later frame. - /// Gated on RYUJINX_DLSS_UPSCALE_PASS; default OFF => the feature is inert. - /// public static class MvppInjectState { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.GAL/MvppVelState.cs b/src/Ryujinx.Graphics.GAL/MvppVelState.cs index 4ee804308..a77b91ee8 100644 --- a/src/Ryujinx.Graphics.GAL/MvppVelState.cs +++ b/src/Ryujinx.Graphics.GAL/MvppVelState.cs @@ -1,69 +1,28 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. namespace Ryujinx.Graphics.GAL { - /// - /// MV++ Phase 2.2: bridge between the velocity pass (GPU side, DrawManager graft) and the - /// backend consumers (debug view / DLSS integration) -- the same static-bridge pattern as - /// DlssCameraState. Written on the GPU thread; the render thread only reads the texture - /// reference for the debug overlay (a benign racy read: worst case one frame late). - /// public static class MvppVelState { - /// Coverage render target (R8, scene-depth-sized), owned by the graft. public static ITexture Coverage; - /// - /// Coverage unwrapped to the backend-level texture: with the threaded GAL the graft - /// stores a ThreadedTexture (internal to this assembly), but the debug view runs on - /// the render thread against backend types. Null until the queued create ran. - /// public static ITexture CoverageBackend => Coverage is Multithreading.Resources.ThreadedTexture t ? t.Base : Coverage; public static int Width; public static int Height; - /// Set at frame boundary; the first graft of the frame clears the coverage. public static bool FrameClearPending; - /// - /// True when the coverage received at least one graft since the last present -- the - /// debug overlay uses it (RYUJINX_MVPP_VEL_FRESH=1) to SKIP compositing a stale - /// coverage instead of freezing it on screen (the artifact that sabotaged the - /// TMIN and v4 run readings). Display-only; benign racy bool like the rest. - /// public static bool CoverageFresh; - /// - /// MV++ double-transform (b1) sub-step 2: the host program of the MvppVariant of the - /// currently-bound graphics shaders, published by StateUpdater.UpdateShaderState so the - /// graft can SetProgram it for the re-draw. Null when no variant was generated (the - /// graft then logs a silent-fallback and keeps the base). GPU thread only. - /// public static IProgram VariantProgram; - /// Set by the graft when it bound the variant; the next UpdateShaderState's - /// unconditional SetProgram(base) is the restore -- it logs (c) once and clears this. public static bool VariantBound; - /// Per-window count of base-program restores after a variant bind (incremented - /// by StateUpdater, read+reset by the velocity window log as vRestore) -- proves the - /// restore fires per graft WITHOUT the 272k-line spam the one-shot (c) replaced. public static int VariantRestores; - /// - /// MV++ (b1) sub-step 5a: host binding of the N-1 palette cbuf that the injector added to - /// the variant VS (the cloned clip_{N-1} slice reads THIS binding instead of cb5). Resolved - /// by StateUpdater from the variant's actual BufferDescriptors (Slot present in the variant - /// but not the base) -- NEVER guessed (loc-31 lesson). -1 = not resolved -> the graft skips - /// the N-1 bind and the clone falls back to reading the current cb5 (MV = 0). GPU thread. - /// public static int VariantN1Binding = -1; - /// Descriptor set of the N-1 palette cbuf (paired with VariantN1Binding). public static int VariantN1Set; } } diff --git a/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs b/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs index 99f110811..d3aa66d1c 100644 --- a/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs +++ b/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs @@ -8,11 +8,6 @@ namespace Ryujinx.Graphics.GAL public int Width { get; } public int Height { get; } - // Active (rendered) sub-rect inside the allocated Width/Height, for the DLSS-SR - // fractional-render path: the target is allocated at an integer (mip-safe) scale but the - // scene renders into this smaller sub-rect. Falls back to Width/Height when constructed - // with 0. Derived metadata -- intentionally NOT part of Equals/GetHashCode so it never - // perturbs texture-cache identity (two infos that differ only here stay equal). public int ActiveWidth { get; } public int ActiveHeight { get; } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs index 4b77ae497..900924feb 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs @@ -203,8 +203,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute _context.Renderer.Pipeline.DispatchCompute(qmd.CtaRasterWidth, qmd.CtaRasterHeight, qmd.CtaRasterDepth); - MvppBlitProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); // read-only (gated) - Threed.MvppDrawStepProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); // read-only (gated) + MvppBlitProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); + Threed.MvppDrawStepProbe.OnDispatch(_channel.TextureManager, (int)qmd.CtaRasterWidth, (int)qmd.CtaRasterHeight, (int)qmd.CtaRasterDepth, shaderGpuVa); _3dEngine.ForceShaderUpdate(); } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs index 29f61be37..04c23f252 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs @@ -216,7 +216,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma _3dEngine.CreatePendingSyncs(); _3dEngine.FlushUboDirty(); - // [TWINXFER] Raw copy-engine tap, BEFORE any branch (buffer-domain blind spot). Self-gated. Image.MvppTwinXferProbe.OnDma(srcGpuVa, dstGpuVa, xCount, yCount, copy2D, srcLinear, dstLinear); if (copy2D) @@ -328,7 +327,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma target.PropagateScale(source); } - Image.MvppTwinXferProbe.OnCopy("DMA", source, target); // [TWINXFER] read-only, self-gated + Image.MvppTwinXferProbe.OnCopy("DMA", source, target); source.HostTexture.CopyTo(target.HostTexture, 0, 0); target.SignalModified(); return; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MvppBlitProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/MvppBlitProbe.cs index a8d3d7068..bbee88160 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/MvppBlitProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/MvppBlitProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,32 +6,18 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine { - /// - /// MV++ 2D/compute composite tracer (RYUJINX_MVPP_BLITPROBE=1). READ-ONLY, default off, nothing in - /// the render path branches on it. The 3D DrawEnd census proved the final 3200x1800 R8G8B8A8 buffer - /// and the HUD are NOT produced by 3D draws (only HDR scene buffers are). This traces the OTHER two - /// paths -- the 2D blit engine (TwodClass.PixelsFromMemory) and compute dispatch -- to find which op - /// writes the final framebuffer, and whether the HUD/UI arrives from a SEPARATE source surface that - /// is composited in (=> capturable => GO) or the final image is produced by a single op (=> NO-GO / - /// compute-fused). Source/destination addresses are the SAME address space the 3D probe logged, so a - /// blit whose src == the scene RT (0x65799C4000 last run) is the scene upscale, and any OTHER source - /// feeding the same final destination is the HUD/UI layer. - /// static class MvppBlitProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_BLITPROBE") == "1"; - private const int MaxSigs = 512; // stop logging new shapes past this (bounds a pathological run) + private const int MaxSigs = 512; private static readonly HashSet _seenBlitSigs = new(); private static readonly HashSet _seenDispatchSigs = new(); private static int _op; private static int _dispatchCount; - // Final-framebuffer tracking, by SIZE CLASS (double/triple buffering rotates the address, so we - // key on the largest LDR destination dims and accumulate the distinct source + destination - // addresses that hit that size). distinctSrcs >= 2 => the HUD is composited from a separate source. private static int _finalW, _finalH; private static readonly HashSet _finalSrcs = new(); private static readonly HashSet _finalDsts = new(); @@ -47,7 +30,6 @@ namespace Ryujinx.Graphics.Gpu.Engine return; } - // A diagnostic on the GPU thread must never take the process down. try { OnBlitImpl(src, dst, duDx, dvDy); @@ -70,13 +52,9 @@ namespace Ryujinx.Graphics.Gpu.Engine int sW = src.Info.Width, sH = src.Info.Height; int dW = dst.Info.Width, dH = dst.Info.Height; - // duDx/dvDy are 32.32 fixed-point SOURCE pixels per DESTINATION pixel; the upscale factor is - // the reciprocal (dst/src). ~1.0 = copy, <1 in duDx (=> >1 here) = upscale. float sx = duDx != 0 ? (float)(4294967296.0 / duDx) : 0f; float sy = dvDy != 0 ? (float)(4294967296.0 / dvDy) : 0f; - // Log each distinct blit SHAPE once: enumerates the whole composite structure compactly - // (scene->output upscale, UI->output composite, bloom chain, ...) without flooding. long sig = Fnv(sAddr, dAddr, (int)sFmt, (int)dFmt, sW, sH, dW, dH); if (_seenBlitSigs.Count < MaxSigs && _seenBlitSigs.Add(sig)) { @@ -93,7 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Engine return; } - // A strictly larger LDR destination resets the size class (first real frame settles it). if ((long)dW * dH > (long)_finalW * _finalH) { _finalW = dW; @@ -129,10 +106,6 @@ namespace Ryujinx.Graphics.Gpu.Engine { _dispatchCount++; - // Log each distinct (grid, shader) ONCE, with its full compute I/O (inputs it samples + - // storage images it writes). The pass that writes the largest LDR store image is the final - // composite; its INPUT list answers the decisive question -- a separate LDR (HUD-sized) - // input texture distinct from the HDR scene = the UI is a separable surface (GO). long sig = ((long)(gridX & 0xFFFF) << 32) ^ ((long)(gridY & 0xFFFF) << 16) ^ (long)(shaderVa & 0xFFFFFFF); if (_seenDispatchSigs.Count >= MaxSigs || !_seenDispatchSigs.Add(sig)) { @@ -176,13 +149,11 @@ namespace Ryujinx.Graphics.Gpu.Engine Logger.Info?.Print(LogClass.Gpu, $"MVPP-CIO s@{sh} IN @0x{ins[i].Addr:X} {ins[i].Fmt} {ins[i].W}x{ins[i].H}"); } - // Decisive flag: does a pass that WRITES a large LDR output also SAMPLE a separate LDR - // input that is not the HDR scene? (= the HUD as its own surface.) foreach ((ulong oa, Format of, int ow, int oh) in outs) { if (!IsLdr(of) || (long)ow * oh < 2_000_000L) { - continue; // only the ~full-res LDR output(s) matter + continue; } int ldrInputs = 0; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ConstantBufferUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ConstantBufferUpdater.cs index 012aeb863..c45ea37b7 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ConstantBufferUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ConstantBufferUpdater.cs @@ -105,14 +105,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [Beast Roofer diag] RYUJINX_UBO_FORCEDIRTY=1 (EXP 11, gated OFF by default): always mark - // the inline-updated UBO range dirty, ignoring the redundancy check's verdict. The XC2 - // velocity chain's only per-frame data path is the constant buffers (journal 141: every - // vertex SSBO is static); if the redundancy check ever wrongly reports "unchanged" (e.g. a - // game double-write making guest memory match while the HOST copy is still old), the GPU - // reads one-frame-stale matrices -- garbage velocities, only in motion, per draw. Forcing - // the dirty flag costs a redundant upload but cannot be wrong. Artifact gone with this on - // => the redundancy-check family is the root; see journal (142). private static readonly bool _uboForceDirty = Environment.GetEnvironmentVariable("RYUJINX_UBO_FORCEDIRTY") == "1"; @@ -138,8 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (!changed) { - // Only reached with the experiment on: these are exactly the flushes the - // redundancy check would have skipped. Their count is the witness. _uboForceDirtyHits++; long now = Environment.TickCount64; if (now - _uboForceDirtyLogMs >= 3000) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs index 57e4ff9f1..465617774 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs @@ -138,8 +138,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed MvppUiProbe.OnDraw(_channel, ref _state.State, _context); MvppCompositeProbe.OnDraw(_channel, ref _state.State); - // [DOFSKIP] Compatibility option, inert unless RYUJINX_DOF_SCATTER_SKIP=1. Matched by - // pipeline shape, so it survives game versions AND a warm shader cache. if (MvppDofSkip.ShouldSkip(_channel)) { _drawState.DrawIndexed = false; @@ -241,9 +239,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _context.Renderer.Pipeline.EndHostConditionalRendering(); } - // [POST 21/07] Capture la cible APRES l'ecriture de ce draw (chemin normal non-instancie). - // La sonde pre-draw (l.133) montre le contenu AVANT ; comparer les deux dit si ce draw - // ECRIT la corruption ou la trouve deja la. MvppDrawStepProbe.OnDrawPost(_channel); } @@ -502,7 +497,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed (Image.Texture texture, Sampler sampler) = _channel.TextureManager.GetGraphicsTextureAndSampler(textureId, samplerId); - MvppFinalDrawProbe.OnDrawTexture(_channel, texture); // read-only: DrawTexture bypasses DrawEnd (gated) + MvppFinalDrawProbe.OnDrawTexture(_channel, texture); float gSrcX0 = srcX0, gSrcY0 = srcY0, gSrcX1 = srcX1, gSrcY1 = srcY1; float gDstX0 = dstX0, gDstY0 = dstY0, gDstX1 = dstX1, gDstY1 = dstY1; @@ -525,7 +520,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (anyScaled) { - Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated); the bound RT is not resolvable here + Image.MvppSeamProbe.OnScaledCopy( "DRAWTEX", texture, null, gSrcX0, gSrcY0, gSrcX1, gSrcY1, @@ -635,9 +630,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int firstInstance, bool indexed) { - MvppFinalDrawProbe.OnDraw(_channel, ref _state.State); // universal draw choke point (gated, read-only) - MvppHdrCensusProbe.OnDraw(_channel, ref _state.State, count, indexed); // HDR-1600 producer census (gated, read-only) - MvppInjectGate.OnDraw(_channel, ref _state.State, count, indexed); // DLSS-injection gate validation, STEP A (gated, read-only) + MvppFinalDrawProbe.OnDraw(_channel, ref _state.State); + MvppHdrCensusProbe.OnDraw(_channel, ref _state.State, count, indexed); + MvppInjectGate.OnDraw(_channel, ref _state.State, count, indexed); if (instanceCount > 1) { @@ -668,16 +663,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ForceStateDirty(); } } - // [INJECT étape C] Generic scene-upscale substitution. Decide detects the pass (pipeline-shape only) - // and captures its real src/dst host textures. When it matches, the mid-frame DLSS hook is queued - // IN ORDER right before the guest draw. The guest draw is ALWAYS queued here; the backend skips it - // in order iff the inject's DLSS evaluate succeeded (C3b) -- otherwise the guest draw runs = fallback. else { if (MvppDlssUpscale.Decide(_channel, ref _state.State, count, indexed, out var injectSrc, out var injectDst, out var injectDepth)) { - // The source + destination + depth handles captured THIS frame travel with the command, - // reaching the render thread in order (no mutable side-channel, no later-frame data). _context.Renderer.Pipeline.MvppInjectDlssUpscale(injectSrc, injectDst, injectDepth); } @@ -690,8 +679,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _context.Renderer.Pipeline.Draw(count, instanceCount, firstVertex, firstInstance); } - // MV++ velocity pass graft point: right after the real draw, all guest state - // still bound (no-op without RYUJINX_MVPP_VEL=1). if (MvppVelocityPass.Enabled) { MvppVelocityPass.AfterDraw(this, engine, _context, _channel, ref _state.State, @@ -894,7 +881,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed // on the screen scissor state, then we need to force only one texture to be bound to avoid // host clipping. ScreenScissorState screenScissorState = _state.State.ScreenScissorState; - + Span scissorStateSpan = _state.State.ScissorState.AsSpan(); bool clearAffectedByStencilMask = (_state.State.ClearFlags & 1) != 0; @@ -960,7 +947,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed engine.UpdateState(updateMask); - int probeScX = -1, probeScY = -1, probeScW = -1, probeScH = -1; // [MVBUF] read-only capture of the effective clear scissor + int probeScX = -1, probeScY = -1, probeScW = -1, probeScH = -1; if (needsCustomScissor) { @@ -994,7 +981,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed new(scissorX, scissorY, scissorW, scissorH) ]; - probeScX = scissorX; // [MVBUF] + probeScX = scissorX; probeScY = scissorY; probeScW = scissorW; probeScH = scissorH; @@ -1010,7 +997,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ColorF color = new(clearColor.Red, clearColor.Green, clearColor.Blue, clearColor.Alpha); - // [MVBUF] Read-only (gated): record engine clears hitting the object-MV buffer. Image.MvppMvBufProbe.OnClear( _channel.TextureManager.GetColorTarget(index), index, @@ -1022,7 +1008,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed probeScW, probeScH); - // [TWINMAP v5] per-twin clear attribution (self-gated). Image.MvppTwinMapProbe.OnClear( _channel.TextureManager.GetColorTarget(index), componentMask, diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCamTrace.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCamTrace.cs index 8ff223bc0..2f13666e0 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCamTrace.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCamTrace.cs @@ -1,58 +1,16 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppCamTrace { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAMTRACE") == "1"; - // ~10 seconds at 60 fps. Long enough to see a cycle repeat many times, short enough that - // the log stays readable and the run stays fast. private const int MaxLines = 600; - // Ordinary reads measured at 4.8 max once the three fixes are armed; the intruders they - // removed were 250. Ten sits between the two by two orders of magnitude on either side. private const float JumpThreshold = 50f; private static int _lines; @@ -61,10 +19,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static float _prevX, _prevY, _prevZ; private static bool _hasPrev; - /// - /// Called on the success path of the per-frame read, with the address currently behind the - /// elected slot and the position that read produced. - /// public static void Note(ulong address, float px, float py, float pz, float jx, float jy, bool deJittered) { if (!Enabled) @@ -72,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // [27/07] The first version stopped after MaxLines and went silent for the rest of the - // session - so Alex's one remaining flick, which happened during a fight nine minutes - // in, left no trace at all. Past the opening window the probe keeps watching but only - // speaks when a read jumps: measured on the run that followed the three fixes, ordinary - // reads peak at 4.8 while the intruders were 250, so a jump this size is an event and - // not a busy log. Cost outside an event: three subtractions. bool opening = _lines < MaxLines; if (!opening) @@ -106,8 +54,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _lines++; - // The step since the previous accepted read is what the reprojection turns into motion - // vectors, so it is printed rather than left to be recomputed by hand afterwards. float step = 0f; bool addrChanged = false; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs index 05a0e8957..90c197bb5 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCameraCapture.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -12,67 +9,38 @@ using System.Threading; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ Phase 1, camera capture (RYUJINX_MVPP_CAP=1). Once per presented frame, on a draw - /// of the scaled 3D pass, finds the game's camera block in the vertex-stage constant - /// buffers and publishes its view-projection to . - /// - /// Detection is structural, not hardcoded: a location qualifies when an orthonormal VIEW, - /// a perspective PROJ and their PRODUCT sit side by side (View@o, Proj@o+0x40, - /// ViewProj@o+0x80, row-major - the layout found on TOTK's cbuf8, and a natural one for - /// any engine). The product check makes false positives implausible, and rejecting - /// orthographic projections filters shadow-cascade cameras. The found location is cached - /// and revalidated every frame for pennies; a full rescan runs on a miss. - /// - /// Fenced like MvppProbe: this runs on the GPU thread, where an escaped exception kills - /// the process silently - on any error the capture disables itself and says why. - /// static class MvppCameraCapture { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAP") == "1"; - // [CAPWHY 02/08] Compteurs de branches du chemin de capture, lecture seule, gate OFF par - // defaut. Question (journal (347)) : sur les images AFFAMEES du village BOTW, ou meurt le - // chemin — OnDrawImpl jamais appelee, cache reussi (?!), relocalisation, ou repli solo ? - // Meme patron que les stats de fenetre : ecrits sur le fil GPU, vides au meme point de - // flush que la ligne "MVPP capture window" (approximation identique a l'existant). private static readonly bool _capWhy = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPWHY") == "1"; - // [DUPSKIP 02/08] Producteur du signal anti-doublons : a chaque present invite portant au - // moins un dessin, incremente GAL.DlssCameraState.RenderedFrameSeq (voir sa doc). Reutilise - // le verrou par image de CAPWHY v2 ; ferme = aucun comportement. + internal static readonly bool Verbose = + Environment.GetEnvironmentVariable("RYUJINX_MVPP_VERBOSE") == "1"; + private static readonly bool _dupSkip = Environment.GetEnvironmentVariable("RYUJINX_DLSS_DUPSKIP") == "1"; - // [FGFEED 02/08] Meme signal, autre consommateur : Vulkan Window.Present AVALE les - // presentations-doublons pour que Streamline/FG ne voie que les vraies images et - // interpole entre elles (journal (353)). Le producteur est strictement identique a - // DUPSKIP ; seul le consommateur change. Ferme = aucun comportement. private static readonly bool _fgFeed = Environment.GetEnvironmentVariable("RYUJINX_DLSS_FGFEED") == "1"; - private static int _cwQual; // dessins ayant passe le prefiltre (OnDraw) - private static int _cwNeed; // ... dont l'image n'avait pas encore capture - private static int _cwImpl; // entrees dans OnDrawImpl - private static int _cwCachedOk; // lecture au cache reussie (publie) - private static int _cwHeld; // quarantaine gate (slot tenu ouvert) - private static int _cwRescanOk; // relocalisation reussie (publie) - private static int _cwSoloCall; // repli solo atteint - private static int _cwSoloOk; // repli solo a publie - // "rien" = impl - cachedOk - held - rescanOk - soloOk, derivable, pas de compteur dedie. + private static int _cwQual; + private static int _cwNeed; + private static int _cwImpl; + private static int _cwCachedOk; + private static int _cwHeld; + private static int _cwRescanOk; + private static int _cwSoloCall; + private static int _cwSoloOk; - // [CAPWHY v2 02/08] Discriminant demande par la correction d'Alex (« les FPS ne descendent - // pas ») : par PRESENTATION, l'image avait-elle des dessins DU TOUT (avant prefiltre), et - // en avait-elle apres ? Separe « le jeu n'a pas rendu » (monde 1) de « tout est mort au - // prefiltre » (monde 2). Verrous par image poses sur le fil GPU, releves au present. - private static int _cwRaw; // dessins vus a l'entree de OnDraw (avant prefiltre) - private static int _cwFrameHadRaw; // verrou : cette image a vu >= 1 dessin brut - private static int _cwFrameHadQual; // verrou : cette image a vu >= 1 dessin qualifie - private static int _cwFramesNoDraw; // presentations sans AUCUN dessin brut - private static int _cwFramesPrefiltred; // presentations avec des bruts mais 0 qualifie - private static int _cwFramesOk; // presentations avec >= 1 dessin qualifie + private static int _cwRaw; + private static int _cwFrameHadRaw; + private static int _cwFrameHadQual; + private static int _cwFramesNoDraw; + private static int _cwFramesPrefiltred; + private static int _cwFramesOk; private static int _capturedThisFrame; private static int _cachedSlot = -1; @@ -81,13 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static ulong _captures; private static ulong _rescans; - // Publisher-side cold-start forensics (J2): correlate WHAT gets published with the - // classifier windows. Per 5 s window: frames enqueued vs frames that actually - // published (a gap means SnapshotCurrent serves an old value), whether the - // once-per-frame WINNER draw had the published SCENE depth bound or an AUX one - // (the capture slot is first-winner-takes-all), and how many DISTINCT VP values - // were published (a still camera should publish ONE; the 05:59 run showed wildly - // different world positions while the user stood still). private static long _pubWindowMs; private static int _statEnqueues; private static int _statPubFrames; @@ -99,36 +60,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _vpTransitionLogs; private const int VpTransitionLogCap = 200; - // Run 06:34 lesson: the full-block fingerprint changes EVERY frame (sub-millidecimal - // camera drift) -- the transition cap burned in one second on noise. The datum that - // actually names mechanism B2 is the TELEPORT: a published VP whose bottom row moves - // by more than any pan could in one frame (foreign camera winning the slot). Only - // those get a log line; pans and micro-noise stay silent. private static float _lastR3X, _lastR3Y, _lastR3Z, _lastR3W; private static bool _hasR3; private static int _statVpJumps; - // Publication gate (RYUJINX_MVPP_CAP_GATE=1, default OFF -- daily driver intact). - // The teleport run named mechanism B2: an iterating family of structurally valid - // cameras from a half-res pass (impostor/far-terrain updates) hijacks the once-per- - // frame slot, and the lottery FREEZES on whatever won last when the player stops. - // The gate publishes only the LINEAGE: candidates continuous with the last published - // VP (per-frame thresholds below -- measured on the 06:51 run: pans stay under, - // teleports exceed). A discontinuous candidate is a RIVAL: never published, the slot - // stays OPEN so the lineage can still draw later in the same frame; a rival takes - // over only after persisting ALONE for GateResyncFrames consecutive frames (a real - // camera cut) -- the proven edge+cooldown pattern. Degradation = today's behavior: - // no stable candidate means nothing new is published (principle #1: never worse). private static readonly bool _gateEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAP_GATE") == "1"; - // Gate v2 (run 07:08 falsified v1's |dW| <= 50: w = -dir.pos is AMPLIFIED by |pos| - // (~1500-2000 at the village), so a plain pan hits 40-80/frame and the gate self- - // quarantined the REAL camera -- 40 false "cut" resyncs, frozen overlay through - // every pan). v2 tests the rotation-INVARIANT identity instead: camera POSITION - // (pos = -R^T t from the view matrix, already in hand) + view direction. Measured - // margins: pan dir ~0.05/frame vs 0.35 allowed; travel <= 2 units/frame vs 30 - // allowed; impostor jumps = hundreds of units, orders of magnitude above. private const float GateDirEps = 0.35f; private const float GatePosEps = 30f; private const int GateResyncFrames = 30; @@ -144,25 +82,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _statLineageLate; private static int _statResyncs; - // Movement/alternation proxy (user observation 03/07: green only appears while the - // camera MOVES): cross-draw churn of the cached camera block, counted where the FIFO - // probe already fingerprints it. Still user + still publish + LOW churn = one static - // camera everywhere; HIGH churn while still = several cameras ALTERNATING across - // draws (the once-per-frame winner is then a lottery). Read against the user's noted - // movement phases. private static int _statBlockChanges; - // RYUJINX_MVPP_PAIRPROBE=1: count DISTINCT camera-block values per enqueue interval, - // across every scaled draw. Precondition check for order-based (FIFO) VP pairing -- - // the fix for the enqueue-snapshot race is only safe if the scaled passes write - // exactly ONE camera per frame (a scaled reflection pass carrying its own - // structurally-valid camera would make an ordered queue drift). private static readonly bool _pairProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_PAIRPROBE") == "1"; - // Change detection on the 0xC0 camera block: raw byte compare against the last copy. - // Vectorized SequenceEqual beats hashing all 192 bytes on every scaled draw, and an - // exact compare cannot collide. private static readonly byte[] _probeLastBlock = new byte[0xC0]; private static bool _probeHasFp; private static int _probeChanges; @@ -170,17 +94,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _probeFrames; private static long _probeLogMs; - // RYUJINX_MVPP_VPFIFO=1: order-based camera pairing. Each DISTINCT camera value seen on - // the scaled passes is pushed in draw order and consumed one-per-present at dequeue -- - // immune to the enqueue-snapshot race the pair probe measured (its precondition, exactly - // one camera per frame while moving, was confirmed on TOTK: histogram all 1s, the - // 0/2 pairs being the measurement window racing, not a second camera). private static readonly bool _fifoMode = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VPFIFO") == "1"; - // [VPSOLO] Last matrix pushed into the ordered FIFO by the solo fallback, so it pushes - // only on change (the triplet producer gates on its block changing; without an - // equivalent here the queue would be fed an identical matrix every single frame). private static bool _hasSoloPushed; private static Matrix4x4 _lastSoloPushed; @@ -207,10 +123,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _cwFrameHadRaw = 1; } - // [VPSOLO] Aspect reference for the solo fallback, fed on EVERY draw with a bound - // depth -- both regimes, before any filtering. Feeding it only from the native arm - // left a scaled config (res_scale > 1) with no reference at all, which silently - // blocks the election for ever. Guarded: nothing runs unless the fallback is on. if (MvppSoloCamera.Enabled) { Image.Texture aspectDs = channel.TextureManager.RenderTargetDepthStencil; @@ -221,17 +133,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Two regimes for recognizing the main 3D scene pass (everything BEHIND this - // pre-filter -- square guard, largest-wins vote, structural camera validation -- - // is scale-agnostic and unchanged): - // - scaled config (res_scale > 1): the scaled pass IS the main pass. Historic - // path, byte-identical: native-res draws (UI/2D) are skipped here. - // - native config (res_scale == 1, e.g. in-engine 4K render mods): NOTHING is - // ever scaled, so the scale test alone left the capture blind for the whole - // session (proven 19/07: 4K-native sonde, pushes 0 over the full run, MV++ - // silent, clouds ghosting raw). Recognize scene draws by a bound NON-SQUARE - // depth-stencil instead. The square skip here also keeps the probe path away - // from shadow-pass draws -- the same invariant the scaled filter provided. if (channel.TextureManager.RenderTargetScale == 1f) { if (GraphicsConfig.ResScale != 1f) @@ -253,9 +154,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [CTXPROBE 29/07 SOIR] Lecture seule. Recense TOUTES les passes qui arrivent jusqu'ici, pas - // seulement la premiere : c'est ce chiffre qui dit s'il y a un choix a faire, puisque la - // capture ci-dessous prend la premiere venue et se tait ensuite. Voir MvppCtxProbe. if (MvppCtxProbe.Enabled) { MvppCtxProbe.NoteQualifyingDraw(channel); @@ -275,21 +173,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [SCENEPASS 29/07 SOIR] Le pre-filtre ci-dessus reconnait "une passe 3D", pas "LA passe - // de la scene", et la capture prend la premiere venue. Mesure CTXPROBE du soir : la passe - // 8 bits porte 67 % des intruses, la couleur HDR de scene 1,7 %. On saute donc la - // tentative sur les passes qui ne sont pas la scene -- la capture se fera plus loin dans - // la MEME image. Filet de securite et auto-desarmement : voir MvppScenePass. if (needCapture && MvppScenePass.Enabled && !MvppScenePass.Allows(channel)) { needCapture = false; } - // [NOLDR 29/07 SOIR] L'inverse de SCENEPASS, et c'est la mesure qui a impose l'inversion : - // la passe de scene HDR existe dans 98,9 % des images mais la camera n'y est lisible que - // dans 36,5 % des cas, donc n'autoriser QUE celle-la affame la camera. On ECARTE plutot - // la pire passe -- la 8 bits, 19,6 % d'intruses, quatre intruses sur cinq -- et on garde - // toutes les autres comme occasions de capture. Voir MvppLdrSkip. if (needCapture && MvppLdrSkip.Active && !MvppLdrSkip.Allows(channel)) { needCapture = false; @@ -299,12 +187,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { StashSceneDepth(channel); - // [CAPTIME_FIX 31/07] Mesure du 31/07 : sur 55 % des images le jeu reecrit sa camera - // APRES notre capture, vers le 39e dessin sur ~160. On laisse donc passer les - // dessins tant que le lieu elu porte encore la valeur deja publiee, et la capture - // normale se fait des qu'une valeur NOUVELLE apparait. Une seule capture par image, - // par le chemin normal : SNAPGUARD, la file et les gardes gardent leur hypothese. - // Eteint par defaut ; le doute profite toujours au comportement existant. if (needCapture && MvppSoloCamera.StaleAtElectedLocation(channel)) { needCapture = false; @@ -316,10 +198,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else if (MvppSoloCamera.CapTime && Volatile.Read(ref _capturedThisFrame) == 1) { - // [CAPTIME 31/07] LA SEULE ligne ajoutee hors de MvppSoloCamera, et elle est - // inevitable : le chemin solo n'est appele qu'au-dessus, sous `needCapture`, - // donc une fois l'image capturee MvppSoloCamera ne serait plus jamais sollicite. - // Lecture pure, eteinte par defaut, aucun effet sur la capture ni sur l'election. MvppSoloCamera.NoteLateDraw(channel); } @@ -336,60 +214,26 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // The depth published by SetRenderTargetDepthStencil is "the last depth bound by ANY - // pass" -- a roulette that lands on shadow maps or bloom-chain buffers depending on - // where the present falls (measured: 487 source flips in ~2 min = the mode 2 - // whole-image tremble; DLSS was fed real/zero/garbage MVs in alternation). The scene - // depth is pinned HERE instead: the depth bound while the SCALED main pass draws, - // keeping the largest one per present interval. private static Image.Texture _sceneDepth; - // Plumbing counters for the depth-pinning path (read+reset by the present probe): - // scaled draws seen, of which how many had a depth-stencil bound. public static int StatScaledDraws; public static int StatDepthBound; - // Host dims of the pinned scene depth, kept across frames (dyn-res updates them a - // frame late at worst). The velocity pass uses them to tell scene-pass grafts apart - // from shadow/aux passes whose DS sizes differ (3b re-draw + debug-bridge gate). public static int SceneDepthHostWidth; public static int SceneDepthHostHeight; - // (b1) aspect-guard instrumentation: how often and when the guard fires (must hit - // jumps/climbs, never normal scenes -- readable straight off the log). private static int _guardRejects; private static long _guardLogMs; - // [native arm, 19/07] one-shot marker that the res_scale==1 recognition path engaged - // this session (reads user logs apart: native-mod configs vs scaled configs). private static bool _nativeArmLogged; - // RYUJINX_MVPP_DEPTHPROBE=1 (depth-source verdict session, 05/07): in the "garbage" - // frames, is the real D32 scene depth STILL bound during scaled draws (out-voted at the - // size tie) or has it VANISHED? Largest-wins is format-blind AND same-dims-blind, so the - // dumps (one depth per frame) can't answer. This census logs EVERY DS candidate per - // present -- deduped by (format, dims), IN binding order (which breaks the tie), plus how - // many DISTINCT depth-target identities alternated under each (fmt, dims) bucket -- and - // marks the winner. Default OFF; the non-gated path is byte-identical. private static readonly bool _depthProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHPROBE") == "1"; private static bool _depthProbeFaulted; - // RYUJINX_MVPP_DEPTHFIX (05/07): at EXACTLY equal area, break the depth-capture tie by - // precision (D32 > D24 > D16) instead of bind order. GRAVED DEFAULT-ON. Verdict A: the real - // D32Float scene depth was bound every frame but lost a ~50/50 same-dims tie to a D16Unorm - // 8-level buffer -> DLSS ate a posterized depth ~half the time. Measured 0 D16 winners with - // this on. Pure correctness win (DLSS always gets the true depth, all modes); it did NOT fix - // the far shimmer -- that lives elsewhere (open). RYUJINX_MVPP_DEPTHFIX=0 reverts to the - // strict-> behavior (one-flag rollback). (GAL ScalarSize conflates D24/D32 at 4 bytes, so - // DepthRank below maps the precision ourselves.) private static readonly bool _depthFix = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHFIX") != "0"; - // [COLORCAP probe, 07/07] Prototype: is the scene-pass COLOR attachment capturable EXACTLY like - // the depth? Reads color[0] bound at the SAME instant as a scene depth candidate (same framebuffer, - // same scaled pass), logs dims/format/address vs the depth's, deduped by (fmt,dims). Gated on - // RYUJINX_MVPP_COLORCAP_PROBE=1 (requires RYUJINX_MVPP_CAP=1). Reads only, NOTHING wired to DLSS. private static readonly bool _colorCapProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORCAP_PROBE") == "1"; private static readonly System.Collections.Generic.HashSet _colorCapSeen = new(); @@ -401,17 +245,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly int[] _censusH = new int[DepthCensusMax]; private static readonly int[] _censusDraws = new int[DepthCensusMax]; private static readonly bool[] _censusSq = new bool[DepthCensusMax]; - // Per-bucket same-dims competition: last identity seen (guest base address) + the - // increment-on-change count (the _probeChanges pattern). >1 on the WINNER line = several - // depth targets alternate under one (fmt, dims) -- the hijack a dims-only census misses. private static readonly ulong[] _censusIdent = new ulong[DepthCensusMax]; private static readonly int[] _censusIdentN = new int[DepthCensusMax]; private static int _censusCount; private static int _censusOverflow; - // Edge-trigger: a census line prints only when the signature (ordered buckets + whether - // each alternates + winner + overflow) changes. Capped; the 5 s window keeps counting - // flips so a "487 flips" flip-flop stays visible after the cap is spent. private static ulong _depthSigLast; private static bool _hasDepthSig; private static int _depthTransitionLogs; @@ -420,11 +258,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _statDepthPresents; private static int _statDepthCensusChanges; - // [COLORCAP snapshot] the pinned HDR scene color (same framebuffer as the pinned scene depth), - // consumed at present by TakeSceneColor. Parallel to _sceneDepth. Set only when the snapshot is on. private static Image.Texture _sceneColor; - /// [COLORCAP snapshot] Consumes the pinned scene color since the last present (GPU thread). public static Image.Texture TakeSceneColor() { Image.Texture c = _sceneColor; @@ -432,14 +267,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return c; } - /// [COLORCAP probe] Monotonic present frame id, exposed for the snapshot log. public static long ColorCapFrameId => _colorCapFrameId; - /// - /// [MV++ UI audit, read-only] GPU address of the pinned HDR scene color, or 0 if none is pinned - /// (requires RYUJINX_MVPP_COLORCAP_SNAP). The gated composite-locator probe compares this against - /// each graphics draw's sampled textures to find the draw that consumes the scene. - /// public static ulong SceneColorAddress => _sceneColor != null ? _sceneColor.Range.GetSubRange(0).Address : 0UL; private static void StashSceneDepth(GpuChannel channel) @@ -461,12 +290,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ds.Info.Width == ds.Info.Height, ds.Range.GetSubRange(0).Address); } - // (b1) FIX carres-saut -- aspect guard, PROVEN by the 04/07 log census: no TOTK - // scene is ever SQUARE (all 16:9 -- 2666x1500 / 3200x1800 / 1332x750 / 1600x900); - // square DS = shadow atlas (8192^2 and one 4096^2 observed). The largest-wins - // pick below let the 67 Mpx atlas beat the 4 Mpx scene whenever the cascades - // re-render (every jump / some climb angles) -> polluted scene publication -> - // graft + 8192 coverage -> on-screen squares. if (ds.Info.Width == ds.Info.Height) { _guardRejects++; @@ -482,9 +305,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // [COLORCAP PROBE] Non-square scene-pass depth candidate: read the color[0] attachment bound - // at this SAME instant (same framebuffer, same scaled pass). Prove it exists, matches the depth - // dims (2666x1500), and is reached BEFORE the game's composite/present. Deduped by (fmt,dims). if (_colorCapProbe) { Image.Texture col0 = channel.TextureManager.RenderTargetColor0; @@ -507,10 +327,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [COLORCAP snapshot] Independent color pin: keep the LARGEST non-square HDR color seen this - // present interval (its OWN largest-wins, decoupled from the depth pin -- the HDR scene color - // is bound on different draws than the depth winner, so tying it to the depth pin missed it). - // Consumed at present by TakeSceneColor. HDR formats only; LDR / half-res lose the size race. if (MvppColorSnapshot.Enabled) { Image.Texture c0 = channel.TextureManager.RenderTargetColor0; @@ -522,8 +338,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed (long)c0.Info.Width * c0.Info.Height > (long)_sceneColor.Info.Width * _sceneColor.Info.Height)) { _sceneColor = c0; - // [ALIGN0 fix] stamp the color pin with the SAME counter as the depth pin - // (DlssJitterState.FrameId), so nativeVsDepthSameFrame compares like-for-like. DlssJitterState.SceneColorFrameId = DlssJitterState.FrameId; } } @@ -531,19 +345,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_sceneDepth == null || (long)ds.Info.Width * ds.Info.Height > (long)_sceneDepth.Info.Width * _sceneDepth.Info.Height || - // [DEPTHFIX] Verdict A: the real D32 is bound every frame but loses a same-dims tie - // to a D16Unorm coarse buffer (first-bound wins the format-blind >). ONLY at exactly - // equal area, prefer the higher depth precision. The `_sceneDepth == null` clause - // above short-circuits, so this never dereferences null; OFF => `false &&` => the - // strict-> behavior byte-for-byte. (_depthFix && (long)ds.Info.Width * ds.Info.Height == (long)_sceneDepth.Info.Width * _sceneDepth.Info.Height && DepthRank(ds.Info.FormatInfo.Format) > DepthRank(_sceneDepth.Info.FormatInfo.Format))) { _sceneDepth = ds; - // [ALIGN0 probe] stamp the frame this depth was pinned in (= the jitter FrameId), to - // compare against the color/present frame id at the DLSS eval site. Diagnostic only. DlssJitterState.SceneDepthFrameId = DlssJitterState.FrameId; GAL.ITexture host = ds.HostTexture; @@ -555,9 +362,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [DEPTHFIX] Depth precision rank for the same-dims tie-break. No GAL accessor separates - // D24 from D32 (ScalarSize conflates both at 4 bytes), so map it here. Higher = keep; a - // depth-stencil target always ranks > 0, so this only ever orders real depth candidates. private static int DepthRank(Format fmt) => fmt switch { Format.D32Float or Format.D32FloatS8Uint => 32, @@ -566,21 +370,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _ => 0, }; - /// - /// Consumes the scene depth seen since the last present dequeue (GPU thread). - /// - /// [INJECT étape C4-1, read-only] GPU address of the currently-pinned scene depth (0 if none). - /// Parallel to ; used to identify/log the depth candidate at the injection - /// point and to confirm it belongs to the same framebuffer as the scene colour. public static ulong SceneDepthAddress => _sceneDepth != null ? _sceneDepth.Range.GetSubRange(0).Address : 0UL; - /// [INJECT étape C4-1, read-only] Format of the currently-pinned scene depth. public static Ryujinx.Graphics.GAL.Format SceneDepthFormat => _sceneDepth != null ? _sceneDepth.Info.FormatInfo.Format : default; - /// [INJECT étape C4, read-only] Host texture handle of the currently-pinned scene depth (null - /// if none). Carried through the GAL command (like the scene colour) so the mid-frame DLSS gets this - /// frame's own depth, in order. Null-safe. public static Ryujinx.Graphics.GAL.ITexture SceneDepthHost => _sceneDepth?.HostTexture; public static Image.Texture TakeSceneDepth() @@ -590,9 +384,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_depthProbe && !_depthProbeFaulted) { - // Called OUTSIDE the OnDraw fence (Window.cs present) -- own guard so an emission - // fault disables the probe instead of killing the present. Reset lives inside the - // gate so the non-gated path stays byte-identical. try { EmitDepthCensus(d); @@ -610,10 +401,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return d; } - // Depth-probe census recorder (RYUJINX_MVPP_DEPTHPROBE). Append-on-first-sighting keeps - // binding ORDER (which breaks the largest-wins tie) and dedups by (format, dims); per - // bucket, tracks how many DISTINCT depth-target identities alternated (increment-on- - // change, like _probeChanges). Fixed arrays, zero alloc per draw. private static void RecordDepthCensus(Format fmt, int w, int h, bool sq, ulong ident) { for (int i = 0; i < _censusCount; i++) @@ -649,9 +436,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _censusIdentN[n] = 1; } - // Emitted from TakeSceneDepth (once per present, outside the fence -- the caller guards - // it). Edge-triggered per-change line + a 5 s window that counts flips regardless of the - // per-change cap, so a "487 flips" flip-flop stays visible once the cap is spent. private static void EmitDepthCensus(Image.Texture winner) { bool hasWinner = winner != null; @@ -665,7 +449,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed wH = winner.Info.Height; } - // Signature over ordered buckets (+ whether each alternates) + the winner + overflow. ulong sig = 14695981039346656037UL; for (int i = 0; i < _censusCount; i++) { @@ -780,8 +563,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_fifoMode) { - // Same structural validation as TryCapture before trusting the new value: - // the block could be repurposed under our cached location. ReadOnlySpan f = MemoryMarshal.Cast(block); ReadOnlySpan view = f.Slice(0, 16); ReadOnlySpan proj = f.Slice(16, 16); @@ -807,21 +588,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// Called when a frame is enqueued for presentation: re-arms the once-per-frame capture. - /// public static void OnFrameEnqueued() { - _colorCapFrameId++; // [COLORCAP probe] one id per presented frame, for the capture prototype log + _colorCapFrameId++; - // [HANGWATCH 29/07] Une ecriture d'entier, sur un fil de fond independant qui, lui, - // survivra au gel du fil graphique. Voir MvppHangWatch. if (MvppHangWatch.Enabled) { MvppHangWatch.Ping(); } - // [CTXPROBE 29/07 SOIR] Re-arme le rang de dessin, au meme endroit que la capture elle-meme. if (MvppCtxProbe.Enabled) { MvppCtxProbe.OnFrame(); @@ -831,22 +606,17 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { if (Volatile.Read(ref _capturedThisFrame) == 1) { - // Lineage published this frame: rivals evaporate. _rivalStreak = 0; _hasRival = false; } else if (_rivalSeenThisFrame) { - // Rival-only frame: the persistence clock ticks. _rivalStreak++; } _rivalSeenThisFrame = false; } - // [SCENEPASS 29/07 SOIR] ICI, avant le re-armement : c'est le seul endroit ou l'on sait - // si l'image a fini par obtenir une camera. Une image restee sans camera alors qu'elle - // avait des passes 3D est une image potentiellement perdue -- le filet la compte. if (MvppScenePass.Enabled) { MvppScenePass.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1); @@ -857,16 +627,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed MvppLdrSkip.OnFrame(Volatile.Read(ref _capturedThisFrame) == 1); } - // [CAPTIME_AB 31/07] Meme endroit, meme raison que les deux au-dessus : c'est le seul - // point ou l'on sait si l'image a fini par obtenir une camera. Inerte tant que - // RYUJINX_MVPP_CAPTIME_AB n'est pas renseigne. MvppSoloCamera.OnPresentBoundary(Volatile.Read(ref _capturedThisFrame) == 1); MvppSoloCamera.LogWhy(); Volatile.Write(ref _capturedThisFrame, 0); - // [CAPWHY v2] Classement de la presentation qui se termine, puis reset des verrous. - // [DUPSKIP] La meme frontiere nourrit la sequence des vraies images rendues. if (_capWhy || _dupSkip || _fgFeed) { if ((_dupSkip || _fgFeed) && _cwFrameHadRaw == 1) @@ -894,14 +659,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _cwFrameHadQual = 0; } - // Publisher window: ticks on EVERY present so a zero-publish phase still logs - // (the in-capture heartbeat is silent exactly when things go wrong). _statEnqueues++; long nowPub = Environment.TickCount64; if (nowPub - _pubWindowMs >= 5000) { - if (_pubWindowMs != 0 && _enabled) + if (_pubWindowMs != 0 && _enabled && Verbose) { Logger.Info?.Print(LogClass.Gpu, $"MVPP capture window: enq {_statEnqueues}, pub {_statPubFrames} " + @@ -964,16 +727,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [DUPPAIR 28/07] Sonde du POINT DE PRODUCTION de la camera, cadencee a 1 Hz et sans - // dependance au mode dev (qui coute des images). Mesure du 28/07 cote consommation : - // pendant les pans, 6 a 15 valeurs distinctes par seconde pour 30 images -- et le - // manque est INVERSEMENT correle a la vitesse de la camera. Rien n'est perdu en route - // (drops=0), donc la valeur n'est jamais produite. Reste a savoir si on lit et que la - // valeur ne bouge pas, ou si la lecture echoue -- et alors pour quel motif. private static readonly bool _dupPair = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUPPAIR") == "1"; - // [JITTRACE 28/07] Voir les etapes A / B / C. Journal seul, une ligne par seconde par etape. internal static readonly bool JitTrace = Environment.GetEnvironmentVariable("RYUJINX_MVPP_JITTRACE") == "1"; @@ -1045,7 +801,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _gateHeldSlot = false; - // Cached location first: one read + three cheap checks per frame in steady state. if (_cachedSlot >= 0 && TryCapture(channel, _cachedSlot, _cachedOffset)) { if (_capWhy) @@ -1058,9 +813,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_gateHeldSlot) { - // The cached block validated but the gate quarantined it (rival camera): - // the lineage lives at the SAME location on a LATER draw -- a full rescan - // would only re-find the rival. Keep the slot open, skip the scan. if (_capWhy) { _cwHeld++; @@ -1069,7 +821,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Full rescan of the vertex-stage cbufs (first frame, or the game moved its block). uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(0); for (int slot = 0; mask != 0; slot++, mask >>= 1) @@ -1102,14 +853,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [VPSOLO fallback, 21/07 -- RYUJINX_MVPP_VPSOLO=1, off by default] - // Reached ONLY when the triplet scan above found nothing, so a game whose camera the - // historic contract recognises never gets here and its behaviour is unchanged by - // construction rather than by testing. Games that store their view-projection ALONE - // (Xenoblade 2: view and proj in separate buffers, column-major, canonical triplet - // count 0 across every measured run) had MV++ silently disarmed for the whole - // session; this gives them a camera. See MvppSoloCamera for how the matrix is - // recognised and how the location is elected. if (_capWhy && MvppSoloCamera.Enabled) { _cwSoloCall++; @@ -1127,40 +870,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _captures++; _statPubFrames++; - // Publishing is only half the pipe. In VPFIFO mode the present side pairs the - // camera to the presented frame by CONSUMING an ordered queue, and the only - // producer for that queue is ProbeDistinctValues -- which requires _cachedSlot, - // i.e. a triplet that was found. On a game where the triplet does not exist the - // queue therefore stayed empty for ever: measured 21/07 on XC2, pub 150/151 on - // the GPU side while the consumer logged "pairOk=0" every 5 s for a whole - // 3-minute run, so the reprojection pass never once ran. Push here too, and only - // on a CHANGED value, exactly like the triplet producer does -- a queue fed the - // same matrix every frame would just grow. if (_fifoMode && (!_hasSoloPushed || soloVp != _lastSoloPushed)) { _hasSoloPushed = true; _lastSoloPushed = soloVp; - // [GAMEJITTER 28/07] Le decalage sous-pixel MESURE SUR CETTE LECTURE part avec - // la matrice : c'est la seule facon qu'il reste apparie a la bonne image. float pushJx = MvppSoloCamera.LastJitterX; float pushJy = MvppSoloCamera.LastJitterY; - // ⛔ [28/07 17h40] Surcharge a 3 parametres NEUTRALISEE, meme dette que dans - // Window.cs : PushOrdered(vp, jx, jy) a ete ajoutee au GAL a 11:46, le GAL.dll - // de l'installation date de 10:07 et ne la contient pas -> MissingMethodException - // des que le projet Gpu est recompile. La surcharge a 1 parametre existe des - // deux cotes. Zero perte : GAMEJITTER ne transporte rien (mesure du 28/07, - // `brut (0,000;0,000)` cote DLSS). A retablir avec la reconstruction du GAL. _ = pushJx; _ = pushJy; DlssCameraState.PushOrdered(in soloVp); MvppFamHold.NotePush(); _soloDistinct++; - // [JITTRACE 28/07] ETAPE A. La valeur est mesuree d'un cote et vaut zero de - // l'autre ; deux explications de ma part se sont deja revelees fausses. On - // trace donc a CHAQUE saut au lieu de raisonner. Ici : ce qu'on POUSSE. if (_jitTrace && Environment.TickCount64 - _jitTraceMs >= 1000) { _jitTraceMs = Environment.TickCount64; @@ -1171,16 +894,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else if (_fifoMode) { - // [DUPPAIR 28/07] Lecture reussie qui rend la MEME matrice que la derniere - // poussee. C'est la moitie manquante du diagnostic : une image sans nouvelle - // camera vient soit d'ici (on lit, mais la valeur ne bouge pas), soit d'un - // echec de lecture (compte par motif dans MvppSoloCamera). Les deux ne se - // corrigent pas au meme endroit. _soloSame++; } - // Same scene/aux attribution as the triplet path: did the draw that carried this - // camera have the pinned scene depth bound? GAL.ITexture soloDs = channel.TextureManager.RenderTargetDepthStencil?.HostTexture; if (soloDs != null && soloDs.Width == SceneDepthHostWidth && soloDs.Height == SceneDepthHostHeight) @@ -1194,13 +910,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Center-pixel MV spikes of +-40 px at 1-3 Hz with a near-still camera can only come - // from a FOREIGN camera entering the reprojection pair: the math pins |mv| ~ 0 when - // the two matrices are near-identical, whatever the depth does. The episodic - // perspective camera in TOTK is the environment-cubemap update -- perspective, - // orthonormal, exact product, structurally indistinguishable -- but its faces are - // SQUARE (m00 == m11), while a real presentation camera carries the screen aspect. - // Rejects are counted to prove the gate fires at the spike frequency. public static int StatSquareRejects; private static bool IsSquareProjection(ReadOnlySpan proj) @@ -1250,7 +959,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // Camera position (rotation-invariant): view = [R|t] row-major, pos = -R^T t. float camPosX = -(view[0] * view[3] + view[4] * view[7] + view[8] * view[11]); float camPosY = -(view[1] * view[3] + view[5] * view[7] + view[9] * view[11]); float camPosZ = -(view[2] * view[3] + view[6] * view[7] + view[10] * view[11]); @@ -1280,8 +988,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (!sameRival) { - // A different rival restarts the persistence clock: the impostor - // rafale (a new camera every few frames) can never build a streak. _hasRival = true; _rivalStreak = 0; } @@ -1295,15 +1001,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_rivalStreak < GateResyncFrames) { - // Quarantined: not published, and the slot stays OPEN -- the - // lineage may still draw later in this frame. _gateHeldSlot = true; return false; } - // A rival that persisted ALONE for the full window is the camera now - // (real cut): adopt it as the new lineage. _statResyncs++; _hasRival = false; _rivalStreak = 0; @@ -1314,8 +1016,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else if (_rivalSeenThisFrame) { - // The impostor drew first, the gate held the slot open, and the real - // lineage arrived later in the same frame -- the exact rescue case. _statLineageLate++; } } @@ -1331,8 +1031,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _captures++; _statPubFrames++; - // Who won the once-per-frame slot: a draw of the published scene pass, or an - // auxiliary scaled pass carrying its own structurally-valid camera? Image.Texture winnerDs = channel.TextureManager.RenderTargetDepthStencil; GAL.ITexture winnerDsHost = winnerDs?.HostTexture; bool sceneWinner = winnerDsHost != null && @@ -1348,7 +1046,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _statCapAux++; } - ulong pubFp = 14695981039346656037UL; // FNV-1a over the published VP block + ulong pubFp = 14695981039346656037UL; foreach (byte b in MemoryMarshal.AsBytes(vp)) { pubFp = (pubFp ^ b) * 1099511628211UL; @@ -1361,9 +1059,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _statVpChanges++; } - // Teleport detector, same metric as the gate (v2: position + direction, both - // rotation-safe): with the gate ON every TELEPORT line must pair with a resync - // line -- an orphan TELEPORT is a gate hole. bool teleport = _hasR3 && (MathF.Abs(camPosX - _lastPosX) > GatePosEps || MathF.Abs(camPosY - _lastPosY) > GatePosEps || @@ -1413,10 +1108,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// World-to-camera view: bottom row [0,0,0,1], upper 3x3 orthonormal (unit rows, - /// mutually orthogonal). - /// private static bool IsOrthonormalView(ReadOnlySpan m) { if (MathF.Abs(m[12]) > 1e-3f || MathF.Abs(m[13]) > 1e-3f || @@ -1451,10 +1142,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// Perspective projection: diagonal focal terms, zeros elsewhere, w-carry row - /// [0, 0, +/-1, 0]. Orthographic (shadow cascades) fails the +/-1 w-carry on purpose. - /// private static bool IsPerspectiveProj(ReadOnlySpan m) { return m[0] > 0.05f && MathF.Abs(m[1]) < 1e-4f && MathF.Abs(m[2]) < 1e-4f && MathF.Abs(m[3]) < 1e-4f && @@ -1465,10 +1152,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed MathF.Abs(m[15]) < 1e-3f; } - /// - /// The clincher: the third matrix must be the product of the first two (proj x view), - /// element by element, within float tolerance. - /// private static bool ProductMatches(ReadOnlySpan p, ReadOnlySpan v, ReadOnlySpan vp) { for (int r = 0; r < 4; r++) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompInputsProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompInputsProbe.cs index 668a7c419..3a45bd689 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompInputsProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompInputsProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,16 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppCompInputsProbe { private static bool _enabled = @@ -53,8 +40,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // On logue le gobBlocksInY brut : l'anomalie = deux textures de MÊME taille avec des gobY - // différents. Le stride est ajouté car un pitch block-linear faux est l'autre forme du bug. string sig = $"{role} {i.Width}x{i.Height} {i.FormatInfo.Format} gobY={i.GobBlocksInY} lin={i.IsLinear} stride={i.Stride}"; if (_seen.Add(sig)) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompositeProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompositeProbe.cs index 3eb6ec641..405ea7afc 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompositeProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCompositeProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,25 +6,16 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ composite-locator probe (RYUJINX_MVPP_COMPOSITEPROBE=1). READ-ONLY, default off, nothing in the - /// render path branches on it. Proves or disproves that the final composite is a GRAPHICS draw that - /// samples the captured HDR scene texture. For each draw it compares every bound sampled texture's GPU - /// address to MvppCameraCapture.SceneColorAddress; on a match it logs the 8 colour render targets - /// (address/format/dims/slot), blend/depth/projection/shader, then the next 20 draws to see whether the - /// HUD is drawn afterwards. A 2 s summary counts scene-sampling draws per window (single vs multiple - /// composite). Requires RYUJINX_MVPP_COLORCAP_SNAP so the scene is pinned. - /// static class MvppCompositeProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_COMPOSITEPROBE") == "1"; - private const int MaxFullDumps = 4; // fully characterize the first few composite draws, then only summarize + private const int MaxFullDumps = 4; private static int _drawNo; private static int _fullDumps; - private static int _following; // countdown: log the next N draws after a composite hit (HUD check) + private static int _following; private static long _lastSummaryMs; private static int _sceneReadsSinceSummary; @@ -55,7 +43,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ulong sceneAddr = MvppCameraCapture.SceneColorAddress; - // Does THIS draw sample the scene texture? (address match against any bound input) bool samplesScene = false; if (sceneAddr != 0) { @@ -91,7 +78,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"MVPP-COMPOSITE OUT slot{slot} @0x{rt.Range.GetSubRange(0).Address:X} {rt.Info.FormatInfo.Format} {rt.Info.Width}x{rt.Info.Height}"); }); - _following = 20; // characterize what is drawn AFTER the composite (HUD?) + _following = 20; } } else if (_following > 0) @@ -133,8 +120,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Best-effort projection hint from stage-0 cbuf5 matrix0 (same slot MvppP2Probe reads). A fullscreen - // composite quad may have no real projection; the raw ortho/persp/none hint is advisory only. private static string ClassifyProj(GpuChannel channel) { try diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCtxProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCtxProbe.cs index be3678cc4..0160c5369 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCtxProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppCtxProbe.cs @@ -1,86 +1,21 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// [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. 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. - /// static class MvppCtxProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CTXPROBE") == "1"; - /// - /// [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. - /// public static readonly bool TrialRead = Environment.GetEnvironmentVariable("RYUJINX_MVPP_HDRTRY") == "1"; - /// - /// 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. - /// private const float IntruderDist = 100f; - /// - /// 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. - /// private const long FreshRefMs = 60; - // Releve a 32 le 29/07 au soir : l'adresse de la profondeur est entree dans la cle, donc une - // meme forme peut desormais occuper plusieurs lignes. Le debordement reste journalise. private const int MaxCtx = 32; private const int ReportMs = 5000; @@ -104,11 +39,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly Ctx[] _readCtx = new Ctx[MaxCtx]; private static readonly Ctx[] _qualCtx = new Ctx[MaxCtx]; - // [RANG REPARE 29/07 soir] La v1 remettait _rank a zero depuis OnFrame, c'est-a-dire depuis - // le fil de PRESENTATION, pendant que OnDraw l'incrementait depuis le fil GPU -- d'ou des - // "rang 0" impossibles et une colonne inutilisable. Ici OnFrame ne touche plus qu'un JETON, - // et la remise a zero se fait paresseusement sur le fil GPU, seul ecrivain de _rank. Plus de - // course : le seul echange entre fils est un int, indechirable. private static int _rank; private static int _frameMark; private static int _seenMark; @@ -122,17 +52,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _intruders; private static int _ctxOverflow; - // [VERDICT PASSE HDR, 29/07 soir] La question qui decide du chantier SCENEPASS. Le - // recensement a montre OU sont les intruses (8,1 % sur la passe 8 bits, 0,08 % sur la - // couleur HDR) ; deplacer la capture n'a de sens que si la camera est LISIBLE au moment de - // la passe HDR. Une lecture d'essai par image, a la premiere passe HDR rencontree. - // [IDENTITE DES INTRUSES, 29/07 soir, 3e passe] La question qui peut FERMER le dossier. Le - // residu vaut 20 intruses par run, et toutes plafonnent a 415,2 u -- or (272) a etabli que - // "415,2 depuis [403,3 -28 436,4]" est la transition de camera au CHARGEMENT : deterministe, - // une fois par lancement, PAS un symptome. Si les 20 sont toutes de cette famille, il ne - // reste rien a corriger. On les recense donc une par une, dedupliquees par distance arrondie - // au dixieme, avec la position d'ou elles viennent : une valeur ET une position identiques - // au chiffre pres d'un lancement a l'autre = evenement deterministe (regle de (272)). private const int MaxIntr = 16; private static readonly float[] _intrDist = new float[MaxIntr]; private static readonly int[] _intrCount = new int[MaxIntr]; @@ -142,12 +61,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _intrN; private static int _intrOverflow; - // [RANG DES INTRUSES, 29/07 soir, 4e passe] Le seul axe jamais regarde, et disponible - // uniquement depuis que la colonne "rang" est reparee. La question : les intruses arrivent- - // elles a un rang systematiquement PLUS TOT que les lectures saines ? Si oui, "ne pas - // capturer dans les N premiers dessins qualifiants" est un correctif generique, simple et - // testable -- et ce serait le meme genre de discriminant que celui qui a tue la passe 8 bits. - // Moyennes + histogramme grossier : on cherche un ecart franc, pas une decimale. private static long _rankSumClean; private static int _rankNClean; private static long _rankSumIntr; @@ -170,11 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static long _lastReportMs; - /// - /// Appelee pour CHAQUE dessin qui passe le pre-filtre de , - /// 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. - /// public static void NoteQualifyingDraw(GpuChannel channel) { int mark = _frameMark; @@ -187,10 +95,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _rank++; - // [VERDICT PASSE HDR] Une seule lecture d'essai par image, a la PREMIERE passe de scene - // HDR rencontree -- c'est exactement l'instant ou SCENEPASS voudrait capturer. Lecture - // seule et sans effet de bord (voir MvppSoloCamera.TryProbeRead) : rien n'est publie, - // aucun garde n'est traverse, la paire de jitter est restauree. if (TrialRead && !_probedThisFrame && MvppScenePass.IsSceneColorPass(channel)) { _probedThisFrame = true; @@ -217,30 +121,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Record(_qualCtx, fmt, cw, ch, dw, dh, cAddr, dAddr, _rank, 0f, false); } - /// - /// 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. - /// est le pas depuis la derniere position ACCEPTEE, c'est-a-dire - /// exactement la distance que mesure SNAPGUARD : les chiffres des deux se comparent. - /// public static void NoteRead(GpuChannel channel, float dist, float px, float py, float pz, long ageMs) { _reads++; - // [DEFINITION CORRIGEE, 29/07 soir, 7e passe] MESURE : les "intruses" avaient un age de - // reference de 2 443 ms de moyenne (max 2 875) contre 37 ms pour les lectures saines. - // 🐛 Donc elles n'etaient PAS des cameras rivales : c'etait ma definition qui se - // declenchait sur une reference PERIMEE. Pendant 2,4 s le joueur se deplace, et la - // lecture legitime suivante ressemble forcement a un saut de 400 u. Ca explique tout ce - // qui m'intriguait : distances toujours vers 415 (ce qu'on parcourt en 2,4 s), "familles" - // changeant de run en run, "position fixe" revue 19 fois (l'endroit ou les lectures - // reprennent). - // - // Une intruse n'est donc comptee que si le saut est mesure contre une reference FRAICHE. - // Les sauts a reference perimee sont comptes A PART : ils ne disparaissent pas du - // journal, ils changent de nom -- ils mesurent les TROUS d'acceptation, ce qui est une - // vraie grandeur, mais pas celle qu'on croyait. bool freshRef = ageMs <= FreshRefMs; bool bigJump = dist >= IntruderDist; bool intruder = bigJump && freshRef; @@ -250,15 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _staleJumps++; } - // [AGE DE LA REFERENCE, 29/07 soir, 6e passe] LE DOUTE SUR MA PROPRE DEFINITION. - // "Intruse" = pas de 100 u ou plus depuis la DERNIERE POSITION ACCEPTEE. Or quand - // SNAPGUARD refuse un moment, cette reference VIEILLIT pendant que le joueur avance : - // une lecture parfaitement legitime peut alors ressembler a un saut de 400 u. Ce qui - // collerait avec la famille qui DEFILAIT au plan Y du joueur ([115,1 -11,5 142,1] -> - // [117,0 -11,5 143,7]) : peut-etre sa propre camera, lue apres une tenue longue. - // Si les intruses ont un age de reference bien plus eleve que les lectures saines, une - // partie du "residu" est un artefact de MA sonde, pas un defaut -- et il ne faut surtout - // pas coder un correctif contre ca. if (intruder) { _ageSumIntr += ageMs; @@ -296,10 +171,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _rankHistClean[bucket]++; } - // [NOLDR A/B] Etiquette la lecture avec la moitie d'experience en cours. C'est le seul - // endroit qui sait si une lecture est une intruse, et MvppLdrSkip le seul qui sache - // quelle moitie tourne : les deux moities se comparent alors sur la MEME scene, ce qui - // supprime la variable qui a fait juger trois correctifs sur du hasard ce matin. MvppLdrSkip.NoteRead(intruder); Describe(channel, out int fmt, out int cw, out int ch, out int dw, out int dh, @@ -316,7 +187,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// Re-arme le rang de dessin, une fois par image presentee. public static void OnFrame() { if (_rank > 0) @@ -335,29 +205,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // On ne touche PLUS a _rank ici (fil de presentation) : on avance un jeton, et le fil GPU - // fera la remise a zero lui-meme. C'est la reparation de la colonne "rang". _frameMark++; _probedThisFrame = false; } - /// - /// 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. - /// private static void NoteIntruder(float dist, float px, float py, float pz) { for (int i = 0; i < _intrN; i++) @@ -400,7 +251,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Image.Texture col0 = channel.TextureManager.RenderTargetColor0; Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil; - // Dimensions HOTES : c'est ce que le rendu occupe vraiment, mise a l'echelle comprise. GAL.ITexture colHost = col0?.HostTexture; GAL.ITexture dsHost = ds?.HostTexture; @@ -413,24 +263,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed dAddr = ds != null ? ds.Range.GetSubRange(0).Address : 0UL; } - /// - /// 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. - /// private static void Record( Ctx[] table, int fmt, @@ -492,8 +324,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Table pleine : compte l'oubli plutot que de le taire. Une sonde qui tronque en - // silence se lit comme une sonde qui a tout vu. _ctxOverflow++; } @@ -507,10 +337,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"max {_qualMax} / moyenne {(_frames > 0 ? (float)_qualSum / _frames : 0f):0.##} sur {_frames} images" + $"{(_ctxOverflow > 0 ? $" · TABLE PLEINE, {_ctxOverflow} contextes non comptes" : "")}"); - // LA LIGNE QUI DECIDE. Si "passe HDR" est proche de 100 % des images 3D ET que la camera - // y est lisible presque toujours, alors deplacer la capture est jouable et c'est mon - // garde-fou qui etait mal regle. Si l'un des deux s'effondre, l'approche est morte -- et - // c'est un resultat, pas un echec. Logger.Info?.Print(LogClass.Gpu, $"MVPP CTXPROBE VERDICT PASSE HDR : {_frames} images 3D · " + $"{_hdrFrames} avec une passe HDR ({(_frames > 0 ? 100f * _hdrFrames / _frames : 0f):0.#} %) · " + @@ -520,18 +346,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"intruses parmi les lisibles : {_hdrIntruders} " + $"({(_hdrReadable > 0 ? 100f * _hdrIntruders / _hdrReadable : 0f):0.##} %)"); - // L'AGE DE LA REFERENCE : le test qui dit si mes "intruses" sont reelles ou si c'est ma - // definition qui les fabrique. Ages comparables => intruses reelles. Age des intruses - // bien plus eleve => une partie du residu est un artefact de la sonde. Logger.Info?.Print(LogClass.Gpu, $"MVPP CTXPROBE AGE DE LA REFERENCE · saines : " + $"{(_ageNClean > 0 ? (float)_ageSumClean / _ageNClean : 0f):0.#} ms de moyenne sur {_ageNClean} · " + $"intruses : {(_ageNIntr > 0 ? (float)_ageSumIntr / _ageNIntr : 0f):0.#} ms sur {_ageNIntr} " + $"(max {_ageMaxIntr} ms)"); - // LE RANG : intruses contre lectures saines. Un ecart franc ici = un correctif generique - // ("ne pas capturer trop tot dans l'image"). Des profils identiques = axe mort, et c'est - // un resultat aussi. Logger.Info?.Print(LogClass.Gpu, $"MVPP CTXPROBE RANG · saines : moyenne " + $"{(_rankNClean > 0 ? (float)_rankSumClean / _rankNClean : 0f):0.#} sur {_rankNClean} " + @@ -546,9 +366,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"4-10 {Pct(_rankHistIntr[2], _rankNIntr)} · 11-50 {Pct(_rankHistIntr[3], _rankNIntr)} · " + $"51+ {Pct(_rankHistIntr[4], _rankNIntr)}]"); - // LA LIGNE QUI PEUT FERMER LE DOSSIER : si toutes les intruses tiennent sur une ou deux - // valeurs, depuis une position fixe, ce sont des evenements DETERMINISTES (transition de - // chargement) et non le symptome qu'on chasse. for (int i = 0; i < _intrN; i++) { Logger.Info?.Print(LogClass.Gpu, diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDlssUpscale.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDlssUpscale.cs index ceb6eaccc..195bc0c54 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDlssUpscale.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDlssUpscale.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -8,58 +5,23 @@ using System; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// [INJECT étape C] GENERIC detection + capture for replacing a game's scene-upscale pass with DLSS. - /// Deliberately game-agnostic: it keys ONLY on graphics-pipeline characteristics -- - /// - exactly one sampled input texture, - /// - that input is an HDR-colour format (source scene colour), - /// - a fullscreen pass (indexed triangle/quad), - /// - depth test off and blend off (the pass replaces, it does not composite), - /// - the render target slot 0 is an HDR-colour format LARGER than the source (a spatial upscale), - /// - coherence with the engine-captured scene colour. - /// NO shader hash, NO GPU address literal, NO fixed resolution, NO game name, NO Zelda-specific branch. - /// It is a SEPARATE class from the read-only (which stays untouched, still - /// used for validation). Gated by RYUJINX_DLSS_UPSCALE_PASS; OFF => early no-op, daily byte-identical. - /// - /// STEP C1: detect the pass, capture the real source/destination host textures and publish them to - /// . The guest draw is NOT skipped and no DLSS runs yet - /// ( = false) -- this only proves the generic detection + cross-layer - /// resource capture. C2 adds the mid-frame DLSS into an intermediate; C3 flips SubstituteActive to write - /// the real destination and skip the guest draw. - /// static class MvppDlssUpscale { - // The GPU-thread decision is only "is this the scene-upscale pass, and is its source scene-coherent?" - // (=> queue the mid-frame inject command). Whether the guest draw is actually SKIPPED is decided on the - // render thread, in command order, gated on the real DLSS evaluate result (see PipelineBase / the - // SubstituteActive switch in DlssUpscaler). This keeps the skip tied to evaluate==OK with no side-channel. private static readonly bool _enabled = MvppInjectState.Enabled; - // RYUJINX_MVPP_INJECT_QUIET=1: silence the per-frame INJECT-C detector log (perf cleanup; render unchanged). private static readonly bool _quiet = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_QUIET") == "1"; private static long _statusMs; private static long _statusSig = -1; - private static int _diagCount; // [DIAG C2b vertical-offset] one-shot capture-side detail counter + private static int _diagCount; - // HDR colour formats a scene-upscale pass realistically produces. Role-based, not a game constant: - // an SDR/8-bit or depth format never qualifies. Extend if a future backend exposes more HDR formats. private static bool IsHdrColor(Format f) => f == Format.R11G11B10Float || f == Format.R16G16B16A16Float; - /// - /// Called at the universal draw choke point. Returns true when this draw IS the scene-upscale pass and - /// its source is scene-coherent => the caller queues the mid-frame inject command (and still queues the - /// guest draw). The actual SKIP of the guest draw is decided on the render thread, gated on the real - /// DLSS evaluate result. Also outputs the frame's own source + destination handles to travel with the - /// command. Purely generic pipeline-shape detection; no shader hash / address literal / resolution / game. - /// public static bool Decide(GpuChannel channel, ref ThreedClassState state, int count, bool indexed, out ITexture src, out ITexture dst, out ITexture depth) { - // The caller passes these straight into the GAL command (captured this frame, carried in order). - // Host dimensions are read from the TextureViews at execution; only the handles travel. src = null; dst = null; depth = null; @@ -69,14 +31,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // Cheap pipeline-shape gate first (skips the vast majority of draws before any enumeration). - bool fullscreen = indexed && (count == 3 || count == 6); // fullscreen triangle or quad + bool fullscreen = indexed && (count == 3 || count == 6); if (!fullscreen || state.DepthTestEnable || state.BlendEnable[0]) { return false; } - // Destination = render target slot 0. ulong dstAddr = 0; Format dstFmt = default; int dw = 0, dh = 0; @@ -95,7 +55,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } }); - // Source = the single sampled input. int texCount = 0; ulong srcAddr = 0; Format srcFmt = default; @@ -116,36 +75,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed }); bool detected = - texCount == 1 && // exactly one sampled texture + texCount == 1 && haveDst && srcHost != null && dstHost != null && IsHdrColor(srcFmt) && IsHdrColor(dstFmt) && - sw > 0 && sh > 0 && dw > sw && dh > sh; // dst strictly larger than src = spatial upscale (relative dims only) + sw > 0 && sh > 0 && dw > sw && dh > sh; if (!detected) { return false; } - // Coherence with the engine-captured scene colour. This is the ALLOWED "coherence with already - // captured buffers" criterion -- a comparison against a dynamically-pinned buffer, NOT a hardcoded - // address. It disambiguates the scene upscale from other fullscreen upscales (e.g. bloom), and - // substitution requires it so the wrong pass is never replaced -- any miss falls back to the draw. ulong sceneAddr = MvppCameraCapture.SceneColorAddress; bool inject = sceneAddr != 0 && srcAddr == sceneAddr; if (inject) { - // Hand the caller THIS frame's own source + destination handles; they travel with the command - // in order. Host dimensions are read from the TextureViews on the render thread (host space), - // so guest Info dims never mix with host dims. Detection above stays in guest space (Info), - // self-consistent. src = srcHost; dst = dstHost; - depth = MvppCameraCapture.SceneDepthHost; // this frame's pinned scene depth (may be null -> fallback) + depth = MvppCameraCapture.SceneDepthHost; - // [DIAG, read-only] Capture-side picture (GPU thread): the exact moment of the detected draw + - // source/destination in BOTH guest (Info) and host (ITexture) spaces + the guest addresses - // (buffer identity / scene-pin coherence / real dst RT address). Correlate with the replay line. if (_diagCount < 4) { _diagCount++; @@ -154,11 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"srcGuest={sw}x{sh} srcHost={srcHost.Width}x{srcHost.Height} srcAddr=0x{srcAddr:X} sceneAddr=0x{sceneAddr:X} " + $"dstGuest={dw}x{dh} dstHost={dstHost.Width}x{dstHost.Height} dstAddr=0x{dstAddr:X} dstFmt={dstFmt}"); - // [C4 Étape 1, read-only] Identify + log the scene DEPTH candidate the engine already pins - // (MvppCameraCapture, scaled main pass, non-square, DEPTHFIX precision tie-break). Address / - // format / host dims + timing vs the scene colour (same DlssJitterState.FrameId = aligned). - // Nothing is wired to DLSS yet -- Evaluate still uses the zero depth. depthMatchesSrcHost - // confirms it is at the same host resolution as the DLSS colour input. Logger.Info?.Print(LogClass.Gpu, $"MVPP-INJECT-DEPTH candidate: depthAddr=0x{MvppCameraCapture.SceneDepthAddress:X} " + $"depthFmt={MvppCameraCapture.SceneDepthFormat} " + diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDofSkip.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDofSkip.cs index 2d5ae2f9d..8b17cb898 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDofSkip.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDofSkip.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gpu.Image; @@ -8,44 +5,8 @@ using System; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// [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. - /// static class MvppDofSkip { - /// - /// 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. - /// public static readonly int Mode = ParseMode(); public static bool Enabled => Mode != 0; @@ -60,13 +21,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed }; } - // The pass renders at 512x288 when the game renders at 1280x720 - that is 40% of the - // render width, in 16:9. Matching those PROPORTIONS instead of an absolute pixel size is - // what makes the option survive every scaling path: DLSS multiplies the whole frame - // (quality made 512x288 come through as 768x432, x1.5, which an absolute match missed - // entirely) and ResScale multiplies it again, neither of them reported by - // RenderTargetScale alone. - private const float WidthRatio = 0.4f; // 512 / 1280 + private const float WidthRatio = 0.4f; private const float AspectRatio = 16f / 9f; private const float Tolerance = 0.02f; @@ -75,19 +30,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static long _lastLogMs; private static int _renderWidth; - // Census of the DISTINCT target shapes this rule matches, and of every render-width change. - // Both are silent in mode 1 beyond the first announcement; mode 2 reports them. A second - // distinct shape appearing only during cutscenes IS the false positive we are hunting: the - // rule cannot tell it apart from the bokeh pass, so it skips a legitimate draw and leaves - // its surface untouched. private const int CensusCap = 16; private static readonly long[] _shapes = new long[CensusCap]; private static int _shapeCount; private static readonly object _censusLock = new(); - /// - /// True when this draw is the scatter/bokeh accumulation and should not be issued. - /// public static bool ShouldSkip(GpuChannel channel) { if (!Enabled || channel == null) @@ -114,15 +61,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int rawW = target.Info.Width; int rawH = target.Info.Height; - // Track the widest colour target seen: that is the frame's render width, whatever - // DLSS and ResScale multiplied it by. Everything else is judged relative to it. if (rawW > _renderWidth) { int previous = _renderWidth; _renderWidth = rawW; - // The reference only ever grows, so a wider target appearing in a cutscene silently - // moves the 40% goalpost for every later frame. Worth seeing, not just inferring. if (Mode == 2 && previous != 0) { Logger.Warning?.Print(LogClass.Gpu, @@ -147,9 +90,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _skipped++; - // Report every DISTINCT shape the rule claims, not just the first one. The original - // code announced once and stayed silent forever, so a second matching shape appearing - // later in the session - exactly the cutscene case - was invisible in the log. ReportShape(rawW, rawH, widthRatio); if (!_announced) @@ -170,15 +110,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"[DOFSKIP{(Mode == 2 ? "/DRY" : "")}] draws matched so far: {_skipped}"); } - // Dry run: everything above is measurement, nothing is skipped. The frame renders exactly - // as it would with the option off, so the eye verdict and the census come from one run. return Mode != 2; } - /// - /// 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. - /// private static void ReportShape(int width, int height, float widthRatio) { long key = ((long)width << 32) | (uint)height; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDrawStepProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDrawStepProbe.cs index 51e0d69d1..13933bd6d 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDrawStepProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppDrawStepProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,70 +5,29 @@ using System.IO; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppDrawStepProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP") == "1"; - /// Secondes avant de capturer l'image (RYUJINX_DRAWSTEP_START, défaut 45). private static readonly int _startSeconds = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_START"), out int st) && st > 0 ? st : 45; - /// Un cliché tous les N draws (RYUJINX_DRAWSTEP_EVERY, défaut 50). private static readonly int _everyDraws = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_EVERY"), out int ev) && ev > 0 ? ev : 50; - /// - /// 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. - /// private static readonly int _fromDraw = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FROM"), out int fdr) && fdr > 0 ? fdr : 0; - /// Plafond de clichés PAR image (RYUJINX_DRAWSTEP_MAX, défaut 25). private static readonly int _maxSteps = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_MAX"), out int mx) && mx > 0 ? mx : 25; - /// - /// 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. - /// private static readonly int _frames = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_FRAMES"), out int fr) && fr > 0 ? fr : 5; - /// Pause entre deux images instrumentées (RYUJINX_DRAWSTEP_GAP, défaut 3000 ms). private static readonly int _gapMs = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_GAP"), out int gp) && gp > 0 ? gp : 3000; - /// - /// 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. - /// private static readonly int _vkey = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DRAWSTEP_VKEY"), out int vk) && vk > 0 ? vk : 0x79; @@ -106,10 +62,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _stepNo; private static string _dir; - /// - /// 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. - /// public static void OnPresent(Image.Texture presented) { if (!_enabled || _done) @@ -137,9 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_capturing) { - // L'image instrumentée vient de se terminer. On écrit d'abord CE QUE LE JOUEUR VOIT : - // sans ça la séquence s'arrête sur un buffer HDR intermédiaire, qu'Alex ne peut pas - // reconnaître -- c'est ce qui a rendu les 5 premières séquences inexploitables. if (presented?.HostTexture != null) { try @@ -173,7 +122,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Anti-rebond seulement : la décision appartient entièrement au joueur. if (now < _nextMs || !TriggerHeld()) { return; @@ -196,16 +144,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// 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. - /// public static void OnDispatch(Image.TextureManager tm, int gridX, int gridY, int gridZ, ulong shaderVa) { if (!_enabled || !_capturing || _stepNo >= _maxSteps || tm == null) @@ -217,8 +155,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { int idx = 0; - // Compteurs VISIBLES : sans eux, "aucune capture" ne distingue pas "aucun dispatch" de - // "dispatch sans image en ecriture". C'est la regle du projet, et je l'ai deja violee une fois. _csDispatches++; tm.MvppEnumerateComputeBindings((isImage, isStore, tex) => @@ -278,9 +214,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // [21/07] TOUS les slots couleur, plus seulement le 0 : la texture PRÉSENTÉE est un - // R8G8B8A8 alors que les derniers draws visaient le HDR R11G11B10 en slot 0. En ne - // regardant que le slot 0 on capturait la mauvaise cible et on croyait la scène propre. int localDraw = _drawNo; channel.TextureManager.MvppEnumerateRenderTargets((slot, rt) => @@ -300,12 +233,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Logger.Info?.Print(LogClass.Gpu, $"MVPP DRAWSTEP: {name} ({bytes.Length} octets)."); }); - // [DRAWSTEP-INPUTS 21/07] Dump aussi CE QUE CE DRAW ECHANTILLONNE. Trancher hérité vs né : - // si une sortie capturée est striée ALORS QUE ses entrées le sont déjà -> corruption - // HERITEE (remonter à qui a écrit cette entrée). Si les entrées sont propres et la sortie - // striée -> corruption NEE à ce draw (shader / mise en place de la cible). C'est la - // bifurcation que la capture par-slot du 21/07 ne pouvait pas résoudre : elle ne voyait - // que les sorties. channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) => { if (tex?.HostTexture == null || _stepNo >= _maxSteps) @@ -330,11 +257,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// 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. - /// public static void OnDrawPost(GpuChannel channel) { if (!_enabled || !_capturing || _stepNo >= _maxSteps) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppExtProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppExtProbe.cs new file mode 100644 index 000000000..fead75953 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppExtProbe.cs @@ -0,0 +1,216 @@ + +using Ryujinx.Common.Logging; +using System; +using System.Numerics; + +namespace Ryujinx.Graphics.Gpu.Engine.Threed +{ + static class MvppExtProbe + { + public static readonly bool Enabled = + Environment.GetEnvironmentVariable("RYUJINX_MVPP_EXTPROBE") == "1"; + + private const float FloorPos = 1f; + private const float FloorRot = 0.01f; + private const float CeilPos = 10f; + private const float CeilRot = 0.5f; + private const int MaxLinesPerSecond = 12; + + private static Matrix4x4 _m1, _m2; + private static bool _has1, _has2Consec, _lastWasFresh; + private static int _chain; + private static int _lastTpSeq = int.MinValue; + private static int _tpAge = 99; + + private static bool _pending; + private static Matrix4x4 _pred, _held; + private static float _pStepPos, _pStepRot; + private static bool _pTpNear; + + private static long _wLogMs; + private static int _wHolds, _wQualifies, _wResolus, _wSousPlancher, _wSurPlafond, + _wSansPaire, _wEnchaines, _wTp, _wInvRatees, _wChaines2; + + private static long _lineWindowMs; + private static int _lineCount; + + public static void OnPresent(bool fresh, in Matrix4x4 m, int teleportSeq) + { + if (teleportSeq != _lastTpSeq) + { + _lastTpSeq = teleportSeq; + _tpAge = 0; + } + else if (_tpAge < 99) + { + _tpAge++; + } + + if (fresh) + { + if (_pending) + { + Resolve(in m, _chain); + _pending = false; + } + + _has2Consec = _has1 && _lastWasFresh; + _m2 = _m1; + _m1 = m; + _has1 = true; + _lastWasFresh = true; + _chain = 0; + } + else + { + _chain++; + _lastWasFresh = false; + _wHolds++; + + if (_chain == 2) + { + _wChaines2++; + } + + if (_chain == 1) + { + Qualify(); + } + else + { + _wEnchaines++; + } + } + + long now = Environment.TickCount64; + + if (now - _wLogMs >= 5000) + { + if (_wLogMs != 0 && _wHolds > 0) + { + Logger.Info?.Print(LogClass.Gpu, + $"MVPP extpred fenetre: holds={_wHolds} qualifies={_wQualifies} " + + $"resolus={_wResolus} sous-plancher={_wSousPlancher} " + + $"sur-plafond={_wSurPlafond} sans-paire={_wSansPaire} " + + $"enchaines={_wEnchaines} tp={_wTp} inv-ratees={_wInvRatees} " + + $"chaines2+={_wChaines2}"); + } + + _wLogMs = now; + _wHolds = _wQualifies = _wResolus = _wSousPlancher = _wSurPlafond = 0; + _wSansPaire = _wEnchaines = _wTp = _wInvRatees = _wChaines2 = 0; + } + } + + private static void Qualify() + { + if (!_has1 || !_has2Consec) + { + _wSansPaire++; + + return; + } + + if (_tpAge <= 2) + { + _wTp++; + + return; + } + + float stepRot = RotDelta(in _m1, in _m2); + float stepPos = -1f; + + if (MvppFamHold.PosFromVp(in _m1, out Vector3 p1) && + MvppFamHold.PosFromVp(in _m2, out Vector3 p2)) + { + stepPos = (p1 - p2).Length(); + } + + if ((stepPos < 0f || stepPos <= FloorPos) && stepRot <= FloorRot) + { + _wSousPlancher++; + + return; + } + + if (stepPos > CeilPos || stepRot > CeilRot) + { + _wSurPlafond++; + + return; + } + + if (!Matrix4x4.Invert(_m2, out Matrix4x4 inv2)) + { + _wInvRatees++; + + return; + } + + _pred = Matrix4x4.Multiply(Matrix4x4.Multiply(_m1, inv2), _m1); + _held = _m1; + _pStepPos = stepPos; + _pStepRot = stepRot; + _pTpNear = false; + _pending = true; + _wQualifies++; + } + + private static void Resolve(in Matrix4x4 real, int chainLen) + { + _wResolus++; + + float errErot = RotDelta(in _pred, in real); + float errArot = RotDelta(in _held, in real); + + string errEpos = "?", errApos = "?"; + + if (MvppFamHold.PosFromVp(in real, out Vector3 pr)) + { + if (MvppFamHold.PosFromVp(in _pred, out Vector3 pe)) + { + errEpos = (pe - pr).Length().ToString("0.###"); + } + + if (MvppFamHold.PosFromVp(in _held, out Vector3 pa)) + { + errApos = (pa - pr).Length().ToString("0.###"); + } + } + + long now = Environment.TickCount64; + + if (now - _lineWindowMs >= 1000) + { + _lineWindowMs = now; + _lineCount = 0; + } + + if (++_lineCount <= MaxLinesPerSecond) + { + Logger.Info?.Print(LogClass.Gpu, + $"MVPP extpred: errE-pos={errEpos} errE-rot={errErot:0.####} " + + $"errA-pos={errApos} errA-rot={errArot:0.####} " + + $"step-pos={(_pStepPos >= 0f ? _pStepPos.ToString("0.###") : "?")} " + + $"step-rot={_pStepRot:0.####} chaine={chainLen} tp={(_pTpNear ? "oui" : "non")}"); + } + } + + private static float RotDelta(in Matrix4x4 a, in Matrix4x4 b) + { + float d = 0f; + d = MathF.Max(d, MathF.Abs(a.M11 - b.M11)); + d = MathF.Max(d, MathF.Abs(a.M12 - b.M12)); + d = MathF.Max(d, MathF.Abs(a.M13 - b.M13)); + d = MathF.Max(d, MathF.Abs(a.M21 - b.M21)); + d = MathF.Max(d, MathF.Abs(a.M22 - b.M22)); + d = MathF.Max(d, MathF.Abs(a.M23 - b.M23)); + d = MathF.Max(d, MathF.Abs(a.M31 - b.M31)); + d = MathF.Max(d, MathF.Abs(a.M32 - b.M32)); + d = MathF.Max(d, MathF.Abs(a.M33 - b.M33)); + + return d; + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFamHold.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFamHold.cs new file mode 100644 index 000000000..183a63b44 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFamHold.cs @@ -0,0 +1,312 @@ + +using Ryujinx.Common.Logging; +using System; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Gpu.Engine.Threed +{ + static class MvppFamHold + { + public static readonly bool Enabled = + Environment.GetEnvironmentVariable("RYUJINX_MVPP_FAMHOLD") == "1"; + + private const int MaxBytesPerBuffer = 4096; + private const int SampleStride = 8; + private const int MaxLinesPerSecond = 12; + + private static readonly (string Nom, int Stage, int Slot, int Offset, bool T)[] _familles = + { + ("s4c3+000", 4, 3, 0x000, false), + ("s0c6+000", 0, 6, 0x000, false), + ("s0c6+040", 0, 6, 0x040, false), + ("s0c4+000T", 0, 4, 0x000, true), + ("s0c3+040", 0, 3, 0x040, false), + }; + + private static readonly object _lock = new(); + private static readonly Matrix4x4[] _cur = new Matrix4x4[5]; + private static readonly bool[] _curSampled = new bool[5]; + private static readonly Matrix4x4[] _prev = new Matrix4x4[5]; + private static readonly int[] _prevAge = new int[5]; + + private static int _drawTick; + private static int _pushed; + + private static long _wLogMs; + private static int _wHolds, _wFrais, _wRien, _wSansVerif, _wSupprimees; + private static readonly int[] _wParFamille = new int[5]; + + private static long _lineWindowMs; + private static int _lineCount; + + public static void NotePush() + { + System.Threading.Interlocked.Exchange(ref _pushed, 1); + } + + public static void Sample(GpuChannel channel) + { + if (_drawTick++ % SampleStride != 0) + { + return; + } + + for (int i = 0; i < _familles.Length; i++) + { + if (TryReadFamille(channel, i, out Matrix4x4 m)) + { + lock (_lock) + { + _cur[i] = m; + _curSampled[i] = true; + } + } + } + } + + public static void OnPresent(bool fresh, in Matrix4x4 publiee) + { + bool pushed = System.Threading.Interlocked.Exchange(ref _pushed, 0) == 1; + + lock (_lock) + { + if (!fresh && !pushed) + { + Evaluate(in publiee); + } + + for (int i = 0; i < _familles.Length; i++) + { + if (_curSampled[i]) + { + _prev[i] = _cur[i]; + _prevAge[i] = 1; + _curSampled[i] = false; + } + else if (_prevAge[i] > 0) + { + _prevAge[i]++; + } + } + + long now = Environment.TickCount64; + + if (now - _wLogMs >= 5000) + { + if (_wLogMs != 0 && (_wHolds > 0 || _wSupprimees > 0)) + { + Logger.Info?.Print(LogClass.Gpu, + $"MVPP famhold fenetre: holds={_wHolds} frais-ailleurs={_wFrais} " + + $"rien={_wRien} sans-verif={_wSansVerif} supprimees={_wSupprimees} | " + + $"{_familles[0].Nom}={_wParFamille[0]} {_familles[1].Nom}={_wParFamille[1]} " + + $"{_familles[2].Nom}={_wParFamille[2]} {_familles[3].Nom}={_wParFamille[3]} " + + $"{_familles[4].Nom}={_wParFamille[4]}"); + } + + _wLogMs = now; + _wHolds = _wFrais = _wRien = _wSansVerif = _wSupprimees = 0; + Array.Clear(_wParFamille, 0, _wParFamille.Length); + } + } + } + + private static void Evaluate(in Matrix4x4 publiee) + { + _wHolds++; + + int verifies = 0; + int fraisIdx = -1; + + for (int i = 0; i < _familles.Length; i++) + { + if (!_curSampled[i] || _prevAge[i] != 1) + { + continue; + } + + verifies++; + + if (!_cur[i].Equals(_prev[i]) && LooksLikeVp(in _cur[i])) + { + _wParFamille[i]++; + + if (fraisIdx < 0) + { + fraisIdx = i; + } + } + } + + string ligne; + + if (verifies == 0) + { + _wSansVerif++; + ligne = $"MVPP famhold: HOLD verifies=0/{_familles.Length} (aucune famille lisible cette image)"; + } + else if (fraisIdx >= 0) + { + _wFrais++; + + string dist = "?"; + + if (PosFromVp(in _cur[fraisIdx], out Vector3 pf) && PosFromVp(in publiee, out Vector3 pp)) + { + dist = (pf - pp).Length().ToString("0.##"); + } + + ligne = $"MVPP famhold: HOLD frais=oui famille={_familles[fraisIdx].Nom} " + + $"dist={dist} verifies={verifies}/{_familles.Length}"; + } + else + { + _wRien++; + ligne = $"MVPP famhold: HOLD frais=non verifies={verifies}/{_familles.Length}"; + } + + long now = Environment.TickCount64; + + if (now - _lineWindowMs >= 1000) + { + _lineWindowMs = now; + _lineCount = 0; + } + + if (++_lineCount <= MaxLinesPerSecond) + { + Logger.Info?.Print(LogClass.Gpu, ligne); + } + else + { + _wSupprimees++; + } + } + + private static bool TryReadFamille(GpuChannel channel, int i, out Matrix4x4 m) + { + m = default; + + (_, int stage, int slot, int offset, bool t) = _familles[i]; + + ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot); + int size = Math.Min(channel.BufferManager.GetGraphicsUniformBufferSize(stage, slot), MaxBytesPerBuffer); + + if (address == 0 || address == ulong.MaxValue || size < offset + 64) + { + return false; + } + + ReadOnlySpan data; + + try + { + data = MemoryMarshal.Cast(channel.MemoryManager.Physical.GetSpan(address, size)); + } + catch + { + return false; + } + + int idx = offset / 4; + + if (idx + 16 > data.Length) + { + return false; + } + + ReadOnlySpan r = data.Slice(idx, 16); + + for (int k = 0; k < 16; k++) + { + if (!float.IsFinite(r[k])) + { + return false; + } + } + + m = t + ? new Matrix4x4(r[0], r[4], r[8], r[12], + r[1], r[5], r[9], r[13], + r[2], r[6], r[10], r[14], + r[3], r[7], r[11], r[15]) + : new Matrix4x4(r[0], r[1], r[2], r[3], + r[4], r[5], r[6], r[7], + r[8], r[9], r[10], r[11], + r[12], r[13], r[14], r[15]); + + return true; + } + + private static bool LooksLikeVp(in Matrix4x4 m) + { + Vector3 r0 = new(m.M11, m.M12, m.M13); + Vector3 r1 = new(m.M21, m.M22, m.M23); + Vector3 r2 = new(m.M31, m.M32, m.M33); + Vector3 r3 = new(m.M41, m.M42, m.M43); + + float n3 = r3.Length(); + + if (n3 < 0.9f || n3 > 1.1f) + { + return false; + } + + float n0 = r0.Length(), n1 = r1.Length(), n2 = r2.Length(); + + if (n0 < 1e-6f || n1 < 1e-6f || n2 < 1e-6f) + { + return false; + } + + if (MathF.Abs(Vector3.Dot(r0, r1)) / (n0 * n1) > 0.05f || + MathF.Abs(Vector3.Dot(r0, r3)) / n0 > 0.05f || + MathF.Abs(Vector3.Dot(r1, r3)) / n1 > 0.05f) + { + return false; + } + + if (Vector3.Cross(r2, r3).Length() / n2 > 0.05f) + { + return false; + } + + return true; + } + + internal static bool PosFromVp(in Matrix4x4 m, out Vector3 p) + { + p = default; + + float a0 = m.M11, b0 = m.M12, c0 = m.M13, d0 = m.M14; + float a1 = m.M21, b1 = m.M22, c1 = m.M23, d1 = m.M24; + float a3 = m.M41, b3 = m.M42, c3 = m.M43, d3 = m.M44; + + float det = a0 * (b1 * c3 - c1 * b3) - b0 * (a1 * c3 - c1 * a3) + c0 * (a1 * b3 - b1 * a3); + + if (MathF.Abs(det) < 1e-12f) + { + return false; + } + + float dx = -d0 * (b1 * c3 - c1 * b3) - b0 * (-d1 * c3 + c1 * d3) + c0 * (-d1 * b3 + b1 * d3); + float dy = a0 * (-d1 * c3 + c1 * d3) + d0 * (a1 * c3 - c1 * a3) + c0 * (-a1 * d3 + d1 * a3); + float dz = a0 * (-b1 * d3 + d1 * b3) - b0 * (-a1 * d3 + d1 * a3) - d0 * (a1 * b3 - b1 * a3); + + p = new Vector3(dx / det, dy / det, dz / det); + + float scale = 1f + p.Length(); + float rw = MathF.Abs(a3 * p.X + b3 * p.Y + c3 * p.Z + d3); + float rx = MathF.Abs(a0 * p.X + b0 * p.Y + c0 * p.Z + d0); + + if (rw > 1e-2f * scale || rx > 1e-1f * scale || !float.IsFinite(p.X + p.Y + p.Z)) + { + p = default; + + return false; + } + + return true; + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFinalDrawProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFinalDrawProbe.cs index 69fc40bc2..eafb8944c 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFinalDrawProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppFinalDrawProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,28 +6,17 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ composite-chain tracer (RYUJINX_MVPP_FINALPROBE=1). READ-ONLY, default off. Anchored on the - /// destination: the presented framebuffer address comes from MvppDestProbe. Hooked from DrawManager's - /// universal DrawImpl choke point (catches every draw path, unlike DrawEnd alone). For any draw whose - /// colour RT is a TRACED buffer -- the presented final, or an intermediate we are climbing towards -- - /// it logs the draw's shader, RT, depth and ALL sampled inputs, and AUTOMATICALLY adds every HDR input - /// to the trace set so the producer of that intermediate is caught next. This walks the chain - /// final <- composite <- (HDR intermediate) <- ... back to the native scene, naming the DLSS injection - /// point (the pass that first reads the native scene + its depth). - /// static class MvppFinalDrawProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_FINALPROBE") == "1"; - private const int MaxPerRt = 40; // log many draws into each traced RT (the scene reader may be late) - private const int MaxTargets = 24; // bound the climb + private const int MaxPerRt = 40; + private const int MaxTargets = 24; - private static readonly HashSet _targets = new(); // intermediate RT addresses being climbed + private static readonly HashSet _targets = new(); private static readonly Dictionary _loggedPerRt = new(); - // Kept: DrawTexture never fired for TotK, but the hook is harmless and covers other titles. private static int _texCount; private const int MaxTexLogged = 40; @@ -96,7 +82,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state) { - // Is this draw writing a traced buffer? (the presented final, or an intermediate we are climbing.) ulong rtAddr = 0; int rtSlot = -1; Format rtFmt = default; @@ -130,7 +115,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Per-RT budget so each stage of the chain logs a few times, no flood. _loggedPerRt.TryGetValue(rtAddr, out int n); if (n >= MaxPerRt) { @@ -163,8 +147,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Logger.Info?.Print(LogClass.Gpu, $"MVPP-CHAIN IN @0x{a:X} {f} {w}x{h}{(hdr ? " [HDR]" : "")}{(isScene ? " [=SCENE-PIN]" : "")}"); - // Climb: trace the producer of every HDR input we have not seen yet -- both as a DrawImpl RT - // target (3D producers) AND via the destination-anchored probe (catches compute/DMA/2D too). if (hdr && !isScene) { if (_targets.Count < MaxTargets) @@ -175,8 +157,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } }); - // Storage images (imageLoad/Store) -- the blind spot: a fullscreen upscale may read its source - // this way, not as a sampled texture. channel.TextureManager.MvppEnumerateGraphicsImages((tex, isStore) => { ulong a = tex.Range.GetSubRange(0).Address; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppGlowProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppGlowProbe.cs index d66e7c599..2737d63d2 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppGlowProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppGlowProbe.cs @@ -1,5 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. using Ryujinx.Common.Logging; using System; @@ -7,28 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppGlowProbe { private static bool _enabled = @@ -60,8 +36,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state) { - // Only the HDR float target the halos live in. Identified from the dumps, by FORMAT and - // shape rather than by any address or game-specific value. Image.Texture c0 = channel.TextureManager.RenderTargetColor0; if (c0 == null || !c0.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal)) @@ -83,8 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _scissored++; - // Round to 8 px so the same box seen over consecutive frames aggregates instead of - // producing a line per pixel of camera drift. string key = $"{sc.X1 / 8 * 8},{sc.Y1 / 8 * 8} -> {sc.X2 / 8 * 8},{sc.Y2 / 8 * 8} " + $"({(sc.X2 - sc.X1) / 8 * 8}x{(sc.Y2 - sc.Y1) / 8 * 8})"; @@ -92,14 +64,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _boxes[key] = n + 1; } - // The scissor answered "no" (0 boxes over the whole run), so the rectangles are the - // glow QUADS themselves -- which is normal geometry. What is NOT normal, proven - // against a TOTK control where the same buffer shows zero hard edges, is that the halo - // is still bright AT the quad border instead of having faded to nothing. So look at - // what these draws READ: the halo texture and, above all, its ADDRESSING MODE, which - // is exactly what decides the value returned past the texture edge. ClampToEdge repeats - // the last texel for ever (a bright rim stays bright); ClampToBorder returns the border - // colour (normally transparent black, which fades correctly). channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) => { if (tex == null || smp == null || _inputs.Count >= 24) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHangWatch.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHangWatch.cs index edfed778e..0f07b6ecc 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHangWatch.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHangWatch.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,52 +5,13 @@ using System.Threading; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// [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 (, appelé depuis OnFrameEnqueued) ; - /// - le compteur de DESSINS de . - /// 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. - /// static class MvppHangWatch { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_HANGWATCH") == "1"; - /// - /// 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. - /// private const long StallMs = 3000; - /// Rappel pendant que le gel dure, pour voir si les dessins avancent ou non. private const long RepeatMs = 5000; private static long _lastPresentMs; @@ -63,10 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _drawsAtStall; private static bool _inStall; - /// - /// 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. - /// public static void Ping() { Volatile.Write(ref _lastPresentMs, Environment.TickCount64); @@ -111,7 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // Le chiffre qui tranche : les dessins avancent-ils PENDANT le gel ? int draws = MvppCameraCapture.StatScaledDraws; if (!_inStall) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHazardProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHazardProbe.cs index 1642d3e81..b1d9182ee 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHazardProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHazardProbe.cs @@ -1,5 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. using Ryujinx.Common.Logging; using System; @@ -7,31 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppHazardProbe { private static bool _enabled = @@ -55,7 +28,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Never take the process down from a diagnostic on the GPU thread. try { OnDrawImpl(channel); @@ -107,8 +79,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed hitThisDraw = true; - // Distinct shapes, not one line per draw: the same few buffers repeat - // thousands of times a second and what matters is WHICH ones. string key = $"{tex.Info.Width}x{tex.Info.Height} {tex.Info.FormatInfo.Format} " + $"{tex.Info.Target} @{addr:X10} (rt{i}, stage{stage})"; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHdrCensusProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHdrCensusProbe.cs index f569f0960..c04b4f472 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHdrCensusProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppHdrCensusProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -10,15 +7,6 @@ using System.Numerics; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ HDR-1600 producer census (RYUJINX_MVPP_CENSUS=1). READ-ONLY, default off. Hooked from the - /// universal DrawImpl choke point. For EVERY draw whose colour RT is the pre-tonemap HDR buffer - /// (R11G11B10Float, width >= 1500), it censuses the passes by (vertex, fragment) shader pair and, on - /// first sight of each pair, logs the full geometry/pipeline/data-source metrics and an automatic - /// A/B/C classification. Answers definitively whether the HDR 1600 is: A) the real scene rendered at - /// 1600 (many material shaders, heavy geometry, depth), B) a fullscreen upscale/post that samples an - /// HDR source, or C) a fullscreen pass fed by buffers/bindless (no visible texture). - /// static class MvppHdrCensusProbe { private static bool _enabled = @@ -50,7 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static void OnDrawImpl(GpuChannel channel, ref ThreedClassState state, int count, bool indexed) { - // Target: the pre-tonemap HDR buffer -- a large R11G11B10Float colour RT. ulong rtAddr = 0; int rtW = 0, rtH = 0; bool isHdrTarget = false; @@ -90,17 +77,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } _pairLogged.Add(key); - // Geometry int vtx = indexed ? -1 : count; int idx = indexed ? count : 0; - // Pipeline bool depthT = state.DepthTestEnable; bool depthW = state.DepthWriteEnable; bool blend = state.BlendEnable[0]; int vpW = (int)MathF.Round(MathF.Abs(state.ViewportTransform[0].ScaleX) * 2f); - // Vertex buffers int vbCount = 0; for (int i = 0; i < 16; i++) { @@ -110,7 +94,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Data sources int texCount = 0; channel.TextureManager.MvppEnumerateGraphicsInputs(_ => texCount++); int imgCount = 0; @@ -131,7 +114,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Classification string cls; bool geomLike = depthW && (idx > 100 || vtx > 100) && vbCount > 0; bool fullscreen = (vtx >= 3 && vtx <= 6) || (idx >= 3 && idx <= 6); @@ -155,8 +137,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"vtx={vtx} idx={idx} vb={vbCount} vpW={vpW} depthT={depthT} depthW={depthW} blend={blend} | " + $"tex={texCount} arr={arrCount} img={imgCount} ubuf={ubufCount} ubufMax={ubufMaxSize} => {cls}"); - // The decisive data: the ADDRESS of every sampled input, so we can name the pass that reads the - // native scene (0x65799C4000 class) and writes this HDR 1600 = the DLSS injection point. ulong sceneAddr = MvppCameraCapture.SceneColorAddress; bool readsLowerResHdr = false; channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) => @@ -166,7 +146,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int w = tex.Info.Width, h = tex.Info.Height; bool hdr = f == Format.R11G11B10Float || f == Format.R16G16B16A16Float; bool isScenePin = sceneAddr != 0 && a == sceneAddr; - bool lowerResHdr = hdr && w < 1500; // an HDR source SMALLER than the 1600 output = candidate scene + bool lowerResHdr = hdr && w < 1500; readsLowerResHdr |= lowerResHdr || isScenePin; Logger.Info?.Print(LogClass.Gpu, diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppInjectGate.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppInjectGate.cs index 5781beeff..9d5e54845 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppInjectGate.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppInjectGate.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,25 +6,13 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ DLSS-injection GATE validation (RYUJINX_MVPP_INJECTGATE=1). STRICTLY READ-ONLY, default off. - /// STEP A of the injection plan: prove that a robust multi-criteria gate matches ONLY the identified - /// scene-upscale pass (scene 1333 HDR -> HDR 1600), with zero false positives and ZERO change to the - /// rendered image. Nothing is cancelled, no DLSS is called, no texture/barrier is touched. The FS shader - /// id is a SECONDARY signal only; the decisive criteria are "reads exactly the captured SceneColor" and - /// "writes the HDR 1600 RT". Also tracks near-misses (draws that read the scene but fail the gate) to - /// prove there is no source/destination ambiguity. - /// static class MvppInjectGate { - // Verbose = the Étape A read-only validation run (CANDIDATE/SUMMARY/near-miss spam). The gate also - // runs -- silently -- under the Étape B inject flag, where its only job is to publish the identified - // upscale destination dims to MvppInjectState so the Vulkan present can size the intermediate target. private static readonly bool _verbose = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECTGATE") == "1"; private static bool _enabled = _verbose || MvppInjectState.Enabled; - private const ulong TargetFs = 0x559E30; // secondary confirmation only + private const ulong TargetFs = 0x559E30; private const int MaxFullLogged = 40; private static long _total; @@ -37,9 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static long _summaryMs; private static readonly HashSet<(ulong, ulong)> _candidatePairs = new(); - // RELAXED gate = the 5 stable criteria WITHOUT the fragile "src == flapping SceneColor pin" (c2). If - // this stays 1 distinct pair, it is both specific AND complete (fires every frame), and is the gate - // to use. Its distinct pairs are logged so we can see if dropping c2 introduces any ambiguity. private static long _relaxedCandidates; private static readonly HashSet<(ulong, ulong)> _relaxedPairs = new(); @@ -65,12 +47,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _total++; - // Cheap pipeline-signature criteria first. bool idx3 = indexed && count == 3; bool depthOff = !state.DepthTestEnable; bool blendOff = !state.BlendEnable[0]; - // Destination RT slot 0. ulong dstAddr = 0; Format dstFmt = default; int dw = 0, dh = 0; @@ -87,7 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } }); - // Sampled input textures (count + first). int texCount = 0; ulong srcAddr = 0; Format srcFmt = default; @@ -106,16 +85,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ulong sceneAddr = MvppCameraCapture.SceneColorAddress; - // The six gate criteria. - bool c1 = texCount == 1; // exactly one sampled texture - bool c2 = sceneAddr != 0 && srcAddr == sceneAddr; // source == captured SceneColor - bool c3 = srcFmt == Format.R11G11B10Float; // source format - bool c4 = haveDst && dstFmt == Format.R11G11B10Float && dw >= 1500 && dw <= 1700; // dest == HDR 1600 - bool c5 = sw > 0 && dw > 0 && sw < dw; // coherent dims (upscale) - bool c6 = idx3 && depthOff && blendOff; // fullscreen triangle, no depth/blend + bool c1 = texCount == 1; + bool c2 = sceneAddr != 0 && srcAddr == sceneAddr; + bool c3 = srcFmt == Format.R11G11B10Float; + bool c4 = haveDst && dstFmt == Format.R11G11B10Float && dw >= 1500 && dw <= 1700; + bool c5 = sw > 0 && dw > 0 && sw < dw; + bool c6 = idx3 && depthOff && blendOff; bool gate = c1 && c2 && c3 && c4 && c5 && c6; - bool relaxed = c1 && c3 && c4 && c5 && c6; // no c2 (drops the flapping SceneColor pin match) + bool relaxed = c1 && c3 && c4 && c5 && c6; ulong vs = (ulong)state.ShaderState[1].Offset; ulong fs = (ulong)state.ShaderState[5].Offset; @@ -141,7 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else if (c2) { - // Reads the captured scene but is NOT the upscale gate -- proves specificity (should be rare/zero). _nearMiss++; if (_verbose && _nearMiss <= 30) { @@ -161,8 +138,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"strict(candidates={_candidates} pairs={_candidatePairs.Count}) " + $"RELAXED(candidates={_relaxedCandidates} pairs={_relaxedPairs.Count}) nearMiss={_nearMiss}"); - // If the relaxed gate found extra shader-pairs beyond the upscale, name them (would be - // false positives if we dropped c2). if (_relaxedPairs.Count > 1) { foreach ((ulong pvs, ulong pfs) in _relaxedPairs) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLayoutProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLayoutProbe.cs index a395be0b1..e97b5e97b 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLayoutProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLayoutProbe.cs @@ -1,23 +1,9 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppLayoutProbe { private static bool _enabled = diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLdrSkip.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLdrSkip.cs index 7baf78815..08d0c9eb5 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLdrSkip.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppLdrSkip.cs @@ -1,127 +1,25 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// [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 . 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. - /// static class MvppLdrSkip { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR") == "1"; - /// - /// [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. - /// public static readonly bool SkipRank1 = Environment.GetEnvironmentVariable("RYUJINX_MVPP_NORANK1") == "1"; - /// - /// 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. - /// public static readonly bool AbMode = Environment.GetEnvironmentVariable("RYUJINX_MVPP_NOLDR_AB") == "1"; - /// - /// [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`.) - /// public static readonly bool MissWhy = Environment.GetEnvironmentVariable("RYUJINX_MVPP_MISSWHY") == "1"; - /// Le crochet de capture consulte ceci, pas les interrupteurs un par un. public static bool Active => Enabled || SkipRank1 || AbMode; - /// - /// 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). - /// private const int WarmCaptures = 200; private const int CalibrationFrames = 600; @@ -129,19 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private const float MarginPoints = 0.10f; private const int ReportMs = 5000; - /// - /// [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. - /// private const int SuspendFrames = 3000; private const int SuspendsBeforeGiveUp = 4; @@ -169,18 +54,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _frameMark; private static int _seenMark; - // Par image, ecrits par le fil GPU seulement (voir MissWhy). private static int _frSkipped; private static int _frAllowed; - // A : rien d'autre n'etait disponible · B : alternatives tentees et echouees (ambigu) - // C : rien n'a ete ecarte, ratage de fond. private static int _missA; private static int _missB; private static int _missC; private static long _lastReportMs; - // Mode A/B : indice 1 = restriction ACTIVE (avec), indice 0 = restriction LEVEE (sans). private static bool _abArmed; private static int _abWin; private static bool _abStarted; @@ -189,21 +70,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly int[] _abReads = new int[2]; private static readonly int[] _abIntruders = new int[2]; - /// Vrai quand la restriction s'applique a cet instant. Sert a etiqueter les mesures. internal static bool ArmedNow => AbMode ? _abArmed : _phase == PhaseArmed; - /// - /// 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. - /// public static bool Allows(GpuChannel channel) { _qualThisFrame = true; - // Rang de la TENTATIVE dans l'image. Meme mecanique que la reparation de MvppCtxProbe : - // OnFrame (fil de presentation) n'avance qu'un jeton, la remise a zero se fait ici, sur - // le fil GPU, seul ecrivain de _attempt. Pas de course. int mark = _frameMark; if (_seenMark != mark) @@ -237,18 +109,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // Une tentative AUTORISEE : si l'image finit quand meme sans camera, c'est que la lecture - // a echoue ici, pas que la restriction a ferme la porte. _frAllowed++; return true; } - /// - /// 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 . - /// private static bool IsIntegerColorPass(GpuChannel channel) { Image.Texture col0 = channel.TextureManager.RenderTargetColor0; @@ -281,10 +146,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// Etiquette une lecture de camera avec la moitie d'experience en cours. Appelee par - /// , qui est le seul a savoir si une lecture est une intruse. - /// internal static void NoteRead(bool intruder) { if (!AbMode || !_abStarted) @@ -302,19 +163,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// Une fois par image presentee, AVANT le re-armement du drapeau de capture. public static void OnFrame(bool captured) { if (!_qualThisFrame) { - // Image sans passe 3D (menu, chargement) : elle n'avait pas de camera a prendre. return; } _qualThisFrame = false; - // [MISSWHY] Classement de l'image ratee, ICI : le dernier dessin est passe, les deux - // compteurs de l'image sont complets, et le jeton n'a pas encore ete avance. if (MissWhy && !captured && ArmedNow) { if (_frSkipped == 0) @@ -331,12 +188,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Jeton pour le rang de tentative : seul echange entre fils, un int, indechirable. _frameMark++; - // CHAUFFE, commune aux deux modes : rien n'est mesure ni arme avant que la camera - // produise vraiment. C'est la correction de la faute de la v1, qui calibrait pendant le - // chargement et en tirait un seuil inatteignable. if (_phase == PhaseWarming) { if (captured) @@ -394,8 +247,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // SUSPENSION : la restriction est levee (ArmedNow est faux), on laisse la scene passer, - // puis on RECALIBRE ici meme au lieu de reprendre un etalon perime. if (_phase == PhaseSuspended) { if (--_suspendLeft <= 0) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2BProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2BProbe.cs index 25ac9f0e7..882de3952 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2BProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2BProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -10,19 +7,6 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ Phase 2.1 run B v4 (RYUJINX_MVPP_P2B_PROBE=1): empirical transform hunt. - /// v3 verdict: cb9 holds NO world translation at any 16-byte-aligned offset (the broad - /// low-percent hits were the T~0 artifact: a zero translation "projects" whenever the - /// world origin is on screen), and cb10[0x20] is neither an int nor an integral float. - /// Current theory: TOTK's static world geometry is PRE-TRANSFORMED in the vertex data - /// (no model matrix exists -- which is exactly why camera reprojection already nails it), - /// and the transforms that matter for MV++ are the skinning bone palettes (cb5) of the - /// ACTORS. v4 therefore scans DENSELY (every 4-byte float offset, both the contiguous - /// vec3 and the spread row-major x/+4/+8 pattern), across slots {1, 5, 9, 13}, excluding - /// |T| < 0.5 (the origin artifact), and dumps the cb9/cb10 headers as hex (v3 samples - /// looked like handles/addresses, not matrices). CPU-only, zero rendering change. - /// static class MvppP2BProbe { private static bool _enabled = @@ -30,10 +14,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private const int LogIntervalMs = 2000; private const float FrustumMargin = 1.3f; - private const float MinTranslation = 0.5f; // kills the T~0 "origin visible" artifact + private const float MinTranslation = 0.5f; private const int SlotCount = 18; - private const int ScanSampleShift = 4; // scan every 16th draw - private const int MaxScanFloats = 5120; // covers cb5's 20 KB palettes + private const int ScanSampleShift = 4; + private const int MaxScanFloats = 5120; private static readonly int[] _scanSlots = { 1, 5, 9, 13 }; @@ -45,7 +29,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static bool _sampleDumped; private static readonly int[] _slotBound = new int[SlotCount]; private static readonly int[] _scanned = new int[4]; - // [slot][conv 0=contiguous vec3, 1=spread x/+4/+8][float offset] private static readonly int[][][] _hits = CreateHits(); private static int[][][] CreateHits() @@ -167,8 +150,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // One header dump per window: v3's cb9 samples looked like handles/addresses. - // Show cb9 and cb10 heads as raw hex so their true nature is on the record. if (!_sampleDumped && _named >= 500) { _sampleDumped = true; @@ -201,7 +182,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed (ax >= MinTranslation || ay >= MinTranslation || az >= MinTranslation); } - /// Row-major, column-vector convention (clip = M * world), as the whole MV++ chain. private static bool ProjectsIntoFrustum(in Matrix4x4 vp, float x, float y, float z) { float cw = vp.M41 * x + vp.M42 * y + vp.M43 * z + vp.M44; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2Probe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2Probe.cs index 4f99fcbee..2eedfdce6 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2Probe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppP2Probe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -10,16 +7,6 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ Phase 2.0 feasibility probe (RYUJINX_MVPP_P2_PROBE=1): measures whether a scaled - /// draw can be re-identified from one frame to the next ("the same object") from the draw - /// state alone (guest shader offsets, vertex/index buffer addresses, draw range, occurrence - /// ordinal), with cbuf5 matrix 0 as the tracked payload. This is the foundation the - /// per-object-MV plan (velocity pass fed with previous-frame transforms) stands on: if - /// matching fails here, Phase 2 dies cheap. CPU-only reads, no GAL calls, zero rendering - /// change; completely off without the env var. Metric definitions and the per-scene success - /// thresholds live in docs/MVPP-JOURNAL.md (Phase 2.0 entry). - /// static class MvppP2Probe { private static bool _enabled = @@ -27,9 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private const int LogIntervalMs = 2000; - // Real per-frame motion produces small element deltas (a galloping actor moves about - // 0.25 world units per frame); a MISmatched pair jumps by world-position magnitudes - // (tens to hundreds). The gap between the two classes is what makes 1.0 a safe cut. private const float CoherentMaxDelta = 1.0f; private struct DrawRecord @@ -37,8 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public Matrix4x4 Mat0; } - // Frame N-1 and frame N tables, swapped at each present enqueue. The enqueue may run on - // a different thread than the draws, so all state below is under one (uncontended) lock. private static Dictionary _prev = new(); private static Dictionary _curr = new(); private static readonly Dictionary _sigCounts = new(); @@ -53,13 +35,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _ambiguous; private static int _matched; private static int _coherent; - private static double _sumDtRow; // |delta translation| of coherent pairs, row-major candidate [12..14] - private static double _sumDtCol; // column-major candidate [3,7,11] - private static float _maxDelta; // max element delta over ALL matched pairs (mismatch witness) + private static double _sumDtRow; + private static double _sumDtCol; + private static float _maxDelta; private static long _sumCbufBytes; private static int _maxCbufBytes; - /// Present enqueue = frame boundary: close the frame, swap tables, maybe log. public static void OnFrameEnqueued() { if (!_enabled) @@ -69,8 +50,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed lock (_sync) { - // Ambiguity is exact per frame: every draw whose signature occurred more than - // once this frame counts (including the first occurrence of each group). foreach (KeyValuePair kv in _sigCounts) { if (kv.Value > 1) @@ -97,7 +76,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// Per scaled draw, called from DrawManager.DrawEnd on the GPU thread. public static void OnDraw( GpuChannel channel, ref ThreedClassState state, @@ -111,8 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Same hardening as the Phase 0 probe: an escaped exception on the GPU thread is a - // silent process kill. On any unexpected error: disable and say why. try { OnDrawImpl(channel, ref state, firstIndex, indexCount, drawFirstVertex, drawVertexCount); @@ -132,7 +108,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int drawFirstVertex, int drawVertexCount) { - // Only the scaled 3D scene pass; UI/native passes stay at 1x. if (channel.TextureManager.RenderTargetScale == 1f) { return; @@ -145,7 +120,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed "MVPP P2 probe: active (frame-to-frame draw-identity matching, thresholds in docs/MVPP-JOURNAL.md)."); } - // Draw-identity signature: the exact field list is contractual (journal, Phase 2.0). ulong sig = 14695981039346656037UL; sig = FnvStep(sig, state.ShaderState[1].Offset); sig = FnvStep(sig, state.ShaderState[5].Offset); @@ -156,7 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed sig = FnvStep(sig, (ulong)(uint)drawFirstVertex); sig = FnvStep(sig, (ulong)(uint)drawVertexCount); - // cbuf5 (vertex stage) coverage + matrix 0 snapshot. ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(0, 5); int size = channel.BufferManager.GetGraphicsUniformBufferSize(0, 5); @@ -248,9 +221,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _coherent++; - // Translation candidates for both storage layouts; which one carries - // the actual world motion (visible on actors, so best read in the - // combat scene) settles the layout question without a dump session. float drx = b[12] - a[12], dry = b[13] - a[13], drz = b[14] - a[14]; float dcx = b[3] - a[3], dcy = b[7] - a[7], dcz = b[11] - a[11]; _sumDtRow += MathF.Sqrt(drx * drx + dry * dry + drz * drz); diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppPreSyncProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppPreSyncProbe.cs index e6ab33360..3df50d64d 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppPreSyncProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppPreSyncProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,19 +5,6 @@ using System.IO; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppPreSyncProbe { private static bool _enabled = @@ -60,13 +44,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// 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. - /// public static void DumpGuest(Image.Texture texture) { if (!_enabled || texture == null) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProbe.cs index dc0e96f5a..8f7561ea5 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -9,22 +6,11 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ Phase 0 discovery probe (RYUJINX_MVPP_PROBE=1). Once every couple of seconds, on a - /// draw of the scaled 3D pass, scans the bound graphics uniform buffers for 4x4-matrix-shaped - /// float windows and fingerprints each candidate. Only candidates whose CONTENT CHANGED since - /// the previous sample are logged, so the in-game protocol is: hold the camera still (expect - /// silence), then pan it (the candidates that light up are camera-dependent - the - /// view/view-projection matrices MV++ needs). Read-only; completely off without the env var. - /// static class MvppProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_PROBE") == "1"; - // Dump mode (RYUJINX_MVPP_DUMP=): instead of scanning, print the first 512 bytes - // of stage-0 cbuf as float rows every sample, to map the block's exact layout - // (which matrix is view / proj / view-proj / inverses / previous frame). private static readonly int _dumpSlot = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUMP"), out int s) ? s : -1; @@ -43,9 +29,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // A diagnostic probe must never take the process down: it runs on the GPU thread, - // where an escaped exception is fatal and unlogged (crash of 2026-07-02 08:22, - // WER e0434352 with a silent Ryujinx log). On any error: disable and tell why. try { OnDrawImpl(channel); @@ -59,7 +42,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static void OnDrawImpl(GpuChannel channel) { - // The main 3D scene pass is the scaled one; UI/native passes stay at 1x. if (channel.TextureManager.RenderTargetScale == 1f) { return; @@ -95,8 +77,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // The bound ranges are already TRANSLATED: physical addresses, not GPU VAs - // (SetGraphicsUniformBuffer runs TranslateAndCreateBuffer). Read physical. ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot); int size = Math.Min(channel.BufferManager.GetGraphicsUniformBufferSize(stage, slot), MaxBytesPerBuffer); @@ -122,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // 16-byte aligned windows of 16 floats. for (int i = 0; i + 16 <= data.Length; i += 4) { ReadOnlySpan window = data.Slice(i, 16); @@ -151,7 +130,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _lastFingerprint[key] = fp; - i += 12; // jump past this matrix (loop adds 4 -> next window starts right after it). + i += 12; } } } @@ -179,10 +158,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// Cheap shape filter: 16 finite floats of sane magnitude with enough non-zero terms. - /// Material constants pass it too - the temporal changed-filter does the real sorting. - /// private static bool LooksLikeMat4(ReadOnlySpan m) { int nonZero = 0; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProjAudit.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProjAudit.cs index c8f6ea246..d1dd0b453 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProjAudit.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppProjAudit.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,60 +5,6 @@ using System.Numerics; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// PROJECTIVE AUDIT of the view-projection elected by . - /// 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. - /// static class MvppProjAudit { public static readonly bool Enabled = @@ -71,10 +14,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static long _lastMs; - /// - /// 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. - /// public static void Audit(in Matrix4x4 vp, float camX, float camY, float camZ) { if (!Enabled) @@ -91,41 +30,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _lastMs = now; - // Recovered here rather than passed in, so the hook at the call site stays a single - // line and TryReadAt keeps its signature untouched. Same definition as IsViewProj. float fx = MathF.Sqrt(vp.M11 * vp.M11 + vp.M12 * vp.M12 + vp.M13 * vp.M13); float fy = MathF.Sqrt(vp.M21 * vp.M21 + vp.M22 * vp.M22 + vp.M23 * vp.M23); - // Row layout, matching the constructor in MvppSoloCamera.TryReadAt: - // row0 = M11 M12 M13 M14 row2 = M31 M32 M33 M34 - // row1 = M21 M22 M23 M24 row3 = M41 M42 M43 M44 float n3 = MathF.Sqrt(vp.M41 * vp.M41 + vp.M42 * vp.M42 + vp.M43 * vp.M43); float a = MathF.Sqrt(vp.M31 * vp.M31 + vp.M32 * vp.M32 + vp.M33 * vp.M33); - // Documented shape: row2.xyz = A*R.row2 and row3.xyz = s*R.row2 with s = +/-1, so the - // sign is read off the dot product rather than assumed. float dot23 = vp.M31 * vp.M41 + vp.M32 * vp.M42 + vp.M33 * vp.M43; float s = dot23 >= 0f ? 1f : -1f; - // row3.w = s*tz => tz = row3.w / s ; row2.w = A*tz + B => B = row2.w - A*tz. float tz = s != 0f ? vp.M44 / s : 0f; float b = vp.M34 - a * tz; - // OpenGL perspective: A' = -(f+n)/(f-n), B' = -2fn/(f-n) with the sign carried by s. - // Inverting gives n = B/(A-1) and f = B/(A+1); both are printed raw so an absurd pair - // is visible rather than silently normalised into something plausible. float aSigned = a * s; float near = MathF.Abs(aSigned - 1f) > 1e-6f ? b / (aSigned - 1f) : float.NaN; float far = MathF.Abs(aSigned + 1f) > 1e-6f ? b / (aSigned + 1f) : float.NaN; - // The perspective w at the screen centre for a point on the camera axis: w is row3 - // dotted with the world point plus row3.w. Taking the camera itself gives the offset, - // so the magnitude that matters is how w grows per world unit along the view axis -- - // with n3 forced to 1 that rate is 1, and w IS the distance in world units. float wAtCam = vp.M41 * camX + vp.M42 * camY + vp.M43 * camZ + vp.M44; float camDist = MathF.Sqrt(camX * camX + camY * camY + camZ * camZ); - // Conditioning: unproject the centre of the screen at mid depth, project it back. float rt = float.NaN; if (Matrix4x4.Invert(vp, out Matrix4x4 inv)) @@ -142,8 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (MathF.Abs(back.W) > 1e-20f) { - // Half the render width is the worst case for a normalised device unit, - // 1920 is only a scale for readability - the verdict is "near zero or not". rt = MathF.Sqrt(back.X / back.W * (back.X / back.W) + back.Y / back.W * (back.Y / back.W)) * 1920f; } @@ -151,7 +72,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else { - rt = -1f; // not invertible at all + rt = -1f; } Logger.Info?.Print(LogClass.Gpu, diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppRtDumpProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppRtDumpProbe.cs index 91ab76c07..793d58351 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppRtDumpProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppRtDumpProbe.cs @@ -1,5 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. using Ryujinx.Common.Logging; using System; @@ -8,80 +6,26 @@ using System.IO; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppRtDumpProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP") == "1"; - /// - /// 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. - /// private static readonly int _startSeconds = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_START"), out int st) && st > 0 ? st : 90; - /// Seconds between capture bursts (RYUJINX_RT_DUMP_EVERY, default 10). private static readonly int _everySeconds = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_EVERY"), out int s) && s > 0 ? s : 10; - /// How many bursts before the probe stops for good (RYUJINX_RT_DUMP_MAX, default 4). private static readonly int _maxBursts = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_MAX"), out int m) && m > 0 ? m : 4; - /// RYUJINX_RT_DUMP_ALL=1: capture every bound target, not just the scene buffer. private static readonly bool _dumpAll = Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_ALL") == "1"; - /// RYUJINX_RT_DUMP_INPUTS=1: also capture the textures the draws SAMPLE. private static readonly bool _dumpInputs = Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_INPUTS") == "1"; - /// - /// 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. - /// private static readonly bool _frameMode = Environment.GetEnvironmentVariable("RYUJINX_RT_DUMP_FRAME") == "1"; @@ -95,11 +39,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static bool _frameArmLogged; private static readonly HashSet _doneThisBurst = new(); - /// - /// 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. - /// public static void NotePresented(Image.Texture texture) { if (!_enabled) @@ -126,13 +65,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // --- Mode IMAGE : ce present est la frontière entre deux images. --- try { if (_bursting) { - // Fin de l'image capturée : son résultat présenté ferme le dossier, à côté des - // cibles intermédiaires des draws de CETTE image, et d'aucune autre. if (texture != null) { TryDump(texture, "PRESENTED"); @@ -173,8 +109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Ouverture : les draws de l'image SUIVANTE seront capturés, puis le present suivant - // fermera le dossier avec le résultat de cette même image. _burstIndex++; _doneThisBurst.Clear(); _burstDir = Path.Combine("rtdump", $"image{_burstIndex:D2}"); @@ -212,9 +146,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { long now = Environment.TickCount64; - // En mode IMAGE, l'ouverture et la fermeture de la rafale appartiennent à NotePresented : - // ici on ne fait que capturer les cibles des draws de l'image en cours, sans toucher au - // découpage. Toute logique de temps ici casserait l'appariement. if (_frameMode) { if (!_bursting) @@ -245,8 +176,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // A burst spans a full second: the low-resolution buffers are bound on their own - // draws, so capturing a single draw would only ever catch the main target. _bursting = true; _burstEndsMs = now + 1000; _burstIndex++; @@ -288,11 +217,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed TryDump(ds, "depth"); } - // Also capture what the draws READ. Measured 21/07: the 640x360 R32G32Uint buffer that - // the glow draws sample never showed up among the render targets, because nothing - // DRAWS into it -- it is written by a COMPUTE shader. That is exactly how tiled/ - // clustered lighting builds its per-tile light lists, so the buffer has to be captured - // from the read side or not at all. if (_dumpInputs) { channel.TextureManager.MvppEnumerateGraphicsInputsStage((stage, tex) => TryDump(tex, $"input{stage}")); @@ -313,27 +237,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // One capture per distinct SHAPE, not per texture: the same few buffers are rebound - // thousands of times a second and what we need is one picture of each kind. string name = $"{role}_{tex.Info.Width}x{tex.Info.Height}_{tex.Info.FormatInfo.Format}"; - // The artefact is INTERMITTENT and only shows while the camera moves, so a handful of - // snapshots will miss it. We need many captures over a long window -- which means the - // disk budget has to be spent where the answer is. The 21/07 dumps showed the composed - // scene lives in the 1280x720 R11G11B10Float target; that is the one that answers the - // bisection question "do the blocks exist in the game's own image?". Everything else - // is skipped unless RYUJINX_RT_DUMP_ALL=1. if (role != "PRESENTED" && !_dumpAll && !_dumpInputs && !tex.Info.FormatInfo.Format.ToString().StartsWith("R11G11B10", StringComparison.Ordinal)) { return; } - // [28/07] L'ADRESSE GPU DANS LE NOM DU FICHIER. Sans elle, les dumps sont nommes par - // SLOT (color0, color5...) alors que MvppUiProbe raisonne en ADRESSES : impossible de - // croiser les deux, et un meme buffer lie a trois slots ressort en triple exemplaire - // (color0/5/6 du 28/07 avaient un decor identique a +0,957 -- c'etait le meme). - // Deduplication par ADRESSE et plus par nom : un buffer par contenu reel. ulong gpuAddr = 0; try @@ -342,7 +253,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } catch { - // pas d'adresse exploitable : on garde 0, le nom reste unique par le slot } if (!_doneThisBurst.Add(gpuAddr != 0 ? $"@{gpuAddr:X}" : name)) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScanProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScanProbe.cs index 57165f62a..4ed57866d 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScanProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScanProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,32 +5,6 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ pipeline-shape inventory probe (RYUJINX_MVPP_SCAN=1). Purpose: games whose camera the - /// capture never arms on (XC2: pub 0 across three configs, native and modded alike - the - /// existing counters only tick AFTER a successful publication, so they cannot say WHICH stage - /// of the chain fails). - /// - /// v1 (21/07 morning) answered the first question and killed the working hypothesis: XC2 DOES - /// have non-square depth passes (1280x720 D24S8, ~9k draws / 5 s) so the scene-pass filter is - /// not the blocker; the camera IS present (PROJ fx=1.358 aspect=1.778, exact 16:9); and the - /// canonical [view][proj][viewproj] contiguous row-major triplet the capture requires exists - /// NOWHERE (canonical 0 over the whole run) because XC2 stores view and proj in SEPARATE - /// constant buffers, column-major. - /// - /// v2 fixes v1's own defect and lifts the VIEW noise: - /// - v1 only paired proj x view WITHIN one buffer, so on a game that splits them across - /// buffers the product search could never fire (the vp-products 0 was the probe's limit, - /// not the game's shape). v2 pairs GLOBALLY, across every bound buffer and stage. - /// - Any proper rotation passes IsOrthonormalView, so bone palettes / normal matrices / - /// identities flooded the candidate list (~400 hits). v2 rejects near-identity matrices - /// and matrices sitting in a stride-64 RUN (the signature of a skinning palette), then - /// ranks what survives by PROOF: a (proj, view) pair whose product is actually STORED in - /// guest memory is the camera - that is evidence, not a heuristic. - /// - Both multiplication conventions are tested (proj x view AND view x proj), and the - /// product is looked for in either storage order. - /// Read-only, completely off without the env var, self-disabling on any error. - /// static class MvppScanProbe { private static bool _enabled = @@ -42,9 +13,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private const int PassWindowMs = 5000; private const int DeepScanIntervalMs = 1000; private const int DeepScanFallbackMs = 500; - // Sized so one deep scan stays a few milliseconds at 1 Hz: the product search is - // buffers x windows x products, and that product must not become a visible hitch. - // 4 KiB covers everything measured so far (proj at +0x1F0, views at +0x000). private const int MaxBytesPerBuffer = 4096; private const int MaxDetailLines = 28; @@ -54,8 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private const int MaxViewPaired = 10; private const int MaxProducts = MaxProjPaired * MaxViewPaired * 2; - // ---- Stage A: pass inventory (cheap per-draw counters, 5 s windows) ---- - private struct DepthForm { public int Width; @@ -65,12 +31,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public int Count; } - // Aspect of the MAIN render target, i.e. the largest NON-SQUARE depth bound during the - // window. Measured 21/07 on XC2: taking the aspect of whatever draw the 1 Hz tick landed - // on made 56 of 117 verdicts empty, because the tick often lands on a shadow pass - // (1024x1024 -> aspect 1.0) and every 16:9 camera was then rejected as mismatched. The - // reference has to be the scene target, not the current draw. Largest-area wins, promoted - // once per window so a resolution change is picked up without flapping. private static int _liveMainW; private static int _liveMainH; private static long _liveMainArea; @@ -88,8 +48,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _colorFormCount; private static int _colorFormOverflow; - // ---- Stage B/C: deep scan (1 Hz, prefers a draw with a depth-stencil bound) ---- - private static long _lastDeepScanMs; private static bool _lastScanOnMainPass; private static bool _wantDeepScan; @@ -113,9 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // A diagnostic probe must never take the process down: it runs on the GPU thread, - // where an escaped exception is fatal and unlogged (crash of 2026-07-02 08:22, - // WER e0434352 with a silent Ryujinx log). On any error: disable and tell why. try { OnDrawImpl(channel); @@ -131,8 +86,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { long now = Environment.TickCount64; - // ---- Stage A: per-draw counters (int adds + a tiny linear form table) ---- - _draws++; Image.Texture ds = channel.TextureManager.RenderTargetDepthStencil; @@ -156,8 +109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _liveMainW = w; _liveMainH = h; - // Usable straight away: the first deep scans happen before the first - // window flush, and an empty reference is what caused the 56 blanks. if (_mainAspect <= 0f) { _mainAspect = (float)w / h; @@ -168,11 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed RecordDepthForm(w, h, ds.Info.FormatInfo.Format, channel.TextureManager.RenderTargetScale > 1f); } - // COLOUR targets too. Measured 21/07 indoors on XC2: the depth stayed a rock-steady - // 1280x720 for 20 straight windows while the artefact was on screen -- but a game can - // perfectly well scale its COLOUR target and leave depth alone, which is a common way - // to do dynamic resolution. Reporting only depth would have let me call dynamic - // resolution "eliminated" while never having looked at the buffer it actually resizes. Image.Texture c0 = channel.TextureManager.RenderTargetColor0; if (c0 != null) @@ -190,22 +136,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _passWindowStartMs = now; } - // ---- Stage B/C: 1 Hz deep scan. Prefer a draw with ANY depth bound (most likely - // scene-adjacent); fall back to any draw if none showed up for 500 ms. ---- - if (!_wantDeepScan && now - _lastDeepScanMs >= DeepScanIntervalMs) { _wantDeepScan = true; _wantDeepScanSinceMs = now; } - // Scan on a draw of the MAIN scene pass whenever possible: the constant buffers bound - // at that instant are the ones the game is drawing the world with. Measured 21/07 on - // XC2 -- two legitimate 16:9 cameras move at once (same fx, different positions), and - // neither the vote nor the motion can separate them; what separates them is WHICH - // pass is being drawn when they are bound. That is the pipeline SHAPE, which is the - // criterion this whole chantier is built on. Falls back to any depth-bound draw, then - // to any draw, so a game that never matches still gets measured. bool onMainPass = ds != null && ds.Info.Width != ds.Info.Height && (long)ds.Info.Width * ds.Info.Height >= _liveMainArea; @@ -325,7 +261,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Logger.Info?.Print(LogClass.Gpu, sb.ToString()); - // Promote the window's main target as the aspect reference for the next window. if (_liveMainH > 0) { _mainAspect = (float)_liveMainW / _liveMainH; @@ -345,27 +280,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _deepScans = 0; } - // ---- Deep scan ---- - private enum MatClass { - Proj, // perspective projection, row-major - ProjT, // perspective projection, column-major (transposed storage) - View, // orthonormal view, row-major - ViewT, // orthonormal view, column-major + Proj, + ProjT, + View, + ViewT, } private struct Candidate { public int Stage; public int Slot; - public int Offset; // in BYTES from the start of the buffer + public int Offset; public MatClass Class; - public float A; // proj: m00 (fx) | view: camera pos X - public float B; // proj: m11 (fy) | view: camera pos Y - public float C; // view: camera pos Z (unused for proj) - public bool InRun; // sits in a stride-64 chain = skinning palette signature - public bool Identity; // near-identity = structurally meaningless + public float A; + public float B; + public float C; + public bool InRun; + public bool Identity; } private struct BufRef @@ -380,7 +313,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly BufRef[] _bufs = new BufRef[MaxBuffers]; private static int _bufCount; - // Products to look for, built in phase 2 and searched in phase 3. private static readonly float[] _products = new float[MaxProducts * 16]; private static readonly int[] _prodProjIdx = new int[MaxProducts]; private static readonly int[] _prodViewIdx = new int[MaxProducts]; @@ -391,19 +323,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly float[] _tmpB = new float[16]; private static readonly float[] _tmpT = new float[16]; - // ---- Stage E: consensus verdict ---- - // Measured 21/07 on the TOTK control run (ground truth = the triplet's viewproj at - // stage0 cbuf8 +0x080, the address the current capture publishes): a lone structural - // validator is NOT enough - it named 13 candidates, and picking any of them would have - // put the camera on a wrong matrix ~10% of frames = the ghosting family we spent weeks - // killing. What DOES separate them: the game copies its real view-projection into - // SEVERAL constant buffers (one per shader that needs it), so the true camera is the - // position that independent buffers AGREE on. Scored over 52 scans: - // consensus alone ................ 47/52 (5 misses) - // consensus + degenerate guard ... 52/52 (0 miss) - // The 5 misses were a 3-strong cluster at (2, -0, -0): two coordinates EXACTLY zero, - // the signature of a misaligned window landing on padding rather than a camera. - // Still measurement only - nothing here feeds MvppCameraCapture. private struct VpSolo { public int Stage; @@ -419,15 +338,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public float Motion; public float Straightness; - // [DIVERG C 01/08] La matrice elle-meme (transposition resolue), pour mesurer la - // divergence de ROTATION contre la camera publiee — dimension absente de tous les - // journaux jusqu'ici (revue 310). Scratch local au fil GPU, 8 Ko statiques. public System.Numerics.Matrix4x4 M; - // Physical address of the buffer this was read from. The SAME buffer is routinely - // bound to several shader stages (TOTK: stage0 cbuf8 and stage4 cbuf8 are the same - // bytes), and counting it once per stage would inflate the consensus with copies of - // a single source. Votes are counted per distinct address. public ulong Address; } @@ -435,39 +347,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly VpSolo[] _vpSolos = new VpSolo[MaxVpSolos]; private static int _vpSoloCount; - // Position agreement tolerance: the same camera read from two buffers can differ by a - // float rounding, never by a world unit. private const float PosAgreeEps = 0.5f; - // Temporal lock on the elected location (rule 4), keyed on the same stable identity as - // the motion history: address + offset, never the cbuf slot. private static bool _hasLock; private static ulong _lockAddress; private static int _lockOffset; - // Per-location motion history (rule 3). Measured 21/07 on XC2: the runner-up was not - // junk at all, it was the CAMERA-RELATIVE view-projection (the one engines build for the - // skybox, with the translation removed by construction). It is structurally a perfect - // view-projection, carries the SAME fx as the real camera, and no static test can tell - // them apart -- which is why every static guard I tried failed on one game or the other. - // What separates them is movement: - // real camera .......... 67 world units, and it TRAVELS - // camera-relative ...... ~0.3, pinned to the origin forever - // placeholder sentinel .. 20000, constant forever - // Comparing positions cannot work (the 20000 sentinel would out-scale the real camera - // and get it rejected). Comparing MOTION separates all three, with no threshold and no - // unit: the camera is the one that moves. Motion is remembered per location for the whole - // session, so a single moment of movement is enough to mark it for good. private struct Tracked { - // Identity is (physical ADDRESS, offset), NOT (stage, slot, offset). Measured 21/07 - // on XC2 and this settled a whole day of wandering verdicts: stage0 cbuf4 +0x000 was - // bound to 27 DIFFERENT buffers across 113 sightings. A cbuf slot is a drawer the - // game rebinds between draws, so every motion and straightness score computed on a - // slot was averaging unrelated objects -- which is exactly why a real camera looked - // like it "hopped around". Addresses, by contrast, persist (45 distinct in a session, - // the top one seen 91 times), and address+offset separates cleanly: @2297C86100+0x070 - // is the travelling camera while @2297C86100+0x180 is the frozen sky matrix. public ulong Address; public int Offset; public float LastX; @@ -477,7 +364,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public float MaxStep; public int Seen; - // Ring of recent positions, used for the straightness ratio (see TrackMotion). public int RingHead; public int RingCount; public float PathLength; @@ -489,7 +375,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly Tracked[] _tracks = new Tracked[MaxTracks]; private static int _trackCount; - // Positions ring, flattened: track i occupies [i * RingSize, (i + 1) * RingSize). private static readonly float[] _ringX = new float[MaxTracks * RingSize]; private static readonly float[] _ringY = new float[MaxTracks * RingSize]; private static readonly float[] _ringZ = new float[MaxTracks * RingSize]; @@ -513,8 +398,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed System.Text.StringBuilder detail = null; int detailLines = 0; - // ---- Phase 1: collect every matrix-shaped candidate across ALL stages/slots ---- - for (int stage = 0; stage < Constants.ShaderStages; stage++) { uint mask = channel.BufferManager.GetGraphicsUniformBufferUseMask(stage); @@ -596,13 +479,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed MarkRuns(firstCand); - // ---- Stage D: STANDALONE view-projection recognition ---- - // Measurement only: this is the candidate contract for games that never - // store the [view][proj][viewproj] triplet (XC2 stores the VP alone at - // stage0 cbufN +0x000, row-major, proven 21/07 by cross-buffer pairing). - // Nothing is wired into MvppCameraCapture yet - the point of this pass is - // to show, in a log, that it accepts the RIGHT matrix on XC2 (same campos - // as the pair proof) and does not fire wrongly on TOTK. for (int i = 0; i + 16 <= data.Length; i += 4) { ReadOnlySpan m = data.Slice(i, 16); @@ -622,8 +498,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ReadOnlySpan vpm = transposed ? _tmpT : m; - // Same rival guard as the capture: a SQUARE projection is the - // environment-cubemap camera, structurally indistinguishable otherwise. if (MathF.Abs(fy / fx - 1f) < 0.2f) { _lastStandaloneSquare++; @@ -637,8 +511,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ViewProjPos(vpm, fx, fy, out float px, out float py, out float pz); - // Sentinel/degenerate positions (XC2 showed a [20000 20000 20000] - // placeholder camera): not a place anything is ever rendered from. if (!float.IsFinite(px) || !float.IsFinite(py) || !float.IsFinite(pz) || MathF.Abs(px) > 1e6f || MathF.Abs(py) > 1e6f || MathF.Abs(pz) > 1e6f) { @@ -669,20 +541,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed }; } - // The physical address goes in the log because a cbuf SLOT is not an - // identity: the game rebinds slot N to a different buffer between draws, - // so tracking (stage, slot, offset) may be following a drawer rather than - // what is inside it. Hypothesis raised 21/07 after the lock wandered over - // five separately-proven addresses; this line is what will settle it. AppendDetail(ref detail, ref detailLines, $"VP-SOLO{(transposed ? "^T" : " ")} stage{stage} cbuf{slot} +0x{i * 4:X3} " + $"@{channel.BufferManager.GetGraphicsUniformBufferAddress(stage, slot):X10} " + $"fx={fx:0.###} fy={fy:0.###} aspect={fy / fx:0.###} campos=[{px:0.#} {py:0.#} {pz:0.#}]"); } - // Canonical triplet the capture expects TODAY ([view][proj][viewproj], - // row-major, contiguous). Kept so the log can still say outright whether the - // CURRENT contract would match anywhere at all. for (int i = 0; i + 48 <= data.Length; i += 4) { if (IsOrthonormalView(data.Slice(i, 16)) && @@ -699,8 +563,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // ---- Phase 2: build the products of every (proj, view) pair, GLOBALLY ---- - int projCount = 0; int viewCount = 0; int viewKept = 0; @@ -726,8 +588,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed BuildProducts(channel); - // ---- Phase 3: is any of those products actually STORED in guest memory? ---- - int vpProducts = SearchProducts(channel, ref detail, ref detailLines, ref foundFp); if (layoutFp != _lastCbufLayoutFp) @@ -743,9 +603,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _lastCanonicalCount = canonical; _lastStandaloneVp = standaloneVp; - // Stage E verdict: emitted on EVERY scan (not gated on the found-set changing) -- - // its whole point is to be readable as a time series, so a wrong pick on a single - // scan cannot hide behind an unchanged fingerprint. System.Text.StringBuilder verdict = null; int verdictLines = 0; LogVerdict(ref verdict, ref verdictLines); @@ -759,7 +616,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _lastFoundSetFp = foundFp; - // Projections first (the solid signal), then the views that survived the filter. for (int i = 0; i < _candCount && detailLines < MaxDetailLines; i++) { ref Candidate c = ref _cands[i]; @@ -803,23 +659,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// Stage E: picks THE camera among the standalone view-projection candidates, by the two - /// rules the 21/07 TOTK control run scored (52/52 together, 47/52 for the vote alone): - /// 1. the projection's aspect must match the render target's (kills the inverse-matrix - /// family at 0.563 and the junk at 0.16 - a ratio, never a resolution, so it stays - /// game-agnostic); - /// 2. among survivors, the winner is the position the MOST INDEPENDENT BUFFERS agree - /// on, after dropping degenerate positions (two coordinates exactly zero = a window - /// straddling two matrices, not a camera). - /// Logged, never published: this is the candidate contract for step 4, not a decision. - /// - /// - /// Records this candidate's displacement since the previous scan and returns the largest - /// single-step displacement ever observed at that exact location. Session-wide, so one - /// moment of movement marks a location as mobile for good - the camera does not stop - /// being the camera when the player stands still. - /// private static float TrackMotion(in VpSolo c, out float straightness) { straightness = 1f; @@ -850,19 +689,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ref Tracked t = ref _tracks[idx]; t.Seen++; - // RECENT motion, not the session maximum. Measured 21/07 on XC2: with a session-wide - // maximum, stage0 cbuf6 +0x040 held the lock for 72 scans while reporting the exact - // same [67 -7.8 53.9] every single time -- a constant that had changed value once (a - // load, a spawn) and stayed "mobile" for ever after. Decaying the score makes a - // matrix that stopped moving fade out over ~10 scans while a travelling camera keeps - // its score high. When EVERYTHING is still (player not moving) all scores decay - // together, bestMotion goes to zero, rule 3 stops filtering and the lock simply - // holds -- which is the correct behaviour, not a special case. - // 0.5 chosen by simulation on the measured numbers, not by feel: replaying the run's - // own values (a 163-unit load spike then nothing, against a camera stepping ~12/scan) - // a frozen matrix falls under rule 3's bar after 8 scans at 0.5, versus 22 at 0.8 -- - // and the travelling camera's score is unchanged either way (~13). Faster decay buys - // responsiveness for free here. t.MaxStep *= 0.5f; if (t.HasLast) @@ -872,28 +698,16 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed float dz = c.Z - t.LastZ; float step = MathF.Sqrt(dx * dx + dy * dy + dz * dz); - // A teleport-sized jump is a scene cut or a garbage read, not travel: it must not - // crown a constant matrix that flipped value once. if (float.IsFinite(step) && step < 1e5f && step > t.MaxStep) { t.MaxStep = step; } } - // STRAIGHTNESS: net displacement over the ring divided by the path actually walked. - // Measured 21/07 on XC2, and this is the criterion that finally separates the last - // two survivors -- both 16:9, both genuinely changing every scan: - // camera (cbuf5 +0x000) .......... 0.57 it goes somewhere - // per-object matrix (cbuf4+0x000) 0.02 it hops around a point - // A camera travels; a per-draw object transform jumps to wherever the next object - // is. Raw motion magnitude cannot tell them apart -- it actively favours the jumper, - // which is exactly how the lock landed on it. A ratio of two lengths: no unit, no - // threshold in world units, nothing tuned to a game. int slot = idx * RingSize; if (t.RingCount == RingSize) { - // Drop the step leaving the window. t.PathLength -= _ringStep[slot + t.RingHead]; } @@ -952,8 +766,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed float targetAspect = _mainAspect; - // Rule 1 + motion bookkeeping: keep only the aspect-valid candidates, and update - // each one's motion history BEFORE any decision is taken. float bestMotion = 0f; float bestStraight = 0f; int considered = 0; @@ -999,20 +811,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // Rule 3: it has to MOVE like the most mobile candidate does. A location whose - // position never budged while another travelled is the camera-relative matrix - // (or a constant placeholder), not the camera. Relative to the best mover, so - // there is no threshold in world units and nothing tuned to a game. While - // nothing has moved yet (start of session, player standing still) bestMotion is - // 0 and this test lets everything through -- the verdict then says "CHAUFFE". if (bestMotion > 0f && a.Motion < bestMotion * 0.1f) { continue; } - // Rule 3b: it has to TRAVEL, not hop. A per-draw object transform changes every - // scan (so it sails through rule 3) but wanders around a point instead of going - // anywhere. Relative to the straightest mover, so still no absolute threshold. if (bestStraight > 0f && a.Straightness < bestStraight * 0.35f) { continue; @@ -1020,8 +823,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed movers++; - // Rule 2: how many DISTINCT buffers report this same world position? Counted per - // physical address, so one buffer bound to several stages is one voice, not two. int votes = 0; for (int j = 0; j < _vpSoloCount; j++) @@ -1072,22 +873,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // The lock must never survive its own location failing a rule: that is exactly how - // the 21/07 run stayed pinned on the sky matrix for 15 scans while the DIVERGENCE - // line showed the consensus was right all along. if (_hasLock && lockedIdx < 0) { _hasLock = false; } - // Rule 3: TEMPORAL LOCK. Consensus alone still lost on XC2, where a 3-strong cluster - // sitting near the origin outvoted the real 2-strong camera on some scans (its - // coordinates were tiny but NOT exactly zero, so the TOTK-tuned degenerate guard -- - // cut against a single game, the very trap the 3-case rule exists to catch -- never - // fired). Once a location has been elected, we keep READING THAT LOCATION as long as - // it still yields a consensus-backed camera, and only re-elect when it stops. Same - // shape as MvppCameraCapture's cached slot + lineage gate, which is proven in the - // field. Divergences are logged, never silently resolved. int winner; string mode; @@ -1100,11 +890,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { winner = elected; - // A lock must never be taken on a decision we could not check. Measured 21/07: - // during warm-up the aspect reference is still unknown, EVERY candidate passes - // rule 1 by default, and a lock taken then (on a 0.563 portrait matrix) survived - // long after the reference became available. Only lock on a scan that was both - // aspect-checked AND taken on the main scene pass. bool trustworthy = targetAspect > 0f && bestMotion > 0f && _lastScanOnMainPass; mode = trustworthy ? (_hasLock ? "RE-ELU" : "ELU") : "PROVISOIRE"; @@ -1147,16 +932,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"mouvement={w.Motion:0.##} -- {(lockedIdx >= 0 ? lockedVotes : electedVotes)} sources d'accord, " + $"{movers} mobiles sur {considered} au bon aspect ({_vpSoloCount} bruts){pass}{divergence}"); - // [DIVERG C 01/08] PHASE C SEULE — calibrage du declencheur consensus-divergence - // (journal 308/310/311). LECTURE PURE : une ligne par verdict, aucune decision, - // aucun chemin de comportement. Compare le VAINQUEUR (par VALEUR, jamais par siege) - // a la camera PUBLIEE. dpos = distance des positions ; drot = ecart max des 9 - // elements du bloc 3x3 (meme metrique que RotGap cote capture). ⚠️ La publiee est - // POST-DEJITTER : les deltas portent ce terme sous-pixel, negligeable devant les - // tolerances visees (~0,1) — dit pour l'interpretation. bornes : OK si dpos < 150 u - // (borne de concordance, tue l'intruse a 546/626 u) ET |pos| >= 5 u (anti-origine). - // n = divergences CONSECUTIVES en bornes (dpos>3 ou drot>0,1) — la matiere premiere - // des distributions demandees (Δpos, Δrot, persistance, corr. fenetres de gel). if (MvppSoloCamera.TryGetPublishedForProbe(out System.Numerics.Matrix4x4 pubVp, out float pubX, out float pubY, out float pubZ)) { @@ -1189,7 +964,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [DIVERG C 01/08] Divergences consecutives en bornes (voir la ligne DIVERG). private static int _dgConsecutive; private static bool TryReadBuffer(GpuChannel channel, int stage, int slot, out ReadOnlySpan data) @@ -1206,8 +980,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed try { - // The bound ranges are already TRANSLATED: physical addresses, not GPU VAs - // (SetGraphicsUniformBuffer runs TranslateAndCreateBuffer). Read physical. data = MemoryMarshal.Cast(channel.MemoryManager.Physical.GetSpan(address, size)); } catch @@ -1218,12 +990,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// Flags candidates that sit in a stride-64-byte chain of 3+ orthonormal matrices: the - /// signature of a skinning palette (an array of bone matrices), which is what floods the - /// VIEW candidate list on any character-heavy game. The camera's view matrix lives in a - /// small per-frame header, not in such a run. - /// private static void MarkRuns(int firstCand) { for (int i = firstCand; i < _candCount; i++) @@ -1300,7 +1066,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed viewUsed++; - // Both conventions: proj x view (row-vector engines) and view x proj. Multiply(_tmpA, _tmpB, _products.AsSpan(_prodCount * 16, 16)); _prodProjIdx[_prodCount] = pi; _prodViewIdx[_prodCount] = vi; @@ -1344,7 +1109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { ReadOnlySpan exp = _products.AsSpan(k * 16, 16); - // Two-element prefilter before the full 16-element compare. bool direct = Close(w[0], exp[0]) && Close(w[5], exp[5]) && MatchesLoose(w, exp, false); bool transposed = !direct && Close(w[0], exp[0]) && Close(w[5], exp[5]) && MatchesLoose(w, exp, true); @@ -1409,27 +1173,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// Structural recognition of a STANDALONE view-projection matrix, row-major. - /// - /// VP = P x V with P perspective (row 3 = [0,0,+/-1,0]) and V rigid ([R|t], R orthonormal) - /// expands to: - /// row0.xyz = fx * R.row0 row1.xyz = fy * R.row1 - /// row2.xyz = A * R.row2 row3.xyz = +/- R.row2 - /// so the shape is fully constrained without ever seeing P or V: - /// (1) ||row3.xyz|| == 1 (it IS a rotation row) - /// (2) ||row0.xyz|| = fx > 0, ||row1.xyz|| = fy > 0 - /// (3) row0.xyz, row1.xyz, row3.xyz mutually orthogonal (scaled rotation rows) - /// (4) row2.xyz parallel to row3.xyz (both along R.row2) - /// A skinning bone matrix fails (1) outright: its row 3 is [0,0,0,1], xyz norm 0. - /// The caller still has to apply the square-projection rival guard (cubemap cameras). - /// private static bool IsViewProj(ReadOnlySpan m, out float fx, out float fy) { fx = 0f; fy = 0f; - // (1) the w-carry row must be a unit direction. float n3 = MathF.Sqrt(m[12] * m[12] + m[13] * m[13] + m[14] * m[14]); if (MathF.Abs(n3 - 1f) > 0.02f) @@ -1437,9 +1185,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // (2) focal scales, with sanity bounds. Measured 21/07 on XC2: without an upper - // bound, junk windows sail through (fx=1318930, fy=2.88e18) and poison the position - // maths into NaN. A real focal term lives in a narrow band whatever the game. float n0 = MathF.Sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]); float n1 = MathF.Sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]); @@ -1449,7 +1194,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // (3) mutual orthogonality of the three rotation-derived rows. float d01 = (m[0] * m[4] + m[1] * m[5] + m[2] * m[6]) / (n0 * n1); float d03 = (m[0] * m[12] + m[1] * m[13] + m[2] * m[14]) / n0; float d13 = (m[4] * m[12] + m[5] * m[13] + m[6] * m[14]) / n1; @@ -1459,7 +1203,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // (4) row2.xyz must be collinear with row3.xyz (cross product ~ 0). float n2 = MathF.Sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]); if (n2 > 1e-4f) @@ -1474,11 +1217,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // (5) a BARE perspective projection satisfies (1)-(4) exactly, because a projection - // IS a view-projection whose view is the identity. Mathematically right, useless to - // us: it carries no camera. Measured 21/07 on XC2 - the pure projections at cbuf3 - // +0x030/+0x180/+0x1F0 all reported campos=[0 0 0]. Reject the identity-view case: - // rotation ~ identity AND translation ~ 0. bool rotIsIdentity = MathF.Abs(m[0] / n0 - 1f) < 1e-3f && MathF.Abs(m[1]) < 1e-3f && MathF.Abs(m[2]) < 1e-3f && MathF.Abs(m[4]) < 1e-3f && MathF.Abs(m[5] / n1 - 1f) < 1e-3f && MathF.Abs(m[6]) < 1e-3f && @@ -1498,13 +1236,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// Recovers the camera position from a STANDALONE view-projection matrix, by rebuilding - /// the rigid view rows: R.row0 = row0.xyz / fx, R.row1 = row1.xyz / fy, R.row2 = row3.xyz, - /// and t = (row0.w / fx, row1.w / fy, row3.w). Position = -R^T t, same convention as - /// , so the two can be compared directly - that comparison is the - /// probe's self-test on XC2 (both must land on the same coordinates). - /// private static void ViewProjPos(ReadOnlySpan m, float fx, float fy, out float x, out float y, out float z) { float r00 = m[0] / fx, r01 = m[1] / fx, r02 = m[2] / fx; @@ -1522,7 +1253,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static void ViewPos(ReadOnlySpan m, out float x, out float y, out float z) { - // view = [R|t] row-major, camera position = -R^T t. x = -(m[0] * m[3] + m[4] * m[7] + m[8] * m[11]); y = -(m[1] * m[3] + m[5] * m[7] + m[9] * m[11]); z = -(m[2] * m[3] + m[6] * m[7] + m[10] * m[11]); @@ -1657,10 +1387,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return fp; } - // ---- Structural validators: exact replicas of MvppCameraCapture's (kept private - // there on purpose - the probe must observe the SAME contract it diagnoses, without - // touching the capture code). ---- - private static bool IsOrthonormalView(ReadOnlySpan m) { if (MathF.Abs(m[12]) > 1e-3f || MathF.Abs(m[13]) > 1e-3f || diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScenePass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScenePass.cs index aa935a8d6..227d207bb 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScenePass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppScenePass.cs @@ -1,68 +1,9 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// ⛔⛔ 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 , qui ECARTE la pire - /// passe au lieu d'en ELIRE une. Ce fichier est conserve pour la trace, et parce que - /// 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. 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 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. - /// static class MvppScenePass { public static readonly bool Enabled = @@ -86,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _catchUpFramesSeen; private static long _lastReportMs; - /// - /// 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. - /// public static bool Allows(GpuChannel channel) { _qualThisFrame = true; @@ -110,12 +46,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - /// - /// Reconnait la passe de la scene a la FORME de sa cible couleur : attachee, non carree, - /// format flottant HDR. Meme critere que . 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. - /// internal static bool IsSceneColorPass(GpuChannel channel) { Image.Texture col0 = channel.TextureManager.RenderTargetColor0; @@ -130,17 +60,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return f == GAL.Format.R11G11B10Float || f == GAL.Format.R16G16B16A16Float; } - /// - /// 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. - /// 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; } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSoloCamera.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSoloCamera.cs index 60eaf824b..6d8669a01 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSoloCamera.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSoloCamera.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -9,41 +6,6 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// Standalone view-projection camera source (RYUJINX_MVPP_VPSOLO=1, OFF by default). - /// - /// WHY IT EXISTS. only recognises a camera stored as the - /// contiguous row-major triplet [view][proj][viewproj] inside one vertex-stage constant - /// buffer. Measured 21/07 on Xenoblade 2: that triplet exists NOWHERE (canonical 0 over - /// every run) because the game keeps view and proj in SEPARATE buffers, column-major -- so - /// MV++ never armed there, in three different configurations. The camera itself is present - /// and findable: what is missing is a contract that accepts a view-projection matrix ALONE. - /// - /// WHERE IT SITS. Strictly a FALLBACK: MvppCameraCapture calls in here only after its own - /// triplet scan has failed. On a game whose triplet is found (TOTK) this file is never - /// reached, so that path stays bit-identical by construction rather than by testing. - /// - /// HOW IT PICKS. Four rules, every one of them RELATIVE - no world units, no resolution, no - /// per-game anything. All four were forced on us by measurement, each after a wrong pick: - /// 1. the projection's aspect must match the MAIN render target's (largest non-square - /// depth), not the current draw's -- the 1 Hz tick often lands on a shadow pass; - /// 2. at least 2 DISTINCT buffers (counted by address) must report the same position; - /// 3. it must MOVE recently (score halved each election) and TRAVEL rather than hop - /// (straightness = net displacement / path walked; camera 0.46 vs per-object 0.065); - /// 4. once elected the location is held, and released the moment it fails a rule. - /// These reject, in order of discovery: NaN/absurd values, bare projections (a projection IS - /// a view-projection with an identity view), the [20000 20000 20000] placeholder, the - /// CAMERA-RELATIVE sky matrix (structurally perfect, same fx, pinned to the origin), a - /// matrix frozen after one load-time spike, and per-object transforms. - /// - /// IDENTITY, the thing that took five failed attempts to see: a cbuf SLOT is not an identity. - /// stage0 cbuf4 +0x000 was bound to 27 DIFFERENT buffers across 113 sightings, so every - /// statistic computed per slot was averaging unrelated objects. Learning is therefore keyed - /// on (physical address + offset). But the per-frame READ must use the slot: the camera sits - /// at a stable (stage, slot, offset) while its ADDRESS rotates through a ring buffer - /// (XC2: ...ED00 / ...EE00 / ...EF00, one continuous trajectory). Learn by address, read by - /// slot -- both halves are needed, and each is wrong on its own. - /// static class MvppSoloCamera { public static readonly bool Enabled = @@ -55,11 +17,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private const int MaxTracks = 64; private const int RingSize = 6; - // The same tolerances the probe validated on both games. They are world-unit constants - // and therefore the only part of this file that assumes anything about a game's scale: - // if a title ever measures its world in millimetres these have to become relative too. private const float PosAgreeEps = 0.5f; private const float TeleportEps = 1e5f; + + private static readonly float ElectMaxStep = + float.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_ELECT_MAXSTEP"), + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out float electMaxStep) && electMaxStep > 0f + ? electMaxStep + : 0f; private const float AbsurdPos = 1e6f; private struct Candidate @@ -76,8 +42,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public float Motion; public float Straightness; - // [VPSOLO_ROT 02/08] Direction de visee (rangee 3 du VP, normalisee, meme source que - // le "r3" du gate de capture). Renseignee seulement sous _rotMotion ; sinon zero. public float DirX; public float DirY; public float DirZ; @@ -96,7 +60,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public int RingCount; public float PathLength; - // [VPSOLO_ROT 02/08] Derniere direction vue (voir Candidate.Dir*). public float LastDirX; public float LastDirY; public float LastDirZ; @@ -117,92 +80,35 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly float[] _tmp = new float[16]; private static readonly float[] _read = new float[16]; - // Elected location, used for the cheap per-frame read. private static bool _hasElection; private static int _elStage; private static int _elSlot; private static int _elOffset; private static bool _elTransposed; - // ========================================================================================== - // [CAPTIME 31/07] SONDE DE TIMING DE CAPTURE -- LECTURE SEULE, ETEINTE PAR DEFAUT. - // - // LA QUESTION, ET UNE SEULE : la matrice qu'on capture a l'image N+1 etait-elle DEJA - // visible a l'emplacement elu pendant l'image N, apres notre capture ? - // OUI -> la camera existait, on l'a manquee : on capture trop tot (needCapture ne laisse - // passer QUE le premier dessin qualifiant de l'image). - // NON -> le jeu ne l'avait pas encore produite : le moment de capture n'est pas en cause. - // - // POURQUOI CETTE FORME PLUTOT QU'UNE COMPARAISON D'ADRESSE. Le commentaire du chemin - // courant le dit : « the ADDRESS behind (stage, slot) may well have rotated ». Comparer des - // octets a une adresse melangerait « la camera a ete reecrite » et « un autre tampon est - // lie » -- et avec ~125 passes qualifiantes par image, le second domine. Ici la comparaison - // est une EGALITE BIT-EXACTE sur 16 flottants contre une matrice precise : un tampon - // etranger ne peut pas coincider par hasard, donc la rotation d'adresse ne peut pas mentir. - // - // ⛔ N'APPELLE JAMAIS TryReadAt (qui incremente _fail[] et mene a AcceptRead, lequel deplace - // la reference de SNAPGUARD). Ne touche ni DEJITTER, ni MULTIADDR, ni l'election, ni aucun - // compteur de diagnostic. IsViewProj est utilise en FILTRE seulement -- verifie ligne a - // ligne le 31/07 : aucune ecriture d'etat statique dans tout son corps. public static readonly bool CapTime = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPTIME") == "1"; private const int CtMax = 16; - private static readonly float[] _ctHist = new float[CtMax * 16]; // valeurs BRUTES distinctes vues APRES la capture - private static readonly int[] _ctHistDraw = new int[CtMax]; // rang du dessin ou chacune est apparue + private static readonly float[] _ctHist = new float[CtMax * 16]; + private static readonly int[] _ctHistDraw = new int[CtMax]; private static int _ctCount; - private static int _ctBound; // dessins qualifiants ou le slot elu ETAIT lie <- la COUVERTURE - private static int _ctMissing; // dessins qualifiants ou il ne l'etait pas - private static int _ctDraw; // rang du dessin dans la fenetre post-capture - private static readonly float[] _ctPending = new float[16]; // brut du dernier TryReadAt, PRE-dejitter - private static readonly float[] _ctCaptured = new float[16]; // brut de la lecture ACCEPTEE de l'image + private static int _ctBound; + private static int _ctMissing; + private static int _ctDraw; + private static readonly float[] _ctPending = new float[16]; + private static readonly float[] _ctCaptured = new float[16]; private static bool _ctHasCapture; private static long _ctFrameId; - // ========================================================================================== - // [CAPTIME_FIX 31/07] MODE DE MESURE, ETEINT PAR DEFAUT. Ne corrige rien en aval. - // - // "Capturer la premiere valeur NOUVELLE au lieu de la premiere valeur." Tant que le lieu elu - // porte encore, octet pour octet, la valeur deja publiee, on laisse passer le dessin sans - // rien tenter ; des qu'une valeur differente apparait, la capture normale a lieu par le - // chemin normal, avec toutes ses gardes. - // - // CE QUE CETTE FORME PRESERVE, ET C'EST LA RAISON DE CE CHOIX : - // - UNE seule capture par image => SNAPGUARD voit un seul pas, comme aujourd'hui ; - // - UNE seule entree FIFO => l'appariement camera <-> image est intact ; - // - AUCUN second AcceptRead => aucune interaction DEJITTER/MULTIADDR ; - // - si aucune valeur nouvelle n'apparait de l'image, on ne capture pas : c'est EXACTEMENT - // le comportement d'aujourd'hui, et c'est le cas legitime (45 % des images, mesure du - // 31/07 : le jeu n'avait vraiment rien produit). - // - // MESURE DE REFERENCE A BATTRE, annoncee AVANT le run : A = 55 %, transitions A<->B = 6,2 %, - // couverture 160/0. Reussite = A -> ~0 ET transitions -> ~0 SANS hausse des compteurs - // d'intruses (_fail, refus SNAPGUARD, MULTIADDR). Si A tombe et que les intruses montent, - // c'est SCENEPASS qui recommence et on annule. public static readonly bool CapTimeFix = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPTIME_FIX") == "1"; - // ========================================================================================== - // [CAPTIME_AB 31/07] ALTERNANCE DANS UN SEUL RUN. RYUJINX_MVPP_CAPTIME_AB=, - // 0 = eteint. Le correctif s'allume et s'eteint par blocs de N presents, dans la MEME - // session, au MEME endroit, au MEME geste. - // - // POURQUOI CETTE FORME ET PAS DEUX RUNS. Mesure du 30/07 : le taux de publication vaut - // 83 / 70 / 63 / 60 % selon le seul ENDROIT dans le jeu. Comparer deux runs, c'est comparer - // deux endroits -- la chute de 14 points observee le 31/07 entre les runs 21:06 et 21:29 - // tombe entierement dans ce bruit et ne prouve RIEN, ni dans un sens ni dans l'autre. - // Precedent dans ce depot : NOLDR_AB, qui alternait par tranches de 600 images. - // - // CRITERES DE REUSSITE, ARRETES AVANT LE RUN : - // succes = A baisse cote ON ET transitions baissent ET ni les compteurs d'echec ni - // le taux de capture ne se degradent ; - // compromis/regression = A baisse MAIS la publication baisse. Pas un succes. private static readonly int _abBlock = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPTIME_AB"), out int b) && b > 0 ? b : 0; private static long _abPresents; private static bool _abArmed; - // [0] = cote ETEINT, [1] = cote ARME. private static readonly long[] _abFrames = new long[2]; private static readonly long[] _abCaptures = new long[2]; private static readonly long[] _abMoved = new long[2]; @@ -213,133 +119,46 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly long[] _abFailAtSwitch = new long[2]; private static char _abLastVerdict; - /// - /// [CAPTIME_AB] Le correctif est-il actif SUR CE PRESENT. En alternance c'est le bloc qui - /// decide ; sinon c'est la variable simple. Un seul point de verite, lu partout. - /// private static bool FixActive => _abBlock > 0 ? _abArmed : CapTimeFix; - // [CAPTIME_WHY 31/07] Compteurs de motifs du gate. Instrument seul. private static long _whyCalled, _whyOff, _whyArmedCall, _whyNoElection, _whyNoRef; private static long _whyUnbound, _whyReadable, _whyBadShape, _whyDiffers, _whyArmed; private static long _whyDeadline, _whyNetted; private static long _whyLogMs; - // ========================================================================================== - // [CAPTIME_MAX 31/07 nuit] LA BORNE. RYUJINX_MVPP_CAPTIME_MAX=, 0 = gate ETEINT. - // - // La forme SANS limite est morte le 31/07, tuee par alternance dans un seul run : - // publication 68 % cote arme contre 90 % cote eteint, sansCapture 406/10/171/174 contre - // 140/23/20, et tauxA qui ne baissait meme pas. Cause : quand aucune valeur nouvelle - // n'arrivait, on ne capturait JAMAIS. Meme cause que SCENEPASS le 29/07. - // - // Ici on laisse passer AU PLUS K dessins, puis on capture ce qu'il y a. Le nombre de - // captures par image reste donc 1, MATHEMATIQUEMENT et pas experimentalement. - // K vient de deux mesures independantes du 31/07 : la valeur nouvelle apparait au rang - // MEDIAN 39 sur ~160 dessins, et quand la capture s'est reellement deplacee elle l'a fait - // de 36 a 40 dessins. K = 60 laisse donc ~1,5x de marge au-dessus du besoin mesure, tres - // loin sous les ~160 disponibles. - // - // ⛔ Le gate ne s'arme QUE si K > 0 : la forme non bornee est desormais impossible a lancer - // par accident. private static readonly int _ctMaxSkip = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAPTIME_MAX"), out int k) && k > 0 ? k : 0; - // FILET : si l'image PRECEDENTE s'est terminee sans capture, le gate ne s'arme pas du tout - // sur celle-ci. Le gate ne peut donc coûter au maximum QU'UNE image ratee d'affilee, meme - // sur une image pauvre en dessins qualifiants -- le trou par lequel la version sans limite - // affamait la camera. private static bool _ctPrevFrameMissed; - // [RANKPROBE 01/08] Sonde du RANG D'ARRIVEE, demandee par Alex apres (293). - // RYUJINX_MVPP_RANKPROBE=1, OFF par defaut. Lecture seule, AUCUNE lecture memoire - // nouvelle : tout vient de _ctHist/_ctHistDraw, deja remplis par NoteLateDraw. - // Question binaire : quand l'image n'a pas de valeur neuve dans ses premiers dessins, - // la valeur arrive-t-elle plus tard dans la MEME image (rang 60-160), ou JAMAIS avant - // la fin d'image ? Le rang est compte parmi les dessins qualifiants APRES la capture - // (gate eteint = capture au 1er dessin, donc rang ~= rang absolu). private static readonly bool _rankProbe = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_RANKPROBE") == "1"; - private static readonly int[] _rkBuckets = new int[6]; // <=40, 41-60, 61-80, 81-120, 121-160, >160 - private static int _rkNever; // couverture > 0 et aucune valeur differente vue - private static int _rkNoCover; // slot jamais lie sur l'image : rien pu voir, ECARTE - private static int _rkSaturated; // table des distinctes pleine : rang peut-etre manque, compte A PART + private static readonly int[] _rkBuckets = new int[6]; + private static int _rkNever; + private static int _rkNoCover; + private static int _rkSaturated; private static int _rkFrames; private static long _rkDrawSum; private static long _rkLogMs; - private static int _ctPreDraw; // dessins laisses passer AVANT la capture, cette image - private static int _ctMovedFrames; // images ou la capture a ete deplacee (rang > 1) + private static int _ctPreDraw; + private static int _ctMovedFrames; private static int _ctTotalFrames; private static long _ctPreDrawSum; private static long _ctMovedLogMs; private static long _lastElectionMs; - // ========================================================================================== - // [ELECT_HYST 01/08] HYSTERESIS DE SORTANT. RYUJINX_MVPP_ELECT_HYST=, 0 = COUPE - // (comportement d'aujourd'hui, a l'octet pres sur le chemin de decision). - // - // POURQUOI (verifie le 01/08 sur les 45 logs du 29/07, AUDIT Phase 2 bis) : le scrutin ne - // se tient QUE sur un dessin ou le sortant est illisible -- et chaque raison d'illisibilite - // l'exclut aussi du scrutin. Le sortant ne peut donc jamais gagner sa propre reelection - // (mesure : 35 elections / 35 changements le 01/08). Consequence mesuree : 47 elections de - // cbuf5+0x000 le 29/07, 44 sur les deux points fixes intrus (546/626 u), salve SNAPGUARD - // co-horodatee a +32 ms, une adoption forcee par cycle. On ne repare pas le vote, on - // refuse de le tenir tant que le sortant est vivant. - // - // REGLE : tant que le siege elu a valide STRUCTURELLEMENT il y a moins de ms - // (TryReadAt vrai au chemin commun -- independant du verdict des gardes), l'election ne - // s'ouvre pas. En regime sain le siege revalide plusieurs fois par image : un dessin - // etranger ne l'age pas. Une vraie mort (bloc deplace, chargement, coupure) l'age au-dela - // de la barre et l'election s'ouvre comme aujourd'hui. - // - // FILET ANTI-VERROUILLAGE (ratio fixe 2x, pas une variable de plus) : si RIEN n'a ete - // ACCEPTE depuis 2* ms (_lastAcceptMs -- la publication est affamee pendant que le - // siege pretend vivre), l'election s'ouvre malgre la tenue. Un siege porteur d'une autre - // camera qui ne nourrit jamais la publication tombe donc en 2* ms au plus. - // ⚠️ Ce filet ne couvre PAS le cas « adoption forcee puis intruse acceptee en continu » : - // ce cas est celui de SNAPGUARD aujourd'hui, INCHANGE -- et son declencheur (l'election - // de l'intruse) est precisement ce que la tenue supprime. private static readonly int _electHystMs = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_ELECT_HYST"), out int hy) && hy > 0 ? hy : 0; - // [ELECT_HYST_AB 01/08] Alternance DANS le run par tranches de temps fixes (30 s), pilotee - // par l'horloge du fil GPU -- aucun etat partage entre fils, aucun crochet de present. - // Jamais deux runs : la reference varie de 4 a 13 elections/min selon le run (29/07). - // ⚠️ Contamination de bord assumee et CONSERVATRICE : une tranche armee laisse un siege - // sain a la tranche eteinte suivante, ce qui REDUIT l'ecart mesure, jamais ne l'invente. private static readonly bool _electHystAb = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_ELECT_HYST_AB") == "1"; - // [ELECT_WARP 01/08] Le bouchon du « flash » : RYUJINX_MVPP_ELECT_WARP=1, OFF par defaut. - // Voir le site d'usage dans TryGetViewProjection, apres l'election. private static readonly bool _electWarp = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_ELECT_WARP") == "1"; - // ========================================================================================== - // [VPSOLO_ROT 02/08] LA ROTATION COMPTE COMME MOUVEMENT. RYUJINX_MVPP_VPSOLO_ROT=1, OFF par - // defaut (ferme = comportement a l'octet pres : aucun champ ci-dessous n'est lu). - // - // POURQUOI (journal (345)-(346), campagne BOTW Quality) : Track() mesure la vie d'une - // candidate par le DEPLACEMENT DE SA POSITION. Un jeu en rendu monde-relatif-camera (BOTW : - // vraie camera a cbuf3 +0x070^T, campos ~[0,0,0] MEME EN COURANT, mouvement mesure - // 0,02-1,8/scrutin) reste sous la barre des 10 % de la regle 3 des qu'une candidate - // monde-espace existe (le triplet cbuf8, ~7/scrutin en course) — et celle-ci n'a qu'UNE - // source (votes < 2). Resultat mesure : AUCUNE election en 4 min, zero ligne, famine - // 22-67 % en mouvement, images fantomes. La vie d'une camera, c'est position OU rotation. - // - // CALIBRAGE de RotMotionScale : un pan modere (~30 deg/s) donne un pas de direction - // ~0,5/scrutin (1 s) => x30 = ~15 unites-equivalentes, meme ordre qu'une course (5-7 u/s). - // Un micro-bruit de direction (0,001) => 0,03 : sous tout seuil. Le pas de direction est - // borne par construction (<= ~1,41 signe-insensible) => rotStep <= ~42, loin de tout budget - // teleport. Signe-insensible car la rangee 3 vaut +/-R.row2 selon la convention. - // - // ⚠️ INTERACTION CONNUE, NON TRAITEE ICI : SKYREJECT rejette les candidates epinglees a - // l'origine relativement a la plus lointaine — sur un jeu monde-relatif il rejetterait la - // VRAIE camera (et la matrice ciel tourne aussi, donc pas d'exemption par rotation). Ne pas - // armer SKYREJECT avec ce gate sur un jeu monde-relatif tant que ce dossier n'est pas fait. private static readonly bool _rotMotion = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_VPSOLO_ROT") == "1"; @@ -348,28 +167,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static bool _rotArmedLogged; private static bool _armWitnessLogged; - // ========================================================================================== - // [MIRRORS M1 01/08] LE DETOUR DE PEREMPTION. RYUJINX_MVPP_MIRRORS=1, OFF par defaut - // (variable absente => seul delta : la copie brute de TryReadAt s'arme aussi sous _mirrors, - // qui est faux => comportement a l'octet pres). - // - // POURQUOI (journal 291-299) : pendant un gel, MULTIADDR ne tourne PAS — le repli n'existe - // que sur echec STRUCTUREL, or un siege perime lit avec SUCCES (la vieille valeur est une - // camera valide). Sur une image de gel, l'ensemble consulte = le siege seul, pendant que la - // valeur neuve vit dans un autre miroir lisible (~95 % des echantillons de gel, (298)). - // - // REGLE M1 : si la valeur BRUTE du siege est identique octet pour octet a celle vue a la - // derniere image capturee (siege perime), consulter l'historique EXISTANT (TryReadHistory : - // saute le siege, saute les valeurs egales a la derniere retenue, exige dot>0, MULTIROT, - // choisit LA MEILLEURE) et, si un miroir frais existe, accepter CETTE valeur par le chemin - // normal (AcceptRead, tous les gardes). Rien trouve ou refuse => le chemin d'aujourd'hui - // reprend tel quel. UNE acceptation/publication par image, jamais deux. Aucune decouverte - // de miroir neuve (M2 = plus tard), aucune liste d'adresses, election/tenue intouchees. - // - // [MIRRORS_AB] Alternance dans le run par tranches de 30 s (meme patron que ELECT_HYST_AB), - // fil GPU seul. Metriques de verdict : les figes (`camera VP changed`, inconditionnel) par - // cote — NON trichables ici, le detour ne publie que des valeurs LUES — plus la ligne - // `MVPP mirrors:` 5 s ci-dessous. private static readonly bool _mirrors = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIRRORS") == "1"; @@ -383,14 +180,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static bool _mirHasSeatLast; private static bool _mirAbArmed; private static bool _mirAbKnown; - private static int _mirStale; // images ou le siege etait perime (octets identiques) - private static int _mirTried; // detours tentes (perime + MULTIADDR arme + cote arme) - private static int _mirFound; // l'historique avait un miroir frais (dot>0, MULTIROT ok) - private static int _mirAccepted; // ... et les gardes l'ont accepte (publie) - private static int _mirRefused; // ... mais un garde l'a refuse (le chemin normal reprend) - private static int _mirNone; // rien de frais dans l'historique + private static int _mirStale; + private static int _mirTried; + private static int _mirFound; + private static int _mirAccepted; + private static int _mirRefused; + private static int _mirNone; private static long _mirLogMs; - private static int _mirSeatStage, _mirSeatSlot, _mirSeatOffset; // dernier miroir CHOISI (diagnostic) + private static int _mirSeatStage, _mirSeatSlot, _mirSeatOffset; private static void MirrorLog(long now) { @@ -420,119 +217,70 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static long _lastSeatOkMs; private static bool _hystAbArmed; private static bool _hystAbSideKnown; - private static int _hystBlocked; // elections bloquees par la tenue du siege - private static int _hystExpired; // ouvertes car siege mort depuis >= hyst - private static int _hystStarved; // ouvertes par le filet de famine (>= 2x hyst) + private static int _hystBlocked; + private static int _hystExpired; + private static int _hystStarved; private static long _hystLogMs; private static long _liveMainArea; private static float _mainAspect; private static int _elections; + + private static int _electMaxStepRejected; + + private const int ElectBannedMax = 16; + private static readonly int[] _electBanned = new int[ElectBannedMax]; + private static int _electBannedCount; + + private static int ElectBanKey(int stage, int slot, int offset) + { + return (stage << 24) | (slot << 16) | (offset & 0xFFFF); + } + + private static bool ElectIsBanned(int stage, int slot, int offset) + { + int key = ElectBanKey(stage, slot, offset); + + for (int i = 0; i < _electBannedCount; i++) + { + if (_electBanned[i] == key) + { + return true; + } + } + + return false; + } private static long _lastLogMs; private static int _reads; private static int _readFails; - // ---- [CAMGUARD 27/07] Continuity guard on the READ path. Gate RYUJINX_MVPP_CAMGUARD=1, - // OFF by default => without the variable this file behaves exactly as before. - // - // WHY. The camera lives at a stable (stage, slot, offset) while its ADDRESS rotates - // through a ring buffer, so the per-frame read is keyed on the SLOT. That read validates - // structure only (IsViewProj + the square-projection guard + absurd-position): it has no - // notion of WHICH camera it just read. Xenoblade 2 legitimately runs more than one - - // Alex points out the game's own adjustable gameplay camera, and the minimap is rendered - // from another - so when the slot happens to hold a different one, a perfectly valid - // matrix for a DIFFERENT viewpoint is handed to the reprojection. - // - // The cost of one such frame is not subtle: the camera delta becomes enormous, every - // motion vector explodes, gets clipped to MAXMOTION and flips sign. Measured 27/07 in the - // proj audit (153 samples at 1 Hz): three distinct focal lengths where one camera has - // one, a position jump of 34699 units, and the [20000 20000 20000] placeholder handed out - // despite the election claiming to reject it. - // - // WHAT IT DOES. Compares each read against the previous accepted one. A jump beyond - // MaxJump world units in a single frame, or a focal change beyond MaxFocalDrift, is a - // teleport rather than movement: the read is refused and the LAST GOOD matrix is returned - // instead. The frame then sees a still camera (zero reprojection) rather than a stranger, - // which is the same conservative fallback the pipeline already uses when a camera is held. - // - // WHY THESE THRESHOLDS. Measured on Alex's own run: median displacement 5.8 units PER - // SECOND, i.e. under 0.1 per frame at 60 fps; only 2 samples out of 152 exceeded 50, and - // both were the placeholder. 25 units in ONE frame is 1500 units/second - far beyond any - // real camera, including pushing the view in and out, so genuine motion cannot trip it. private static readonly bool _camGuard = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CAMGUARD") == "1"; - // [CAMGUARD v2 27/07] The budget is a SPEED, not a fixed distance. A fixed 25-unit step was - // wrong and I caught it before Alex tested it: the same run showed the elected location - // failing for long stretches (5 successful reads against 1278 misses over 5 seconds), and - // after such a gap a perfectly legitimate update carries a whole second of camera travel. - // A fixed threshold would have refused exactly the updates that matter and frozen the - // camera further. So the allowance grows with the time since the last accepted read. - // - // MaxSpeed is 150 world units per second against a measured median of 5.8 - a 25x margin, - // so ordinary play cannot reach it even while pushing the view in and out. The elapsed - // time is capped at CatchUpCapSec so an arbitrarily long gap cannot buy an arbitrarily - // large teleport, and MinBudget keeps a floor for back-to-back reads where dt is ~0. - // The measured intruders are 150 and 34699 units: both stay far outside every budget. private const float MaxSpeed = 150f; private const float MinBudget = 5f; private const float CatchUpCapSec = 2f; private const float MaxFocalDrift = 0.15f; - // ---- [DEJITTER 27/07] Gate RYUJINX_MVPP_DEJITTER=1, OFF by default => without the - // variable this file behaves exactly as before, bit for bit. - // - // WHAT WAS MEASURED. Camera immobile, controller down, 600 consecutive reads: the - // reconstructed position takes EXACTLY EIGHT values, each appearing ~75 times out of 600, - // cycling forever in the same order, spread over 0.14 world units, with a step of 0.03 to - // 0.13 units EVERY frame. Eight values in perfect rotation is not movement and not noise: - // it is a periodic sub-pixel sequence. The game runs its own temporal AA and jitters its - // projection on an 8-phase pattern. - // - // WHY IT REACHES US. A jitter is applied as row0 += jx*row3 and row1 += jy*row3 (a shear - // of the projection in x and y). ViewProjPos rebuilds the translation from m[3] and m[7], - // which the shear has just contaminated, so a purely sub-pixel offset is read back as the - // camera having MOVED. The reprojection then writes ~1 px of motion, alternating in sign - // with the phase - which is exactly the flicker Alex sees while standing still, and part - // of the sliding while moving, since the false step adds to the real one. - // - // IsViewProj cannot notice: it accepts |dot(row0,row3)|/n0 up to 0.02, and the measured - // jitter sits inside that tolerance. It was written to reject strangers, not shears. - // - // THE FIX. Remove from rows 0 and 1 whatever component lies along row3 - that component IS - // the jitter, by construction, since an unjittered perspective has rows 0/1 orthogonal to - // row3. Nothing else is touched: rotation, focal lengths and depth mapping are unchanged, - // and a game that does NOT jitter has a zero component removed, so this is a no-op there. - // Motion vectors must describe scene movement, never the sampling pattern; DLSS is told - // about the jitter through its own dedicated input, not through the vectors. private static readonly bool _deJitter = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEJITTER") == "1"; - // [WHYFAIL 27/07] Why the per-frame read fails, by reason. Measured on Alex's run: 382 - // successful reads against 11 713 failures - a 3.2 % success rate - which means SKYROT is - // handed two camera matrices that are often identical or several frames apart. That alone - // could produce the elastic sky: no rotation for three frames, then three frames' worth at - // once. The code counted a single anonymous "miss" for five very different causes, so the - // number said something was wrong without ever saying what. Counting only; no behaviour - // changes, and the reasons are mutually exclusive by construction (each is a return path). private const int FailReasons = 7; private static readonly int[] _fail = new int[FailReasons]; private static readonly string[] _failNames = { - "buffer absent", // 0: nothing bound at the elected slot this draw - "hors bornes", // 1: the offset no longer fits the buffer - "pas une camera", // 2: IsViewProj rejects the shape - "projection carree", // 3: cubemap/environment camera guard - "aspect", // 4: ASPECTLOCK - not the main render target's ratio - "position absurde", // 5: NaN or beyond AbsurdPos - "matrice a l'origine",// 6: SKYREJECT - camera-relative (sky) + "buffer absent", + "hors bornes", + "pas une camera", + "projection carree", + "aspect", + "position absurde", + "matrice a l'origine", }; private static long _lastFailLogMs; - // [ALTPROBE 28/07] Vrai pendant que la sonde essaie les adresses PRECEDEMMENT elues. Les - // echecs de ces essais ne doivent pas entrer dans les compteurs de motifs : ils - // gonfleraient « pas une camera » d'un facteur 3 et l'instrument mentirait. private static bool _probing; private static bool Fail(int reason) @@ -579,36 +327,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - /// - /// [DUPPAIR 28/07] Instantane des motifs d'echec, pour le compte-rendu 1 Hz de la sonde - /// DUPPAIR (cote capture). Lecture seule ; ne remet rien a zero, l'appelant fait ses - /// propres deltas -- le logger 5 s du mode dev, lui, remet a zero, et les deux ne - /// doivent pas se voler leurs compteurs. - /// - // [ALTPROBE 28/07] Historique des dernieres adresses elues, distinctes. SONDE SEULE : quand - // la lecture a l'adresse elue echoue, la camera est-elle a l'une des precedentes ? - // - // Ce qui la motive : on obtient ~1 lecture reussie par image alors que le jeu bouge sa - // camera a CHAQUE image (prouve par le pas double apres chaque trou), et 100 % des echecs - // sont « pas une camera » A L'ADRESSE SURVEILLEE. Si le jeu fait tourner son bloc de - // constantes entre plusieurs tampons, n'en garder qu'un en cache EST le bug -- et alors la - // camera se trouve, au meme instant, a une adresse qu'on connait deja. - // - // Ne change ni la valeur rendue, ni l'election, ni le rendu : elle compte, c'est tout. private static readonly bool _altProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUPPAIR") == "1"; - // [MULTIADDR 28/07] Le correctif : lire les adresses DEJA ELUES quand l'adresse courante - // ne porte pas la camera. OFF par defaut -> sans la variable, rien n'est lu en plus et le - // comportement est identique a l'octet pres. private static readonly bool _multiAddr = Environment.GetEnvironmentVariable("RYUJINX_MVPP_MULTIADDR") == "1"; - // Trace de trajectoire (matrice aplatie) : les deux dernieres cameras RETENUES. Elles - // donnent la direction dans laquelle la camera evolue, seul moyen de distinguer une - // valeur qui AVANCE d'une valeur PERIMEE dormant dans un autre tampon -- une position - // seule ne le peut pas, puisqu'un pan ne deplace presque pas l'oeil (mesure : moins d'une - // unite d'ecart, y compris pour les valeurs neuves). private static readonly float[] _goodFlat = new float[16]; private static readonly float[] _goodFlatPrev = new float[16]; private static int _goodCount; @@ -623,11 +347,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed dst[12] = m.M41; dst[13] = m.M42; dst[14] = m.M43; dst[15] = m.M44; } - /// - /// [MULTIADDR] Projection du deplacement propose sur le deplacement recemment observe. - /// Positif = la valeur continue le mouvement, negatif = elle revient en arriere (valeur - /// perimee). Neutre (1) tant que deux cameras n'ont pas encore ete retenues. - /// private static float TrajectoryDot(in Matrix4x4 cand) { if (_goodCount < 2) @@ -649,32 +368,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly float[] _candFlat = new float[16]; - // [MULTIROT 28/07] Garde d'AMPLITUDE sur les candidats de MULTIADDR. Le choix ne reposait - // que sur la DIRECTION (TrajectoryDot > 0, « la valeur continue le mouvement ») et ne - // regardait jamais de COMBIEN. Une matrice appartenant a une AUTRE camera passe donc des - // qu'elle pointe grossierement dans le meme sens. - // - // Mesure du 28/07 (sonde JUMPDIST, cote consommateur) : avec MULTIADDR arme, 10 a 26 sauts - // de rotation par fenetre de 5 s valant tous ~1,13-1,17 ; MULTIADDR coupe, 0 a 3. Aucune - // camera ne tourne de 1,13 en une image : les mouvements legitimes mesures montent au plus - // a ~1,0, l'immense majorite vivant sous 0,1. Un candidat au-dela du seuil n'est pas une - // continuation, c'est une autre camera. - // - // Pourquoi ici et pas cote consommateur : si aucun candidat ne passe, on ne substitue - // RIEN et on retombe sur le comportement d'origine. Pas de camera figee, pas de plafond - // de rejets consecutifs a regler -- les deux faiblesses de la garde cote consommation. private static readonly float _multiMaxRot = float.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_MULTIADDR_MAXROT"), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float mr) && mr > 0f ? mr : 0f; private static int _multiFar; - // [ELECTROT 28/07] Garde d'amplitude sur la lecture PRINCIPALE (voir AcceptRead). - // [ELECTROT] Refus CONSECUTIFS avant acceptation forcee. 1 par defaut : une intruse ne - // dure qu'une image isolee et reste filtree, tandis qu'une VRAIE coupure de plan persiste - // sur l'image suivante et passe aussitot. A 8 (premier essai), chaque changement de plan - // de cinematique figeait la camera assez longtemps pour faire flicker le personnage -- - // regression rapportee par Alex le 28/07 au soir. private static readonly int ElectRotMaxStreak = Math.Max(1, (int)(float.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_ELECTROT_STREAK"), System.Globalization.NumberStyles.Float, @@ -688,34 +387,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _electRejected; private static int _electForced; - // [SUBPROBE 29/07] LA question qui decide s'il existe un correctif, posee avant d'en ecrire un. - // - // CE QUI EST MESURE, ET D'OU CA VIENT (run du 29/07 08h14, sonde JUMPDIST cote consommateur) : - // le slot elu adopte par moments une AUTRE camera. Signature = des sauts de rotation dont la - // valeur SE REPETE dans la bande 1,00-1,15 (1,10401x4, 1,06384x4, 1,13291x3, 1,08205x3...), - // ~26 en 85 s, contre 3 singletons de valeurs toutes differentes sur les 3 minutes qui - // precedent. Une valeur repetee a 5 decimales = deux etats FIXES, pas un geste. - // Ces intrusions n'ont PAS pu passer par les adresses de secours : MULTIROT y refuse deja - // tout candidat au-dela de 0,5, et celles-ci valent 1,06. Elles entrent donc par la lecture - // du slot elu lui-meme. Ce point est deductif, il n'a pas besoin d'un test de plus. - // - // POURQUOI UNE SONDE ET PAS UN BOUTON : la voie « refuser la lecture » est FERMEE - // (ELECTROT, 28/07). Refuser laisse la paire (curr, prev) immobile, donc l'image suivante - // voit un vecteur qui couvre DEUX intervalles : le defaut est deplace d'une image, pas - // supprime. Aucun reglage de seuil ni de plafond ne change ca. La seule voie restante est - // de SUBSTITUER -- lire la bonne camera ailleurs -- et elle n'existe que si la bonne camera - // est effectivement disponible ailleurs A CET INSTANT. C'est ce que cette sonde repond : - // secours = 0 -> la voie est morte, chercher ailleurs, ne rien coder - // secours > 0 -> la substitution est possible, et on sait a quel rang aller la chercher - // - // ELLE MESURE AUSSI LE DISCRIMINANT DE POSITION, parce que la mesure du 29/07 dit qu'il - // separe mieux que la rotation : ecart de translation 484-563 unites pour l'intruse contre - // 10-86 pour un vrai grand mouvement de camera (un vide de facteur 6), la ou les bandes de - // ROTATION se touchent (legitime jusqu'a 0,99 contre intruse 1,00-1,15). Le temoin - // « legitime » de la ligne de journal donne le cote sain du vide, sur le meme run : c'est - // la lecon du 28/07, un seuil se calibre sur le regime ou il s'appliquera. - // - // LECTURE SEULE : ne change ni la valeur rendue, ni l'election, ni le rendu. Defaut 0 = coupe. private static readonly float _subProbe = float.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_SUBPROBE"), System.Globalization.NumberStyles.Float, @@ -733,10 +404,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static float _okDistMax; private static long _subLogMs; - /// - /// [ELECTROT] Plus grand ecart element-a-element sur le bloc 3x3 de ROTATION entre deux - /// matrices. Meme mesure que RotGap, mais contre une reference explicite. - /// private static float RotGapTo(in Matrix4x4 a, in Matrix4x4 b) { Flatten(in a, _candFlat); @@ -757,11 +424,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly float[] _goodFlatPrev2 = new float[16]; - /// - /// [MULTIROT] Plus grand ecart element-a-element sur le bloc 3x3 de ROTATION, entre un - /// candidat et la derniere camera retenue. La rotation est choisie exprès : le jitter - /// sous-pixel du jeu vit dans la projection et la translation, jamais dans ce bloc. - /// private static float RotGap(in Matrix4x4 cand) { if (_goodCount < 1) @@ -784,12 +446,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return m; } - /// - /// [MULTIADDR] Cherche la camera aux adresses deja elues. Retient celle qui va le PLUS - /// LOIN dans le sens du mouvement en cours : les tampons du jeu tournent, plusieurs - /// peuvent etre valides en meme temps et tous ne portent pas la meme fraicheur. Une - /// valeur qui recule est refusee, jamais substituee. - /// private static bool TryReadHistory(GpuChannel channel, out Matrix4x4 best, out float bx, out float by, out float bz) { best = default; @@ -831,7 +487,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // [MULTIROT] La direction ne suffit pas : verifier de COMBIEN (voir _multiMaxRot). if (_multiMaxRot > 0f && RotGap(in cand) >= _multiMaxRot) { _multiFar++; @@ -839,10 +494,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // [v1] On garde celui qui va le PLUS LOIN dans le sens du mouvement, pas le - // premier acceptable : plusieurs tampons peuvent etre valides en meme temps et - // tous ne portent pas la meme fraicheur. La v2 s'arretait au premier et - // promouvait son adresse ; c'est ce qui a ramene le flick. if (!found || dot > bestDot) { found = true; @@ -852,8 +503,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed by = cy; bz = cz; - // [MIRRORS M1] Diagnostic seul : QUEL siege a fourni la meilleure valeur. - // Ecriture de champ pure, aucune decision ne la lit. _mirSeatStage = stage; _mirSeatSlot = slot; _mirSeatOffset = offset; @@ -873,16 +522,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return found; } - // [RETOUR A LA v1, 28/07] 4, pas 12. - // - // La v2 elargissait l'historique a 12 adresses (tous les candidats qui s'accordent en - // position avec l'elu) et prenait la PREMIERE acceptable au lieu de la meilleure. Elle - // n'a rien apporte de mesurable -- les recuperations sont restees a ~4 par seconde -- et - // Alex a constate le retour du flick ciel->montagnes que la v1 avait fait disparaitre. - // Une adresse "acceptable" prise en premier peut etre plus vieille que la meilleure. - // - // Regle qui en sort : ne pas elargir un mecanisme valide par l'oeil pour ameliorer un - // chiffre qui, lui, ne bougeait pas. private const int HistMax = 4; private static readonly (int Stage, int Slot, int Offset, bool Transposed)[] _hist = new (int, int, int, bool)[HistMax]; @@ -919,15 +558,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _hist[0] = (stage, slot, offset, transposed); } - /// - /// [SUBPROBE 29/07] Lecture seule. Appelee juste avant qu'une matrice devienne la nouvelle - /// camera retenue, l'ancienne etant encore intacte : c'est exactement la paire que JUMPDIST - /// mesure cote consommateur, donc les deux instruments sont comparables chiffre a chiffre. - /// Compte-rendu a 1 Hz, avec son PROPRE minuteur -- volontairement : le compte-rendu - /// d'ALTPROBE, lui, est enferme dans le bloc d'une autre sonde et n'a jamais ete emis une - /// seule fois de tout le dossier. Une sonde dont la sortie depend d'un autre interrupteur - /// est une sonde muette. - /// private static void SubProbe(GpuChannel channel, in Matrix4x4 vp, float apx, float apy, float apz, long now) { float dx = apx - _lastPx; @@ -939,8 +569,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (rot < _subProbe) { - // Adoption ordinaire. Elle sert de TEMOIN : c'est le cote sain du vide, mesure sur - // le meme run et le meme regime que les intrusions. _okRotMax = Math.Max(_okRotMax, rot); _okDistMax = Math.Max(_okDistMax, dist); } @@ -953,8 +581,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed bool found = false; - // Les echecs des essais ci-dessous ne doivent pas entrer dans les compteurs de - // motifs, sinon l'instrument ment (meme precaution qu'ALTPROBE). _probing = true; try @@ -979,9 +605,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // Candidat proche en rotation. Sa distance compte aussi : substituer une - // rivale eloignee reintroduirait l'intrus que SNAPGUARD a mis une journee - // a chasser. float ex = bpx - _lastPx; float ey = bpy - _lastPy; float ez = bpz - _lastPz; @@ -1054,10 +677,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Array.Clear(_subByRank); } - /// - /// [ALTPROBE] Essaie les adresses precedemment elues, sans rien changer. Appelee seulement - /// quand la lecture principale a echoue et que la sonde est armee. - /// public static void ProbeAlternates(GpuChannel channel) { _probing = true; @@ -1079,11 +698,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _altFound++; _altByRank[i]++; - // [ALTDIST 28/07] LA question avant tout correctif : est-ce LA MEME camera, - // ou une rivale (ombre, reflet, interface) ? Une caisse par ordre de - // grandeur, avec l'echelle deja mesuree le 27/07 : un pas ordinaire vaut - // 0,2 (99e centile 0,7), l'intrus attrape par SNAPGUARD valait 400. Lire une - // rivale reintroduirait exactement ce qu'il a coute une journee a chasser. if (_hasLastGood) { float dx = apx - _lastPx; @@ -1099,8 +713,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _altDistMax = dist; } - // Et surtout : cette valeur apporterait-elle quelque chose ? Si elle est - // identique a la derniere retenue, la lire ne comblerait aucun trou. if (alt.Equals(_lastGoodVp)) { _altSame++; @@ -1123,7 +735,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _altNone++; } - /// [ALTPROBE] Compte-rendu 1 Hz, remis a zero a chaque lecture. public static string AltSummaryAndReset() { var sb = new System.Text.StringBuilder(); @@ -1177,10 +788,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static float _lastJx, _lastJy; private static int _rejectLogs; - /// - /// [GAMEJITTER 28/07] Le decalage sous-pixel de la DERNIERE LECTURE ACCEPTEE, en NDC. Lu par - /// la capture au moment ou elle pousse la matrice, pour que les deux voyagent ensemble. - /// public static float LastJitterX => _lastAcceptedJx; public static float LastJitterY => _lastAcceptedJy; @@ -1188,22 +795,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static float _lastAcceptedJx; private static float _lastAcceptedJy; - // [JITPROBE v2, 28/07] Le decalage sous-pixel que LE JEU applique a sa projection, mesure - // sur la matrice elue et rapporte ICI, dans le fichier qui possede deja la donnee. - // - // ⚠️ La v1 faisait transiter la valeur par un champ AJOUTE dans Ryujinx.Graphics.GAL, ce qui - // a oblige a redeployer cette DLL partagee dans la copie de test -- defaut visuel neuf, et - // le binaire precedent perdu. Regle qui en sort : UNE SONDE NE TOUCHE PAS A UNE - // BIBLIOTHEQUE PARTAGEE. Si elle y oblige, elle ne vaut pas son prix. - // - // Ce qu'on cherche : XC2 decale sa projection sur un motif a 8 PHASES (mesure le 27/07 : - // la position camera prenait exactement huit valeurs). On declare zero decalage a DLSS. Si - // le jeu decale et qu'on annonce zero, DLSS recale chaque echantillon au mauvais endroit - // selon un cycle de 8 images -- environ 3,7 fois par seconde. Ca colle a « ca bouge un peu - // et ca revient », a « seulement avec DLSS », et surtout au fait que le defaut est - // IDENTIQUE entre deux familles de modele : une erreur de geometrie, pas de modele. - // - // Journal seul. Aucune valeur du rendu n'est touchee. private static readonly bool _jitProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_JITPROBE") == "1"; @@ -1223,7 +814,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Le decalage est en NDC (x_ndc += jx) ; en pixels c'est jx * largeur / 2. float px = jx * _mainWidth * 0.5f; float py = jy * _mainHeight * 0.5f; @@ -1233,8 +823,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _jitMinY = MathF.Min(_jitMinY, py); _jitMaxY = MathF.Max(_jitMaxY, py); - // Le nombre de valeurs DISTINCTES est le seul chiffre qui prouve un motif : huit - // valeurs qui reviennent, c'est un cycle ; un nuage de valeurs, c'est du bruit. bool seen = false; for (int i = 0; i < _jitSeenN; i++) @@ -1283,128 +871,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _jitSeenN = 0; } - // [ASPECTLOCK 27/07] Gate RYUJINX_MVPP_ASPECTLOCK=1, OFF by default. See the check in - // TryReadAt: it is rule 1 of the election, finally applied to the per-frame read too. private static readonly bool _aspectLock = Environment.GetEnvironmentVariable("RYUJINX_MVPP_ASPECTLOCK") == "1"; - // [SKYREJECT 27/07] Gate RYUJINX_MVPP_SKYREJECT=1, OFF by default. See the check in - // TryReadAt: the election's rejection of camera-relative matrices, applied to the read. private static readonly bool _skyReject = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SKYREJECT") == "1"; - // [SNAPGUARD 27/07] Gate RYUJINX_MVPP_SNAPGUARD=1, OFF by default. See AcceptStep. private static readonly bool _snapGuard = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SNAPGUARD") == "1"; - // 20x the largest step of the last ~2 s. Ordinary steps measured at 0.2 median / 0.7 at the - // 99th percentile against an intruder at 400: any factor between 5 and 100 gives the same - // verdict, so this is a separation, not a tuning. Three consecutive refusals mean the - // camera genuinely moved and the scale is relearned. private const float SnapFactor = 20f; private const int SnapWindow = 120; private const int SnapConfirm = 3; - // [SNAPHOLD 29/07] Remplace la REGLE DE REDDITION de SNAPGUARD. Defaut 0 = regle d'origine, - // chemin d'execution inchange. - // - // CE QUI A ETE MESURE (run 08h40, sondes SUBPROBE + le journal de SNAPGUARD lui-meme) : - // le garde VOIT deja les intruses -- 49 refus, avec des sauts de 537,27 / 546,19 / 629,05 - // unites -- et il en laisse quand meme passer 13. Ce n'est donc pas un defaut de detection, - // c'est la reddition qui fuit. Deux causes, distinctes : - // - // 1. IL CEDE APRES 3 REFUS CONSECUTIFS. L'intruse arrive en SALVES de plusieurs images - // d'affilee : atteindre 3 ne lui coute rien. Le critere « ca insiste donc c'est reel » - // ne separe pas les deux, parce que l'intruse insiste aussi. Ce qui les separe est - // ailleurs, et c'est dans les donnees : les positions de reference alternent entre - // DEUX SIEGES FIXES, [117,5 -10,7 205,7] et [58,7 -1,4 -337], distants de ~546 u -- - // la valeur exacte du saut. L'intruse REVIENT. Une vraie teleportation est a SENS - // UNIQUE (fiche CUTONJUMP : apres un vrai saut, les 170 lectures suivantes restent - // immobiles). - // ⇒ Le bon critere est donc : « l'ancien siege n'a plus ete vu depuis longtemps ». - // Et il s'exprime avec le compteur qui existe deja : toute lecture proche de l'ancien - // siege est acceptee et remet _consecutiveRejects a zero. Il suffit d'exiger BEAUCOUP - // PLUS que 3. On peut se le permettre : un refus ne fige rien -- l'appelant rend la - // camera precedente et la publication continue (ce n'est PAS le gel d'ELECTROT, qui - // lui ne publiait plus rien). - // - // 2. EN CEDANT, IL EFFACE SON ECHELLE APPRISE (_accCount = 0), donc les 8 lectures - // suivantes passent SANS AUCUN CONTROLE (`if (_accCount < 8 || ...) return true`). - // C'est par la que rentre le VOYAGE RETOUR de la salve. Et c'est injustifiable en soi : - // que la camera se teleporte ne change pas a quoi ressemble un pas ordinaire (0,2 u, - // 99e centile 0,7). ⇒ avec SNAPHOLD arme, l'echelle n'est plus effacee. - // - // ⚠️ RISQUE ASSUME, A SURVEILLER : sur une VRAIE teleportation, la camera est tenue jusqu'a - // SNAPHOLD images avant d'etre adoptee (~0,6 s a 30 images/s). Le journal sort `salve-max` : - // si elle reste COLLEE a la valeur de SNAPHOLD, le plafond est trop bas et on le voit -- - // c'est exactement le diagnostic qui avait condamne CAMSPLIT. - // ⛔⛔ LES DEUX FUITES SONT SEPAREES EN DEUX INTERRUPTEURS, ET CE N'EST PAS UN DETAIL DE - // CONFORT : elles n'ont PAS le meme risque, donc elles ne se testent pas ensemble. - // SNAPKEEP (fuite 2) : ne change RIEN au moment de la reddition -> aucun effet sur une - // vraie coupure, donc aucun risque sur les cinematiques. - // SNAPHOLD (fuite 1) : retarde la reddition -> sur une vraie coupure de plan, la camera - // est tenue jusqu'a N images (~0,6 s a 30 i/s). RISQUE CINEMATIQUE - // REEL, et Alex a pose les cinematiques en contrainte dure. - // ⇒ On arme SNAPKEEP seul d'abord. SNAPHOLD ne s'arme qu'apres, et seulement s'il reste - // des intrusions ET que le gain justifie de risquer une cinematique. private static readonly bool _snapKeep = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SNAPKEEP") == "1"; - // ⛔⛔ [SNAPLEARN 29/07] **REJETE PAR L'OEIL D'ALEX LE 29/07 : BANDE NOIRE SUR LA MOITIE DE - // L'ECRAN.** Ne plus apprendre les pas nuls affame `_accCount`, qui sert AUSSI de compteur - // d'echauffement (`_accCount < 8` = tout passe) : le garde reste alors DESARME (0 refus - // mesures, contre 7 au temoin) et une intruse a 546 u est adoptee sans resistance. - // Laisse en place, desarme par defaut, UNIQUEMENT pour que l'interrupteur reste - // documente -- **ne pas rearmer**. Voir journal (267). private static readonly bool _snapLearn = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SNAPLEARN") == "1"; - // [SNAPFLOOR 29/07] LA REPARATION DE LA BARRE A ZERO, faite du bon cote cette fois. - // - // LE DEFAUT, MESURE TROIS FOIS (runs 08h40, 09h23, 09h50) : la fenetre d'apprentissage ne - // retient que 120 valeurs. Camera posee plus de ~4 s, le pas vaut 0 : les zeros remplissent - // la fenetre et EVINCENT les vrais pas, `scale` tombe a 0, donc `step <= scale * SnapFactor` - // devient `step <= 0` et le garde REFUSE TOUT. Releve en plein milieu du run 09h50 : - // `19 refus (saut > 0), derniere = 0,06 | echelle 0 sur 79 pas appris` - // -- 19 refus consecutifs sur un pas de 0,06 unite, un mouvement parfaitement ordinaire. - // ⚠️ Et avec SNAPHOLD=20 chaque episode coute desormais 20 images de camera tenue au lieu - // de 3 : le defaut existait avant, le plafond l'a rendu six fois plus cher. - // - // ⛔ PREMIERE VERSION, REJETEE PAR LA MESURE LE 29/07 -- « se replier sur la derniere echelle - // NON NULLE ». Elle a rendu les choses PIRES (vraies intruses 3 -> 5, refus 26 -> 40/min) et - // le journal disait pourquoi noir sur blanc : - // `50 refus (saut > 0) ... SNAPFLOOR : 2 replis sur la derniere echelle reelle (0)` - // Le maximum de la fenetre peut etre une valeur MICROSCOPIQUE (niveau du jitter, ~0,0004) : - // non nulle, donc memorisee comme reference valable, mais une barre de 0,008 refuse tout - // exactement comme une barre a zero. 🔑 **J'avais corrige « la barre ne doit pas etre NULLE » - // alors que le probleme est « la barre doit etre SIGNIFICATIVE ». Non nul != utilisable.** - // - // LE CORRECTIF, VERSION RETENUE : plancher l'echelle a une valeur MESUREE, pas a « ce qui - // n'est pas zero ». `RYUJINX_MVPP_SNAPFLOOR=` (0 = coupe), teste a 0,5 => la barre - // ne descend jamais sous 10 unites. Le chiffre vient des runs du 29/07 : - // jitter du jeu 0,14 · pas ordinaire 0,2 (99e centile 0,7) · plus grand pas LEGITIME - // observe 4,9 u · plus petite INTRUSE 415 u - // Barre a 10 u = 2x au-dessus du plus gros mouvement legitime vu, 40x sous la plus petite - // intruse. Le vide est mesure des deux cotes, il n'est pas choisi au jugé. - // - // 🔑 CE QUI LE DISTINGUE DE SNAPLEARN, QUI A ECHOUE EXACTEMENT ICI : **les zeros restent - // APPRIS**. `_accCount` grandit normalement, le compteur d'echauffement n'est jamais - // affame, le garde n'est jamais desarme. On ne touche qu'au CALCUL de la barre, pas a ce - // qui est memorise. - // - // 🔒 LES DEUX BORNES, PARCE QUE LA LECON DU JOUR EST QU'IL FAUT ENCADRER LES DEUX (et que je - // ne l'ai fait ni pour SNAPLEARN ni pour la premiere version de SNAPFLOOR) : - // - vers le HAUT : `scale` ne peut qu'AUGMENTER (un plancher) => la barre monte => le - // garde refuse MOINS ou autant, jamais plus. Pas de camera figee, pas de texture qui - // nage (l'echec de SNAPKEEP est structurellement impossible ici). - // - vers le BAS : la barre ne descend JAMAIS sous le plancher, donc le garde ne peut PAS - // devenir absent -- toute intruse au-dela de 10 u est refusee en toutes circonstances, - // y compris au demarrage a froid et juste apres une reddition. Il n'y a AUCUNE branche - // qui accepte sans juger : c'est precisement ce qui manquait a SNAPLEARN (compteur - // d'echauffement affame => `_accCount < 8` => tout passe) et a SNAPFLOOR v1 (branche - // "aucune echelle connue" => tout passe, declenchee 41 fois). - // ⚠️ Les zeros restent APPRIS : `_accCount` grandit normalement, l'echauffement n'est - // jamais affame. On ne touche qu'au CALCUL de la barre, jamais a ce qui est memorise. private static readonly float _snapFloor = float.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_SNAPFLOOR"), System.Globalization.NumberStyles.Float, @@ -1427,14 +912,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _snapRefusals; private static long _lastSnapLogMs; - // [CUTONJUMP 27/07] Gate RYUJINX_MVPP_CUTONJUMP=1, OFF by default. See NoteJump. private static readonly bool _cutOnJump = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CUTONJUMP") == "1"; - // The scale is the 90th percentile of the last WindowSize steps - a robust statistic, not a - // record. A record can only go up and, once it is wrong, nothing can bring it back down; a - // percentile over a sliding window follows what the camera is actually doing right now and - // a single outlier cannot move it. WindowSize is ~5 s at 60 fps. private const float TeleportFactor = 50f; private const int WindowSize = 300; private const int ScaleRefresh = 30; @@ -1452,11 +932,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static Matrix4x4 _lastGoodVp; private static int _guardRejects; - /// - /// [DIVERG C 01/08] Instantane en lecture pure de la camera PUBLIEE (la derniere acceptee, - /// celle que MV++ consomme), pour la sonde de divergence de MvppScanProbe. Meme fil GPU, - /// aucun verrou necessaire, aucune ecriture. ⚠️ Valeur POST-DEJITTER (documente cote sonde). - /// internal static bool TryGetPublishedForProbe(out Matrix4x4 vp, out float px, out float py, out float pz) { vp = _lastGoodVp; @@ -1467,16 +942,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return _hasLastGood; } - /// - /// Refreshes the main render target aspect from the depth bound on this draw. Cheap, and - /// it has to run on every draw so the reference is the scene target rather than whatever - /// pass the election happened to land on. - /// public static void NoteDepth(int width, int height) { - // [VPSOLO_ROT 02/08 v2] Temoin d'armement DECOUPLE de l'election : il imprime des le - // premier dessin qualifiant, que l'election tourne ou non. Lecon du run 10:59 : un - // temoin place DANS Elect() ne distingue pas « pas arme » de « jamais appele ». if (!_armWitnessLogged) { _armWitnessLogged = true; @@ -1500,12 +967,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// Fallback camera source. Returns true and fills with the guest's - /// view-projection when a camera could be read. Cheap on the common path: one memory read - /// at the elected location plus a structural check; the full multi-buffer election only - /// runs once a second, or when the elected location stops validating. - /// public static bool TryGetViewProjection(GpuChannel channel, out Matrix4x4 vp) { vp = default; @@ -1515,8 +976,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // [FAMHOLD 01/08] Echantillonnage des adresses-famille (lecture pure, stride 1/8, - // voir MvppFamHold). Place ICI parce que ce point passe a CHAQUE dessin qualifiant. if (MvppFamHold.Enabled) { MvppFamHold.Sample(channel); @@ -1524,17 +983,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed long now = Environment.TickCount64; - // Common path: read the location elected earlier. The ADDRESS behind (stage, slot) - // may well have rotated since - that is exactly why the read is keyed on the slot. if (_hasElection && TryReadAt(channel, _elStage, _elSlot, _elOffset, _elTransposed, out vp, out float apx, out float apy, out float apz)) { - // [ELECT_HYST 01/08] Le siege vient de valider structurellement : il est vivant. - // Simple horodatage, toujours ecrit (aucun comportement) pour que l'alternance - // puisse armer/desarmer la tenue a chaud sans trou de mesure. _lastSeatOkMs = now; - // [MIRRORS M1 01/08] Le detour de peremption — voir le bloc de champs. Snapshot du - // brut du siege AVANT toute sonde d'historique (TryReadHistory ecrase _ctPending). if (_mirrors) { _ctPending.AsSpan().CopyTo(_mirSeatNow); @@ -1567,11 +1019,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _mirFound++; - // UNE seule acceptation par image : si le miroir passe les gardes, - // c'est LUI qui est publie et on sort ; s'il est refuse (jamais vu - // en pratique : pas de 0,2-2 u sous une barre de 10-20 u), le - // chemin normal reprend juste en dessous — le doute profite a - // l'existant, la publication ne peut pas baisser. Matrix4x4 mirCand = mirVp; if (AcceptRead(channel, ref mirCand, mpx, mpy, mpz, now)) @@ -1601,36 +1048,24 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed MirrorLog(now); } - // [CTXPROBE 29/07 SOIR] Lecture seule. ICI et pas plus bas : SNAPGUARD refuse les - // intruses, donc placee apres les gardes la sonde ne verrait QUE les lectures - // saines -- exactement l'inverse de ce qu'on cherche. Le pas est mesure depuis la - // derniere position ACCEPTEE, la meme reference que SNAPGUARD : les deux se - // comparent chiffre pour chiffre. Voir MvppCtxProbe. if (MvppCtxProbe.Enabled && _hasLastGood && !_probing) { float cdx = apx - _lastPx; float cdy = apy - _lastPy; float cdz = apz - _lastPz; - // L'age de la reference part avec la mesure : c'est lui qui dira si un "saut de - // 400 u" est une vraie intruse ou une reference perimee par une tenue longue. MvppCtxProbe.NoteRead(channel, MathF.Sqrt(cdx * cdx + cdy * cdy + cdz * cdz), apx, apy, apz, now - _lastAcceptMs); } bool accepted = AcceptRead(channel, ref vp, apx, apy, apz, now); - // [MIRRORS M1] Reference de peremption : le brut du siege tel que vu a la derniere - // image CAPTUREE (chemin normal). Mise a jour aussi cote eteint de l'alternance, - // pour que le cote arme reprenne toujours sur une reference fraiche. if (_mirrors && accepted) { _mirSeatNow.AsSpan().CopyTo(_mirSeatLast); _mirHasSeatLast = true; } - // [CAPTIME 31/07] Apres AcceptRead, jamais avant : on ne veut horodater que la - // lecture qui devient REELLEMENT la camera de l'image. if (CapTime && accepted) { NoteCapture(); @@ -1643,25 +1078,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _readFails++; - // [ALTPROBE 28/07] Sonde seule, armee par RYUJINX_MVPP_DUPPAIR. Ne modifie ni la - // valeur rendue, ni l'election. if (_altProbe) { ProbeAlternates(channel); } - // [MULTIADDR 28/07] Le correctif. Mesure du 28/07, trois runs : quand la lecture - // echoue a l'adresse elue, la camera est retrouvee a une adresse DEJA ELUE dans - // 100 % des cas (jusqu'a 3 429 fois par seconde), a moins d'une unite de la - // derniere retenue (max 1,05 ; un pas ordinaire vaut 0,2, l'intrus de SNAPGUARD - // valait 400) et avec une valeur NOUVELLE. Le jeu fait tourner son bloc de - // constantes entre plusieurs tampons ; n'en garder qu'un en cache faisait perdre - // une image sur deux pendant les pans, et la reprojection ecrivait alors ZERO sur - // toute l'image. - // - // Ne devine rien : la valeur existe deja, on regardait au mauvais endroit. Elle - // passe par le MEME chemin d'acceptation que la lecture principale (structure, - // aspect, ciel, SNAPGUARD) -- aucun garde-fou n'est contourne. if (_multiAddr && TryReadHistory(channel, out Matrix4x4 hvp, out float hpx, out float hpy, out float hpz)) { vp = hvp; @@ -1670,17 +1091,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // The elected location stopped validating, or there is none yet: re-elect, but never - // more than once a second - the election scans every bound buffer. if (now - _lastElectionMs < ElectionIntervalMs) { return false; } - // [ELECT_HYST 01/08] LA DECISION VIT ICI, au declencheur, et nulle part ailleurs : le - // scrutin ne peut pas defendre le sortant (jamais candidat sur le dessin qui echoue). - // Bloquer N'ARME PAS le cooldown (_lastElectionMs inchange) : l'election s'ouvre a - // l'instant exact ou la tenue expire, pas une seconde plus tard. if (_electHystMs > 0 && _hasElection) { bool armed = true; @@ -1750,12 +1165,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // [ELECTPROBE 01/08] Lecture seule, une ligne par election. CE retour est LA SEULE - // image qui contourne AcceptRead (dette notee au journal (283)) : suspect n1 du - // « flash de temps en temps, pas juste les nuages » decrit par Alex le 01/08 - // (13 elections / 11 min dans sa session = le bon ordre de grandeur). On mesure de - // COMBIEN cette image non gardee s'ecarte de la derniere camera retenue -- pas de - // decision, pas de refus : d'abord savoir, ensuite boucher. if (_hasLastGood) { float epdx = epx - _lastPx; @@ -1764,23 +1173,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed float epStep = MathF.Sqrt(epdx * epdx + epdy * epdy + epdz * epdz); float epRot = RotGap(in vp); - // [ELECT_WARP 01/08] LE BOUCHON. Confirme par la sonde sur la session d'Alex du - // 01/08 : l'election de 01:06 a publie CETTE image-ci a ecartRot=2,4457 (5x le - // seuil « autre camera » de MULTIROT) contre une reference vieille de 1 500 ms -- - // DLSS a recu une paire qui condense 1,5 s de mouvement en une seule image : son - // « flash de temps en temps ». Les elections saines mesurent 0,007 (separation - // 350x, pas un reglage fin). - // - // Remede : le tuyau EXISTANT TeleportSeq -> camera-warp -> reset d'historique - // (DlssUpscaler l.1862) : une coupure propre d'une image au lieu d'un champ de - // vecteurs qui condense tout l'intervalle. GAL et Vulkan INTOUCHES. - // - // ⚠️ Ce n'est PAS CUTONJUMP qui ressuscite (461-652 resets/run, mort le 27/07, - // NoteJump reste gate a part) : ici on ne peut tirer QUE sur une election - // (0,7/min sous la tenue), et seulement si l'image s'ecarte VRAIMENT -- - // rot >= 0,5 (seuil etabli de MULTIROT) ou pas >= 10 u (plancher etabli de - // SNAPFLOOR). Attendu : ~1 reset par session, visible ligne par ligne ci-dessous - // et cote DLSS (resetReason "camera-warp" dans ALIGN1). bool epWarp = false; if (_electWarp && (epRot >= 0.5f || epStep >= 10f)) @@ -1798,33 +1190,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// Chemin d'acceptation commun a la lecture principale et au repli MULTIADDR : sondes de - /// journal, CAMGUARD, SNAPGUARD, puis memorisation de la derniere camera retenue. Extrait - /// tel quel du corps de le 28/07, sans changement de - /// comportement, pour qu'un repli ne puisse PAS contourner un garde-fou. - /// private static bool AcceptRead(GpuChannel channel, ref Matrix4x4 vp, float apx, float apy, float apz, long now) { { _reads++; - // [ELECTROT 28/07] Meme garde que MULTIROT, mais sur la lecture PRINCIPALE. - // - // MULTIROT a supprime les intrusions qui passaient par les adresses de secours - // (valeur 1,14691, constante). Il en reste, mesurees APRES lui a 1,03 · 1,07 · - // 1,09 : elles entrent donc par la lecture du slot elu lui-meme, pas par le - // repli. Verdict d'Alex : « le clignotement des nuages n'est pas parti au - // complet, il y en a encore un peu ». - // - // Une caméra ne tourne pas de plus d'une unite entre deux images. Au-dela, ce - // n'est pas un mouvement, c'est une AUTRE matrice. - // - // Refuser une lecture n'invente rien : TryGetViewProjection rend false, MV++ ne - // publie pas cette image, la paire precedente reste en place -- bien moins nocif - // qu'un vecteur calcule entre deux cameras differentes. Le plafond de refus - // consecutifs empeche le gel qui avait tue CAMGUARD : passe ce plafond, la - // lecture est acceptee de force et une vraie coupure de scene passe toujours. if (_electRot > 0f && _hasLastGood) { if (RotGapTo(in vp, in _lastGoodVp) >= _electRot) @@ -1846,12 +1216,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [PROJAUDIT 27/07] Logging only, gated by RYUJINX_MVPP_PROJAUDIT=1. Reads the - // matrix that was already accepted and prints scalars; changes nothing. MvppProjAudit.Audit(in vp, apx, apy, apz); - // [CAMTRACE 27/07] Logging only, gated by RYUJINX_MVPP_CAMTRACE=1. The address is - // fetched only when the gate is armed: the read path stays untouched otherwise. if (MvppCamTrace.Enabled) { MvppCamTrace.Note( @@ -1859,10 +1225,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed apx, apy, apz, _lastJx, _lastJy, _deJitter); } - // [CAMGUARD 27/07] The continuity test runs on EVERY read so the rejection rate is - // measured at full frame rate, but the substitution only happens when the gate is - // armed. Gate off => _camGuard is false => vp is untouched and this block is a - // handful of comparisons: the measurement costs nothing and changes nothing. bool continuous = IsContinuous(in vp, apx, apy, apz); if (!continuous) @@ -1872,34 +1234,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_camGuard && !continuous && _hasLastGood) { - // Hand back the previous camera rather than a stranger. The frame then sees a - // still camera - zero reprojection - which is the conservative fallback the - // pipeline already applies whenever a camera is held. vp = _lastGoodVp; return true; } - // [SNAPGUARD 27/07] Last intruder standing, and the only one distance can catch - - // because it is the only one left. Measured after the three identity fixes: two - // addresses (227D8B8800/8900) return a FROZEN position 400 units from the scene, on - // 53 reads out of 600, alternating with the real camera. It is not at the origin - // (sky rejection blind), it has the right aspect ratio (aspect lock blind), and it - // is a structurally perfect view-projection. Nothing identifies it except that the - // camera cannot be in two places at once. - // - // WHY DISTANCE WORKS *NOW* AND FAILED THIS MORNING. CAMGUARD refused 91-98 % of the - // reads because it was fighting the jitter (0.14 units, same order as real motion), - // the interface and the sky all at once, with a threshold sized for none of them. - // Those three are handled at the source now, so ordinary steps sit at 0.2 with a - // 99th percentile of 0.7, while this intruder is 400 - three orders of magnitude of - // clearance instead of none. - // - // AND IT CANNOT SEIZE UP, which is what broke both earlier attempts. A refusal is - // never final: three refusals in a row are read as "the camera really did move" - - // the scale is thrown away and relearned. An intruder alternates one frame in ten - // and is never confirmed; a genuine teleport persists and is accepted on the third - // frame. Persistence, not amplitude, is what separates them. if (_snapGuard && !AcceptStep(apx, apy, apz)) { vp = _lastGoodVp; @@ -1907,12 +1246,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - // [SUBPROBE 29/07] Lecture seule, armee par RYUJINX_MVPP_SUBPROBE (defaut 0 = coupe). - // ICI et pas ailleurs : la matrice a passe TOUS les gardes et va devenir la camera - // retenue, tandis que l'ancienne n'est pas encore ecrasee. C'est la meme paire que - // celle mesuree cote consommateur par JUMPDIST, donc les chiffres des deux sondes - // se comparent directement. Placee plus haut, elle compterait des lectures que - // SNAPGUARD ou CAMGUARD allaient de toute facon refuser. if (_subProbe > 0f && _hasLastGood && !_probing) { SubProbe(channel, in vp, apx, apy, apz, now); @@ -1926,12 +1259,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _lastAcceptMs = now; _hasLastGood = true; - // [JITPROBE v3, 28/07] Mesure du decalage sous-pixel ICI, et pas au moment de la - // lecture brute. La v2 le faisait avant les filtres : elle rapportait donc le - // cisaillement de blocs qui allaient etre REJETES -- d'ou des valeurs de -664 px - // et +2400 px, qui ne sont pas un jitter mais d'autres matrices a la meme adresse. - // A cet endroit, la lecture a passe la structure, l'aspect, le rejet du ciel, le - // guard de saut : c'est la camera que le rendu consomme, et elle seule. if (!_probing) { _lastAcceptedJx = _lastJx; @@ -1952,9 +1279,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _guardRejects = 0; } - // [MULTIADDR] Trace de trajectoire : sert au repli a distinguer une valeur qui - // AVANCE d'une valeur perimee restee dans un autre tampon. Tenue a jour seulement - // quand le repli est arme. if (_multiAddr) { _goodFlat.CopyTo(_goodFlatPrev, 0); @@ -1970,20 +1294,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// Reads and structurally validates the matrix at one exact location. - /// - /// [CTXPROBE 29/07 SOIR] Lecture d'ESSAI a l'emplacement elu, pour la sonde SEULE. Repond a - /// la seule question qui decide du chantier SCENEPASS : quand une passe de scene HDR est - /// branchee, la camera est-elle LISIBLE a cet instant precis ? Le recensement du soir a - /// montre que les intruses se concentrent hors de cette passe (8,1 % contre 0,08 %), mais - /// deplacer la capture n'a de sens que si la lecture y aboutit. - /// - /// NE TOUCHE A RIEN : ne publie pas, ne passe par aucun garde, ne met a jour ni la derniere - /// bonne camera ni la trace de trajectoire. Le SEUL etat que ecrive - /// est la paire de jitter (_lastJx/_lastJy), sauvee et restauree ici -- elle alimente - /// LastJitterX/Y, consomme au present, donc la laisser deriver serait un effet de bord. - /// Restent les compteurs de , purement diagnostiques. - /// internal static bool TryProbeRead(GpuChannel channel, out float dist) { dist = 0f; @@ -2064,21 +1374,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed raw.CopyTo(_read.AsSpan()); } - // [CAPTIME 31/07] Snapshot du BRUT MEMOIRE. Deux raisons de copier `raw` et surtout PAS - // `_read` : (1) trois lignes plus bas RemoveJitter modifie _read EN PLACE, donc la - // matrice acceptee est POST-dejitter ; (2) _read est TRANSPOSE quand _elTransposed est - // vrai. Dans les deux cas la comparaison contre des octets relus en memoire ne - // correspondrait jamais, et la sonde rendrait "B" systematiquement, a tort. - // `raw` est la tranche memoire telle quelle : c'est la seule reference comparable. if (CapTime || CapTimeFix || _mirrors) { raw.CopyTo(_ctPending); } - // [DEJITTER 27/07] Applied BEFORE recognition and before the position is rebuilt, so - // every downstream consumer - the structural test, the position, the matrix handed to - // the reprojection - sees one single, coherent, jitter-free camera. The measurement is - // taken either way: with the gate off the amounts are reported and nothing is changed. float jx = 0f, jy = 0f; if (_deJitter || MvppCamTrace.Enabled || _jitProbe) @@ -2100,34 +1400,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return Fail(2); } - // Same rival guard as the triplet path: a SQUARE projection is the environment - // cubemap camera, otherwise structurally indistinguishable from the real one. if (MathF.Abs(fy / fx - 1f) < 0.2f) { return Fail(3); } - // [ASPECTLOCK 27/07] Rule 1 of the election, applied to the PER-FRAME read as well. - // - // The election checks that a candidate's projection aspect matches the main render - // target's, and it was written precisely because the 1 Hz tick kept landing on the - // wrong pass. But the per-frame read never repeated that check - it validated shape - // only - so whenever the elected slot happened to hold a different projection that - // frame, it was handed straight to the reprojection. - // - // Measured on Alex's run, 600 consecutive reads: 22 of them (3.7 %, about one frame in - // 27) returned a matrix at [0 0 1.25] with an aspect of 0.5625 where the camera's is - // 1.7778 - the flat 2D interface projection. The distance between the two viewpoints - // is 273 world units, so on those frames every motion vector explodes and the whole - // image jumps. With the jitter now removed, the rest of the trace is stable, which is - // exactly why those isolated jumps became the visible defect. - // - // This is the RIGHT discriminator, and not for XC2: an interface is flat, a camera is - // perspective, and their aspects differ by construction. It uses the same 5 % tolerance - // and the same reference as the election, so it can never disagree with it. Contrast - // with CAMGUARD, which tried to separate them by DISTANCE and refused 91-98 % of the - // reads on the same run: distance cannot tell a rival camera from a moving one, an - // aspect can. if (_aspectLock && _mainAspect > 0f && MathF.Abs(fy / fx / _mainAspect - 1f) > 0.05f) { return Fail(4); @@ -2141,24 +1418,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return Fail(5); } - // [SKYREJECT 27/07] Refuse a CAMERA-RELATIVE matrix on the per-frame read. - // - // The election already rejects it - the class is named in this file's own header, "the - // CAMERA-RELATIVE sky matrix (structurally perfect, same fx, pinned to the origin)" - - // but the per-frame read never repeated that check either. Same hole as the interface - // projection, and the aspect lock cannot close this one: the sky matrix shares the real - // camera's focal lengths exactly. Only its TRANSLATION gives it away, because a - // camera-relative transform has none by construction. - // - // Measured after the first two fixes, 471 reads: FOUR of them land at the origin and - // back - [0.25 -0.02 0.21] then [-239.9 -5.3 84.0] - a 250-unit round trip in two - // frames. That is the flick that survived, and it is why removing the warp detector - // brought it straight back: the resets were masking these, not fixing them. - // - // The test is RELATIVE, so no world scale is assumed: a position within 1 % of the - // distance the accepted camera sits at is a matrix pinned to the origin, not a camera - // that walked there. If the game's real camera genuinely works near world origin, the - // reference shrinks with it and the test stays silent. if (_skyReject && _hasLastGood) { float here = MathF.Sqrt(px * px + py * py + pz * pz); @@ -2179,11 +1438,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - // ---- Election ---- - private static bool Elect(GpuChannel channel) { - // [VPSOLO_ROT 02/08] Temoin d'armement, une fois par session. if (_rotMotion && !_rotArmedLogged) { _rotArmedLogged = true; @@ -2242,8 +1498,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // [VPSOLO_ROT 02/08] Direction de visee : rangee 3 du VP (+/-R.row2), - // normalisee. Gate ferme => zeros, jamais lus. float cdx = 0f, cdy = 0f, cdz = 0f; if (_rotMotion) @@ -2282,19 +1536,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // [SKYREJECT 27/07] The camera-relative matrix must be thrown out HERE too, not only on - // the per-frame read. Measured on Alex's run: the election picked - // "stage4 cbuf3 +0x180 pos=[-0.1 -0 0.1] motion=297.64" - the sky matrix, elected - // outright. It wins rule 3 for a perverse reason: pinned to the origin while the scene - // camera is 300 units away, it appears to MOVE by that whole distance every time the - // slot alternates, so it scores a huge "motion" and looks like the most active camera - // in the frame. The rule meant to find the camera crowned its impostor. - // - // The discriminator is the same one as on the read path, and just as relative: a - // candidate sitting at a hundredth of the distance of the furthest candidate is pinned - // to the origin, not standing there. If a game's real camera works near world origin, - // every candidate is near it too and the scale collapses with them - the test stays - // silent instead of rejecting everything. float posScale = 0f; if (_skyReject) @@ -2311,7 +1552,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Rule 1 + motion/straightness bookkeeping. float bestMotion = 0f; float bestStraight = 0f; int considered = 0; @@ -2322,7 +1562,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_mainAspect > 0f && MathF.Abs(a.Aspect / _mainAspect - 1f) > 0.05f) { - a.Motion = -1f; // marks "rejected by aspect" + a.Motion = -1f; continue; } @@ -2330,7 +1570,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (posScale > 0f && MathF.Sqrt(a.X * a.X + a.Y * a.Y + a.Z * a.Z) < posScale * 0.01f) { - a.Motion = -1f; // marks "rejected as camera-relative" + a.Motion = -1f; continue; } @@ -2339,12 +1579,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed a.Motion = Track(in a, out float straight); a.Straightness = straight; - if (a.Motion > bestMotion) + bool aberrant = ElectMaxStep > 0f && + (a.Motion > ElectMaxStep || ElectIsBanned(a.Stage, a.Slot, a.Offset)); + + if (a.Motion > bestMotion && !aberrant) { bestMotion = a.Motion; } - if (a.Motion > 0f && straight > bestStraight) + if (a.Motion > 0f && straight > bestStraight && !aberrant) { bestStraight = straight; } @@ -2362,7 +1605,37 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // Rule 3: recent motion, and travel rather than hopping. + if (ElectMaxStep > 0f) + { + bool abberantMaintenant = a.Motion > ElectMaxStep; + bool grille = ElectIsBanned(a.Stage, a.Slot, a.Offset); + + if (abberantMaintenant && !grille && _electBannedCount < ElectBannedMax) + { + _electBanned[_electBannedCount++] = ElectBanKey(a.Stage, a.Slot, a.Offset); + grille = true; + + Logger.Info?.Print(LogClass.Gpu, + $"MVPP ELECTMAXSTEP: emplacement GRILLE -- stage{a.Stage} cbuf{a.Slot} " + + $"+0x{a.Offset:X3} motion={a.Motion:0.##} > seuil {ElectMaxStep:0.##} " + + $"(il ne pourra plus etre elu de la session ; {_electBannedCount} grille(s))."); + } + + if (abberantMaintenant || grille) + { + _electMaxStepRejected++; + + if ((_electMaxStepRejected % 20000) == 0) + { + Logger.Info?.Print(LogClass.Gpu, + $"MVPP ELECTMAXSTEP: {_electMaxStepRejected} ecartements cumules, " + + $"{_electBannedCount} emplacement(s) grille(s)."); + } + + continue; + } + } + if (bestMotion > 0f && a.Motion < bestMotion * 0.1f) { continue; @@ -2373,7 +1646,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // Rule 2: agreement between DISTINCT buffers (by address). int votes = 0; for (int j = 0; j < _candCount; j++) @@ -2421,9 +1693,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Nothing may be elected before the evidence exists: without an aspect reference or - // without a single moving candidate, every rule above is inert and an election taken - // then survives long after the evidence arrives (measured, and it cost a whole run). if (best < 0 || _mainAspect <= 0f || bestMotion <= 0f) { return false; @@ -2441,9 +1710,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _elTransposed = w.Transposed; _elections++; - // [v1] Seul le VAINQUEUR de l'election entre dans l'historique. La v2 y ajoutait aussi - // tous les candidats qui s'accordent en position : retire le 28/07, elle a ramene le - // flick ciel->montagnes sans rien apporter aux mesures. PushHistory(w.Stage, w.Slot, w.Offset, w.Transposed); if (changed) @@ -2487,8 +1753,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ref Tracked t = ref _tracks[idx]; - // Halved every election: a matrix that moved once at load time and then froze fades - // under the bar in ~8 elections instead of holding the seat for ever. t.Motion *= 0.5f; float step = 0f; @@ -2511,9 +1775,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [VPSOLO_ROT 02/08] Pas de rotation, signe-insensible (rangee 3 = +/-R.row2). Nourrit - // t.Motion seulement — le ring et PathLength restent nourris par la POSITION (la - // rectitude est un concept de trajectoire ; voir l'exemption plus bas). float rotStep = 0f; if (_rotMotion && t.HasLastDir) @@ -2569,10 +1830,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed straightness = MathF.Sqrt(nx * nx + ny * ny + nz * nz) / t.PathLength; } - // [VPSOLO_ROT 02/08] Une camera dont la vie est ROTATIONNELLE (position quasi immobile, - // cas monde-relatif) aurait une rectitude de bruit sur un chemin microscopique — la - // regle 3 la veto-erait pour une trajectoire qu'elle n'a pas. Quand la rotation domine, - // la rectitude ne s'applique pas. if (_rotMotion && rotStep > step) { straightness = 1f; @@ -2618,27 +1875,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - // ---- Structural recognition (validated 21/07: 3000/3000 synthetic cameras accepted with - // exact position recovery, 1000/1000 bare projections rejected, 0 false positives over - // 6000 bone/noise/absurd matrices; then confirmed in-game on XC2 and TOTK). ---- - - /// - /// VP = P x V with P perspective (row 3 = [0,0,+/-1,0]) and V rigid expands to - /// row0.xyz = fx*R.row0 row1.xyz = fy*R.row1 row2.xyz = A*R.row2 row3.xyz = +/-R.row2 - /// so the shape is fully constrained without ever seeing P or V: row3 is a unit vector, - /// rows 0/1/3 are mutually orthogonal, and row2 is collinear with row3. A skinning bone - /// matrix fails immediately: its row 3 is [0,0,0,1], xyz norm 0. - /// - /// - /// [CAPTIME_AB 31/07] Frontiere d'image. Appelee au meme endroit que MvppScenePass.OnFrame - /// et MvppLdrSkip.OnFrame -- le seul endroit ou l'on sait si l'image a fini par obtenir une - /// camera, donc le seul endroit ou "images sans capture" est connaissable. - /// N'ecrit que dans les compteurs de l'experience. - /// public static void OnPresentBoundary(bool captured) { - // [CAPTIME_MAX] Le filet se renseigne TOUJOURS, meme hors alternance : c'est lui qui - // garantit qu'on ne peut pas rater deux images de suite a cause du gate. _ctPrevFrameMissed = !captured; if (_abBlock <= 0) @@ -2664,7 +1902,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Changement de bloc : on publie le cote qui se termine, puis on repart a zero pour lui. long f = _abFrames[side]; long fails = 0; @@ -2695,25 +1932,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _abArmed = armedNext; } - /// - /// [CAPTIME_FIX 31/07] Vrai si le lieu elu porte encore, BIT POUR BIT, la valeur brute de la - /// derniere lecture acceptee -- donc rien de neuf a capturer sur ce dessin. - /// - /// Lecture pure : n'appelle ni TryReadAt, ni AcceptRead, ni l'election, et n'incremente - /// aucun compteur d'echec. Retourne FAUX des qu'on ne peut pas comparer (pas d'election, - /// slot non lie, forme invalide) : dans ce cas le chemin normal reprend la main et le - /// comportement est celui d'aujourd'hui. Le doute profite toujours au comportement existant. - /// public static bool StaleAtElectedLocation(GpuChannel channel) { - // [CAPTIME_WHY 31/07] Compteurs de motifs, AUX POINTS DE DECISION DEJA EXISTANTS. - // Aucune lecture nouvelle, aucun comportement change : ils repondent a la seule question - // "pourquoi le gate ne s'arme-t-il jamais" (run du 31/07 : deplacee = 0 % dans TOUS les - // blocs armes, donc l'alternance n'a rien teste). Sans eux je devinerais, et j'ai deja - // casse trois instruments aujourd'hui. _whyCalled++; - // [CAPTIME_MAX] K = 0 => gate eteint, la forme non bornee ne peut pas etre lancee. if (!FixActive || _ctMaxSkip <= 0) { _whyOff++; @@ -2723,8 +1945,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _whyArmedCall++; - // [CAPTIME_MAX] La borne. Au-dela on capture ce qu'il y a : 1 capture par image, - // garanti par construction. if (_ctPreDraw >= _ctMaxSkip) { _whyDeadline++; @@ -2732,7 +1952,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // [CAPTIME_MAX] Le filet : jamais deux images ratees d'affilee a cause du gate. if (_ctPrevFrameMissed) { _whyNetted++; @@ -2776,8 +1995,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // Contre _ctCaptured (la derniere lecture ACCEPTEE) et surtout pas _ctPending, qui est - // ecrase a chaque TryReadAt, accepte ou non. if (!vals.SequenceEqual(_ctCaptured)) { _whyDiffers++; @@ -2791,9 +2008,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// [CAPTIME_WHY 31/07] Table de verite du gate, cadence 5 s. Lecture seule. - /// public static void LogWhy() { if (!CapTime && !CapTimeFix && _abBlock <= 0) @@ -2831,11 +2045,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _whyNetted = 0; } - /// - /// [CAPTIME 31/07] Appelee sur les dessins qualifiants QUI SUIVENT la capture de l'image. - /// Lecture pure : resout l'adresse derriere le slot elu, copie 64 octets, deduplique. - /// N'appelle ni TryReadAt, ni AcceptRead, ni l'election. N'ecrit que dans les champs _ct*. - /// public static void NoteLateDraw(GpuChannel channel) { if (!CapTime || !_hasElection || !_ctHasCapture) @@ -2848,9 +2057,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ulong address = channel.BufferManager.GetGraphicsUniformBufferAddress(_elStage, _elSlot); int size = channel.BufferManager.GetGraphicsUniformBufferSize(_elStage, _elSlot); - // Le slot elu n'est pas lie a tous les dessins ("buffer absent" est un motif d'echec - // documente). Compte SEPAREMENT : sans ca, "aucune valeur vue" serait indiscernable de - // "on n'a jamais regarde", et le verdict B serait ininterpretable. if (address == 0 || address == ulong.MaxValue || _elOffset + 64 > size) { _ctMissing++; @@ -2863,9 +2069,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ReadOnlySpan block = channel.MemoryManager.Physical.GetSpan(address + (ulong)_elOffset, 64); ReadOnlySpan vals = System.Runtime.InteropServices.MemoryMarshal.Cast(block); - // IsViewProj en FILTRE seulement (fonction pure, verifiee sans ecriture d'etat) : evite - // de remplir les 16 emplacements avec les octets d'un tampon etranger. Le verdict, lui, - // ne repose que sur l'egalite bit-exacte plus bas. if (!IsViewProj(vals, out _, out _)) { return; @@ -2887,12 +2090,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// [CAPTIME 31/07] Appelee quand une lecture vient d'etre ACCEPTEE comme camera de l'image. - /// L'historique _ctHist contient, a cet instant precis, les valeurs vues APRES la capture de - /// l'image PRECEDENTE -- rien n'y a ete ajoute depuis. Aucune accroche de fin d'image n'est - /// donc necessaire : la rotation se fait ici. - /// private static void NoteCapture() { if (_ctHasCapture) @@ -2909,11 +2106,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // A = deja vue pendant l'image precedente => on a capture trop tot. - // B = jamais vue ALORS QU'ON REGARDAIT (bound > 0) => le jeu ne l'avait pas produite. - // Couverture nulle (bound == 0) => echantillon a ECARTER, la sonde n'a rien pu voir. - // [CAPTIME_AB] Le verdict part dans les compteurs DU BLOC EN COURS. Les transitions - // A<->B sont comptees ici, sur la suite des images, cote par cote. if (_abBlock > 0) { int side = _abArmed ? 1 : 0; @@ -2950,8 +2142,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed : _ctBound > 0 ? "B (jamais vue -> le jeu ne l'avait pas produite)" : "ECARTE (couverture nulle)"; - // En alternance on ne journalise pas image par image : seuls les resumes de bloc - // comptent, et 30 lignes par seconde noieraient le journal. if (_abBlock == 0) { Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu, @@ -2961,8 +2151,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [CAPTIME_FIX] De combien la capture a-t-elle ete deplacee, et sur quelle proportion - // d'images. Cadence 5 s : c'est un taux, pas un evenement. if (CapTimeFix) { _ctTotalFrames++; @@ -2989,13 +2177,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [RANKPROBE 01/08] Verdict par image, sur l'etat AVANT rotation : _ctCaptured = la - // capture de l'image ecoulee, _ctHist = les distinctes vues apres elle, avec leur rang. - // rang = premier dessin ou une valeur != la capture est apparue ; - // JAMAIS = aucune valeur differente ALORS QU'ON REGARDAIT (couverture > 0) ; - // ECARTE = couverture nulle (slot jamais lie) — indiscernable de « pas produit » ; - // SATURE = table pleine (CtMax) : le rang a pu etre manque, compte a part pour ne - // jamais gonfler JAMAIS en silence. if (_rankProbe && _ctHasCapture) { int firstNew = -1; @@ -3109,9 +2290,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // A BARE perspective projection satisfies everything above, because a projection is a - // view-projection whose view is the identity. Correct, and useless: it carries no - // camera. Measured on XC2, those all reported campos [0 0 0]. bool rotIsIdentity = MathF.Abs(m[0] / n0 - 1f) < 1e-3f && MathF.Abs(m[1]) < 1e-3f && MathF.Abs(m[2]) < 1e-3f && MathF.Abs(m[4]) < 1e-3f && MathF.Abs(m[5] / n1 - 1f) < 1e-3f && MathF.Abs(m[6]) < 1e-3f && @@ -3131,12 +2309,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// Camera position from a standalone view-projection: rebuild the rigid rows - /// (R.row0 = row0.xyz/fx, R.row1 = row1.xyz/fy, R.row2 = row3.xyz), take - /// t = (row0.w/fx, row1.w/fy, row3.w) and return -R^T t. The sign ambiguity on R.row2 - /// cancels because it appears twice in the product. - /// private static void ViewProjPos(ReadOnlySpan m, float fx, float fy, out float x, out float y, out float z) { float r00 = m[0] / fx, r01 = m[1] / fx, r02 = m[2] / fx; @@ -3152,19 +2324,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed z = -(r02 * tx + r12 * ty + r22 * tz); } - /// - /// [CAMGUARD 27/07] Is this read the SAME camera as the previous one, or a different - /// viewpoint that happens to sit in the same slot this frame? - /// - /// Two tests, both relative, no world units baked in beyond the jump budget itself: - /// - the camera cannot teleport. MaxJump is 25 units in ONE frame = 1500 units/second - /// at 60 fps, against a measured median of 5.8 units per SECOND on Alex's run, so - /// real movement - including pushing the view towards or away from the character - - /// stays two orders of magnitude below the bar; - /// - the focal length cannot step. A zoom is continuous; a different camera is not. - /// - /// The first read of a session has nothing to compare against and is always accepted. - /// private static bool IsContinuous(in Matrix4x4 vp, float px, float py, float pz) { if (!_hasLastGood) @@ -3176,8 +2335,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed float dy = py - _lastPy; float dz = pz - _lastPz; - // How much travel this read is allowed to carry, given how long ago the last one was - // accepted. Starved periods legitimately deliver a whole second of movement at once. float dtSec = MathF.Max(0f, (Environment.TickCount64 - _lastAcceptMs) / 1000f); float budget = MathF.Max(MinBudget, MathF.Min(dtSec, CatchUpCapSec) * MaxSpeed); float dist = MathF.Sqrt(dx * dx + dy * dy + dz * dz); @@ -3200,11 +2357,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// [SNAPGUARD 27/07] True when this read continues the camera's trajectory, false when it - /// belongs to somebody else. See the call site for why distance is the right test here and - /// was the wrong one this morning. - /// private static bool AcceptStep(float px, float py, float pz) { if (!_hasLastGood) @@ -3222,31 +2374,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // [SNAPLEARN 29/07] LA REPARATION DE L'ESTIMATEUR. Un pas EXACTEMENT NUL n'apprend rien - // sur l'echelle des mouvements de la camera : il est accepte, et on n'y touche pas. - // - // POURQUOI. Mesure du 29/07 09h : la barre du garde etait tombee a `saut > 0`, et il - // refusait des pas de 0,03 / 0,06 / 0,26 unite -- des mouvements parfaitement ordinaires - // -- ~90 fois par tranche de 5 s. Verdict d'Alex : les textures lointaines se mettent a - // NAGER (camera tenue en permanence => vecteurs perimes). - // Le mecanisme : quand la camera est posee, le pas vaut exactement 0 ; ces zeros sont - // appris et, la fenetre ne faisant que 120 entrees, ils EVINCENT les vrais pas. `scale` - // tombe a 0, donc `step <= scale * SnapFactor` devient `step <= 0` et TOUT est refuse. - // ⚠️ Et ce qui sortait le garde de cet etat, c'etait l'effacement `_accCount = 0` de la - // branche de reddition (8 passages libres, la fenetre se remplit de vrais pas). Cet - // effacement n'etait donc pas SEULEMENT une fuite : c'etait aussi le seul mecanisme de - // RECUPERATION -- le retirer sans reparer l'estimateur (SNAPKEEP) a fige la barre a - // zero pour de bon. C'est pour ca que SNAPKEEP a ete rejete en deux minutes. - // - // 🔒 PROPRIETE DE SURETE, ET C'EST ELLE QUI REND CE CHANGEMENT SUR : ne plus apprendre - // les zeros ne peut qu'AUGMENTER `scale` (un maximum sur une fenetre dont on retire des - // entrees nulles qui evincaient des entrees utiles). Une barre plus haute refuse MOINS. - // ⇒ ce correctif ne peut JAMAIS produire plus de refus qu'aujourd'hui, donc il ne peut - // ni figer la camera, ni faire nager les textures. Le pire cas est « aussi permissif - // qu'avant », jamais plus strict. - // - // Le compteur d'echauffement ne bouge pas non plus : un zero n'est pas une preuve que la - // camera bouge, il ne doit donc pas rapprocher le garde de son etat arme. if (_snapLearn && step <= 0f) { _consecutiveRejects = 0; @@ -3264,16 +2391,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [SNAPFLOOR 29/07] Une barre doit etre SIGNIFICATIVE, pas seulement non nulle. Voir le - // champ _snapFloor : la mesure d'ou sort le chiffre, et l'encadrement des DEUX bornes. - // Une seule ligne, aucune branche qui accepte sans juger. if (_snapFloor > 0f && scale < _snapFloor) { scale = _snapFloor; _snapFloorUsed++; } - // Below the bar, or nothing learned yet: this is the camera doing camera things. if (_accCount < 8 || step <= scale * SnapFactor) { _accSteps[_accHead] = step; @@ -3289,23 +2412,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - // Over the bar. Refuse - but count, because a real displacement will insist. - // - // [SNAPHOLD 29/07] Le seul changement : COMBIEN de refus avant de ceder. Voir le champ - // _snapHold. Toute lecture proche de l'ancien siege est acceptee plus haut et remet ce - // compteur a zero -- exiger un long compte revient donc a dire « l'ancien siege n'a - // plus ete vu depuis longtemps », ce qui est la definition d'une vraie teleportation. int confirm = _snapHold > 0 ? _snapHold : SnapConfirm; _snapStreakMax = Math.Max(_snapStreakMax, ++_consecutiveRejects); if (_consecutiveRejects < confirm) { - // [27/07] Reported because the trace alone cannot answer the only question that - // matters: it logs BEFORE this filter, so a refused intruder still appears in it. - // Without this line "51 intruders in the log" and "51 intruders on screen" are - // indistinguishable - the same trap as every probe that was too quiet to be - // trusted today. _snapRefusals++; long snapNow = Environment.TickCount64; @@ -3330,15 +2442,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // It insisted. This is movement, not an impostor: forget the old scale entirely and - // learn again from here. This branch is what makes the guard unable to jam. _consecutiveRejects = 0; _snapForced++; - // [SNAPKEEP 29/07] L'effacement de l'echelle est la seconde fuite mesuree : il ouvre 8 - // lectures sans controle (`if (_accCount < 8 || ...) return true`), par ou rentre le - // VOYAGE RETOUR de la salve. Un pas ordinaire vaut 0,2 u que la camera se soit - // teleportee ou non -- il n'y a rien a reapprendre, et tout a perdre. if (!_snapKeep) { _accCount = 0; @@ -3348,30 +2454,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// [CUTONJUMP 27/07] Detects a camera warp and tells the upscaler its history is stale. - /// Gate: RYUJINX_MVPP_CUTONJUMP=1, OFF by default. - /// - /// WHY IT EXISTS. With the jitter removed and the interface projection refused, Alex's - /// 14-minute run left EXACTLY ONE aberrant step out of 600 reads - and he saw EXACTLY ONE - /// flick. That step is 829 world units and lands on a position where the next 170 reads sit - /// still: the game genuinely teleported him to another zone. The camera did not lie, so - /// there is nothing to refuse here. What is wrong is reprojecting the new view from the old - /// one: for one frame the vectors describe a journey nobody made. - /// - /// THE SCALE COMES FROM THE CAMERA ITSELF, not from a constant. Anything the camera has - /// ever done under continuous movement is remembered as _maxStep; a warp is declared only - /// beyond FIFTY times that. Measured on the run: ordinary steps peak around 0.3 and the - /// warp is 829, four orders of magnitude apart, so the factor is not a tuned value - any - /// number between 10 and 1000 gives the same verdict. No world units are baked in, which is - /// what CAMGUARD got wrong, and nothing can fire before real movement has been observed: - /// while _maxStep is still zero the test is inert by construction. - /// - /// AND IT IS NOT THE RULE THAT REGRESSED TWICE. SCENECUT_MAX_MOTION thresholds the FRACTION - /// OF THE IMAGE that moves, which is why slow pans fell under it and ghosted. This reads a - /// camera displacement that no controller input can produce. Different quantity, different - /// failure mode. - /// private static void NoteJump(float px, float py, float pz) { if (!_cutOnJump || !_hasLastGood) @@ -3389,14 +2471,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // EVERY step enters the window, warps included. Excluding them is what broke the first - // version: the reference could only ever be raised by a step that had not been called a - // warp, so once it settled low, every real movement cleared the bar and was called a - // warp too, which in turn kept the reference low. A ratchet that jams shut. Measured on - // Alex's 9-minute run: reference stuck at 0.0001, ordinary walking at 0.19 reported as - // "x3064 over", 461 history resets instead of one. A warp is one sample in thousands, - // so it cannot move a high percentile - letting it in costs nothing and removes the - // only way this can seize up. _window[_windowHead] = step; _windowHead = (_windowHead + 1) % WindowSize; @@ -3404,13 +2478,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _windowCount++; - // Inert until the window is full: no verdict before there is something to compare - // against. At 60 fps that is about five seconds. return; } - // Refreshed periodically rather than per read: the scale of a camera's motion does not - // change meaningfully within a few frames, and this keeps the common path cheap. if (--_scaleCountdown <= 0) { _scaleCountdown = ScaleRefresh; @@ -3429,13 +2499,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// [DEJITTER 27/07] How much shear rows 0 and 1 carry along row3. For an unjittered - /// perspective both are zero by construction: PxV keeps rows 0/1 orthogonal to row3 - /// (that orthogonality is one of the tests IsViewProj already relies on). Anything - /// non-zero here is the temporal-AA offset the game applied to its projection, expressed - /// in the same units as the NDC shear - typically a fraction of a pixel. - /// private static void MeasureJitter(ReadOnlySpan m, out float jx, out float jy) { float n3Sq = m[12] * m[12] + m[13] * m[13] + m[14] * m[14]; @@ -3452,13 +2515,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed jy = (m[4] * m[12] + m[5] * m[13] + m[6] * m[14]) / n3Sq; } - /// - /// [DEJITTER 27/07] Subtracts the shear measured above, on all FOUR components of each - /// row: the jitter was added as row += j*row3 in full, and m[3]/m[7] are precisely the - /// terms ViewProjPos uses to rebuild the camera position, so leaving them contaminated - /// would defeat the whole point. Rotation, focal lengths and the depth mapping (row2, - /// row3) are never touched. - /// private static void RemoveJitter(Span m, float jx, float jy) { for (int i = 0; i < 4; i++) @@ -3468,13 +2524,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// [27/07] Why a read was refused, and with which numbers. Written because the armed guard - /// refused 91 to 98 % of the reads on Alex's run while only 24 reads out of 600 carried a - /// step above the 5-unit floor: those two facts cannot both describe a guard rejecting - /// intruders, so the reason has to be read rather than guessed. Gated on the trace, capped, - /// and it never influences the decision it reports. - /// private static void LogReject(string reason, float dist, float budget, float fx, float dtSec) { if (!MvppCamTrace.Enabled || _rejectLogs >= 40) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSurviveProbeBuffer.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSurviveProbeBuffer.cs index 69f5bf752..b4f57179c 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSurviveProbeBuffer.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppSurviveProbeBuffer.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -12,20 +9,8 @@ using System.Text; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// [MVPP SURVIVE PROBE -- TEMPORARY DIAGNOSTIC, DISPOSABLE] Owns the host buffer backing the - /// fragment survivor-map SSBO (fixed binding MvppSurviveProbe.HostBinding). Lazily creates the - /// buffer on first use, clears it to zero, hands out the range for the per-session graphics - /// bind in StateUpdater.CommitBindings, and (Phase 2) periodically reads the map back and - /// dumps it as a PGM image for offline inspection. Gated by RYUJINX_MVPP_SURVIVE_PROBE - /// (checked by the caller); when OFF nothing here ever runs. - /// static class MvppSurviveProbeBuffer { - // Window count is env-tunable: the timer starts at the first scene bind, and the TotK - // TITLE SCREEN is a real 3D scene (inject runs on it) -- with a slow load (shader cache - // off) the default 3 windows can be fully consumed before gameplay starts. More windows - // keep rolling into gameplay; menu windows are discarded offline. private static readonly int MaxDumps = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_SURVIVE_DUMPS"), out int maxDumps) && maxDumps > 0 ? maxDumps @@ -37,12 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static long _nextDumpMs = -1; private static int _dumpCount; - /// - /// Gets the survivor-map buffer range, creating and zero-clearing the buffer on first call. - /// Must be called from the GPU thread (StateUpdater runs there; GAL calls are queued). - /// - /// GPU context used to reach the host renderer - /// Range covering the whole survivor map public static BufferRange GetRange(GpuContext context) { if (_handle == BufferHandle.Null) @@ -56,19 +35,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"binding {MvppSurviveProbe.HostBinding}) and cleared to zero."); } - // write:true -- the fragment epilogue stores into this buffer; the flag keeps the - // backend's modification tracking honest (and the mirror capture carries it too). return new BufferRange(_handle, 0, MvppSurviveProbe.BufferSize, true); } - /// - /// [P3 scene window] Gets the DUMMY buffer range: same size as the real mask (the shader's - /// bounds guard is sized for the full map, so a smaller target would be out-of-bounds), - /// bound for every draw that is NOT on the pinned scene depth. Write-only garbage sink, - /// never read back, never cleared after creation. - /// - /// GPU context used to reach the host renderer - /// Range covering the dummy buffer public static BufferRange GetDummyRange(GpuContext context) { if (_dummyHandle == BufferHandle.Null) @@ -82,14 +51,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return new BufferRange(_dummyHandle, 0, MvppSurviveProbe.BufferSize, true); } - /// - /// [Phase 2] Every DumpIntervalMs (first window starts after the bind), reads the survivor - /// map back to the CPU, logs the non-zero pixel count, writes a binary PGM (P5) next to the - /// executable, then clears the buffer so the next window accumulates fresh. At most - /// MaxDumps dumps per session; a no-op before the buffer exists. The readback is a - /// deliberate one-shot stall (diagnostic only). - /// - /// GPU context used to reach the host renderer public static void MaybeDump(GpuContext context) { if (_handle == BufferHandle.Null || _dumpCount >= MaxDumps) @@ -113,7 +74,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _nextDumpMs = now + DumpIntervalMs; _dumpCount++; - // Make the fragment shader stores visible, then read the whole map back. context.Renderer.Pipeline.Barrier(); long nonZero = 0; @@ -130,16 +90,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed for (int i = 0; i < words.Length; i++) { - // Words now hold the packed viewport size (W<<16|H) of the writing pass; the - // raw .bin dump below carries the full values for the offline histogram. if (words[i] != 0) { nonZero++; pgmPixels[i] = 255; - // MapStride is 4096 = 2^12, so y = i >> 12 and x = i & 4095. The bounding - // box of written pixels measures the REAL scene write extent (settles the - // dynamic-resolution question from the quadrant observation). int x = i & (MvppSurviveProbe.MapStride - 1); int y = i >> 12; @@ -179,7 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"sceneDepthPin={MvppCameraCapture.SceneDepthHostWidth}x{MvppCameraCapture.SceneDepthHostHeight} " + $"-> {path} + raw .bin"); - // Fresh window for the next dump. context.Renderer.Pipeline.ClearBuffer(_handle, 0, MvppSurviveProbe.BufferSize, 0); } } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppUiProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppUiProbe.cs index 5311a913e..9087d702c 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppUiProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppUiProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -10,29 +7,14 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ UI-separability probe (RYUJINX_MVPP_UIPROBE=1). READ-ONLY, default off, nothing in the - /// render path branches on it. Answers ONE question with measured data: are the HUD/UI draws - /// written into a render target whose GPU address DIFFERS from the final composite buffer (= - /// separable = capturable), or are they alpha-blended IN-PLACE onto the same buffer that already - /// holds the game-upscaled scene (= not separable without a diff)? - /// - /// Method: at every DrawEnd it groups consecutive draws that share the same colour-RT address into - /// "epochs" and records per epoch { RT address, format, dims, RenderTargetScale, any-depth-test, - /// any-blend, draw count }. A frame boundary is the rising edge of RenderTargetScale (UI/native - /// passes run at 1x, the scaled 3D scene at >1x -- established in MvppP2Probe). At each boundary it - /// classifies the epochs (HDR scene / LDR composite / blended overlay) and logs whether the blended - /// overlay writes the composite address (IN-PLACE) or a different one (SEPARATE). The raw epoch list - /// is logged for the first frames so the verdict can be read from data, not from the classifier. - /// static class MvppUiProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE") == "1"; - private const int MaxEpochsPerFrame = 64; // hard cap so a pathological frame cannot grow unbounded - private const int FullDetailFrames = 8; // log every epoch for the first frames, then only the verdict - private const int VerdictThrottleMs = 2000; // steady-state: one verdict line per change or per 2 s + private const int MaxEpochsPerFrame = 64; + private const int FullDetailFrames = 8; + private const int VerdictThrottleMs = 2000; private struct Epoch { @@ -44,8 +26,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public bool AnyDepth; public bool AnyBlend; public int Draws; - public int NoDepthDraws; // [HUDSPLIT] dessins sans test de profondeur = candidats HUD - public int FirstNoDepthAt; // rang du premier d'entre eux dans l'epoque (1 = des le debut) + public int NoDepthDraws; + public int FirstNoDepthAt; } private static readonly List _epochs = new(); @@ -53,60 +35,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _frameNo; private static int _framesLoggedFull; private static long _lastVerdictMs; - private static long _lastDetailMs; // [28/07] cadence du dump detaille en regime de jeu + private static long _lastDetailMs; - // [EPOCHDUMP 28/07] RYUJINX_MVPP_UIPROBE_DUMP=, 0 = OFF. - // - // POURQUOI CE MODE EXISTE. Les captures de render targets photographient soit un instant - // arbitraire, soit la PRESENTATION -- et a ce moment-la le buffer LDR pleine resolution - // qui recoit 95 a 99 dessins par image est deja RECYCLE : mesure du 28/07, il ressort - // VIDE alors que la sonde d'epoques le voit travailler. C'est exactement pour ca qu'on - // n'a jamais pu regarder ce que le jeu y peint. - // - // Ici on capture le render target AU MOMENT OU SON EPOQUE SE FERME, c'est-a-dire juste - // apres son dernier dessin et avant qu'il serve a autre chose. Une seule image capturee, - // toutes ses epoques, puis la sonde se desarme d'elle-meme. private static readonly int _dumpAfterSeconds = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_UIPROBE_DUMP"), out int ds) && ds > 0 ? ds : 0; private static long _dumpArmedAtMs; private static bool _dumpDone; private static int _dumpSeq; - private static Image.Texture _epochTex; // le RT de l'epoque en cours, garde pour la capture + private static Image.Texture _epochTex; - // [HUDLESS 28/07] RYUJINX_MVPP_HUDLESS=1 : DETECTION SEULE, ne capture rien, ne change rien. - // - // Reconnait, PAR SA FORME, le render target qui porte l'image tonemappee SANS interface -- - // celui que la Frame Generation reclame et qu'on n'a jamais pu lui donner. Signature - // mesuree le 28/07 sur XC2 : format 8 bits par canal (donc MEME espace colorimetrique que - // l'image presentee, ce qui rend la soustraction UI = backbuffer - hudless valide), - // PLEINE largeur de rendu, et une epoque nourrie (~95-99 dessins) -- une passe d'interface - // n'en aurait que quelques-uns, une passe d'effet serait plus petite. - // - // JAMAIS par adresse : 0x2280250000 change d'un lancement a l'autre. - // - // Ce mode existe pour verifier que la regle attrape le bon buffer A TOUS LES COUPS avant - // qu'on branche quoi que ce soit dessus. Le dossier a deja paye une regle non verifiee. private static readonly bool _hudless = Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS") == "1"; private const int HudlessMinDraws = 20; - // [HUDLESSFEED 28/07] RYUJINX_MVPP_HUDLESS_FEED=1 : COPIE le buffer sans interface vers une - // texture persistante, et la publie pour la couche Vulkan. Desarme par defaut. - // - // POURQUOI UNE COPIE ET PAS UNE REFERENCE. Le render target est RECYCLE juste apres son - // epoque -- c'est demontre : toutes les captures faites au present le trouvaient VIDE. - // Publier une simple reference donnerait donc a la Frame Generation un contenu deja - // ecrase. La copie est faite a l'instant exact de la fermeture, seul moment ou le buffer - // contient l'image. - // - // POURQUOI ON COPIE CHAQUE CANDIDAT PLUTOT QUE LE SEUL ELU. L'election ne peut se faire - // qu'a la fin de l'image, quand le buffer n'existe plus. On copie donc tout candidat au - // moins aussi nourri que le meilleur vu dans cette image : le dernier ecrase les - // precedents, et c'est justement lui le bon (29 dessins puis 81 -- mesure du 28/07). - // - // Cout : une copie GPU pleine resolution par candidat, 1 a 2 par image. private static readonly bool _hudlessFeed = Environment.GetEnvironmentVariable("RYUJINX_MVPP_HUDLESS_FEED") == "1"; @@ -120,13 +63,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _renderWidthSeen; private static int _hudlessHits; private static int _hudlessFrames; - private static int _hudlessMultiple; // images ou PLUSIEURS epoques matchent = regle ambigue + private static int _hudlessMultiple; private static long _hudlessLogMs; private static int _hudlessInFrame; - private static int _hudlessBestDraws; // [v2] meilleur candidat de l'image en cours + private static int _hudlessBestDraws; private static ulong _hudlessBestAddr; private static int _hudlessBestRank; - private static int _hudlessRetenus; // images ou un candidat a bien ete elu + private static int _hudlessRetenus; private static int _hudlessEchecsLogues; private static string _lastVerdictSig = ""; @@ -137,8 +80,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // A diagnostic on the GPU thread must never take the process down: an escaped exception is - // fatal and unlogged. On any error: disable and say why (same hardening as the other probes). try { OnDrawImpl(channel, ref state, context); @@ -150,11 +91,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// 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. - /// public static void OnPresent() { if (!_enabled) @@ -180,14 +116,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { float scale = channel.TextureManager.RenderTargetScale; - // Frame boundary = rising edge of the render scale (1x UI/native -> >1x scaled 3D scene = a - // new frame started). Flush what we accumulated for the previous frame. - // - // [21/07] /!\ CETTE DETECTION NE MARCHE QUE SI ResScale > 1. Sur XC2 (ResScale = 1) le facteur - // ne bouge jamais, la condition n'est JAMAIS vraie et la sonde reste MUETTE -- un run entier - // perdu à cause de ça. C'est la 3e sonde héritée du chantier TOTK inutilisable telle quelle sur - // XC2 (avec MvppGlowProbe, filtrée R11G11B10, et MvppHdrCensusProbe, filtrée width >= 1500). - // La vraie fin d'image est maintenant donnée par OnPresent(), appelé depuis Gpu/Window.Present. if (_prevScale == 1f && scale > 1f && _epochs.Count > 0) { FlushFrame(); @@ -208,10 +136,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed h = col0.Info.Height; } - bool depth = state.DepthTestEnable; // Boolean32 -> implicit bool + bool depth = state.DepthTestEnable; bool blend = state.BlendEnable[0]; - // Same-RT run detection: extend the current epoch, or open a new one on an address change. if (_epochs.Count > 0) { ref Epoch last = ref System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_epochs)[_epochs.Count - 1]; @@ -221,11 +148,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed last.AnyDepth |= depth; last.AnyBlend |= blend; - // [HUDSPLIT 28/07] Granularite PAR DESSIN. AnyDepth agrege toute l'epoque : - // une epoque qui melange scene et interface ressort "depth=True" et cache - // exactement la frontiere qu'on cherche. Le HUD ne teste jamais la profondeur - // -- compter les dessins SANS test, et retenir le rang du PREMIER, donne le - // point ou l'interface commence a etre posee sur l'image. if (!depth) { if (last.NoDepthDraws == 0) @@ -240,7 +162,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [HUDLESS] L'epoque qui vient de se fermer correspond-elle a la signature ? if (_hudless && _epochs.Count > 0) { Epoch fini = _epochs[^1]; @@ -256,19 +177,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _hudlessInFrame++; - // [v2 28/07] LE DERNIER, LE PLUS NOURRI. Mesure : dans certaines situations le - // jeu ecrit DEUX FOIS dans ce buffer par image (29 dessins puis 81) -- la v1 - // comptait les deux (149 % de detections, 74 images ambigues sur 151). L'image - // se construit progressivement : le bon candidat est le dernier ET le plus - // nourri. On ne tranche donc qu'a la FIN de l'image, jamais a la volee. if (fini.Draws >= _hudlessBestDraws) { _hudlessBestDraws = fini.Draws; _hudlessBestAddr = fini.Addr; _hudlessBestRank = _epochs.Count; - // [HUDLESSFEED] C'est ICI, et nulle part ailleurs, que le buffer contient - // encore l'image : son epoque vient de se fermer, il n'a pas encore servi. if (_hudlessFeed && _epochTex != null && context != null) { CopyHudless(context); @@ -277,8 +191,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [EPOCHDUMP] L'epoque precedente vient de se fermer : son render target contient - // encore ce que le jeu vient d'y peindre. C'est le SEUL instant ou on peut le voir. TryDumpEpoch(); _epochTex = col0; @@ -300,12 +212,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// [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. - /// private static void CopyHudless(GpuContext context) { try @@ -322,8 +228,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int h = src.Info.Height; Format fmt = src.Info.FormatInfo.Format; - // Recreation seulement si la FORME change (resolution dynamique, changement de - // cible) : une texture par forme, pas une par image. if (_hudlessCopy == null || _hudlessCopyW != w || _hudlessCopyH != h || _hudlessCopyFmt != fmt) { _hudlessCopy?.Release(); @@ -358,10 +262,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - /// - /// [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. - /// private static void TryDumpEpoch() { if (_dumpAfterSeconds == 0 || _dumpDone || _epochTex == null) @@ -420,9 +320,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _frameNo++; - // Classify. HDR (R11G11B10Float / RG16BA16F) = the 3D scene target; 8-bit = candidate final - // composite / UI. Composite = the largest-area LDR epoch (the output-res backbuffer). Overlay - // = LDR epochs that alpha-blend (the HUD draws). The decision is purely an ADDRESS comparison. int sceneIdx = -1, compIdx = -1; long sceneArea = -1, compArea = -1; @@ -446,8 +343,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ulong compAddr = compIdx >= 0 ? _epochs[compIdx].Addr : 0; - bool blendIntoComposite = false; // HUD alpha-blends onto the final buffer itself - ulong separateUiAddr = 0; // a distinct RT that alpha-blends (would be capturable) + bool blendIntoComposite = false; + ulong separateUiAddr = 0; int separateUiW = 0, separateUiH = 0; for (int i = 0; i < _epochs.Count; i++) @@ -456,7 +353,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed bool hdr = e.Fmt == Format.R11G11B10Float || e.Fmt == Format.R16G16B16A16Float; if (hdr || !e.AnyBlend) { - continue; // only LDR blended (overlay-like) epochs matter for the UI question + continue; } if (e.Addr == compAddr) @@ -476,7 +373,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed blendIntoComposite ? "INPLACE(NO-GO)" : "UNCLEAR"; - // Per-frame verdict line, throttled to a change of the verdict signature or every 2 s. string sig = $"{verdict}|comp0x{compAddr:X}|ui0x{separateUiAddr:X}"; long now = Environment.TickCount64; if (sig != _lastVerdictSig || now - _lastVerdictMs >= VerdictThrottleMs) @@ -499,13 +395,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"scene={sceneStr} composite={compStr} blendIntoComposite={blendIntoComposite} separateUiRT={uiStr}"); } - // First frames: dump the full epoch list so the verdict is auditable from raw data. - // - // [28/07] ET AUSSI UNE FOIS TOUTES LES 5 s ENSUITE. Motif : les 8 premieres images - // tombent sur le logo/l'ecran de chargement (mesure du 28/07 17h51 : une seule epoque, - // 768x432), donc le detail n'a JAMAIS decrit une image de jeu reelle. Or c'est - // exactement ce detail qu'il faut pour savoir comment le HUD est compose avant de - // pouvoir le sortir de l'image donnee a la Frame Generation. bool periodic = now - _lastDetailMs >= 5000; if (_framesLoggedFull < FullDetailFrames || periodic) @@ -526,9 +415,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [HUDLESS] Bilan par image, puis resume a 1 Hz. Ce qu'on veut lire : UNE seule - // epoque candidate par image (regle non ambigue) et UNE detection sur CHAQUE image - // (regle qui ne decroche pas). Tout ecart se voit dans ces trois chiffres. if (_hudless) { _hudlessFrames++; @@ -538,7 +424,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _hudlessMultiple++; } - // [v2] Election de fin d'image : UN candidat retenu, et un seul. if (_hudlessBestDraws > 0) { _hudlessRetenus++; @@ -552,12 +437,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [v2] ECHEC D'ELECTION : dire CE QU'ON A VU plutot que de le contourner par un - // repli. Mesure du 28/07 : la regle tient a 100 % dans la plupart des situations - // mais tombe a 83,9 % par moments -- 24 images sur 149 sans aucun candidat. Un - // HUD-less manquant sur une image = la FG travaille sur du perime, donc exactement - // l'artefact intermittent qu'on veut supprimer. Il faut savoir ce que sont ces - // images avant de decider quoi faire. else if (_hudlessEchecsLogues < 8) { _hudlessEchecsLogues++; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppVelocityPass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppVelocityPass.cs index 573918ca4..3b6abd122 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppVelocityPass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppVelocityPass.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,27 +6,11 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// MV++ Phase 2.2: per-object velocity pass, milestone J1 (RYUJINX_MVPP_VEL=1). - /// Scope V1 (user framing, journal): SKINNED draws only -- instanced vegetation stays on - /// camera MVs. This class starts as the skinned-draw CLASSIFIER with window counters (the - /// first verifiable brick: how many draws per frame would the pass touch, and why), and - /// grows into the inline piggyback pass (re-draw into our RT right after the actor draw, - /// while all guest state is still bound -- zero replay by design). - /// - /// Classifier v1 (heuristic, visually tuned in J2 via the debug view): cb5 bound with a - /// palette-sized buffer (>= 4 KB) + at least one enabled INTEGER vertex attribute (bone - /// indices) on a non-instanced (divisor 0) vertex buffer. Over-coverage (foliage) is - /// accepted at J1; under-coverage of an actor is a J1 failure. - /// static class MvppVelocityPass { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL") == "1"; - // Brick 3 sub-steps: 3a = the graft runs as a PROVEN NO-OP (state overrides + restore, - // no draw command); 3b = ONE re-draw per frame (RYUJINX_MVPP_VEL_DRAW=1); 3c = every - // classified scene draw (RYUJINX_MVPP_VEL_DRAW=all). private static readonly string _graftDrawMode = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DRAW"); private static readonly bool _graftDraw = _graftDrawMode is "1" or "all"; @@ -43,15 +24,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly Dictionary<(int, int), ITexture> _coverageBySize = new(); private static readonly ITexture[] _covColors = new ITexture[1]; private static int _grafts; - private static int _graftSameProg; // Tier 3 sizing: grafts sharing the previous graft's variant program - private static object _lastGraftVariant; // (batchable share -- passive counter, no behavior change) + private static int _graftSameProg; + private static object _lastGraftVariant; private static int _redraws; private static bool _graftLogged; - // 3b framing: ONE re-draw per frame (the first classified scene draw); 3c lifts it. private static bool _redrawDoneThisFrame; - // Depth bias for the EQUAL test of the re-draw (design requirement #3; 0 = off). private static readonly float _depthEps = float.TryParse( Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DEPTH_EPS"), System.Globalization.NumberStyles.Float, @@ -60,38 +39,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly uint[] _covMasks = [0xfu]; - // ── Double-transform JALON (a), sous-pas 1 : RING GPU (RYUJINX_MVPP_VEL_RING=1). - // Transparence du TUYAU avant le contenu : on crée 2 buffers GPU (ring N/N-1), on y - // UPLOAD la palette snapshotée de chaque draw greffé, et PERSONNE ne le lit encore - // (pas de double-skin, pas de shader touché) → la baseline doit rester STRICTEMENT - // identique. Le sous-pas suivant (variante VS) bindera ce ring comme palette N-1. private static readonly bool _ringEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_RING") == "1"; - // Milestone (a) two-pass scaffold: second identical emission in the graft (see the - // re-draw site). Default OFF; requires the ring path too (RYUJINX_MVPP_VEL_RING). private static readonly bool _twoPassEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_TWOPASS") == "1"; - // (b1) sub-step 2: bind the MvppVariant program for the re-draw (byte-identical yet). private static readonly bool _variantEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_VARIANT") == "1"; - // (b1) parking FPS A/B: force the coverage target back to R8Unorm while keeping the WHOLE - // variant path + FS MV work identical (the FS still writes MV.xy; comp1 is dropped and comp0 - // clamped -- irrelevant, we only read FPS). Isolates the RG16F format's bandwidth cost. private static readonly bool _covForceR8 = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_COV_R8") == "1"; - // (b1) Tier 1: per-graft restore that re-emits ONLY the 9 state groups the graft changed - // (instead of SetAllDirty's 28). Same eager UpdateState + RT-cache invalidation. Gated for - // the A/B (ON = targeted, OFF = the old SetAllDirty). private static readonly bool _targetedRestore = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_TARGETED_RESTORE") == "1"; - // (b1) sub-step 5a: bind the N-1 palette cbuf (resolved into MvppVelState.VariantN1Binding) - // to the ring before the variant re-draw, so the cloned clip_{N-1} reads it. Needs the ring - // (RYUJINX_MVPP_VEL_RING) for the palette upload. Gated OFF -> sub-step 4 behaviour. private static readonly bool _n1Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_N1") == "1"; private static bool _n1BindLogged; @@ -99,7 +61,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static bool _variantFallbackLogged; private static int _variantBinds; private static int _variantFallbacks; - private const int RingSlotBytes = 16 * 1024 * 1024; // 16 MB/slot (dense frames hit 4 MB) + private const int RingSlotBytes = 16 * 1024 * 1024; private const int RingMaxPalette = 16384; private const int RingMaxSlots = 4; private static readonly BufferHandle[] _ringBuf = new BufferHandle[RingMaxSlots]; @@ -109,54 +71,32 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _ringDraws; private static long _ringLogMs; - // (b1) sub-step 5b-1 (RYUJINX_MVPP_VEL_PAIR=1, requires RING): N-1 PAIRING, log-only. - // Key = the ProbeDelta pattern reused verbatim: band (cb5 in [4096,5120) = actor band 0, - // else 1) + per-band per-frame ordinal; a draw MATCHES if the same (band, ordinal) - // existed last frame with the SAME cb5 size (the signature check of ProbeDelta). - // This sub-step only MEASURES the match rate; the graft still binds the CURRENT - // palette (5a, MV=0) -- the real N-1 bind is 5b-2, sized by these numbers. private static readonly bool _pairEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_PAIR") == "1"; - private const int PairMaxDraws = 4096; // per-band ordinal cap (observed peak 2434/frame) - // Tables sized for the 4-slot ring unconditionally (slots 2-3 simply never touched - // when N1BIND is off -- zero behavior change, PIdx formula unchanged). + private const int PairMaxDraws = 4096; private static readonly int[] _pairOff = new int[RingMaxSlots * 2 * PairMaxDraws]; private static readonly int[] _pairSize = new int[RingMaxSlots * 2 * PairMaxDraws]; - private static readonly int[] _pairCount = new int[RingMaxSlots * 2]; // filled ordinals of the CLOSED frame, per slot*band + private static readonly int[] _pairCount = new int[RingMaxSlots * 2]; private static readonly int[] _pairOrd = new int[2]; private static int _pairHit; private static int _pairMiss; private static int _pairSzDiff; private static int _pairOvf; - // (b1) 5b-2 SPLIT: the same counters PER BAND (B0 actors / B1 props-decor) -- the GATE - // before the real N-1 bind: per-band ordinals isolate actors from the decor flood, so - // band 0 should pair far above the 45-70% churn aggregate. The aggregate counters above - // are KEPT and incremented in parallel for this run: sums must match, window by window. private static readonly int[] _pairHitB = new int[2]; private static readonly int[] _pairMissB = new int[2]; private static readonly int[] _pairSzDiffB = new int[2]; - // (b1) 5b-2 T2 (RYUJINX_MVPP_VEL_T2=1): temporal palette gate BY ADDRESS. 5b-1/5b-2 - // proved the decor flood is bit-STATIC cb5 content (MV=0 is the RIGHT answer there) - // while actors rewrite their palettes every frame -- so a palette whose content did - // not change since the previous frame LEAVES classification entirely (no band, no - // ordinal, no graft -> camera-MV fallback by construction). Safety both ways: false - // ANIM = benign over-graft (N-1==N -> MV=0), false STATIC = camera fallback = - // baseline, never worse. Hash window = HEAD + TAIL of the span the classifier - // already read (a near-idle actor may only animate late bones -- head alone would - // hold a false STATIC). T2=0/absent: path strictly identical to 295c539c2. private static readonly bool _t2Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_T2") == "1"; - private const int T2WindowBytes = 256; // head window = tail window, centralized - private const int T2EvictAgeFrames = 120; // ~2 s at 60 fps unseen -> evicted (amortized) + private const int T2WindowBytes = 256; + private const int T2EvictAgeFrames = 120; private struct T2Entry { public ulong Hash; public long LastFrame; - public bool AnimThisFrame; // same-frame re-bind keeps the frame's verdict (a - // multi-part actor sharing cb5 must not self-eclipse) + public bool AnimThisFrame; } private static readonly Dictionary _t2Table = new(16384); @@ -166,47 +106,23 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _t2Static; private static int _t2Anim; - // (b1) 5b-2 T2G-O1 (RYUJINX_MVPP_VEL_T2G=1, requires T2): graft gate, band-0 only, - // among ANIMATED palettes -- the V1 scope ("vegetation stays on camera MV") finally - // applied to the graft itself. Band predicate = the EXACT SnapshotToRing expression, - // NO widening (phase 0: the [3072,4096) hole is MEASURED first -- szhist bucket b + - // its dedicated sample are the under-coverage watchdog). PAIR ordinal band untouched: - // graft gate != pairing key; band-0 ordinals never see band-1 draws either way (P2). - // Safety: a mis-banded actor loses the graft -> camera fallback = baseline, never worse. private static readonly bool _t2gEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_T2G") == "1"; private static readonly bool _t2gActive = _t2gEnabled && _t2Enabled; - // (b1) 5b-2 T2G-W1 (RYUJINX_MVPP_VEL_T2G_MIN, default 4096): explicit lower bound of - // the graft gate = [MIN, 5120). W1 = 3840: actor BODIES (3840 B = 80 bones, 12741 - // verts) enter the graft; the 3072/54-verts micro-piece stays PARKED on camera MV - // (revisit trigger = visual A/B at the ring). [MIN,4096) = the EXT population: it - // pairs in B1 (PAIR ordinal band UNTOUCHED [4096,5120) -- countersigned invariant; - // any realignment = separate step). EXT pair rate = THE pre-ring read: a miss is - // MV=0 = baseline, a FALSE hit would be another draw's N-1 palette = wrong MV once - // the real bind lands -- the first mechanism that could do WORSE than baseline. private static readonly int _t2gMin = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_T2G_MIN"), out int t2gMinEnv) ? t2gMinEnv : 4096; private static bool _t2gWarned; private static int _t2gB0Graft; private static int _t2gExtGraft; private static int _t2gB1Gated; - private static readonly int[] _t2gSzHist = new int[5]; // <3072 | [3072,3840) | [3840,4096) | [4096,5120) | >=5120 + private static readonly int[] _t2gSzHist = new int[5]; private static bool _t2gB1Sampled; private static bool _t2gHoleSampled; private static int _t2gExtHit; private static int _t2gExtMiss; private static int _t2gExtSzDiff; - // (b1) 5b-2 RING (RYUJINX_MVPP_VEL_N1BIND=1, requires RING+PAIR+N1): the REAL N-1 - // bind. PAIR HIT -> the graft binds the PREVIOUS slot at the paired offset: the clone - // clip_(N-1) reads the actual previous-frame palette -> first real non-zero MVs. - // MISS/szdiff/ovf -> trivial bind (current palette, MV=0): a failed pairing can never - // produce a wrong MV (graved principle). Real cross-frame READS outgrow the 2-slot - // write/unread double buffer: 4 slots cover <= 2 frames of GPU lag (3 frames = a - // transient one-frame overwrite of a palette being read, invisible pre-J3, bounded). - // Off/absent: 2 slots + current bind = T2G-W1 behavior exact (modulo 2 == XOR flip). - // Declared AFTER every flag it references (static init is textual order). private static readonly bool _n1BindEnvSet = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_N1BIND") == "1"; private static readonly bool _n1BindActive = _n1BindEnvSet && _ringEnabled && _pairEnabled && _n1Enabled; @@ -216,48 +132,27 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _n1RealBinds; private static int _n1TrivialBinds; - // (b1) 5b-3 sentinel diag (RYUJINX_MVPP_VEL_DEBUG_MV=1, debug-only): the coverage/MV - // RT is cleared to a sentinel so the debug view can tell "never written" (background) - // from "written but ~zero MV" (blue marker). No effect outside debug-view runs. private static readonly bool _debugMvSentinel = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DEBUG_MV") == "1"; - // (b1) 5B3 diag: re-draw depth test = Always (write off), see the drawOp override. private static readonly bool _depthAlways = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DEPTH_ALWAYS") == "1"; - // (b1) 5B3 diag (RYUJINX_MVPP_VEL_SAMPLEQ=1): samples-passed query around ONE re-draw - // per 2s window -- the machine answer to "does the variant re-draw rasterize ANY - // fragment?" (all static probes exhausted: state, modules, depth, descriptors). private static readonly bool _samplesProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_SAMPLEQ") == "1"; private static bool _sqArmed; private static int _sqLogged; - // (b1) 5B3 diag (RYUJINX_MVPP_VEL_READBACK=1): CPU readback of the coverage texture - // once per 2s window, right after the probed re-draw -- the PHYSICAL verdict: does the - // texture contain the writes? (fragments rasterize into OUR RG16F per runs 10-11, yet - // the debug view shows nothing -- either the reads are broken or the writes get lost.) private static readonly bool _readbackProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_READBACK") == "1"; private static bool _rbArmed; private static int _rbLogged; - // Per-draw N-1 bind source (set by SnapshotToRing): defaults to the CURRENT palette - // (trivial, MV=0); a PAIR HIT under N1BIND redirects it to the previous slot. private static int _ringN1Slot; private static int _ringN1Offset = -1; private static int _ringN1Size; private static bool _ringN1IsReal; - // (b1) FIX-RACINE F2: the per-draw PLAUSIBILITY GATE the Phase-2 design graved and the - // real bind shipped without ("a false pairing NEVER produces a false MV"). CPU shadow - // of the ROOT translation (bone 0: floats 3/7/11, the P2B spread column) per - // (slot, band, ordinal), captured at upload; at a HIT, the paired N-1 root must lie - // within RYUJINX_MVPP_VEL_PLAUSIBILITY world units of the current one (default 3.0 - // units/frame -- generous: run ~0.3 u/f, boss dash ~2-3 u/f; too tight = real fast MVs - // rejected -> camera-MV ghosting; too loose = false hits pass -> caught by F1 but the - // gate is not doing its job). Out of bounds -> trivial fallback (current palette, MV=0). private static readonly float _plausThreshold = float.TryParse( Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_PLAUSIBILITY"), System.Globalization.NumberStyles.Float, @@ -267,19 +162,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _plausOk; private static int _plausRej; - // With PAIR on, the ring flip leaves OnFrameEnqueued (SurfaceFlinger thread): the - // enqueue only RAISES this pending flag, and the flip itself (slot, offset, ordinals, - // counts) runs at the top of the first SnapshotToRing of the frame = GPU.MainThread. - // Benign as a counter race, intolerable once offsets are PAIRED across frames (the - // grouped-upload crash lesson, applied preventively). PAIR off -> legacy behavior. private static volatile bool _ringFlipPending; private static int PIdx(int slot, int band, int ord) => (slot * 2 + band) * PairMaxDraws + ord; - /// - /// (b1) T2: FNV-1a 64 over 8-byte steps, HEAD + TAIL windows of the palette span the - /// classifier already read. Zero alloc (span recast), no second guest-memory read. - /// private static ulong T2Hash(ReadOnlySpan data) { ReadOnlySpan q = System.Runtime.InteropServices.MemoryMarshal.Cast(data); @@ -301,10 +187,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return h; } - /// - /// (b1) T2: amortized age eviction, called at the 2 s window boundary on the GPU - /// thread. Steady state: zero allocation (reused key list). - /// private static void T2EvictOld() { long frame = System.Threading.Interlocked.Read(ref _t2Frame); @@ -325,25 +207,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _t2EvictList.Clear(); } - // (b1) sub-step 5a: offset+size in the CURRENT ring slot where THIS graft's palette landed, - // so the graft can bind the N-1 cbuf to it. -1 = this draw's palette was not uploaded. private static int _ringLastOffset = -1; private static int _ringLastSize; - /// - /// Snapshots the classified draw's cb5 palette (frame N) into the current ring slot. - /// PER-DRAW SetBufferData, called from Graft on the GPU.MainThread -- the CORRECT - /// thread for renderer commands. (Grouped upload was reverted: OnFrameEnqueued runs on - /// SurfaceFlinger.Composer, so uploading there raced the ThreadedRenderer command queue - /// -> IndexOutOfRange in SetUniformBuffers. Grouped-on-GPU-thread = cold rethink.) - /// NO consumer yet -- transparency of the pipe. Slots kept at 16 MB (saturation fix). - /// private static void SnapshotToRing(GpuContext context, GpuChannel channel) { - // (b1) 5b-1: deferred frame flip, on the GPU thread (see _ringFlipPending). Runs - // BEFORE this frame's first palette lands so it goes into the fresh slot. Freezes - // the closed frame's filled ordinal counts (lookups beyond them = MISS -- kills - // false hits on stale entries from two frames ago). if (_pairEnabled && _ringFlipPending) { _ringFlipPending = false; @@ -353,18 +221,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _pairOrd[0] = 0; _pairOrd[1] = 0; - _ringSlot = (_ringSlot + 1) % _ringSlotCount; // modulo 2 == old XOR when N1BIND off + _ringSlot = (_ringSlot + 1) % _ringSlotCount; _ringOffset = 0; _ringDraws = 0; } if (!_ringCreated) { - // (b1) sub-step 5a fix (ring vector): HOST-MAPPED so per-draw SetData is a DIRECT - // memcpy (BufferHolder.SetData _map!=0 path) -- no command buffer, no EndRenderPass, - // no cbuf-mirror Rebind. Double-buffer (write current slot / read N-1 slot) keeps the - // written range never "in use" -> the direct path holds in 5a AND 5b. - // (b1) RING: 4 slots under N1BIND (real cross-frame reads need in-flight headroom). for (int i = 0; i < _ringSlotCount; i++) { _ringBuf[i] = context.Renderer.CreateBuffer(RingSlotBytes, BufferAccess.HostMemory); @@ -375,7 +238,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"{(_n1BindActive ? " (N1BIND: real N-1 reads armed)" : " (nobody reads it yet -- pipe transparency)")}."); } - _ringLastOffset = -1; // (b1) 5a: default "no palette this draw" -> graft skips N-1 bind + _ringLastOffset = -1; _ringN1Offset = -1; _ringN1IsReal = false; @@ -393,20 +256,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } ReadOnlySpan pal = channel.MemoryManager.Physical.GetSpan(addr, sz); - context.Renderer.SetBufferData(_ringBuf[_ringSlot], _ringOffset, pal); // GPU thread + context.Renderer.SetBufferData(_ringBuf[_ringSlot], _ringOffset, pal); - _ringLastOffset = _ringOffset; // (b1) 5a: this draw's palette location in the ring + _ringLastOffset = _ringOffset; _ringLastSize = sz; - // (b1) RING: N-1 bind source defaults to the CURRENT palette (trivial, MV=0); - // a PAIR HIT below redirects it to the previous slot when N1BIND is armed. _ringN1Slot = _ringSlot; _ringN1Offset = _ringOffset; _ringN1Size = sz; - // (b1) 5b-1: pairing bookkeeping, log-only (the graft still binds the CURRENT - // palette). Look up this draw's (band, ordinal) in the CLOSED frame's table, then - // record it in the current one. CPU-pure: two array reads, two writes, no renderer. if (_pairEnabled) { int band = (sz >= 4096 && sz < 5120) ? 0 : 1; @@ -418,21 +276,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else { - int prevSlot = (_ringSlot + _ringSlotCount - 1) % _ringSlotCount; // == ^1 with 2 slots + int prevSlot = (_ringSlot + _ringSlotCount - 1) % _ringSlotCount; - // (b1) T2G-W1: mirror counters for the EXT sub-population [MIN,4096) - // inside B1 -- THE pre-ring read (false hit = wrong MV once bound). bool extDraw = _t2gActive && band == 1 && sz >= _t2gMin && sz < 4096; if (ord >= _pairCount[prevSlot * 2 + band]) { - _pairMiss++; // last frame had fewer draws in this band (spawn/ordinal shift) + _pairMiss++; _pairMissB[band]++; if (extDraw) { _t2gExtMiss++; } } else if (_pairSize[PIdx(prevSlot, band, ord)] != sz) { - _pairSzDiff++; // signature mismatch -> unusable as N-1 (counted apart from miss) + _pairSzDiff++; _pairSzDiffB[band]++; if (extDraw) { _t2gExtSzDiff++; } } @@ -442,12 +298,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _pairHitB[band]++; if (extDraw) { _t2gExtHit++; } - // (b1) RING: THE real bind -- same (band, ordinal, size) existed last - // frame -> the clone reads the actual N-1 palette. First real MVs. - // (b1) FIX-RACINE F2: gated by per-draw plausibility -- the paired N-1 - // ROOT translation must be within the threshold of the current one; - // out of bounds = implausible pairing -> trivial fallback (MV=0), - // never a false MV (the graved Phase-2 principle, finally in code). if (_n1BindActive) { ReadOnlySpan curF = System.Runtime.InteropServices.MemoryMarshal.Cast(pal); @@ -476,8 +326,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _pairOff[cur] = _ringLastOffset; _pairSize[cur] = sz; - // (b1) F2: shadow the ROOT translation (bone 0) for the plausibility gate - // (sz >= 48 guaranteed above = floats 3/7/11 are always in range). ReadOnlySpan palF = System.Runtime.InteropServices.MemoryMarshal.Cast(pal); int curT = cur * 3; _pairRootT[curT] = palF[3]; @@ -488,11 +336,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - _ringOffset += (sz + 255) & ~255; // 256-align the next entry + _ringOffset += (sz + 255) & ~255; _ringDraws++; } - /// Frame boundary (present enqueue): the next graft clears the coverage. public static void OnFrameEnqueued() { if (Enabled) @@ -500,15 +347,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed MvppVelState.FrameClearPending = true; _redrawDoneThisFrame = false; - // (b1) 5b-2 T2: frame id for the address table (age eviction + same-frame - // verdict). Cross-thread read is counter-grade; Interlocked for cleanliness. if (_t2Enabled) { System.Threading.Interlocked.Increment(ref _t2Frame); } - // Delta probe: flip the palette ring (this frame becomes N-1), restart the - // per-band ordinals. Same GPU thread as AfterDraw, no lock needed. if (_deltaEnabled) { _dSlot ^= 1; @@ -516,12 +359,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _dOrd[1] = 0; } - // GPU ring: flip slots (this frame's palettes become N-1), reset the bump - // offset. NO renderer command here -- OnFrameEnqueued is the SurfaceFlinger - // thread; the per-draw upload already ran on the GPU thread inside Graft. - // (b1) 5b-1: with PAIR on, this thread only RAISES the pending flag -- the - // flip itself runs on GPU.MainThread at the frame's first SnapshotToRing - // (offsets paired across frames can't tolerate the counter-grade race). if (_ringEnabled) { long now = Environment.TickCount64; @@ -537,8 +374,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed pair = $" | pair: hit {_pairHit} miss {_pairMiss} szdiff {_pairSzDiff} ovf {_pairOvf}" + $" (rate {(total > 0 ? 100f * _pairHit / total : 0f):0.0}%)"; - // (b1) 5b-2 SPLIT: same window, same cadence -- the aggregate line above - // is kept for THIS run so the sum check reads straight off the log. int t0 = _pairHitB[0] + _pairMissB[0] + _pairSzDiffB[0]; int t1 = _pairHitB[1] + _pairMissB[1] + _pairSzDiffB[1]; pairSplit = @@ -558,7 +393,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_t2Enabled) { - // (b1) T2 counters ride the PAIR line; sanity: static+anim == seen. pair += $" | T2 seen={_t2Seen} static={_t2Static} anim={_t2Anim}"; _t2Seen = 0; _t2Static = 0; @@ -575,8 +409,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_t2gActive) { - // (b1) T2G-W1 window lines; sanity: X+E+Y == anim, sum(hist) == anim - // (same ring window as the T2 counters, resets grouped). Logger.Info?.Print(LogClass.Gpu, $"MVPP T2G: graft_b0={_t2gB0Graft} graft_ext={_t2gExtGraft} gated={_t2gB1Gated}"); Logger.Info?.Print(LogClass.Gpu, @@ -605,7 +437,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else { - _ringSlot = (_ringSlot + 1) % _ringSlotCount; // modulo 2 == old XOR + _ringSlot = (_ringSlot + 1) % _ringSlotCount; _ringOffset = 0; _ringDraws = 0; } @@ -620,13 +452,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _draws; private static int _writeOffSkips; - // Cold-start forensics (J2): ONE instrumented run must say what the ~25-30 s mute - // phase is made of. Three separable causes, one counter each: draws muted upstream - // (RenderTargetScale still 1), candidates arriving with NO camera snapshot ever - // published (SnapshotCurrent's valid flag is one-way false->true), and candidates - // with a snapshot where NO translation projects (stale/wrong VP -- note this also - // has a steady-state baseline from non-actor palette draws, so read it as a - // TIMELINE, not an absolute). private static int _scale1Skips; private static int _candNoSnap; private static int _candProjFail; @@ -637,106 +462,48 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static bool _sawSnapshot; private static bool _sawClassified; - // Phase-reject probe (dossier 03/07: healthy lineage + 100% publication, yet the - // projection rejects EVERYTHING in 30 s stretches with bimodal projfail levels). - // Two axes, both stamped at the same instant so the dyn-res suspect is convicted - // or cleared ON THE SAMPLE, not by inference: (1) per-window distribution of the - // candidates' DS dims vs the pinned scene dims; (2) up to 5 failed-projection - // samples per window: translation read -> resulting ndc -> draw DS -> pinned DS. - // Predictions written before the run: dyn-res = PLAUSIBLE translations (village - // coords) with consistently off-screen ndc; cb5-content = ABERRANT translations - // (NaN/zeros/huge, or no magnitude-plausible triple at all). private const int ProjSamplesPerWindow = 5; private static int _projSamples; - // Magnitude floor for candidate triples (RYUJINX_MVPP_VEL_TMIN, default 0.5 = the - // historical behavior). Probe run 07:45 (910 samples): the 0.5 floor admits - // unit-length ROTATION ROWS whose projection is dominated by the VP's constant - // 4th column -- LOCKED ndc/cw shared across draws of different cb5 sizes -- i.e. a - // global all-or-nothing flip driven by camera ORIENTATION (the whole phase-reject - // dossier: accept-all, reject-all, pan-freeze). A floor of ~10 excludes every unit - // row; real world-space translations (hundreds-thousands at the village) pass. private static readonly float _tMin = float.TryParse( Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_TMIN"), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float tmin) ? tmin : 0.5f; - // Classifier v4 (RYUJINX_MVPP_VEL_V4=1, default OFF): TMIN run + P2B probe - // reconciliation. The Phase 2.1-proven layout is the probe's SPREAD pattern - // (x/+4/+8 = the 4th COLUMN of a row-major 3x4 = m3/m7/m11 at stride 0x30), and - // the TMIN samples proved the first 0x180 B of actor palettes are LOCAL-space - // (t=NONE: no float >= 10 among the first 8 bones). v4 therefore classifies on - // the SPREAD translations scanned over the FULL palette depth (0xC00 = 64 bones) - // and demotes the contiguous scan (which legitimately matches world-position - // INSTANCE pools = the vegetation over-coverage) to a counter. private static readonly bool _v4Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_V4") == "1"; private static int _accContig; private static int _accSpread; - // COH (RYUJINX_MVPP_VEL_COH=1, on top of V4): the v4 run's flood = instance pools - // share the exact 3x4 stride-0x30 layout (P2.1 already said "palettes d'os + - // instances"), so the spread pattern alone cannot tell them apart. But a real - // skeleton exposes MANY spread translations packed in a SMALL volume (~64 bones - // within a few units -- Link windows read accS 9120 vs accC 168 on 4.3 KB - // palettes), an instance pool spreads its positions over a map chunk, and a lucky - // mixture exposes 1-2. Accept only clustered families: at least CohMinBones - // plausible spread translations whose bounding box stays within CohMaxSpan units - // (15 covers a Zonai machine), one of which projects. private static readonly bool _cohEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_COH") == "1"; private const int CohMinBones = 6; private const float CohMaxSpan = 15f; - private static int _cohRejCount; // spreadProj but spreadCount < CohMinBones - private static int _cohRejSpan; // spreadCount ok but span > CohMaxSpan + private static int _cohRejCount; + private static int _cohRejSpan; private static bool _cohDiagLogged; - // COH v2 (RYUJINX_MVPP_VEL_COH2=1, on top of V4): compactness classifier that - // replaces v1's count+span. Accept if >= CohMinBones2 supra-floor translations, one - // projects, and >= CohInlierFrac of them cluster within CohMaxSpan of the centroid. private static readonly bool _coh2Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_COH2") == "1"; private const int CohMinBones2 = 3; private const float CohInlierFrac = 0.6f; private static int _coh2Rejects; - // COH2 magnitude floor (RYUJINX_MVPP_VEL_COH2_FLOOR, default 4): LOWER than TMIN=10 - // so a compact skeleton whose bones live at ~8-12 keeps all of them (deep run: the - // seed [3 10 3.5] grazed TMIN=10 and starved Link of a bone), but still ABOVE the - // unit-length rotation-row family (~1) which is itself compact and would sneak past - // the inlier test if the floor let it in. private static readonly float _coh2Floor = float.TryParse( Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_COH2_FLOOR"), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float c2f) ? c2f : 4f; - // COH2 no-projection (RYUJINX_MVPP_VEL_COH2_NOPROJ=1): drop the "a bone translation - // lands in the frustum" gate. COH2 run proved that gate is the ORIGIN ARTIFACT the - // Phase 2.1 probe warned about -- actor bone translations are LOCAL (span 2.3, - // clustered ~2-15), so they project iff the WORLD ORIGIN is on screen, not iff the - // actor is (every reject was inliers=0 = spreadProj=false on a compact span-2.3 - // palette). The GRAFT already localizes (depth-Equal re-draw only rasterizes where - // the actor actually is), so classification needs only "compact skinned palette". private static readonly bool _coh2NoProj = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_COH2_NOPROJ") == "1"; - // First accepted big palette per window also gets a diag line (contrast with the - // rejected one): the magnitude histogram reveals WHERE the families live. private static bool _cohDiagAccLogged; - // Integer-attribute probe (J2 props-tightening, MEASURE before filtering): among - // COH3-CLASSIFIED draws, does an integer-valued vertex attribute (bone indices) - // separate actors from palette-placed props? Split by cb5 band (actor-probable = - // 4-5 KB compact palette vs the rest = props), and by strict (Sint/Uint) vs broad - // integer family (Sint/Uint/Uscaled/Sscaled = bits 3-6, since Nintendo titles often - // store bone indices as Uscaled). Prediction: skinned actors carry them per-vertex, - // rigid palette-placed props do NOT. A clean split unlocks the filter; a fuzzy one - // (props with int attrs, or actors without) is learned HERE, before breaking things. - private const uint IntStrictMask = (1u << 3) | (1u << 4); // Sint, Uint - private const uint IntBroadMask = (1u << 3) | (1u << 4) | (1u << 5) | (1u << 6); // + Uscaled, Sscaled + private const uint IntStrictMask = (1u << 3) | (1u << 4); + private const uint IntBroadMask = (1u << 3) | (1u << 4) | (1u << 5) | (1u << 6); private static int _clsActor; private static int _clsActorIntS; private static int _clsActorIntB; @@ -744,19 +511,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _clsPropIntS; private static int _clsPropIntB; - // ── Double-transform ÉTAPE 0 : sonde de deltas (RYUJINX_MVPP_VEL_DELTA=1, CPU-only, - // zéro rendu). For each classified draw matched frame-to-frame by per-band ORDINAL - // (principle #1: signature=band, individual=ordinal; the sonde-2.0 ~92% stability), - // snapshot its cb5 palette's stride-0x30 translations into a 2-slot ring and, next - // frame, compare index-wise (bone k↔bone k, stable within a skeleton). Per window, - // per band: distribution of per-palette max/median translation delta. Prediction: - // palette-placed props = delta ~0 (motion IS the classifier); animated actors = - // nonzero (idle small, gestures large -> the future plausibility-gate thresholds, - // measured not guessed); a "prop" with nonzero delta genuinely moves -> deserves it. private static readonly bool _deltaEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DELTA") == "1"; - private const int DeltaMaxDraws = 512; // per-band ordinal cap; overflow draws skip - private const int DeltaMaxTris = 64; // translations stored per draw (raw order) + private const int DeltaMaxDraws = 512; + private const int DeltaMaxTris = 64; private static readonly float[] _dTris = new float[2 * 2 * DeltaMaxDraws * DeltaMaxTris * 3]; private static readonly int[] _dCb5 = new int[2 * 2 * DeltaMaxDraws]; private static readonly int[] _dCount = new int[2 * 2 * DeltaMaxDraws]; @@ -771,11 +529,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int DIdx(int slot, int band, int ord) => (slot * 2 + band) * DeltaMaxDraws + ord; - /// - /// Snapshots this classified draw's palette translations and, if the same-ordinal - /// same-band draw existed last frame with a matching cb5 size, tallies the per-bone - /// translation delta distribution into the window stats. CPU-only. - /// private static void ProbeDelta(GpuChannel channel, int cb5Size, int band) { int ord = _dOrd[band]; @@ -810,7 +563,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int prev = DIdx(_dSlot ^ 1, band, ord); if (_dCb5[prev] != cb5Size || _dCount[prev] == 0 || n == 0) { - return; // no frame-N-1 match (first frame, spawn/cull ordinal shift, size change) + return; } int m = Math.Min(n, _dCount[prev]); @@ -861,11 +614,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Palette read depth (RYUJINX_MVPP_VEL_PALCAP, bytes; default 0xC00 = 64 matrices = - // the historical v4 cap). The COH diag proved the sampled rejects (14592 B palettes) - // expose only 1-2 spread translations = COUNT starvation, and 14592 B = ~304 - // matrices >> the 64 read. This run lifts the cap to see whether the world-magnitude - // bones live past matrix 64 (cap-driven) or are genuinely absent. private static readonly int _palReadCap = int.TryParse( Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_PALCAP"), out int palcap) && palcap > 0 ? palcap : 0xC00; @@ -875,7 +623,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static int _candDsOtherW; private static int _candDsOtherH; - /// One-shot "first event" stamp, relative to the first classifier call. private static void FirstEvent(ref bool seen, string what) { if (!seen) @@ -887,29 +634,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } private static int _bigCb5; private static int _intAttr; - private static int _skinned; // both conditions met + private static int _skinned; private static long _skinnedCb5Sum; private static int _skinnedCb5Max; private static bool _sampleLogged; - // Attribute-type census on big-cb5 draws: run 1 showed int-attr = 0% everywhere, so - // TOTK does not use Sint/Uint vertex attributes -- this measures what it DOES use - // (bone indices are commonly Unorm/Uscaled bytes on Nintendo titles) before the - // classifier gets fixed on facts instead of a second guess. - private static readonly int[] _typeCensus = new int[8]; // VertexAttribType 1..7 + private static readonly int[] _typeCensus = new int[8]; private static int _censusDraws; private static int _instancedAttr; private static int _nonInstanced; private static int _niCb5None; - private static int _niCb5Small; // < 512 B: single model matrix / params - private static int _niCb5Palette; // 512..4095 B: per-part bone sub-palette band - private static int _niCb5Big; // >= 4 KB + private static int _niCb5Small; + private static int _niCb5Palette; + private static int _niCb5Big; - /// - /// Called from DrawImpl RIGHT AFTER the game's draw was issued -- the single funnel all - /// real draws go through (most TOTK draws are deferred instanced ones that never reach - /// the end of DrawEnd). Classifies the draw and, for skinned candidates, runs the graft. - /// public static void AfterDraw( DrawManager drawManager, ThreedClass engine, @@ -946,8 +684,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static bool ClassifyAndCount(GpuChannel channel, ref ThreedClassState state, int drawCount, int drawInstances) { - // Window bookkeeping runs BEFORE the scale early-out so a fully-muted phase - // (all draws at scale 1) still produces window lines during cold-start. long now = Environment.TickCount64; if (_windowStartMs == 0) { @@ -973,13 +709,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { LogWindow(); - // (b1) T2: amortized table eviction at the same cadence (GPU thread). if (_t2Enabled) { T2EvictOld(); } - // (b1) 5B3 samples probe: arm one instrumented re-draw for this window. if (_samplesProbe) { _sqArmed = true; @@ -1003,10 +737,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _draws++; FirstEvent(ref _sawScaledDraw, "scaled draw"); - // J2 line 2: depth-write-off draws (transparents/effects) can NEVER pass the - // EQUAL re-draw -- they test against a depth they did not write (the proven - // black-hole family) -- and are not skinned actors to begin with. They keep - // the camera-MV fallback by design. if (!state.DepthWriteEnable) { _writeOffSkips++; @@ -1014,7 +744,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return false; } - // Condition 1: palette-sized cb5 on the vertex stage. int cb5Size = 0; bool bigCb5 = false; if ((channel.BufferManager.GetGraphicsUniformBufferUseMask(0) & (1u << 5)) != 0) @@ -1032,9 +761,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _bigCb5++; } - // Condition 2: at least one enabled INTEGER-ish vertex attribute (bone indices) - // whose vertex buffer is per-vertex (divisor 0 -- instanced foliage excluded). - // Census in parallel: what attribute types exist at all on big-cb5 draws. bool intAttr = false; int intAttrIndex = -1; uint seenTypes = 0; @@ -1060,9 +786,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed VertexAttribType type = attr.UnpackType(); seenTypes |= 1u << (int)type; - // Maxwell: the per-buffer Divisor is only meaningful when the buffer's - // instanced ENABLE is set -- a bare Divisor != 0 test reads stale values and - // classified 100% of draws as instanced (census run 2's contamination). if (bufferIndex < 16 && state.VertexBufferInstanced[bufferIndex] && vb.Divisor != 0) @@ -1101,11 +824,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _intAttr++; } - // Classifier v2 (census run 2: big-cb5 draws are 100% Float attrs AND 100% - // instanced = the VEGETATION, not the actors; actors draw in parts with small - // per-part bone sub-palettes and float-encoded bone indices). Actor signature = - // NOT instanced + cb5 in the palette band. The size-band census below validates - // the 512 B threshold in the same run. if (!anyInstanced) { _nonInstanced++; @@ -1128,19 +846,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Classifier v3 (runs 3-4: ~ALL TOTK draws carry an instanced-enabled attribute - // stream, so state-based axes are dead): CONTENT-based, with the tool Phase 2.1 - // proved -- a draw is a plausible actor if its cb5 palette band contains a - // world-space translation that PROJECTS into the camera frustum. This is also the - // seed of the per-draw plausibility gate the design requires anyway. bool skinnedV3 = false; if (cb5Size >= 512) { FirstEvent(ref _sawCandidate, "candidate draw (write-on, cb5 >= 512 B)"); - // Probe axis 1: this candidate's DS dims vs the pinned scene dims, stamped - // per window -- if the bimodal projfail levels track a 2666<->1332 flap, - // the dyn-res suspect is settled before the samples are even read. Image.Texture candDs = channel.TextureManager.RenderTargetDepthStencil; ITexture candDsHost = candDs?.HostTexture; if (candDsHost == null) @@ -1173,9 +883,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ReadOnlySpan data = System.Runtime.InteropServices.MemoryMarshal.Cast( channel.MemoryManager.Physical.GetSpan(cb5Addr, Math.Min(cb5Size, _v4Enabled ? _palReadCap : 0x180))); - // (b1) 5b-2 T2: temporal gate BY ADDRESS, applied BEFORE any band or - // ordinal is assigned -- a bit-static palette leaves classification - // entirely (camera-MV fallback is the correct answer for it). if (_t2Enabled) { _t2Seen++; @@ -1183,7 +890,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ulong t2h = T2Hash(data); long t2f = System.Threading.Interlocked.Read(ref _t2Frame); bool anim; - ulong t2Prev = 0; // (T2G samples: hash delta prev^new) + ulong t2Prev = 0; if (_t2Table.TryGetValue(cb5Addr, out T2Entry t2)) { @@ -1191,7 +898,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (t2.LastFrame == t2f) { - anim = t2.AnimThisFrame; // same frame: keep the verdict + anim = t2.AnimThisFrame; } else { @@ -1205,8 +912,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else { - // Never-seen address: STATIC this frame (anti graft-storm on - // village streaming); turns ANIM at its first variation. anim = false; _t2Table[cb5Addr] = new T2Entry { Hash = t2h, LastFrame = t2f, AnimThisFrame = false }; } @@ -1220,15 +925,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _t2Anim++; - // (b1) 5b-2 T2G-O1: graft gate among ANIMATED palettes -- band 0 - // only. Band predicate = the EXACT SnapshotToRing expression, no - // widening; hole bucket [3072,4096) measured + sampled instead. - // A gated draw returns BEFORE classification: no graft, no - // SnapshotToRing -> band-0 ordinals untouched (P2 control). if (_t2gActive) { int szg = Math.Min(cb5Size, RingMaxPalette); - // W1: 5-bucket histogram; the original hole [3072,4096) = b+c. int bucket = szg < 3072 ? 0 : szg < 3840 ? 1 : szg < 4096 ? 2 : szg < 5120 ? 3 : 4; _t2gSzHist[bucket]++; @@ -1240,7 +939,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"hashDelta=0x{t2Prev ^ t2h:X16}, verts={drawCount} inst={drawInstances}."); } - // W1 gate = [MIN, 5120); [MIN,4096) = EXT (pairs in B1). if (szg < _t2gMin || szg >= 5120) { _t2gB1Gated++; @@ -1267,8 +965,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Probe axis 2: stash the FIRST magnitude-plausible triple and its - // full projection so a failed draw can be sampled with numbers. bool haveSample = false; float sTx = 0f, sTy = 0f, sTz = 0f, sCw = 0f, sCx = 0f, sCy = 0f; @@ -1318,10 +1014,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed bool spreadHit = false; if (_v4Enabled) { - // v4: the P2B "spread" translation column (m3/m7/m11 of each - // stride-0x30 bone/instance matrix), full palette depth. Under - // COH the whole family is collected (count + bounding box) - // instead of stopping at the first projecting hit. bool spreadProj = false; int spreadCount = 0; float minX = 0f, maxX = 0f, minY = 0f, maxY = 0f, minZ = 0f, maxZ = 0f; @@ -1385,19 +1077,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_coh2Enabled) { - // COH v2 (deep-run verdict): reject on COMPACTNESS, not count. - // Deep run proved compact actors (span 2.1, median 0.6) fail - // CohMinBones=6 because TMIN=10 starves them to ~5 supra-floor - // bones, while the flood is a world-scatter batch (span 330, - // median 25-74). Robust discriminant = fraction of supra-floor - // translations within CohMaxSpan of the CENTROID (outlier-proof, - // unlike global span or seed-cluster). Accept if >= 3 supra-floor - // translations, one projects, and >= CohInlierFrac cluster. bool coherent = false; int inliers = 0; - // NoProj: compute compactness regardless of projection (the - // graft localizes); else keep the projection precondition. if (spreadCount >= CohMinBones2 && (spreadProj || _coh2NoProj)) { float ccx = sumTX / spreadCount, ccy = sumTY / spreadCount, ccz = sumTZ / spreadCount; @@ -1436,12 +1118,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _coh2Rejects++; } - // Magnitude-distribution diag: first ACCEPTED and first - // REJECTED big palette per window, so the floor is chosen - // from data, not guessed. Histogram over ALL finite triples - // (no floor) by max-axis magnitude reveals where the unit - // rotation family (~1), the skeleton (~5-15) and the flood - // (>50) actually sit. if (cb5Size >= PaletteMinBytes) { bool acc = spreadHit; @@ -1449,7 +1125,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { if (acc) { _cohDiagAccLogged = true; } else { _cohDiagLogged = true; } - Span hist = stackalloc int[6]; // <2, 2-5, 5-15, 15-50, 50-150, >=150 + Span hist = stackalloc int[6]; for (int b = 3; b + 8 < data.Length; b += 12) { float tx = data[b], ty = data[b + 4], tz = data[b + 8]; @@ -1491,18 +1167,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _cohRejSpan++; } - // COH diagnostic (three signatures, one run): for the - // first rejected big palette per window, characterize - // the whole spread cloud. (a) tight family + 1-2 distant - // parasites: small median dist, cluster ~ spreadCount, - // huge span -> COH v2 cluster. (b) wide-but-bounded - // cloud (dozens of units = skinning-matrix translations, - // world x inverseBind, not spatial positions): median in - // the tens -> COH v2 with a radius sized to the cloud. - // (c) chunk scatter (hundreds): median huge -> the whole - // projected-translation axis is dead, pivot to integer - // attributes. globalSpan + clusterWithin(seed) + - // medianDistToCentroid together separate the three. if (cb5Size >= PaletteMinBytes && !_cohDiagLogged) { _cohDiagLogged = true; @@ -1633,7 +1297,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _skinnedCb5Max = cb5Size; } - // Int-attr probe: split THIS classified draw by cb5 band, tally int-family attrs. bool intStrict = (seenTypes & IntStrictMask) != 0; bool intBroad = (seenTypes & IntBroadMask) != 0; bool actorBand = cb5Size >= 4096 && cb5Size < 5120; @@ -1656,7 +1319,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ProbeDelta(channel, cb5Size, actorBand ? 0 : 1); } - // One classified sample per window, for J2's visual tuning groundwork. if (!_sampleLogged) { _sampleLogged = true; @@ -1667,14 +1329,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return true; } - /// - /// The graft (brick 3): swap to our coverage framebuffer (our R8 + the game's own - /// depth-stencil), clear once per frame, and -- in 3b+ only -- re-draw the classified - /// geometry with an EQUAL depth test. 3a runs this as a proven NO-OP: all the state - /// churn (render pass switch, clears, restore) with no draw command. Restore is the - /// hammer already battle-tested at this exact spot (post-VtgAsCompute): - /// ForceStateDirty() re-applies every guest state group before the next draw. - /// private static void Graft( DrawManager drawManager, ThreedClass engine, @@ -1699,18 +1353,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int w = dsHost.Width; int h = dsHost.Height; - // J2 line 1: the WHOLE graft is scene-gated (3b only gated the re-draw/clear/ - // bridge). Classified draws on shadow/aux passes (8192 atlas & friends) no - // longer switch framebuffers at all -- their velocity has no meaning for the - // DLSS hookup and the FB churn was pure cost. if (w != MvppCameraCapture.SceneDepthHostWidth || h != MvppCameraCapture.SceneDepthHostHeight) { return; } - // Ring snapshot BEFORE the coverage FB dance (upload doesn't touch render state): - // capture this scene-pass classified draw's palette N. Unread this sub-step. if (_ringEnabled) { SnapshotToRing(context, channel); @@ -1718,21 +1366,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_coverage == null || _covWidth != w || _covHeight != h) { - // NEVER Release() a coverage texture mid-session: the classifier fires on - // draws from several passes whose DS sizes differ (scene vs shadows, plus - // dynamic res), so sizes alternate WITHIN a frame -- an inline Release - // destroyed a view still recorded in the live command buffer (C1 triage: - // vkCmdEndRenderPass on invalidated CB, vkDestroyImageView in use). Cache - // per size instead (~1 MB per R8 entry, discrete sizes only); the creation - // log measures the real churn. The J2 gate (DS == published scene depth) - // will collapse this to a single entry. if (!_coverageBySize.TryGetValue((w, h), out ITexture coverage)) { coverage = context.Renderer.CreateTexture(new TextureCreateInfo( w, h, 1, 1, 1, 1, 1, 1, - // (b1) sub-step 4: the variant FS writes a signed 2-channel MV (ndc N - ndc - // N-1) here, so the target must be RG16F; the coverage-only path stays R8. - // Parking FPS A/B: RYUJINX_MVPP_VEL_COV_R8 forces R8 to isolate the format cost. (_variantEnabled && !_covForceR8) ? Format.R16G16Float : Format.R8Unorm, DepthStencilMode.Depth, Target.Texture2D, @@ -1750,7 +1387,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _covWidth = w; _covHeight = h; - // All coverages are scene-sized now (whole-graft gate above). MvppVelState.Coverage = coverage; MvppVelState.Width = w; MvppVelState.Height = h; @@ -1759,9 +1395,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _covColors[0] = _coverage; context.Renderer.Pipeline.SetRenderTargets(_covColors, dsHost); - // Scene coverage cleared ONCE per frame (pre-3b this fired on every size - // alternation -- harmless as a no-op, but it would wipe 3b's drawn coverage - // mid-frame). if (MvppVelState.FrameClearPending) { MvppVelState.FrameClearPending = false; @@ -1769,12 +1402,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _debugMvSentinel ? new ColorF(-1000f, -1000f, 0f, 0f) : new ColorF(0f, 0f, 0f, 0f)); } - // Depth for the armed re-draw. EQUAL is exact -- a depth bias CANNOT loosen it - // (it shifts our value AWAY from equality; 3b measured: only crumbs survive). - // With RYUJINX_MVPP_VEL_DEPTH_EPS != 0 the test becomes the game's own - // inequality direction with an eps bias toward passing (a one-sided - // tolerant-equal): magnitude from the env, SIGN derived from the draw's own - // depth func (handles standard and reversed Z without guessing the convention). CompareOp drawOp = CompareOp.Equal; float biasUnits = 0f; @@ -1796,11 +1423,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // (b1) 5B3 diag (RYUJINX_MVPP_VEL_DEPTH_ALWAYS=1): bypass the depth test entirely - // on the re-draw (write stays OFF). Decisive for the "fragments die at (early-)Z" - // class: the variant VS may drift MORE than a few ulps (the added varying stores - // keep the position expression alive for two consumers -> the driver may re-fuse - // the position math itself), which eps=4 units cannot cover. if (_graftDraw && _depthAlways) { drawOp = CompareOp.Always; @@ -1814,10 +1436,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { _redrawDoneThisFrame = true; - // Coverage semantics = "the game's FS wrote here": kill everything that could - // suppress or alter the write -- stencil, blending, the game's color mask. - // Depth write stays OFF and the test is EQUAL(+eps): the re-draw cannot touch - // the game's depth nor pass fragments the original draw didn't rasterize. context.Renderer.Pipeline.SetStencilTest(new StencilTestDescriptor( false, CompareOp.Always, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep, 0, 0xff, 0xff, @@ -1835,18 +1453,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed 0f, biasUnits, 0f); } - // (b1) sub-step 2: bind the VARIANT program for the re-draw (byte-identical to - // base this sub-step -> transparent). The restore = StateUpdater's unconditional - // SetProgram(base) after the graft's ForceStateDirty (logs (c)). A null variant - // = SILENT FALLBACK on base -- logged DISTINCTLY so the proof can't be faked. if (_variantEnabled) { var variant = MvppVelState.VariantProgram; if (variant != null) { - // Tier 3 sizing (passive): how many grafts land back-to-back on the SAME - // variant program? That share is what a batch (one bind, N re-draws) could - // fuse -- it sizes the pipeline-switch savings before any batching code. if (ReferenceEquals(variant, _lastGraftVariant)) { _graftSameProg++; @@ -1873,16 +1484,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // (b1) sub-step 5a: bind the N-1 palette cbuf to the ring so the cloned clip_{N-1} - // reads it. 5a binds the CURRENT slot at THIS draw's offset (= current palette): the - // clone reads the SAME data as clip_N -> MV == 0, which PROVES the binding is correct - // (any non-zero/NaN in the MV target = ghost binding). GPU.MainThread, like the RT. if (_n1Enabled && MvppVelState.VariantN1Binding >= 0 && _ringN1Offset >= 0) { - // (b1) RING: the source is per-draw -- previous slot at the paired offset - // on a PAIR HIT (real N-1, MV != 0 on movers), current palette otherwise - // (trivial fallback, MV=0). Same binding 91, same layout: only the DATA - // source changes (the fix-B lesson does not apply here). Span n1 = stackalloc BufferAssignment[1]; n1[0] = new BufferAssignment( MvppVelState.VariantN1Binding, @@ -1915,9 +1518,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // (b1) 5B3 samples probe: bracket THIS re-draw with two SamplesPassed reports; - // the second event's delta = the samples rasterized by OUR draw alone (inline, - // nothing draws in between). One per 2s window. bool sqThisDraw = _samplesProbe && _sqArmed; if (sqThisDraw) { @@ -1948,8 +1548,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed }, 1f, false); } - // (b1) 5B3 readback probe: pull the coverage's actual CONTENT right after the - // probed re-draw and census it (RG16F halves): sentinel / ~zero / written. if (_readbackProbe && _rbArmed && _rbLogged < 90) { _rbArmed = false; @@ -1987,12 +1585,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Milestone (a) SCAFFOLD (RYUJINX_MVPP_VEL_TWOPASS=1): a SECOND emission with - // IDENTICAL bindings (same palette N, same depth-Equal, same coverage). The - // rasterization is identical by construction -> idempotent -> output STRICTLY - // identical to the single-pass baseline. Proves the two-pass draw structure is - // sound AND measures the cost of a second full re-draw, before milestone (b) - // makes the 2nd emission a real VS variant (palette N-1 -> clip_{N-1} varying). if (_twoPassEnabled) { if (indexed) @@ -2018,36 +1610,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"MVPP velocity graft: ACTIVE ({(_graftDrawAll ? "DRAW-ALL" : _graftDraw ? "DRAW-1" : "no-op")} mode, depth eps {_depthEps}), coverage {w}x{h}."); } - // Restore, all THREE holes covered (each proven by inspection): - // (1) our direct GAL SetRenderTargets bypassed the TextureManager host-binding - // cache -- without invalidation, its next commit sees 'unchanged' and never - // gives the game its framebuffer back (game renders into our R8 with our - // depth func = black screen); - // (2) PerformDeferredDraws flushes instanced draws through DrawImpl WITHOUT - // UpdateState -- so the restore must be EAGER, not lazy-dirty; - // (3) the full-dirty must be ThreedClass.ForceStateDirty (SetAllDirty on the - // update tracker): DrawManager.ForceStateDirty only resets the topology - // flag despite its XML doc, so the depth override was never re-emitted and - // CompareOp.Never leaked onto every game draw until the GAME's next depth - // register write (the vanishing world chunks of the confirmation run). channel.TextureManager.MvppInvalidateHostRenderTargets(); if (_targetedRestore) { - // Tier 1: re-emit ONLY the 9 groups the graft actually changed (blend, stencil, - // depth, depth-bias, color-mask, rasterizer, scissor, shader, render target) -- - // NOT SetAllDirty's 28. Shader(26) before RenderTarget(27) is respected because - // Update() processes groups in index order. The eager UpdateState below is kept - // (PerformDeferredDraws bypasses lazy update); depth(5) is in the set (no Never leak). - engine.ForceStateDirtyByIndex(StateUpdater.BlendStateIndex); // 2 - engine.ForceStateDirtyByIndex(StateUpdater.StencilTestStateIndex); // 4 - engine.ForceStateDirtyByIndex(StateUpdater.DepthTestStateIndex); // 5 - engine.ForceStateDirtyByIndex(StateUpdater.DepthBiasStateIndex); // 11 - engine.ForceStateDirtyByIndex(StateUpdater.RtColorMaskIndex); // 14 - engine.ForceStateDirtyByIndex(StateUpdater.RasterizerStateIndex); // 15 - engine.ForceStateDirtyByIndex(StateUpdater.ScissorStateIndex); // 16 - engine.ForceStateDirtyByIndex(StateUpdater.ShaderStateIndex); // 26 - engine.ForceStateDirtyByIndex(StateUpdater.RenderTargetStateIndex); // 27 + engine.ForceStateDirtyByIndex(StateUpdater.BlendStateIndex); + engine.ForceStateDirtyByIndex(StateUpdater.StencilTestStateIndex); + engine.ForceStateDirtyByIndex(StateUpdater.DepthTestStateIndex); + engine.ForceStateDirtyByIndex(StateUpdater.DepthBiasStateIndex); + engine.ForceStateDirtyByIndex(StateUpdater.RtColorMaskIndex); + engine.ForceStateDirtyByIndex(StateUpdater.RasterizerStateIndex); + engine.ForceStateDirtyByIndex(StateUpdater.ScissorStateIndex); + engine.ForceStateDirtyByIndex(StateUpdater.ShaderStateIndex); + engine.ForceStateDirtyByIndex(StateUpdater.RenderTargetStateIndex); } else { diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppViewportProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppViewportProbe.cs index b87cca6e5..af3bb8d03 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppViewportProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/MvppViewportProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,24 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Threed { - /// - /// 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. - /// static class MvppViewportProbe { private static bool _enabled = @@ -38,37 +17,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private sealed class Stats { public int Draws; - public int Narrow; // viewport does not cover the target horizontally - public int Short; // ... vertically - public int TransformOff; // ViewportTransformEnable == 0 (screen-scissor path) + public int Narrow; + public int Short; + public int TransformOff; public float MinX, MaxX, MinW, MaxW, MinY, MaxY, MinH, MaxH; public bool Seeded; - // [v2, 21/07] The viewport came back covering the target on 91 398 draws, so the OTHER half of - // the same mechanism is what matters now: the SCISSOR, and the SCREEN scissor that feeds the - // texture-width heuristic. Neither has ever been measured on this game's scene passes -- the one - // scissor test on record only ever looked at draws targeting the HDR buffer. - // [v3, 21/07] The three values NOTHING in this emulator ever reads, and that no probe in this - // dossier has ever looked at. All three are structurally symmetric in X, which is the one - // property the artefact demands and that every mechanism eliminated so far fails to provide. - // - ViewportExtents.X/Width : the guest's viewport CLIP rectangle. The struct is documented - // "viewport extents for viewport clipping", but a grep over StateUpdater shows only - // DepthNear/DepthFar are consumed (l.1240-1241, 2117-2119). Vulkan does not clip to the - // viewport -- only scissor and framebuffer do -- so an ignored clip means we rasterise - // fragments the console discards. X=m, Width=W-2m cuts m pixels off BOTH sides. - // - sign of ScaleX : a negative ScaleX is a horizontal MIRROR. StateUpdater.cs:1204 destroys - // it with MathF.Abs, and so did v1/v2 of this very probe -- the measurement copied the bug. - // A mirror leaves the centre invariant and grows outward symmetrically: the exact geography. - // - swizzle X : NegativeX and the X<->Y permutations are never consumed by the Vulkan backend. - public int ClipZero; // register never written (X==0 && Width==0) - public int ClipFull; // clip covers the target - public int ClipCuts; // clip leaves pixels out on the left or the right + public int ClipZero; + public int ClipFull; + public int ClipCuts; public int ClipMinX, ClipMaxX, ClipMinW, ClipMaxW; public bool ClipSeeded; - public int A2c; // alpha-to-coverage actif sur ce draw - public int A2cDither; // ... ET dither actif => chemin du discard en damier - public int NegScaleX; // ScaleX < 0 -> horizontal mirror the backend cannot express - public int SwizzleXOdd; // swizzle X is not PositiveX + public int A2c; + public int A2cDither; + public int NegScaleX; + public int SwizzleXOdd; public void AddClip(int x, int w) { @@ -86,8 +49,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (w > ClipMaxW) { ClipMaxW = w; } } - public int ScissorOn; // scissor enabled at all - public int ScissorCuts; // scissor leaves uncovered pixels left or right of the target + public int ScissorOn; + public int ScissorCuts; public int ScissorSeen; public int SciMinX1, SciMaxX1, SciMinX2, SciMaxX2; public int ScrMinW, ScrMaxW, ScrMinH, ScrMaxH; @@ -149,16 +112,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private static readonly Dictionary<(int W, int H), Stats> _byShape = new(); - // [ADDR, 21/07] Modes d'adressage réellement demandés par le jeu, TOUTES passes confondues. - // Ryujinx substitue quatre modes (EnumConversion.cs:97-110), chacun marqué "TODO: Should be ..." : - // Clamp -> ClampToEdge · MirrorClamp -> ClampToEdge · MirrorClampToBorder -> ClampToBorder - // + tout mode inconnu -> ClampToEdge - // Le vrai Clamp renvoie la COULEUR DE BORDURE hors [0,1] ; ClampToEdge RÉPLIQUE le dernier texel. - // Demander le premier et recevoir le second ne donne pas une bordure : ça donne une TRAÎNÉE -- - // exactement l'étirement vertical photographié par Alex quand il monte/descend la caméra. - // L'élimination du journal ("modes d'adressage cohérents") venait de MvppGlowProbe, filtrée sur les - // seules cibles R11G11B10 : elle ne couvrait ni le 512x288, ni le buffer d'ids, ni le compute. - // Ici AUCUN filtre. Échantillonné 1 draw sur 16 pour que l'énumération ne coûte pas la frame. private static readonly int[] _addrU = new int[16]; private static readonly int[] _addrV = new int[16]; private static int _addrBindings; @@ -177,7 +130,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } catch (Exception e) { - // A bench instrument must never take the emulator down with it. _enabled = false; Logger.Warning?.Print(LogClass.Gpu, $"MVPP VPPROBE: disabled after unexpected error: {e}"); } @@ -187,7 +139,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { if (!_announced) { - // Armed proof before any measurement: a switch you cannot SEE in the log is worth nothing. _announced = true; Logger.Info?.Print(LogClass.Gpu, "MVPP VPPROBE: ON -- viewport vs render target, read-only, one line per shape per second."); @@ -209,8 +160,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed return; } - // Same arithmetic as StateUpdater.UpdateViewportTransform (l.1204-1221), minus the render-target - // scale (inert at ResScale=1) and the swizzle handling, which do not move the rectangle. ref ViewportTransform transform = ref state.ViewportTransform[0]; float scaleX = MathF.Abs(transform.ScaleX); @@ -236,8 +185,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed st.TransformOff++; } - // "Narrow" = the viewport leaves uncovered pixels on the left or the right of the target. - // One pixel of slack absorbs the float arithmetic; real dyn-res steps are tens of pixels. if (vpX > 1f || vpX + vpW < rtW - 1f) { st.Narrow++; @@ -248,9 +195,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed st.Short++; } - // Scissor: same question as the viewport, other register. A scissor narrower than the target - // leaves its sides unwritten, which is exactly the observed geography. Disabled or maxed out, - // it becomes (0,0,0xffff,0xffff) and protects nothing -- that case counts as "no cut". ScissorState sc = state.ScissorState[0]; bool scEnabled = sc.Enable; @@ -265,13 +209,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Screen scissor: it is the size hint that decides the created texture WIDTH - // (TextureCache.GetMinimumWidthInGob), so a moving screen scissor can hand back the same guest - // surface at slightly different widths. Worth a number before anyone theorises about it. ScreenScissorState scr = state.ScreenScissorState; st.AddScreen(scr.Width, scr.Height); - // [v3] The clip rectangle the emulator decodes and throws away. ViewportExtents ext = state.ViewportExtents[0]; int clipX = ext.X; int clipW = ext.Width; @@ -294,7 +234,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [v3] Raw sign, NOT MathF.Abs -- this is the whole point of the v3 pass. if (transform.ScaleX < 0f) { st.NegScaleX++; @@ -305,12 +244,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed st.SwizzleXOdd++; } - // [A2C, 21/07] LA mesure qui décide du dossier. Ryujinx émule le fondu par transparence en - // REJETANT des pixels selon un damier 2x2 (EmitterContext.GenerateAlphaToCoverageDitherDiscard, - // masque 0xfbb99110, discard si le bit est à 0). Sur la console ce fondu est lissé par le - // multi-échantillonnage ; ici les pixels sont jetés et il reste une grille de trous dans la - // géométrie -- exactement le motif régulier qu'Alex photographie. Ce chemin n'est actif que si - // le jeu pose les DEUX drapeaux. Si a2c=0 sur XC2, l'hypothèse meurt en un run. if ((state.MultisampleControl & 1) != 0) { st.A2c++; @@ -321,8 +254,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Modes d'adressage demandés, toutes passes. 1 draw sur 16 : l'énumération des bindings est - // le seul point coûteux de cette sonde. if ((++_addrSampled & 15) == 0) { channel.TextureManager.MvppEnumerateGraphicsInputsWithSampler((stage, tex, smp) => @@ -360,8 +291,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed continue; } - // Left/right margins of the widest and narrowest viewport seen on this shape. Two roughly - // equal margins = a centred viewport = two lateral bands. One-sided = a single band. float leftMin = s.MinX; float rightMin = entry.Key.W - (s.MinX + s.MinW); @@ -396,8 +325,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_addrBindings > 0) { - // Les modes SUBSTITUES par EnumConversion sont nommes en clair : ce sont les seuls qui - // peuvent transformer une bordure attendue en replication de texel (= trainee). Logger.Info?.Print(LogClass.Gpu, $"MVPP ADDRPROBE: {_addrBindings} bindings echantillonnes | U: {DescribeModes(_addrU)} | V: {DescribeModes(_addrV)}"); diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/SemaphoreUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/SemaphoreUpdater.cs index d836132f0..91e0dbbc3 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/SemaphoreUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/SemaphoreUpdater.cs @@ -182,18 +182,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed float scale = _channel.TextureManager.RenderTargetScale; float divisor = scale * scale; - // Fractional-crash probe (quality-mode dossier): sample counts are divided - // by scale^2 before the game reads them -- under a FRACTIONAL scale the - // divisor itself is fractional (1.333^2 = 1.777...), a value regime the - // integer daily never exercises. Sampled 1/32 to name the family without - // flooding; silent at integer scales. - // [31/07] GATE AJOUTE. Elle etait en release SANS aucun interrupteur : la - // condition « echelle fractionnaire » est remplie par TOUT utilisateur en DLSS - // quality ou performance (DlssIntegration pilote GraphicsConfig.ResScale ; - // 4K + quality = 1440/1080 = 1,334). Mesure du 31/07 sur GYLT : 21 471 lignes - // en 3 minutes -- soit ~690 000 requetes de compteur -- chez quelqu'un qui - // n'avait aucun probleme a diagnostiquer. La sonde rend son service UNE fois - // par rapport de bug ; elle ne doit pas ecrire le reste du temps. if (GAL.MvppDev.Enabled && scale != System.MathF.Floor(scale) && (_fractProbeCount++ & 31) == 0) { Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu, diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs index b29270309..435d0790e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs @@ -20,8 +20,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { public const int ShaderStateIndex = 26; public const int RtColorMaskIndex = 14; - // MV++ (b1) Tier 1 targeted restore: the graft's state groups that lacked a named index - // (positions confirmed against the _updateTracker registration order). public const int StencilTestStateIndex = 4; public const int DepthTestStateIndex = 5; public const int DepthBiasStateIndex = 11; @@ -39,58 +37,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private readonly GpuContext _context; private readonly GpuChannel _channel; - // JITTER-UI probe: unique viewport sizes already logged (one line per pass geometry). private static readonly System.Collections.Generic.HashSet _jitterUiProbeSeen = new(); - // [JITTER-PASSPROBE, 07/07] Coverage audit for the "un-jittered pass inside a de-jittered image" - // hypothesis: under CamPassOnly, log every unique world-pass geometry once with its cameraPass - // verdict. A depth-bound pass reported cameraPass=FALSE = geometry rasterized WITHOUT jitter while - // DLSS treats the whole frame as jittered => wobble (the far-foliage shimmer). Gated on - // RYUJINX_DLSS_JITTER_PASSPROBE=1; off => zero cost, no behavior change. private static readonly bool _camPassProbe = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_PASSPROBE") == "1"; private static readonly System.Collections.Generic.HashSet<(int, int, bool)> _camPassProbeSeen = new(); - // [JITTER-WORLDRES, 07/07] Also jitter world render-resolution passes that do NOT bind depth - // (forward/transparent geometry -- distant tree imposters / alpha foliage). The PASSPROBE showed - // only the single opaque depth-bound pass is jittered while a same-resolution no-depth pass is - // not, which under DLSS wobbles = the far-foliage shimmer. Gated; default OFF, no behavior change. private static readonly bool _worldResJitter = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES") == "1"; - // [WORLDRES-LOW, 20/07 — natif] At native+4K the July selection window ([In/2, In*0.95], - // designed at 2x) leaves the HALF-render-res world passes (1600x900 for a 3200x1800 render: - // cloth/tents/particles/far LOD) un-jittered inside a de-jittered image => residual world - // trembling in motion (run #7, PASSPROBE proof in docs/JITTER-BANC.md). This knob widens the - // low bound to In/3 for 16:9 passes, depth-bound or not (the camera pass needs >= In/2, so no - // overlap). Same NDC path as WORLDRES. Gated; default OFF => byte-identical. private static readonly bool _worldResLow = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_WORLDRES_LOW") == "1"; - // [FULLJIT, 09/07 — kill the inject blur] The camera-pass jitter amplitude denominator is the - // present-time InputWidth (published at present, one frame stale = the FINAL DLSS-input width 3200) - // instead of THIS frame's real render viewport (2666). So the geometry only receives - // vpW/InputWidth ~= 2666/3200 ~= 0.833x of the declared sub-pixel jitter -> DLSS gets less - // sub-pixel coverage than it thinks -> soft/blurry. The magnificent reference (mode B) had - // InputWidth == vpW == 2666 -> full jitter -> sharp. When ON, use the real viewport (vpW/vpH) as - // the denominator so the geometry gets the FULL declared jitter; pair with - // RYUJINX_DLSS_INJECT_JIT_SCALE=1.0 so DLSS is told the same full amount. Gated, default OFF = - // current 0.833x behavior BYTE-IDENTICAL. Daily is jitter-OFF entirely, so daily is unaffected. private static readonly bool _injectFullJit = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_INJECT_FULLJIT") == "1"; - // [ALPHATEST-PROBE, 07/07] Does the game use the FIXED-FUNCTION alpha test (vs an in-shader - // discard)? If enable=True appears during gameplay, the hashed-alpha fix has a clean single - // hook (EmitterContext fragment epilogue). Gated on RYUJINX_DLSS_ALPHATEST_PROBE=1, default off. private static readonly bool _alphaTestProbe = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_ALPHATEST_PROBE") == "1"; private static readonly System.Collections.Generic.HashSet<(bool, int, int)> _alphaTestProbeSeen = new(); - // [JITTERVAL probe, 07/07] Geometry side of the applied-vs-declared jitter proof. On the camera - // pass, once per frame id, log the EXACT inputs used to jitter gl_Position: the generated offset, - // the InputWidth denominator used, the real viewport, the NDC set, and the resulting applied pixel - // shift (offset*vp/InputWidth). Joined offline by frame id with the DLSS-side line. Gated, - // default OFF; reads only, no behavior change. private static readonly bool _jitterval = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTERVAL") == "1"; private static long _jittervalLastFrame = -1; @@ -98,32 +63,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private readonly DrawState _drawState; private readonly AdvancedBlendManager _blendManager; - // [JITTERVAL-RT probe] host (scaled) dims of the last-bound color[0] / depth render target, - // published by UpdateRenderTargetState and read by the camera-pass probe for the resource - // dimension proof (is the color texture really 3200, or 2666?). Written only when the probe is on. private int _jvColorW, _jvColorH, _jvDepthW, _jvDepthH; private readonly StateUpdateTracker _updateTracker; private readonly ShaderProgramInfo[] _currentProgramInfo; - // [SCENEPROBE] guest shader VAs of the current draw (read-only, for the scene-pass probe). private ulong _probeSceneFsAddr; private ulong _probeSceneVsAddr; - // [MVPP CUTOUT PROBE] Read-only per-draw tally: draws whose active FRAGMENT shader emits Discard - // (alpha cutout), + their host viewport, to PROVE where the cutout foliage draws are. Gated by - // RYUJINX_MVPP_CUTOUT_PROBE; default OFF => zero cost, no state touched, no render/DLSS/jitter change. private static readonly bool _cutoutDrawProbe = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_CUTOUT_PROBE") == "1"; private static int _cpTotalDraws, _cpCutoutDraws; private static long _cpSummaryMs; private static readonly System.Collections.Generic.Dictionary<(int, int), int> _cpCutoutViewports = new(); - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] Gate + bind-swap state for the fragment - // survivor-map SSBO (see MvppSurviveProbe / MvppSurviveProbeBuffer). P3 scene window: - // 0 = nothing bound yet, 1 = real mask bound, 2 = dummy sink bound. Default OFF => no - // buffer, no binding, byte-identical. private static readonly bool _surviveProbe = Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled; private static int _surviveProbeTarget; @@ -132,16 +86,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private ShaderSpecializationState _shaderSpecState; private readonly SpecializationStateUpdater _currentSpecState; - // MV++ double-transform (b1) sub-step 2: generate+publish the MvppVariant program. private static readonly bool _mvppVariantEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_VARIANT") == "1"; private static bool _variantRestoreLogged; - // (b1) sub-step 5a fix @91: the N-1 palette cbuf lives at the FIXED host binding 91 -- - // outside every guest window (1..90), declared by the VARIANT programs only (no global - // reservation, no guest shift; single source of truth = - // MvppVariantInjector.MvppN1HostBinding). The run's BINDING MATCH log (injector vs here) - // catches any drift. private static readonly bool _mvppN1Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_N1") == "1"; private static bool _n1ResolveLogged; @@ -429,30 +377,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _prevTfEnable = true; } - // [MVPP CUTOUT PROBE] Read-only per-draw tally. Runs after the bindings are committed, so - // _currentProgramInfo reflects this draw's shaders. Gated; touches no state. if (_cutoutDrawProbe) { CutoutDrawProbeTick(); } - // [CONSTDIFF] Read-only per-draw: trace the builder's prev-frame matrix (journal 221). if (Image.MvppConstDiffProbe.Enabled) { Image.MvppConstDiffProbe.OnDraw(_channel, _probeSceneFsAddr); } - // [BUILDERIN] Read-only per-draw: which surfaces are actually bound to the builder's - // samplers and the exact constants it consumes (journal 226). Self-gated; always - // called so its per-draw input list is cleared even on non-builder draws. Image.MvppBuilderInProbe.OnDraw(_channel, _channel.TextureManager, _probeSceneFsAddr); - // [MVHASH] Read-only: content occupancy of the MV twin at builder-draw time (242). Image.MvppContentProbe.OnBuilderDraw(_channel.TextureManager, _probeSceneFsAddr); - // [SCENEPROBE] Read-only per-draw: identify the pass writing the 720p R11G11B10 scene. - // Same point as the cutout probe (after CommitBindings): RT, inputs and shader VAs are all - // current for this draw. Touches no state. if (Image.MvppScenePassProbe.Enabled) { Image.MvppScenePassProbe.OnDraw( @@ -462,8 +400,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Image.MvppFeedbackProbe.Frame); } - // [MAP64] Read-only per-draw: who writes the small DoF maps, with the draw's viewport - // extents (a partial viewport leaves stale texels = the staleness suspect). Touches no state. if (Image.MvppMap64Probe.Enabled) { Span vpSpan = _state.State.ViewportTransform.AsSpan(); @@ -478,8 +414,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed (int)(MathF.Abs(vp0.ScaleY) * 2f * vpScale)); } - // [MVBUF] Read-only per-draw: draws writing the object-MV buffer (R10G10B10A2 720p), - // with the guest per-RT blend enable and colour write mask (v3: state census). if (Image.MvppMvBufProbe.Enabled) { Span vpSpan = _state.State.ViewportTransform.AsSpan(); @@ -511,10 +445,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed probeMask); } - // [CENSUS] Read-only per-draw: MV-buffer writers vs the fingerprint-armed registry -- - // flags the writers every value scrub missed. v2: write masks passed so bound-but- - // masked-off draws are counted apart. v3: classification by PROGRAM identity via - // ShaderProgramInfo (VA census defeated by code dedup). Touches no state. if (Image.MvppWriterCensusProbe.Enabled) { Span censusMask = stackalloc uint[Constants.TotalRenderTargets]; @@ -544,8 +474,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Image.MvppWriterCensusProbe.OnDraw(_channel.TextureManager, _probeSceneFsAddr, censusMask, censusFragInfo); } - // [TWINMAP] Read-only per-draw: writers per MV twin (by guest VA). v2: + live GMMU - // re-translation. v5: masks passed, all slots scanned (no early return). Touches no state. if (Image.MvppTwinMapProbe.Enabled) { Span twinMask = stackalloc uint[Constants.TotalRenderTargets]; @@ -565,8 +493,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // [MVPP CUTOUT PROBE] Count this draw + (if its fragment shader is a cutout shader) tally it and its - // host viewport; log a summary ~every 3 s. Pure read; never modifies state or the render path. private void CutoutDrawProbeTick() { _cpTotalDraws++; @@ -653,31 +579,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _channel.BufferManager.CommitGraphicsBindings(_drawState.DrawIndexed); - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] Bind the survivor-map SSBO at the fixed - // out-of-window binding 80, ONCE per session, right after the guest storage commit. - // Descriptor updates are sparse per-index (DescriptorSetUpdater.SetStorageBuffers), the - // guest formula can never produce binding 80, and the persisted BufferRef survives - // program changes (SetProgram just marks all descriptors dirty and re-applies the refs) - // -- so one bind is enough and adds zero per-draw cost. Unbound-but-in-layout draws - // before this point fall back to the dummy buffer (UpdateBuffer dummyBuffer path). if (_surviveProbe) { - // [P3 scene window] Route the epilogue writes per draw: draws whose depth-stencil - // is the PINNED scene depth (MvppCameraCapture, requires RYUJINX_MVPP_CAP=1) hit - // the REAL mask; everything else (UI, shadow, texture-processing passes) hits a - // same-size dummy sink. Compared by GUEST ADDRESS -- host texture references can - // be recreated on rescale, which would silently break a reference compare. The - // descriptor is only rewritten on transitions (sparse per-index update, proven - // no-clobber); the check itself is one ulong compare per commit. Image.Texture dsCur = _channel.TextureManager.RenderTargetDepthStencil; ulong sceneDepthAddr = MvppCameraCapture.SceneDepthAddress; int wantTarget = (dsCur != null && sceneDepthAddr != 0 && dsCur.Range.GetSubRange(0).Address == sceneDepthAddr) ? 1 : 2; - // [MIPMARK, 11/07] Preuve pixel : sous RYUJINX_MVPP_MIPMARK=1, seuls les draws de - // scène qui LIENT une texture 2D levels=1 minifiable (>=64px) écrivent le masque - // réel ; les autres vont au dummy. Lecture seule, routage de descriptor seulement. if (wantTarget == 1 && Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.MipMarkEnabled) { @@ -714,10 +623,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Logger.Info?.Print(LogClass.Gpu, "MVPP-SURVIVE: scene-window bind-swap ACTIVE (mask<->dummy at binding 80, first scene match)."); - // [Cutout bias mask] Also mirror the REAL mask once to the pass-through - // binding 81 (never in any layout, never swapped): the Vulkan backend - // captures the mapped range there so the DLSS inject can convert the mask - // (see MvppSurviveMaskState / DlssUpscaler). _context.Renderer.Pipeline.SetStorageBuffers( [new BufferAssignment( Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BackendMirrorBinding, @@ -725,8 +630,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } } - // Periodic readback + PGM dump (cheap tick check per commit, at most 3 dumps per - // session; see MvppSurviveProbeBuffer.MaybeDump). MvppSurviveProbeBuffer.MaybeDump(_context); } } @@ -792,7 +695,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private void UpdateTfBufferState() { Span tfBufferStateSpan = _state.State.TfBufferState.AsSpan(); - + for (int index = 0; index < Constants.TotalTransformFeedbackBuffers; index++) { TfBufferState tfb = tfBufferStateSpan[index]; @@ -965,8 +868,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed oldScaleX != _channel.TextureManager.RenderTargetScaleX || oldScaleY != _channel.TextureManager.RenderTargetScaleY) { - // The shader gl_FragCoord compensation must see the exact (quantized) ratio, - // not the nominal scale, or it lands short of the real content edge. _context.SupportBufferUpdater.SetRenderTargetScale(_channel.TextureManager.RenderTargetScaleX); UpdateViewportTransform(); @@ -1015,7 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Span> regions = stackalloc Rectangle[Constants.TotalViewports]; Span scissorStateSpan = _state.State.ScissorState.AsSpan(); - + for (int index = 0; index < Constants.TotalViewports; index++) { ScissorState scissor = scissorStateSpan[index]; @@ -1055,7 +956,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed width = (int)MathF.Ceiling(width * scaleX); height = (int)MathF.Ceiling(height * scaleY); - Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated); truncated pos + ceiled size = the +-1px seam suspect + Image.MvppSeamProbe.OnScaledCopy( "SCISSOR", null, null, gx, gy, gx + gw, gy + gh, @@ -1148,18 +1049,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Span viewportTransformSpan = _state.State.ViewportTransform.AsSpan(); Span viewportExtentsSpan = _state.State.ViewportExtents.AsSpan(); - // DLSS Mode B: hand the shader this frame's sub-pixel jitter as a clip-space NDC offset, which it - // adds to gl_Position scaled by w (the correct, native-DLSS way to jitter -- not a viewport shift). - // Only the scaled (main) 3D pass; native passes (UI) keep RenderTargetScale 1 and are NOT jittered. - // The offset is 0 unless jitter is enabled, so the default path is unchanged. float jitterNdcX = 0f, jitterNdcY = 0f; - // [JITTER-CAMPASS, 05/07 — the by-the-book mode] NVIDIA's rule: camera jitter is applied at - // ONE place, the main camera geometry pass. Shadows (light space), post chains (inherit) and - // UI are never jittered by an engine. Our legacy mode injected EVERY pass (the actual hack); - // this mode POSITIVELY identifies the camera pass -- the 16:9 pass with a depth buffer bound -- - // and injects only there, with the amplitude expressed in FINAL DLSS-input pixels - // (NDC = offset*2 / inputSize) so applied == declared exactly, whatever the pass resolution. if (DlssJitterState.Enabled && DlssJitterState.Inject && DlssJitterState.CamPassOnly) { ref ViewportTransform vp0 = ref viewportTransformSpan[0]; @@ -1172,8 +1063,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed DlssJitterState.InputWidth > 0 && vpW >= DlssJitterState.InputWidth / 2f && vpW <= DlssJitterState.InputWidth + 2f; - // [SUBRECT measurement] Publish the camera pass's host viewport = the game's current - // dynamic-resolution step, for the inject-side dump gate (see DlssJitterState.CamVpW). if (cameraPass) { DlssJitterState.CamVpW = vpW; @@ -1182,17 +1071,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed bool depthOn = _state.State.RtDepthStencilEnable; - // [JITTER-WORLDRES, 07/07] A world render-resolution pass (16:9, width in the camera-pass - // window) that FAILED cameraPass = a same-res pass without depth bound = forward/transparent - // geometry (distant tree imposters, alpha foliage). Kept strictly BELOW the final input - // width so the final composite / native UI passes stay excluded (they must not be jittered). bool worldResPass = _worldResJitter && !cameraPass && aspect > 1.5f && aspect < 1.95f && DlssJitterState.InputWidth > 0 && vpW >= DlssJitterState.InputWidth / 2f && vpW <= DlssJitterState.InputWidth * 0.95f; - // [WORLDRES-LOW] The half-render-res world passes, below the July window. bool worldResLowPass = _worldResLow && !cameraPass && aspect > 1.5f && aspect < 1.95f && DlssJitterState.InputWidth > 0 && @@ -1202,17 +1086,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (cameraPass || worldResPass) { - // Amplitude denominator: default = the final DLSS-input size (present, 1-frame stale). - // FULLJIT uses THIS frame's real viewport so the geometry receives the FULL declared - // sub-pixel jitter (kills the under-coverage blur). OFF => byte-identical. float denomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth; float denomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight; - // [WORLDRES-LOW v2, 20/07] Sub-window passes are composited with a STRETCH to the - // final image (run #8 proof: vpW denominator => world tremble x2, matching the 2.4x - // overshoot the stretch model predicts). A stretched pass must receive its jitter as - // a fraction of the FINAL input, whatever FULLJIT says, so the on-screen shift equals - // the declared offset exactly. if (worldResLowPass) { denomW = DlssJitterState.InputWidth; @@ -1223,8 +1099,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed jitterNdcY = DlssJitterState.OffsetY * 2f / denomH; } - // [JITTERVAL-GEO, 07/07] Ground truth of what was ACTUALLY applied to the geometry, once - // per frame on the real camera pass. appliedPx = offset * viewport / InputWidth (== ndc/2*vp). if (_jitterval && cameraPass && DlssJitterState.FrameId != _jittervalLastFrame) { _jittervalLastFrame = DlssJitterState.FrameId; @@ -1235,10 +1109,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed float appliedPxX = inW > 0 ? offX * vpW / inW : 0f; float appliedPxY = inH > 0 ? offY * vpH / inH : 0f; var sc = _state.State.ScreenScissorState; - // [JITTERVAL v2, 20/07] The 07/07 fields above assume the LEGACY denominator (inW) and - // are blind to FULLJIT. truePx = offset scaled by the denominator ACTUALLY used for the - // NDC this frame (same expression as the inject block above), so the applied-vs-declared - // proof measures the real consumption point whatever the knobs. Log-only, same gate. float jvDenomW = _injectFullJit && vpW > 0f ? vpW : DlssJitterState.InputWidth; float jvDenomH = _injectFullJit && vpH > 0f ? vpH : DlssJitterState.InputHeight; float truePxX = jvDenomW > 0f ? offX * vpW / jvDenomW : 0f; @@ -1253,8 +1123,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed $"truePxX={truePxX:0.0000} truePxY={truePxY:0.0000}"); } - // [JITTER-PASSPROBE, 07/07] One line per unique (width, height, depth-bound) pass geometry. - // Watch for depth=False on a 16:9 render-res pass -> world geometry drawn without jitter. if (_camPassProbe) { var probeKey = ((int)vpW, (int)vpH, depthOn); @@ -1273,9 +1141,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed float vpWidth = MathF.Abs(vp0.ScaleX) * 2f * jScale; float vpHeight = MathF.Abs(vp0.ScaleY) * 2f * jScale; - // [JITTER-NOSHADOW, 05/07] Camera jitter belongs to CAMERA passes. Square scaled passes - // (TOTK shadow atlas 2048/4096) render in LIGHT space: jittering them makes shadows swim - // -> shimmer on characters/foliage (the user's exact residual symptom with correct signs). float passAspect = vpHeight > 0f ? vpWidth / vpHeight : 0f; bool squarePass = passAspect > 0.95f && passAspect < 1.05f; @@ -1287,13 +1152,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else if (DlssJitterState.Enabled && DlssJitterState.Inject && DlssJitterState.JitterUi) { - // [JITTER-UI, 05/07] The UI/compositing passes run at scale 1 and were NOT jittered, but - // the WHOLE frame is declared as jittered to DLSS -- so DLSS "de-jitters" the static UI - // and shakes it (measured: world regions at the no-jitter floor in every sign combo while - // UI regions tremble). Jitter the full-screen scale-1 color passes too, with the SAME - // guest-relative shift as the world (NDC = offset*2 / (vpWidth * resScale)), so DLSS - // stabilizes the UI like the world. Square/small targets (shadow maps, bloom mips) are - // excluded by the viewport heuristic. ref ViewportTransform vp0 = ref viewportTransformSpan[0]; float vpWidth = MathF.Abs(vp0.ScaleX) * 2f; float vpHeight = MathF.Abs(vp0.ScaleY) * 2f; @@ -1305,8 +1163,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed jitterNdcY = DlssJitterState.OffsetY * 2f / (vpHeight * GraphicsConfig.ResScale); } - // One log per unique scale-1 viewport size: which passes exist and which ones the - // heuristic catches (JITTER-UI probe). int vpKey = ((int)vpWidth << 16) | ((int)vpHeight & 0xFFFF); if (_jitterUiProbeSeen.Add(vpKey)) { @@ -1392,11 +1248,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed // Viewport size is only used on the shader when YNegate is enabled, // and if the fragment shader accesses gl_FragCoord, // so there's no need to update it in other cases. - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] The probe's epilogue store packs - // ViewportSize as the writing-pass identifier, so keep the field current whenever the - // probe is ON (measured: without this the field stays zero on this path and the map - // reads all-zero). SetViewportSize only dirties on actual change; default OFF = - // stock condition, byte-identical. if ((yNegate && _fsReadsFragCoord) || _surviveProbe) { UpdateSupportBufferViewportSize(); @@ -1418,9 +1269,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _context.Renderer.Pipeline.SetDepthMode(mode); - // MV++: the reprojection shader must map window depth back to the NDC z the game's - // matrices produce, and this is where the guest's convention is known. Same thread - // as the draws, so the value read at capture time is the active one. if (DlssCameraState.Enabled) { DlssCameraState.GuestDepthMinusOneToOne = mode == DepthMode.MinusOneToOne; @@ -1568,7 +1416,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Span vertexAttribs = stackalloc VertexAttribDescriptor[Constants.TotalVertexAttribs]; Span vertexAttribStateSpan = _state.State.VertexAttribState.AsSpan(); - + for (int index = 0; index < Constants.TotalVertexAttribs; index++) { VertexAttribState vertexAttrib = vertexAttribStateSpan[index]; @@ -1713,7 +1561,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed int drawFirstVertex = _drawState.DrawFirstVertex; int drawVertexCount = _drawState.DrawVertexCount; uint vbEnableMask = 0; - + Span vertexBufferStateSpan = _state.State.VertexBufferState.AsSpan(); Span vertexBuffersSpan = _pipeline.VertexBuffers.AsSpan(); Span vertexBufferEndAddressSpan = _state.State.VertexBufferEndAddress.AsSpan(); @@ -1892,7 +1740,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed Span blendEnableSpan = _state.State.BlendEnable.AsSpan(); Span blendStateSpan = _state.State.BlendState.AsSpan(); Span blendDescriptorsSpan = _pipeline.BlendDescriptors.AsSpan(); - + for (int index = 0; index < Constants.TotalRenderTargets; index++) { bool enable = blendEnableSpan[index]; @@ -1944,7 +1792,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { dualSourceBlendEnabled = true; } - + Span blendDescriptorsSpan = _pipeline.BlendDescriptors.AsSpan(); for (int index = 0; index < Constants.TotalRenderTargets; index++) @@ -2050,9 +1898,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed addressesSpan[index] = baseAddress + shader.Offset; } - // [SCENEPROBE] stable per-pass identity for the current draw (guest shader VAs). - _probeSceneFsAddr = addressesSpan[5]; // fragment - _probeSceneVsAddr = addressesSpan[1]; // vertex (main) + _probeSceneFsAddr = addressesSpan[5]; + _probeSceneVsAddr = addressesSpan[1]; int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex ? _state.State.TexturePoolState.MaximumId @@ -2067,10 +1914,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed ref _currentSpecState.GetGraphicsState(), addresses); - // MV++ double-transform (b1) sub-step 2: also request/cache the MvppVariant of the - // same shaders (same args + the flag) and publish it for the graft to SetProgram. - // The flag changes NOTHING in the translation yet (sub-step 3 wires the double-skin) - // so the variant is byte-identical to the base -- transparency preserved. Gated OFF. if (_mvppVariantEnabled) { CachedShaderProgram gsVariant = shaderCache.GetGraphicsShader( @@ -2085,17 +1928,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed MvppVelState.VariantProgram = gsVariant.HostProgram; - // (b1) sub-step 5a: resolve the host binding of the N-1 palette cbuf the injector - // added to the variant VS. READ it from the real BufferDescriptors -- the cbuf Slot - // present in the VARIANT VS but not the BASE VS is ours -- never guessed (loc-31 - // lesson). Published for the graft to bind the ring to it; -1 -> graft skips (MV=0). if (_mvppN1Enabled) { - // (b1) 5a fix @91: the N-1 cbuf is at the FIXED host binding 91, outside every - // guest window (1..90) and declared by the variant only -- no guest binding can - // ever alias it, so the graft's direct bind can't pollute a non-classified draw, - // and no guest program is shifted (host disk cache valid both ways). Injector - // logs the same binding -> BINDING MATCH catches drift. MvppVelState.VariantN1Binding = MvppN1ReservedBinding; MvppVelState.VariantN1Set = 0; @@ -2154,7 +1988,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _currentProgramInfo[stageIndex] = info; } - if (gs.Shaders[5]?.Info.UsesFragCoord == true) { // Make sure we update the viewport size on the support buffer if it will be consumed on the new shader. @@ -2186,9 +2019,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _context.Renderer.Pipeline.SetProgram(gs.HostProgram); - // (c) RESTORE proof: this unconditional SetProgram(base) is what re-binds the base - // after the graft bound the variant (graft -> ForceStateDirty -> here). One-shot log - // + per-window counter (vRestore) instead of a line per restore (272k-line spam). if (MvppVelState.VariantBound) { MvppVelState.VariantBound = false; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Twod/MvppTwodProbe.cs b/src/Ryujinx.Graphics.Gpu/Engine/Twod/MvppTwodProbe.cs index 81e4a2250..25e0be40f 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Twod/MvppTwodProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Twod/MvppTwodProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,26 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Engine.Twod { - /// - /// 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. - /// static class MvppTwodProbe { private static bool _enabled = @@ -36,15 +13,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod private static bool _announced; private static long _nextLogMs; private static int _blits; - private static int _negatives; // une coordonnée source ou destination est négative - private static int _resized; // la région source n'a pas la même taille que la destination + private static int _negatives; + private static int _resized; private static readonly Dictionary _shapes = new(); - /// - /// 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. - /// public static void OnPresent() { if (!_enabled) @@ -114,12 +86,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod _resized++; } - // Une forme = la géométrie complète du blit. C'est elle qui dira si XC2 recopie des bandes - // latérales, et à quelles abscisses exactes. string shape = $"src[{srcX1},{srcY1}->{srcX2},{srcY2}]/{srcW}x{srcH} dst[{dstX1},{dstY1}->{dstX2},{dstY2}]/{dstW}x{dstH}"; _shapes[shape] = _shapes.TryGetValue(shape, out int n) ? n + 1 : 1; - // Le log periodique vit dans OnPresent : il doit sortir meme quand il n'y a aucun blit. } catch (Exception e) { diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs index f3a6d5541..ba1681001 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Twod/TwodClass.cs @@ -295,7 +295,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod FormatInfo dstCopyTextureFormat = dstCopyTexture.Format.Convert(); - // Dernier étage jamais instrumenté du dossier XC2 : le moteur 2D. Lecture seule, gated. MvppTwodProbe.OnBlit( srcX1, srcY1, srcX2, srcY2, dstX1, dstY1, dstX2, dstY2, @@ -364,15 +363,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod return; } - MvppBlitProbe.OnBlit(srcTexture, dstTexture, duDx, dvDy); // read-only composite tracer (gated) + MvppBlitProbe.OnBlit(srcTexture, dstTexture, duDx, dvDy); if (srcTexture.Info.Samples > 1 || dstTexture.Info.Samples > 1) { srcTexture.PropagateScale(dstTexture); } - // Exact (quantized) per-axis ratios so full-surface blits cover the whole - // allocation on both ends; matches ScaleFactor when fractional scaling is off. float scaleX = srcTexture.EffectiveScaleX; float scaleY = srcTexture.EffectiveScaleY; float dstScaleX = dstTexture.EffectiveScaleX; @@ -392,7 +389,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod bool linearFilter = _state.State.SetPixelsFromMemorySampleModeFilter == SetPixelsFromMemorySampleModeFilter.Bilinear; - Image.MvppSeamProbe.OnScaledCopy( // read-only edge-hit tracer (gated) + Image.MvppSeamProbe.OnScaledCopy( "2D-BLIT", srcTexture, dstTexture, srcX1 / srcTexture.Info.SamplesInX, srcY1 / srcTexture.Info.SamplesInY, @@ -403,7 +400,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod dstRegion.X1, dstRegion.Y1, dstRegion.X2, dstRegion.Y2, linearFilter ? "linear" : "point"); - Image.MvppTwinXferProbe.OnCopy("2D-BLIT", srcTexture, dstTexture); // [TWINXFER] read-only, self-gated + Image.MvppTwinXferProbe.OnCopy("2D-BLIT", srcTexture, dstTexture); srcTexture.HostTexture.CopyTo(dstTexture.HostTexture, srcRegion, dstRegion, linearFilter); diff --git a/src/Ryujinx.Graphics.Gpu/GpuContext.cs b/src/Ryujinx.Graphics.Gpu/GpuContext.cs index 1b474904e..0f9a65a7f 100644 --- a/src/Ryujinx.Graphics.Gpu/GpuContext.cs +++ b/src/Ryujinx.Graphics.Gpu/GpuContext.cs @@ -47,9 +47,6 @@ namespace Ryujinx.Graphics.Gpu public Window Window { get; } /// - /// Most recently bound depth-stencil render target, captured so the presentation path can hand a - /// real depth buffer to temporal upscalers (DLSS). May be stale or a UI depth, so consumers must - /// validate it (e.g. by dimensions) before use. Written and read on the GPU thread only. /// internal Image.Texture LastPresentDepthStencil { get; set; } @@ -105,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu /// internal DirtyHacks DirtyHacks { get; } - /// /// Host hardware capabilities. /// @@ -435,7 +431,7 @@ namespace Ryujinx.Graphics.Gpu SyncActions.RemoveAt(i--); } } - + for (int i = 0; i < SyncpointActions.Count; i++) { if (SyncpointActions[i].SyncAction(syncPoint)) diff --git a/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs b/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs index c6b5bcce8..487b47fe5 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs @@ -69,7 +69,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// /// If the backend GPU has 0 memory capacity, the cache size defaults to `DefaultTextureSizeCapacity`. - /// + /// /// Reads the current Device total CPU Memory, to determine the maximum amount of VRAM available. Capped to 50% of Current GPU Memory. /// /// The GPU context that the cache belongs to @@ -119,11 +119,6 @@ namespace Ryujinx.Graphics.Gpu.Image { _totalSize += texture.Size; - // Report the cache state on a timer, NOT only when an eviction happens. First version - // of this probe logged from inside RemoveLeastUsedTexture, so a run with zero evictions - // produced zero lines, and "no line" meant either "no eviction" or "probe not running". - // Same trap as the birth-clear switch earlier today: a diagnostic you cannot SEE is - // worth nothing. Now the line always comes, and the eviction count is a field in it. if (CacheProbeEnabled) { long tickMs = System.Environment.TickCount64; @@ -184,15 +179,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Removes the least used texture from the cache. /// - // [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"; diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppBuilderInProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppBuilderInProbe.cs index 3e19c5739..7425a2c33 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppBuilderInProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppBuilderInProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -10,34 +7,11 @@ using System.Text; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppBuilderInProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_BUILDERIN") == "1"; - // The whole DoF/motion chain, not just the builder: (229) showed the builder writes a - // single 320x180 target and does NOT produce the 64x36 maps, so its consumers are now - // the suspects - and they read their OWN constants, which is why the bench found them - // dead when fed the builder's. private static readonly (ulong Addr, string Name)[] Passes = { (0x1000AD730UL, "builder"), @@ -51,7 +25,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static readonly HashSet _census = new(); private static int _censusLines; - /// Cheap identity of the draw's first colour target, for throttling. private static int TargetKey(TextureManager texMgr) { if (texMgr == null) @@ -110,10 +83,8 @@ namespace Ryujinx.Graphics.Gpu.Image } private static string _shape = "?"; - /// The 1280x720 twin the builder sampled on its last draw (journal 242). public static Texture LastTwin { get; private set; } - /// True when the draw writes one of the small DoF map shapes. private static bool MatchesUnknownTarget(TextureManager texMgr, out string shape) { shape = null; @@ -165,12 +136,9 @@ namespace Ryujinx.Graphics.Gpu.Image return null; } - private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order) - private const int CbufSlot = 3; // fp_c3 + private const int FragmentStageIndex = 4; + private const int CbufSlot = 3; - // The vec4 indices the passes read, from its decompiled source: - // [0] output scale / blend / strength / falloff, [5][6][7][8] the reprojection columns, - // [11] the NDC->pixel scale. Anything else in the buffer is not consumed by this shader. private static readonly int[] WantedC3 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13 }; private struct Bound @@ -190,15 +158,8 @@ namespace Ryujinx.Graphics.Gpu.Image private static bool _armed; private static bool _failLogged; - // v2. The first cut logged once and fired at ~8 s - the title screen, where the game - // has motion blur off (fp_c3[0].z = 0 kills the whole camera branch). The bench fed - // with those values is structurally dead, so a one-shot log is worthless. Now it - // re-reports whenever the values actually CHANGE, capped so the log stays readable. private const int MaxReports = 40; private static int _reports; - // v5. A single global throttle meant one sampled draw every 250 ms for the WHOLE - // chain, so the busiest passes ate every slot and coc64/bokeh/pingpong were never - // sampled at all (23rd trap). The throttle is per pass now. private static readonly Dictionary _lastBeat = new(); private static readonly Dictionary _lastSig = new(); private static readonly Dictionary _perConfig = new(); @@ -239,10 +200,6 @@ namespace Ryujinx.Graphics.Gpu.Image H = info.Height, Handle = handle, Binding = binding, - // down64's second invocation reduces 320 -> 64 horizontally with NO - // horizontal taps: that factor-5 reduction rests entirely on the sampler. - // The address mode was checked long ago and matches the console; the FILTER - // never was, on either backend. Levels = texture.Info.Levels, Tex = texture, Filter = sampler == null @@ -253,11 +210,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// - /// 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. - /// public static void OnDraw(GpuChannel channel, TextureManager texMgr, ulong fsAddr) { if (!Enabled) @@ -269,20 +221,12 @@ namespace Ryujinx.Graphics.Gpu.Image { try { - // CENSUS: every (shader, target shape) pair seen, deduplicated. After the - // 2D engine reported ZERO blits, "no draw writes the 64x36" rests entirely - // on my shape detection - so this stops trusting it and enumerates instead. CensusTick(texMgr, fsAddr); string pass = PassName(fsAddr); if (pass == null) { - // The chain read in (232) has a HOLE: down64 emits 64x180 but coc64 and - // the bokeh consume 64x36, so an unlisted pass performs the vertical - // reduction - and that pass produces the very map whose z-channel - // variation Alex proved to be the trigger. Catch it by its target - // shape rather than by an address we do not have yet. if (!MatchesUnknownTarget(texMgr, out string shape)) { return; @@ -299,9 +243,6 @@ namespace Ryujinx.Graphics.Gpu.Image _builderDraws++; - // Latch the twin from THIS draw's inputs, and only when this draw is the - // builder - otherwise the last unrelated pass wins and the content probe - // ends up measuring a different texture entirely (29th trap). if (fsAddr == Passes[0].Addr) { foreach (Bound bb in _inputs) @@ -320,12 +261,6 @@ namespace Ryujinx.Graphics.Gpu.Image "[BUILDERIN] armed: tracking the DoF/motion chain (builder, down64, coc64, bokeh_fs, pingpong)"); } - // Sample at most ~4x/s: reading guest memory on every builder draw would - // cost far more than the answer is worth. - // The throttle was keyed on the shader alone, so down64's two invocations - // (64x180 then 64x36) shared one 250 ms slot and the vertical one was never - // sampled. Key it on shader + target shape: same shader, two targets, two - // independent slots. long now = Environment.TickCount64; ulong beatKey = fsAddr ^ ((ulong)TargetKey(texMgr) << 40); _lastBeat.TryGetValue(beatKey, out long beat); @@ -340,10 +275,6 @@ namespace Ryujinx.Graphics.Gpu.Image _texMgr = texMgr; string signature = BuildSignature(channel, out string[] lines); - // 24th trap: budgeting per PASS let constant churn eat all eight slots, so a - // second invocation of the same shader with a DIFFERENT target could never be - // reported. The key is the pass plus its I/O shape, so every distinct - // configuration gets its own budget. string config = pass + "|" + _shape; _lastSig.TryGetValue(config, out string prev); @@ -390,10 +321,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// - /// Renders the draw's full input state as log lines, and returns a signature that - /// changes exactly when any of those values changes. - /// private static string BuildSignature(GpuChannel channel, out string[] lines) { List outLines = new(); @@ -425,11 +352,6 @@ namespace Ryujinx.Graphics.Gpu.Image return sig.ToString(); } - /// - /// 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. - /// private static void AppendTargets(TextureManager texMgr, List lines, StringBuilder sig, StringBuilder shape) { if (texMgr == null) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppCacheProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppCacheProbe.cs index 3208a48ab..c8aa9a87d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppCacheProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppCacheProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,42 +6,20 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// 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é ». - /// 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 _realShapes = new(); // formes qui rechargent, par fréquence + private static int _copyDeps; + private static int _syncs; + private static int _mismatch; + private static int _realSyncs; + private static int _realNoData; + private static readonly Dictionary _realShapes = new(); private static readonly Dictionary _pairs = new(); - /// Un appel qui recharge RÉELLEMENT des données depuis la mémoire invitée. public static void OnRealSync(int w, int h, Format fmt, bool hadData) { if (!_enabled) @@ -63,7 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Image _realShapes[key] = _realShapes.TryGetValue(key, out int n) ? n + 1 : 1; } - /// Une copie de dépendance vient d'être créée entre deux textures qui se chevauchent. public static void OnCopyDependency(Texture a, Texture b) { if (!_enabled || a == null || b == null) @@ -83,9 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Image _mismatch++; } - // Une largeur ou un format différent entre deux textures qui partagent la MÊME mémoire, - // c'est le mécanisme canonique qui recopie du contenu valide au mauvais endroit : une ligne - // de l'une tombe au milieu d'une ligne de l'autre. string key = $"{a.Info.Width}x{a.Info.Height} {a.Info.FormatInfo.Format} <-> {b.Info.Width}x{b.Info.Height} {b.Info.FormatInfo.Format}{(diff ? " <<< DIFFERENT" : "")}"; _pairs[key] = _pairs.TryGetValue(key, out int n) ? n + 1 : 1; } @@ -96,7 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Une texture vient d'être resynchronisée depuis la mémoire invitée. public static void OnSynchronize() { if (_enabled) @@ -105,7 +75,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Battement, branché sur la fin d'image. Sort même quand tous les compteurs sont à zéro. public static void OnPresent() { if (!_enabled) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppConstDiffProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppConstDiffProbe.cs index 5f7a48972..55a345af9 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppConstDiffProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppConstDiffProbe.cs @@ -1,32 +1,17 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppConstDiffProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_CONSTDIFF") == "1"; private const ulong BuilderFsAddress = 0x1000AD730UL; - private const int FragmentStageIndex = 4; // vertex=0 ... fragment=4 (guest stage order) - private const int CbufSlot = 3; // fp_c3 + private const int FragmentStageIndex = 4; + private const int CbufSlot = 3; private static bool _armedLogged; private static bool _failLogged; @@ -86,8 +71,6 @@ namespace Ryujinx.Graphics.Gpu.Image continue; } - // GetGraphicsUniformBufferAddress renvoie une adresse DEJA physique - // (Range traduite) : lecture directe, PAS de Translate. ulong addr = channel.BufferManager.GetGraphicsUniformBufferAddress(FragmentStageIndex, slot); if (addr == 0 || addr == ulong.MaxValue) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppContentProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppContentProbe.cs index ec85a0569..51ff76a90 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppContentProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppContentProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,26 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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). - /// static class MvppContentProbe { public static readonly bool Enabled = @@ -92,9 +69,6 @@ namespace Ryujinx.Graphics.Gpu.Image double sumAbs = 0; ulong hash = 14695981039346656037UL; - // The twin is A2B10G10R10: R and G carry sqrt-encoded magnitude, the 2-bit alpha the - // signs. A texel still at its clear value has R and G at zero, so "occupancy" is the - // share of texels some draw actually wrote a vector into. for (int i = 0; i < words; i++) { uint w = (uint)(bytes[i * 4] | diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppDofProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppDofProbe.cs index 4170b0cbe..c0e50e25c 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppDofProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppDofProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -9,36 +6,19 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// 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 _smallRtSizes = new(); private static readonly Dictionary _dofInputs = new(); - /// Guest VAs of every texture seen as an input to the low-res DoF pass. Read by - /// to flag which temporal-history buffers actually feed the DoF. public static readonly HashSet DofInputVas = new(); private static long _lastSummaryMs; private static long _lastHeartbeatMs; @@ -46,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static bool _armed; private static int _announced; - /// - /// Logs the gate state exactly once per process, regardless of , 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). - /// public static void AnnounceOnce() { if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0) @@ -65,13 +39,6 @@ namespace Ryujinx.Graphics.Gpu.Image (Enabled ? "Probe ACTIVE." : "Probe OFF -- launch via XC2_DOF_PROBE.bat to enable.")); } - /// - /// Inspects one input texture bound during a draw, with the draw's active colour target - /// (call site is gated on ). Purely observational. - /// - /// The input texture being bound - /// The draw's active colour render target (may be null) - /// The shader stage this texture is bound to (DoF reads are Fragment) public static void OnInput(Texture texture, Texture renderTarget, ShaderStage stage) { if (texture == null || renderTarget == null) @@ -85,11 +52,6 @@ namespace Ryujinx.Graphics.Gpu.Image { _totalBinds++; - // Heartbeat so "no DoF lines" is never ambiguous (a gated switch you cannot SEE - // in the log is worthless -- lesson from this dossier). If OnInput ever runs, we - // log "armed" once; then a periodic "alive" line proves the hook is live and how - // many draws/small-RTs/DoF-inputs it has counted -- distinguishing "probe off" - // from "probe on but never reached the DoF pass (still at a 2D menu/loading)". if (!_armed) { _armed = true; @@ -105,8 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image $"DOFPROBE alive: binds={_totalBinds} smallRTsizes={_smallRtSizes.Count} dofInputs={_dofInputs.Count}"); } - // Census: record every distinct small render-target size once. Confirms the real - // DoF resolution without hard-assuming 512x288. if (rt.Width <= SmallRtMax) { string rtSize = $"{rt.Width}x{rt.Height} {rt.FormatInfo.Format}"; @@ -116,7 +76,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - // Grounding gate: only draws that render INTO the low-res DoF buffer. if (rt.Width != DofWidth || rt.Height != DofHeight) { return; @@ -134,7 +93,7 @@ namespace Ryujinx.Graphics.Gpu.Image va = 0; } - DofInputVas.Add(va); // cross-reference key for the feedback probe + DofInputVas.Add(va); string key = $"stage={stage} @0x{va:X}/{info.FormatInfo.Format}/{info.Width}x{info.Height}/lv{info.Levels}/{info.Target}"; diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppFeedbackProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppFeedbackProbe.cs index 66110d8ed..ff9ed3e77 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppFeedbackProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppFeedbackProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,23 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppFeedbackProbe { public static readonly bool Enabled = @@ -34,8 +14,8 @@ namespace Ryujinx.Graphics.Gpu.Image { public long LastWriteFrame = long.MinValue; public long FirstAccessFrame = long.MinValue; - public long CaseA; // write(N) -> read-first(N+1): temporal history - public long CaseB; // write(N) -> write-first(N+1): transient reused + public long CaseA; + public long CaseB; public string Format; public int Width; public int Height; @@ -47,7 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static long _lastReportMs; private static int _announced; - /// Logs the gate state once per process, even when OFF (removes the "no lines" ambiguity). public static void AnnounceOnce() { if (System.Threading.Interlocked.Exchange(ref _announced, 1) != 0) @@ -61,10 +40,8 @@ namespace Ryujinx.Graphics.Gpu.Image (Enabled ? "Probe ACTIVE." : "Probe OFF -- launch via XC2_FEEDBACK_PROBE.bat to enable.")); } - /// Current guest frame counter (for other probes' log lines). public static long Frame => System.Threading.Interlocked.Read(ref _frame); - /// Read the frame-to-frame classification of a texture VA (for the trace probe). public static (long caseA, long caseB) Classify(ulong va) { lock (_rts) @@ -85,7 +62,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// A texture is bound as a colour render target this frame (a write). public static void OnWrite(Texture texture) { if (texture == null) @@ -107,8 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Image { rec.FirstAccessFrame = _frame; - // First access of this frame is a WRITE. If it was also written the previous - // frame, it is a transient target reused every frame (Case B). if (rec.LastWriteFrame == _frame - 1) { rec.CaseB++; @@ -125,7 +99,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// A texture is bound as a sampled input this frame (a read). Only render targets are tracked. public static void OnRead(Texture texture) { if (texture == null) @@ -139,15 +112,13 @@ namespace Ryujinx.Graphics.Gpu.Image { if (!_rts.TryGetValue(va, out Rec rec)) { - return; // never a render target -> cannot be a history buffer + return; } if (rec.FirstAccessFrame != _frame) { rec.FirstAccessFrame = _frame; - // First access of this frame is a READ. If it was written the previous frame, - // it carries content across the frame boundary = TRUE temporal history (Case A). if (rec.LastWriteFrame == _frame - 1) { rec.CaseA++; @@ -156,7 +127,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Called at the guest frame boundary (Window.Present). Advances the frame and reports periodically. public static void OnFrameBoundary() { lock (_rts) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppFullSyncProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppFullSyncProbe.cs index c9066a356..4360c85b9 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppFullSyncProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppFullSyncProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,28 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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? - /// static class MvppFullSyncProbe { public static readonly bool Enabled = @@ -40,10 +15,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static bool _armedLogged; private static int _count; - /// - /// 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. - /// public static void ReportArmed() { if (!Enabled || _armedLogged) @@ -56,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image Logger.Warning?.Print(LogClass.Gpu, "[FULLSYNC] ARMED (RYUJINX_FULLSYNC=1). Expect one line per distinct texture shape taking the full-upload path."); } - // Per-shape counters. The first run showed that BOTH the corrupted surface (R8G8B8A8 720p) and - // the clean one (R11G11B10 720p) take this path with empty guest memory, so "takes the path" is - // not the discriminator. What can still separate them is HOW OFTEN, and whether the texture is - // a VIEW over a parent that already holds rendered pixels -- an empty upload onto a fresh - // texture is harmless, the same upload onto a parent's memory wipes what was drawn. private static readonly Dictionary _shapeCounts = new(); private static int _fullResLogged; @@ -83,8 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Image _shapeCounts.TryGetValue(key, out int n); _shapeCounts[key] = n + 1; - // Full-screen surfaces are the ones journal (111) is about, and they are rare enough to - // log individually. Capped so a runaway loop cannot fill the log and slow the run. if (info.Width == 1280 && info.Height == 720 && _fullResLogged < FullResLogCap) { _fullResLogged++; @@ -103,9 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - // Sampled rather than exhaustive: these spans reach several megabytes and this runs on the - // render thread. A stride of 4093 (prime) avoids aligning with any power-of-two structure - // in the data, which a round stride could alias with and mistake for "all zero". int nonZero = 0; int sampled = 0; diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppGobProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppGobProbe.cs index 6c2f96220..ef4671159 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppGobProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppGobProbe.cs @@ -1,5 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. using Ryujinx.Common.Logging; using System; @@ -7,39 +5,11 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// 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. - /// static class MvppGobProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_GOBZ_PROBE") == "1"; - /// - /// 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. - /// public static readonly bool ClampDisabled = Environment.GetEnvironmentVariable("RYUJINX_NO_GOBZ_CLAMP") == "1"; @@ -54,8 +24,6 @@ namespace Ryujinx.Graphics.Gpu.Image { _total++; - // Distinct shapes rather than a line per texture: a game binds the same few - // hundred textures over and over, and what we need is WHICH KINDS get clamped. string key = $"{width}x{height}x{layers} {target} {format} gobZ={gobZ}"; _shapes.TryGetValue(key, out int n); diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppMap64Probe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppMap64Probe.cs index 1f881b27a..72514573f 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppMap64Probe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppMap64Probe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -11,31 +8,6 @@ using System.Text; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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". - /// static class MvppMap64Probe { public static readonly bool Enabled = @@ -43,7 +15,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static bool _armedLogged; - /// Positive control (TextureCache ctor). Silence without it is unreadable. public static void ReportArmed() { if (!Enabled || _armedLogged) @@ -75,7 +46,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static readonly List _inputs = new(); - /// Per-draw sampled input (TextureBindingsManager, both bind paths). Self-gated. public static void OnInput(Texture texture, ShaderStage stage, int handle) { if (!Enabled || texture == null) @@ -102,11 +72,8 @@ namespace Ryujinx.Graphics.Gpu.Image } } - // One detailed line per distinct (fragment shader, target shape, viewport) -- a NEW viewport - // on a known writer is a finding on its own (partial write), so it re-triggers the log. private static readonly HashSet<(ulong, string, int, int)> _seenWriters = new(); - // Instance churn per shape, WRITER side (RuntimeHelpers identity, no field added). private static readonly Dictionary> _instances = new(); private static long _totalDraws; @@ -116,10 +83,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static long _groupSyncs; private static long _summaryMs; - /// - /// 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. - /// public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, int vpW, int vpH) { _totalDraws++; @@ -211,7 +174,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static long _groupSyncLogged; private const long PerEventLogCap = 40; - /// Full guest-memory upload onto a watched shape (Texture.SynchronizeFull). Rare = log each, capped. public static void OnFullSync(Texture texture, ReadOnlySpan data) { if (!Enabled || texture == null || !IsWatched(texture.Info)) @@ -221,7 +183,6 @@ namespace Ryujinx.Graphics.Gpu.Image _fullSyncs++; - // The texture is tiny (64x36x4 = 9 KiB guest range): scan EVERY byte, no sampling caveat. bool empty = true; for (int i = 0; i < data.Length; i++) { @@ -247,7 +208,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Partial (per-handle) group sync onto a watched shape (Texture.SynchronizeMemory, _hasData branch). public static void OnGroupSync(Texture texture) { if (!Enabled || texture == null || !IsWatched(texture.Info)) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppMvBufProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppMvBufProbe.cs index b106381e7..5ed3a4905 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppMvBufProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppMvBufProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -10,30 +7,6 @@ using System.Text; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppMvBufProbe { public static readonly bool Enabled = @@ -62,7 +35,6 @@ namespace Ryujinx.Graphics.Gpu.Image t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm; } - // Frame accounting (advanced from the present boundary, always on GPU thread). private static long _frames; private static int _clearsThisFrame; private static int _writesThisFrame; @@ -79,21 +51,13 @@ namespace Ryujinx.Graphics.Gpu.Image private static readonly HashSet _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 _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 _clearedIds = new(); private static readonly HashSet _sampledIds = new(); - /// Guest frame boundary (Window present). Folds the per-frame counters into histograms. public static void OnPresent() { if (!Enabled) @@ -101,8 +65,6 @@ namespace Ryujinx.Graphics.Gpu.Image return; } - // Only count frames once the buffer exists (first clear or write seen), so the - // pre-title-screen frames do not drown the histogram. if (_clears + _writerDraws > 0) { _frames++; @@ -146,7 +108,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Engine clear about to hit a colour target (DrawManager.Clear). Logs each DISTINCT signature. public static void OnClear(Texture target, int index, uint componentMask, ColorF color, bool customScissor, int sx, int sy, int sw, int sh) { if (!Enabled || !IsMvBuffer(target)) @@ -180,7 +141,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Shader sampling of the MV buffer (TextureBindingsManager, both bind paths). Self-gated. public static void OnSampled(Texture texture) { if (!Enabled || !IsMvBuffer(texture)) @@ -202,7 +162,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Per-draw (StateUpdater probe block): draws whose colour target is the MV buffer. public static void OnDraw(TextureManager texMgr, ulong fsAddr, int vpW, int vpH, ReadOnlySpan blendEnable, ReadOnlySpan writeMasks) { if (texMgr == null) @@ -244,8 +203,6 @@ namespace Ryujinx.Graphics.Gpu.Image if (isNew && (blend || mask != 0xF)) { - // Only the suspicious combinations get their own line; clean opaque writers - // stay in the summary counts (there are hundreds of them). Logger.Warning?.Print(LogClass.Gpu, $"[MVBUF/WRITER-STATE] {state}{(blend && mask != 0 ? " *** BLEND INTO MV ***" : "")}"); } diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppMvSyncProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppMvSyncProbe.cs index 689a75202..69719056a 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppMvSyncProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppMvSyncProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -8,28 +5,6 @@ using System; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppMvSyncProbe { public static readonly bool Enabled = @@ -51,7 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Image t.Info.FormatInfo.Format == Format.R10G10B10A2Unorm; } - /// Frame boundary (Window present): arming witness + 3s heartbeat, zeros included. public static void OnPresent() { if (!Enabled) @@ -75,7 +49,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// SynchronizeMemory decided an upload is needed for this storage (dirty branch taken). public static void OnSyncDecision(Texture storage, bool partial, int regionCount, bool anyModified) { if (!Enabled || !IsMvBuffer(storage)) @@ -100,7 +73,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// A copy dependency pulled data from an overlapping texture into this storage. public static void OnCopyIn(Texture storage) { if (!Enabled || !IsMvBuffer(storage)) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppPairProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppPairProbe.cs index 55aa8bd96..eda6e6b51 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppPairProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppPairProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,16 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [PAIRPROBE, READ-ONLY, default OFF] Logs each unique pairing of a mip-less colour texture - /// (levels=1, 2D, >=64px, RGBA8) with the sampler it is bound with, and whether that sampler - /// could ever fetch generated mip levels (a mipmap filter mode AND maxLod > 0). - /// Pre-check for the FORCEMIPS dossier: if the shimmering tile pools are only ever sampled - /// MIP-DISABLED, runtime mip generation cannot have any effect and must not be built. - /// Enable with RYUJINX_MVPP_PAIRPROBE=1. Never modifies bindings, textures or samplers. - /// Limitation: sampler-level only — an explicit textureLod(0)/texelFetch in the shader would - /// still bypass mips even on a MIP-CAPABLE pair. - /// static class MvppPairProbe { public static readonly bool Enabled = @@ -29,12 +16,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static int _disabled; private static long _lastSummaryMs; - /// - /// Inspects one texture+sampler pairing (call sites are gated on ). - /// Logs the pair once per unique content combo, plus a periodic summary. - /// - /// The texture being bound - /// The sampler it is bound with public static void OnPair(Texture texture, Sampler sampler) { if (texture == null || sampler == null) @@ -44,9 +25,6 @@ namespace Ryujinx.Graphics.Gpu.Image TextureInfo info = texture.Info; - // v2 (11/07): census widened to EVERY mip-less texture >= 64px — all formats and all - // targets (arrays included) — to inventory what the FORCEMIPS v1 criteria still miss - // at the spots where the shimmer survives. if (info.Levels != 1 || info.Width < 64 || info.Height < 64) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppScenePassProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppScenePassProbe.cs index 51e88d003..f26b8bbf1 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppScenePassProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppScenePassProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -11,18 +8,6 @@ using System.Text; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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 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). - /// static class MvppScenePassProbe { public static readonly bool Enabled = @@ -35,13 +20,10 @@ namespace Ryujinx.Graphics.Gpu.Image public int W; public int H; public ShaderStage Stage; - public int Handle; // [E0] texture-CB handle -> GLSL sampler name fp_t_tcb_ - public int Binding; // [E0] runtime binding slot -> GLSL layout(binding=N) - public AddressMode WrapU; // [CLAMPPROBE] sampler address mode U (edge/border/wrap) of this input - public AddressMode WrapV; // [CLAMPPROBE] sampler address mode V - // [FILTERPROBE] The address mode was checked and matches the console; the FILTER never was. - // It decides how the tiny 64x36 CoC map is interpolated, and that map sizes every bokeh - // sprite -- nearest vs linear there changes sprite sizes in ~20-pixel blocks. + public int Handle; + public int Binding; + public AddressMode WrapU; + public AddressMode WrapV; public MinFilter MinF; public MagFilter MagF; } @@ -75,7 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Appends one sampled input of the current draw (called from CommitTextureBindings, gated). public static void OnInput(Texture texture, ShaderStage stage, int handle, int binding, Sampler sampler) { if (texture == null) @@ -87,7 +68,7 @@ namespace Ryujinx.Graphics.Gpu.Image { if (_inputs.Count >= 256) { - return; // safety cap; a real draw never samples this many + return; } TextureInfo info = texture.Info; @@ -108,11 +89,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// - /// 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. - /// public static void OnDraw(TextureManager texMgr, ulong fsAddr, ulong vsAddr, long frame) { lock (_inputs) @@ -127,8 +103,6 @@ namespace Ryujinx.Graphics.Gpu.Image TextureInfo rt = colorRt.Info; bool isScene = rt.Width == 1280 && rt.Height == 720 && rt.FormatInfo.Format == Format.R11G11B10Float; - // [CLAMPPROBE] also catch the DoF bokeh scatter pass by its output signature (512x288 R16G16B16A16F), - // so we can log the address mode of the textures IT gathers (the OOB-gather edge suspect). bool isDof = rt.Width == 512 && rt.Height == 288 && rt.FormatInfo.Format == Format.R16G16B16A16Float; if (!isScene && !isDof) { @@ -137,7 +111,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (!_seenShaders.Add(fsAddr)) { - return; // this pass already reported + return; } ulong fbVa = Va(colorRt); @@ -146,8 +120,6 @@ namespace Ryujinx.Graphics.Gpu.Image Logger.Info?.Print(LogClass.Gpu, $"SCENEPROBE pass writes {kind}: fs=0x{fsAddr:X} vs=0x{vsAddr:X} fb=0x{fbVa:X} frame={frame} inputs={_inputs.Count} ====="); - // [E0] Every bound colour render target (out_attr_N) with its runtime format. out_attr1 is - // the resolved-colour / temporal-history feedback target H1 cares about. Read-only. for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++) { Texture ct = texMgr.GetColorTarget(slot); diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppSeamProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppSeamProbe.cs index f5e3a6c79..d684b2d3f 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppSeamProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppSeamProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,21 +5,9 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [SEAMPROBE, READ-ONLY, default OFF] Residual x3 seam hunt: logs every SCALED copy/draw - /// whose source or destination rectangle has an EDGE at a watched guest-space coordinate. - /// A full-surface copy cannot create an interior line, so only edge hits matter; the watched - /// coordinates come from the dump analysis (seamscan), never hard-coded per game. - /// - /// Enable with RYUJINX_MVPP_SEAMPROBE="x60,y727,y810,y844" (guest coords, axis-prefixed); - /// RYUJINX_MVPP_SEAMPROBE_TOL widens the edge match (default 2 guest pixels). - /// Call sites: the 2D blit engine (TwodClass) and DrawTexture (DrawManager) -- the only two - /// paths that copy scaled SUB-regions (texture-cache family copies are full-slice). - /// Never modifies textures, regions or draws. - /// static class MvppSeamProbe { - private const int MaxShapes = 400; // stop logging new shapes past this (bounds a pathological run) + private const int MaxShapes = 400; private static readonly int[] _watchX; private static readonly int[] _watchY; @@ -81,11 +66,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static bool _announced; - /// - /// Prints the probe's status once, from a site that always runs (TextureManager creation). - /// Without this, a run where no call site ever fires is indistinguishable from a run - /// where the environment variable was never set (13/07: two silent logs in a row). - /// public static void Announce() { if (_announced) @@ -98,10 +78,6 @@ namespace Ryujinx.Graphics.Gpu.Image $"SEAMPROBE status: enabled={_enabled} x=[{string.Join(",", _watchX)}] y=[{string.Join(",", _watchY)}] tol={_tol}"); } - /// - /// Inspects one scaled copy/draw. Guest rectangles are PRE-scale (the space the watched - /// coordinates live in); host rectangles are what actually reaches the backend. - /// public static void OnScaledCopy( string op, Texture src, Texture dst, @@ -116,11 +92,8 @@ namespace Ryujinx.Graphics.Gpu.Image return; } - // A diagnostic on the GPU thread must never take the process down. try { - // Unscaled ops cannot produce a scaling seam. When one side is unknown (null, - // e.g. the bound render target of a DrawTexture), the call site gates instead. if (src != null && dst != null && src.EffectiveScaleX == 1f && src.EffectiveScaleY == 1f && dst.EffectiveScaleX == 1f && dst.EffectiveScaleY == 1f) @@ -170,7 +143,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Names the first watched coordinate an edge lands on, or null. private static string EdgeHit(float sA, float sB, float dA, float dB, int[] watch, string axis) { foreach (int w in watch) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppTaaProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppTaaProbe.cs index 9001e0a6d..98b2e843d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppTaaProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppTaaProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -8,32 +5,11 @@ using System; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppTaaProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_TAAPROBE") == "1"; - // 24 emplacements : FEEDBACKPROBE en a compte 33 au total, dont une majorite en resolution - // reduite que le pre-filtre elimine avant d'arriver ici. private const int Slots = 24; private static readonly ulong[] _va = new ulong[Slots]; @@ -61,7 +37,6 @@ namespace Ryujinx.Graphics.Gpu.Image $"TAAPROBE gate check: RYUJINX_MVPP_TAAPROBE={(v ?? "")} -> enabled={Enabled}."); } - /// Frontiere d'image invitee. public static void OnPresent() { if (Enabled) @@ -85,11 +60,6 @@ namespace Ryujinx.Graphics.Gpu.Image return -1; } - /// - /// 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. - /// public static void OnInput(Texture input, Texture target, ShaderStage stage) { if (!Enabled || input == null || target == null) @@ -97,9 +67,6 @@ namespace Ryujinx.Graphics.Gpu.Image return; } - // Pre-filtre : la resolution principale est la plus grande cible couleur vue. Deux - // comparaisons d'entiers eliminent tout le trafic (masques, bloom, ombres, interface) - // avant le moindre parcours. long area = (long)target.Info.Width * target.Info.Height; if (area > _mainArea) @@ -114,7 +81,6 @@ namespace Ryujinx.Graphics.Gpu.Image return; } - // La cible pleine resolution de ce dessin est notee comme "vue a cette image". ulong tva = Va(target); int ti = Find(tva); @@ -129,8 +95,6 @@ namespace Ryujinx.Graphics.Gpu.Image _count++; } - // L'entree doit elle aussi etre pleine resolution : un historique temporel a forcement - // la taille de ce qu'il reconstruit. if (input.Info.Width != _mainW || input.Info.Height != _mainH) { return; @@ -138,7 +102,6 @@ namespace Ryujinx.Graphics.Gpu.Image int ii = Find(Va(input)); - // Ecrite a une image PRECEDENTE et lue maintenant = historique temporel. if (ii < 0 || _lastSeen[ii] >= _frame) { return; diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppTraceProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppTraceProbe.cs index 4fac52019..cd78b58be 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppTraceProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppTraceProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -9,17 +6,6 @@ using System.Text; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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 ), - /// printing the producer chain and flagging every node that - /// 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). - /// static class MvppTraceProbe { public static readonly bool Enabled = @@ -78,7 +64,6 @@ namespace Ryujinx.Graphics.Gpu.Image return n; } - /// Records the edge "renderTarget <- input" for one read (call site gated on Enabled). public static void OnEdge(Texture renderTarget, Texture input) { if (renderTarget == null || input == null) @@ -97,7 +82,7 @@ namespace Ryujinx.Graphics.Gpu.Image lock (_graph) { Node rt = GetOrAdd(rtVa, renderTarget.Info); - GetOrAdd(inVa, input.Info); // ensure the input has a node (its format), even as a leaf + GetOrAdd(inVa, input.Info); rt.Inputs.Add(inVa); } } @@ -133,7 +118,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - // Holds the _graph lock (called from OnFrameBoundary). private static void Walk(ulong va, int depth, HashSet visited) { if (depth > MaxDepth || !visited.Add(va)) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppTwinFixProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppTwinFixProbe.cs index 7d6d415f2..216e39b7b 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppTwinFixProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppTwinFixProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -9,25 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppTwinFixProbe { public static readonly bool Enabled = @@ -38,8 +16,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static readonly HashSet<(ShaderStage, int)> _redirectTuples = new(); - /// 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. public static bool NeedsRebind(Texture cached) { if (!Enabled || cached == null) @@ -60,7 +36,6 @@ namespace Ryujinx.Graphics.Gpu.Image return cached.Range.GetSubRange(0).Address == skyVa; } - /// Slow-path substitution: reads of the sky twin are served by the material twin. public static Texture MaybeRedirect(Texture texture, ShaderStage stage, int handle) { if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture)) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppTwinMapProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppTwinMapProbe.cs index e83587a79..97a5bb22b 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppTwinMapProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppTwinMapProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -9,20 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppTwinMapProbe { public static readonly bool Enabled = @@ -37,24 +20,16 @@ namespace Ryujinx.Graphics.Gpu.Image public long WriteDraws; public int WritersLogged; - // v2 (journal 176): GMMU aliasing check. On console two GPU VAs can alias the same - // physical pages; a missed remap would leave the cached texture on stale backing. public ulong GpuVa; public ulong Backing; public ulong LastTranslation; - // v4 (journal 178): live Texture reference for TWINFIX's read-side redirect. public WeakReference LastTexture; - // v5 (journal 179): per-twin clear attribution + masked-bind census. public long Clears; public int ClearsLogged; public long MaskedBinds; - // v6 (journal 201): clear-COLOUR census, the classification discriminant that - // survives the v5 writer-count correction (both twins have ~730 writers, so the - // old <=4 / >=50 rule never matches any more). Measured (180): X is cleared - // BLACK (0,0,0,1), Y is cleared NEUTRAL (0.5,0.5,1,1) -- every frame. public long BlackClears; public long NeutralClears; } @@ -63,10 +38,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static readonly HashSet<(ulong, ShaderStage, int)> _reads = new(); private static WeakReference _mm; - // v3 POOLTRUTH (journal 177): raw guest pool descriptor vs actually-bound texture. - // The fast bind path skips descriptor re-reads unless the pool is flagged modified; if - // that tracking misses the game's per-frame updates, a stale binding survives forever -- - // which would pin the resolve on twin X while the game re-points it at Y. private static long _poolMatches; private static long _poolMismatches; private static readonly HashSet<(ulong, ulong, int)> _mismatchTuples = new(); @@ -76,8 +47,6 @@ namespace Ryujinx.Graphics.Gpu.Image return t.Range.GetSubRange(0).Address; } - /// v4 (TWINFIX): conservative twin classification. True only with exactly two twins, - /// one clearly sky-only (<= 4 writers) and one clearly material (>= 50 writers). public static bool TryClassify(out ulong skyVa, out Texture materialTwin) { skyVa = 0; @@ -106,10 +75,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - // [v6] (journal 201) fallback: the writer-count rule died with the v5 counting - // correction (both twins have ~730 material writers). The discriminant that - // still holds, measured (180): X is cleared BLACK each frame, Y NEUTRAL. - // Same redirect direction as the (179) worse-mode: reads of X served by Y. if (sky == null || mat == null) { TwinStats black = null, neutral = null; @@ -152,7 +117,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Snapshot of the discovered twin VAs, for cross-probes (TWINXFER's DMA range check). public static ulong[] TwinVas() { lock (_twins) @@ -163,7 +127,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Per-draw (StateUpdater): record writers per MV-shaped colour target. public static void OnDraw(TextureManager texMgr, ulong fsAddr, Memory.MemoryManager memoryManager, ReadOnlySpan writeMasks) { if (!Enabled || texMgr == null || fsAddr == 0) @@ -183,9 +146,6 @@ namespace Ryujinx.Graphics.Gpu.Image "[TWINMAP] ARMED (RYUJINX_TWINMAP=1). Mapping writers and readers of each MV twin by guest VA."); } - // v5: NO early return -- a single draw can bind BOTH twins at different slots - // (the early return hid that for a whole evening, journal 179). Masks passed in: - // a bound-but-masked-off slot writes nothing and is counted apart. bool any = false; for (int slot = 0; slot < texMgr.ColorTargetsLength; slot++) @@ -251,7 +211,6 @@ namespace Ryujinx.Graphics.Gpu.Image return stats; } - /// v5: engine clear attribution per twin (DrawManager.Clear). public static void OnClear(Texture target, uint componentMask, float r, float g, float b, float a) { if (!Enabled || target == null || !MvppMvSyncProbe.IsMvBuffer(target)) @@ -266,7 +225,6 @@ namespace Ryujinx.Graphics.Gpu.Image TwinStats stats = GetOrAdd(va, target, null); stats.Clears++; - // [v6] clear-colour census for the fallback classification. if (r < 0.1f && g < 0.1f && b < 0.1f) { stats.BlackClears++; @@ -285,8 +243,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// 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. public static void OnRead(Texture texture, ShaderStage stage, int handle, TexturePool pool, int textureId, Memory.MemoryManager memoryManager) { if (!Enabled || texture == null || !MvppMvSyncProbe.IsMvBuffer(texture)) @@ -308,7 +264,6 @@ namespace Ryujinx.Graphics.Gpu.Image $"[TWINMAP] READER of twin@0x{va:X}: stage={stage} handle=0x{handle:X} (tcb_{handle:X}) | distinct readers: {_reads.Count}"); } - // v3 POOLTRUTH: guest truth vs emulator binding. if (pool != null && memoryManager != null && textureId >= 0) { try @@ -342,7 +297,6 @@ namespace Ryujinx.Graphics.Gpu.Image } catch { - // Raw read outside mapped memory: report once via the heartbeat counters only. } } @@ -367,8 +321,6 @@ namespace Ryujinx.Graphics.Gpu.Image TwinStats stats = kv.Value; summary += $" twin@0x{kv.Key:X}: writers={stats.Writers.Count} draws={stats.WriteDraws} maskedBinds={stats.MaskedBinds} CLEARS={stats.Clears} |"; - // v2: live GMMU re-translation of the twin's VA. A change or a mismatch - // with the cached backing = the missed-remap smoking gun. if (mm != null && stats.GpuVa != 0) { ulong translated = mm.Translate(stats.GpuVa); diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppTwinXferProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppTwinXferProbe.cs index 5fe88d125..47666bfec 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppTwinXferProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppTwinXferProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -9,25 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppTwinXferProbe { public static readonly bool Enabled = @@ -43,8 +21,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static readonly HashSet<(ulong, ShaderStage, int, bool)> _imageTuples = new(); private static readonly HashSet<(ulong, ulong, bool)> _dmaPairs = new(); - // Generous per-twin window (real surface = 0x3C0000): a copy landing anywhere inside - // still names the mechanism; sub-range starts (per-line copies) must not be missed. private const ulong TwinWindow = 0x400000; private static bool IsMv(Texture t) @@ -57,7 +33,6 @@ namespace Ryujinx.Graphics.Gpu.Image return t.Range.GetSubRange(0).Address; } - /// Frame boundary (Gpu Window present): arming witness + heartbeat, zeros included. public static void OnPresent() { if (!Enabled) @@ -82,7 +57,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Engine texture copy (2D blit or DMA). Logs pairs where either side is MV-shaped. public static void OnCopy(string path, Texture src, Texture dst) { if (!Enabled || (!IsMv(src) && !IsMv(dst))) @@ -108,9 +82,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// 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). public static void OnDma(ulong srcVa, ulong dstVa, int xCount, int yCount, bool copy2D, bool srcLinear, bool dstLinear) { if (!Enabled) @@ -155,7 +126,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// Storage-image binding of an MV-shaped texture (the compute-writer blind spot). public static void OnImageBind(Texture texture, ShaderStage stage, int handle, bool isStore) { if (!Enabled || !IsMv(texture)) diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppViewAliasProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppViewAliasProbe.cs index e2149af0a..f6d561d22 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppViewAliasProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppViewAliasProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,45 +5,17 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// 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 _reported = new(); private static bool _armedLogged; private static bool _firstViewLogged; private static int _viewCount; - /// - /// 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. - /// public static void ReportArmed() { if (!Enabled || _armedLogged) @@ -68,9 +37,6 @@ namespace Ryujinx.Graphics.Gpu.Image TextureInfo p = parent.Info; - // Second control: proves the path is alive, whatever the formats are. Logged before the - // same-format early-out below, otherwise a run with only ordinary views looks identical - // to a run where this code never executed. _viewCount++; if (!_firstViewLogged) @@ -82,14 +48,11 @@ namespace Ryujinx.Graphics.Gpu.Image $"over parent {p.FormatInfo.Format} {p.Width}x{p.Height} | compat {compatibility}"); } - // Periodic tally so the end of the log states how many views happened in total, which turns - // "no differing-format line" into a quantified negative instead of a silence. if ((_viewCount % 500) == 0) { Logger.Warning?.Print(LogClass.Gpu, $"[VIEWALIAS] tally: {_viewCount} views created so far, {_reported.Count} distinct cross-format cases."); } - // Same format is the ordinary, expected case: a plain view. Only report reinterpretation. if (child.FormatInfo.Format == p.FormatInfo.Format) { return; diff --git a/src/Ryujinx.Graphics.Gpu/Image/MvppWriterCensusProbe.cs b/src/Ryujinx.Graphics.Gpu/Image/MvppWriterCensusProbe.cs index ab58fab07..41a985a0d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/MvppWriterCensusProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/MvppWriterCensusProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader.Translation; @@ -9,26 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { - /// - /// [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. - /// static class MvppWriterCensusProbe { public static readonly bool Enabled = @@ -45,7 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Image private static readonly HashSet _unarmedFs = new(); private static readonly HashSet _maskedOffFs = new(); - /// Frame boundary (Window present): arming witness + 3s heartbeat, zeros included. public static void OnPresent() { if (!Enabled) @@ -74,12 +50,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// 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). public static void OnDraw(TextureManager texMgr, ulong fsAddr, ReadOnlySpan writeMasks, Ryujinx.Graphics.Shader.ShaderProgramInfo fragInfo) { if (texMgr == null || fsAddr == 0) @@ -140,7 +110,7 @@ namespace Ryujinx.Graphics.Gpu.Image } } - return; // one classification per draw, even if bound on several slots + return; } } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs b/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs index 7e88f5feb..151ab9873 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs @@ -19,8 +19,6 @@ namespace Ryujinx.Graphics.Gpu.Image public bool IsSrgb { get; } /// - /// [PAIRPROBE, read-only diagnostics] Unpacked filter/LOD parameters, kept because the - /// descriptor is discarded after construction. Never read by any rendering path. /// public MinFilter ProbeMinFilter { get; } public MagFilter ProbeMagFilter { get; } @@ -29,11 +27,6 @@ namespace Ryujinx.Graphics.Gpu.Image public float ProbeMipLodBias { get; } /// - /// [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. /// public AddressMode ProbeAddressU { get; } public AddressMode ProbeAddressV { get; } @@ -49,18 +42,9 @@ namespace Ryujinx.Graphics.Gpu.Image /// private readonly ISampler _anisoSampler; - // [FORCEMIPS_TRILINEAR, default OFF] Companion knob of RYUJINX_MVPP_FORCEMIPS: for textures - // carrying a generated mip chain, promote mip-NEAREST samplers to mip-LINEAR so the level - // transitions blend instead of snapping (under jitter the per-frame LOD oscillation at level - // boundaries otherwise reads as flicker). Only ever selected for forced-mips textures. private static readonly bool _forceMipsTrilinear = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_TRILINEAR") == "1"; - // [FORCEMIPS_BIAS, default 0 = OFF] Extra mip LOD bias applied ONLY to the forced-mips - // sampler variant. The DLSS jitter path adds a global -0.5 at the Vulkan level (kept sharp - // for regular mipped art, user-validated); on the generated chains that -0.5 pins the near - // band onto over-sharp level 0 and the shimmer survives there. +0.5 here nets the total to - // 0 for forced-mips textures only — the global look stays untouched. private static readonly float _forceMipsBias = float.TryParse( System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_BIAS"), @@ -69,8 +53,6 @@ namespace Ryujinx.Graphics.Gpu.Image out float fmBias) ? fmBias : 0f; /// - /// Host sampler object with the mip filter promoted from Nearest to Linear, used for - /// forced-mips textures when the knob above is set. Null otherwise. /// private readonly ISampler _trilinearSampler; @@ -191,7 +173,7 @@ namespace Ryujinx.Graphics.Gpu.Image { if (_trilinearSampler != null && texture?.HasForcedMips == true) { - return _trilinearSampler; // [FORCEMIPS_TRILINEAR] blend between generated levels + return _trilinearSampler; } return _anisoSampler != null && texture?.CanForceAnisotropy == true ? _anisoSampler : _hostSampler; diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 5a35ba383..2cd6e9c92 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -44,23 +44,12 @@ namespace Ryujinx.Graphics.Gpu.Image private SizeInfo _sizeInfo; - // [MIPPROBE / Mesure H1, READ-ONLY, default OFF] Measure alpha coverage across mip levels for cutout - // candidates, to test whether the foliage's alpha coverage collapses in the game's mip chain. Decodes - // each mip to a TEMP buffer, reads alpha, logs stats -- NEVER modifies the texture data or the upload - // (rendering / DLSS / colour / depth / MV / jitter all untouched). Zero cost when the flag is off. private static readonly bool _mipProbe = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPPROBE") == "1"; private static int _mipProbeAstc, _mipProbeUnreadable, _mipProbeCand, _mipProbeLogged; private static long _mipProbeSummaryMs; private bool _mipProbed; - // [FORCEMIPS, default OFF] Runtime mip generation for mip-less colour textures (the levels=1 - // streamed tile pools): the host texture is allocated with extra mip levels and the chain is - // (re)generated from level 0 by GPU blits, lazily, when the texture is bound for sampling - // after its content changed. PAIRPROBE (11/07) measured that the game binds these pools with - // mip-capable samplers (mip filter set, maxLod=13), so generated levels are consumed by the - // ordinary sampling path. Guest data, guest-visible sizes and flushes are untouched (only the - // HOST allocation gains levels). Byte-identical when the flag is off. private static readonly bool _forceMips = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS") == "1"; private static readonly int _forceMipsExtraLevels = @@ -73,33 +62,14 @@ namespace Ryujinx.Graphics.Gpu.Image private static int _forceMipsLost; private static long _forceMipsSummaryMs; - // [MIPS-RT fix 19/07] Corruption 4K natif PROUVÉE au banc (sonde du jour : 17 textures - // boostées liées en cible couleur, ~35k liaisons/3,5 min — toutes petites et carrées = - // les textures procédurales du jeu, dont le portail de sanctuaire mauve du rapport) : - // à res_scale=1 plus rien n'est Scaled, donc l'exclusion ScaleMode==Scaled qui - // protégeait silencieusement les buffers d'écran en 2x disparaît — il ne reste que - // l'heuristique 16:9 (±16 px) et les cibles carrées passent au travers → mips générées - // derrière un render target écrit chaque frame. Fix = rétrogradation au premier bind - // cible (voir DemoteForcedMipsOnRenderTargetBind). Compteur = rétrogradations. private static int _forceMipsRtDemotes; private bool _forceMipsRtDemoted; - // [FORCEMIPS_REFRESH, default 0 = OFF] Regenerate the forced chain of a sampled texture every - // N ms even without a dirty mark. Diagnostic for the progressive-degradation signature (11/07: - // "parfait au début, revient de plus en plus fort" + regens≈boosted in the logs): if periodic - // refresh stops the degradation, level-0 updates are reaching the pools through a path the - // dirty marks do not cover (stale mips), and that path must be found. private static readonly int _forceMipsRefreshMs = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_REFRESH_MS"), out int fmRefresh) ? Math.Max(0, fmRefresh) : 0; - // [MIPDUMP, default OFF] Read-only diagnostic for the SMART-downsample dossier (pépite 2): - // on its first regeneration, each of the first 16 boosted textures writes every generated - // level to disk (_MIPDUMP/, PPM colour + PGM alpha + index line) so the exact damage of the - // plain-blit chain (tile-border bleed? odd-size drift? stale content? alpha decay?) can be - // SEEN before designing the replacement shader. GetData is a synchronous readback (stalls) — - // measurement runs only. Rendering, generation and filtering are untouched. private static readonly bool _mipDump = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPDUMP") == "1"; private const int MipDumpMaxTextures = 16; @@ -111,10 +81,6 @@ namespace Ryujinx.Graphics.Gpu.Image private long _forcedMipsLastRegenMs; /// - /// [FORCEMIPS] True when this texture's host allocation carries a generated mip chain. - /// Read by to pick the trilinear-promoted sampler - /// variant (mip-Nearest sampling snaps between generated levels; under jitter the LOD - /// choice oscillates per frame at level boundaries, which reads as flicker). /// public bool HasForcedMips => _forcedMipLevels > 1; @@ -154,18 +120,12 @@ namespace Ryujinx.Graphics.Gpu.Image public float ScaleFactor { get; private set; } /// - /// Exact horizontal scale of the host texture (host size / guest size). - /// With quantized fractional scaling the host size is snapped up to the mip granularity, - /// so the real ratio sits a hair above the nominal ; consumers - /// that place or sample content (viewport, scissor, shader compensation, present crop) - /// must use this exact value or they under-cover the allocation. /// public float EffectiveScaleX => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null ? HostTexture.Width * Info.SamplesInX / (float)Info.Width : ScaleFactor; /// - /// Exact vertical scale of the host texture (host size / guest size). See . /// public float EffectiveScaleY => TextureCache.DlssSrSpike && ScaleFactor != 1f && HostTexture != null ? HostTexture.Height * Info.SamplesInY / (float)Info.Height @@ -246,7 +206,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// public MultiRange Range { get; private set; } - /// [MVPP] Accès lecture seule à la mémoire physique invitée, pour la sonde PRESYNC GUEST. internal PhysicalMemory PhysicalMemory => _physicalMemory; /// @@ -328,9 +287,6 @@ namespace Ryujinx.Graphics.Gpu.Image InitializeTexture(context, physicalMemory, info, sizeInfo, range); - // [TEXPROBE, 10/07] Read-only: log every guest texture's size/mip-chain/format at creation - // (RYUJINX_MVPP_TEXPROBE=1, default OFF) to test the truncated-mip-chain hypothesis on the - // distant-foliage shimmer (journal entry 52). No behavior change. if (_texProbe && info.Target == Target.Texture2D && info.Width >= 16) { Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu, @@ -338,7 +294,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - // [TEXPROBE] see the gated log in the constructor. private static readonly bool _texProbe = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_TEXPROBE") == "1"; @@ -387,16 +342,11 @@ namespace Ryujinx.Graphics.Gpu.Image Debug.Assert(!isView); TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor); - createInfo = BoostLevelsForForcedMips(createInfo); // [FORCEMIPS] no-op when off + createInfo = BoostLevelsForForcedMips(createInfo); HostTexture = _context.Renderer.CreateTexture(createInfo); SynchronizeMemory(); // Load the data. - // [FORCEMIPS] Generate the chain at birth: freshly allocated levels hold whatever - // the VRAM last contained, and a texture can reach the screen on the frame it is - // created (the ~1s startup ghost image). First tried 11/07 stacked with the 150 ms - // refresh and blamed for the white veil; the veil's real culprit was the 16:9 - // screen-buffer boosting (now excluded), so this returns alone. No-op unless boosted. EnsureForcedMips(); if (ScaleMode == TextureScaleMode.Scaled) @@ -420,32 +370,20 @@ namespace Ryujinx.Graphics.Gpu.Image } TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor); - createInfo = BoostLevelsForForcedMips(createInfo); // [FORCEMIPS] no-op when off + createInfo = BoostLevelsForForcedMips(createInfo); HostTexture = _context.Renderer.CreateTexture(createInfo); } } } /// - /// [FORCEMIPS] If this texture is an eligible mip-less colour texture (2D, levels=1, RGBA8, - /// >= 64px, no array/3D/MSAA), returns a copy of the host creation info with extra mip levels - /// so a generated chain can live in the host allocation. The guest-visible Info is untouched. - /// Returns the input unchanged (and clears the boost state) in every other case. /// - /// The computed host texture creation info - /// The creation info, with boosted levels if eligible private TextureCreateInfo BoostLevelsForForcedMips(TextureCreateInfo createInfo) { _forcedMipLevels = 0; - // Actively res-scaled textures are excluded (their host size is not the guest size, and - // mips would average across dyn-res sub-rect boundaries). Merely scale-ELIGIBLE textures - // stay boosted: excluding them removed the mips from distant-island art (user regression, - // 11/07), and the control run proved the x2 seam lines exist without this fix — they are - // the old upstream res-scale artifact, a separate dossier. If such a texture is later - // actually scaled, ReplaceStorage drops the boost. if (!_forceMips || - _forceMipsRtDemoted || // [MIPS-RT fix] une cible de rendu ne se re-booste jamais + _forceMipsRtDemoted || ScaleMode == TextureScaleMode.Scaled || Info.Target != Target.Texture2D || Info.Levels != 1 || @@ -459,12 +397,6 @@ namespace Ryujinx.Graphics.Gpu.Image return createInfo; } - // MIPDUMP evidence (11/07): 13/16 boosted textures were ~16:9 screen-fraction buffers - // (333x187 = renderRes/8 G-buffer downsamples, false-colour screen captures in the dump). - // The game's screen-space effects sample them at specific LODs; generated deep mips - // (= an average of the whole screen) bleed into those reads -> the white veil and the - // "broken rocks" at 8 levels. Screen-derived buffers are render-aspect; the streamed - // tile pools are square and the art textures arbitrary -> exclude ~16:9 outright. int aspectWidth = (int)MathF.Ceiling((createInfo.Height / 9f) * 16f); if (Math.Abs(createInfo.Width - aspectWidth) <= 16) @@ -505,13 +437,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [MIPS-RT fix 19/07] Appelé quand cette texture (ou la storage derrière sa vue) est - /// liée comme cible couleur. Une texture boostée qui devient render target = le trou de - /// la config native (voir le commentaire des champs) : le jeu écrit le niveau 0 chaque - /// frame, des mips générées derrière lui sont périmées au mieux, la corruption mauve au - /// pire. Le boost est LÂCHÉ : storage recréée SANS niveaux bonus (contenu copié, même - /// mécanique que le chemin de changement d'échelle), vues recréées dessus, et le flag - /// interdit tout re-boost. Une fois par texture ; no-op des le retour (niveaux <= 1). /// public void DemoteForcedMipsOnRenderTargetBind() { @@ -535,7 +460,6 @@ namespace Ryujinx.Graphics.Gpu.Image ITexture newStorage = storage.GetScaledHostTexture(storage.ScaleFactor, true); storage.ReplaceStorage(newStorage); - // Les vues doivent revivre sur la nouvelle storage (miroir du chemin SetScale). foreach (Texture view in storage._views) { TextureCreateInfo viewCreateInfo = TextureCache.GetCreateInfo(view.Info, storage._context.Capabilities, view.ScaleFactor); @@ -547,11 +471,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [FORCEMIPS] Regenerates the forced mip chain from level 0 with GPU blits (linear filter), - /// through transient per-level views (their Release is deferred by the backend until the - /// commands complete). Called from the sampling bind sites and cheap to call: it returns - /// immediately unless this texture was boosted AND its content changed since the last pass, - /// so mips are rebuilt at most once per frame and only for textures actually sampled. /// public void EnsureForcedMips() { @@ -597,7 +516,6 @@ namespace Ryujinx.Graphics.Gpu.Image dstView.Release(); } - // [MIPDUMP] First regeneration of the first N boosted textures: photograph the chain. if (_mipDump && !_mipDumped && _mipDumpCount < MipDumpMaxTextures) { _mipDumped = true; @@ -624,12 +542,7 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [MIPDUMP] Writes every level of this texture's generated mip chain to disk: PPM (P6, - /// colour) + PGM (P5, alpha) per level, plus one line in _MIPDUMP/index.txt with the - /// texture's dimensions, format, level count and memory address (identifier). Read-only: - /// synchronous GetData readback of what the blits produced, nothing is modified. /// - /// Sequential dump index (for file naming) private void DumpForcedMipChain(int index) { string dir = System.IO.Path.Combine(System.Environment.CurrentDirectory, "_MIPDUMP"); @@ -694,13 +607,7 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [FORCEMIPS] Builds the creation info for a transient single-level view used by the mip - /// regeneration blits. /// - /// Host creation info of the storage - /// View width (storage width at the target level) - /// View height (storage height at the target level) - /// The view creation info private static TextureCreateInfo ForcedMipViewInfo(TextureCreateInfo baseInfo, int width, int height) { return new TextureCreateInfo( @@ -836,7 +743,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// True if this texture is first copied to the given one, false for the opposite direction public void CreateCopyDependency(Texture contained, int layer, int level, bool copyTo) { - MvppCacheProbe.OnCopyDependency(this, contained); // read-only (gated) + MvppCacheProbe.OnCopyDependency(this, contained); if (contained.Group == Group) { @@ -906,12 +813,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// A host texture containing a scaled version of this texture private ITexture GetScaledHostTexture(float scale, bool copy, ITexture storage = null) { - // Fractional-crash probe (quality-mode dossier): a descale copy is the moment a - // scaled texture becomes guest-visible data (flush/blacklist). Only fires under a - // FRACTIONAL source scale (integer scales = the proven regime, daily untouched). - // [31/07] GATE AJOUTE, meme raison que sa jumelle dans SemaphoreUpdater : elle etait - // en release sans interrupteur et se declenche chez tout utilisateur en DLSS quality - // ou performance, qui produit une echelle fractionnaire par construction. if (GAL.MvppDev.Enabled && copy && scale == 1f && ScaleFactor != MathF.Floor(ScaleFactor)) { Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu, @@ -1023,7 +924,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SynchronizeMemory() { - MvppCacheProbe.OnSynchronize(); // read-only (gated) + MvppCacheProbe.OnSynchronize(); if (Target == Target.TextureBuffer) { @@ -1037,14 +938,11 @@ namespace Ryujinx.Graphics.Gpu.Image _dirty = false; - // Le VRAI travail : seul un appel qui trouve la texture SALE recharge des données. Les - // ~367 000 "resynchronisations" comptées avant étaient surtout des appels qui ressortent ici - // sans rien faire. C'est ce compteur-ci qui dit si le cache RECHARGE en boucle. - MvppCacheProbe.OnRealSync(Info.Width, Info.Height, Info.FormatInfo.Format, _hasData); // read-only (gated) + MvppCacheProbe.OnRealSync(Info.Width, Info.Height, Info.FormatInfo.Format, _hasData); if (_hasData) { - MvppMap64Probe.OnGroupSync(this); // [MAP64] read-only (gated): partial syncs onto the small DoF maps + MvppMap64Probe.OnGroupSync(this); Group.SynchronizeMemory(this); } else @@ -1078,14 +976,10 @@ namespace Ryujinx.Graphics.Gpu.Image { ReadOnlySpan data = _physicalMemory.GetSpan(Range); - // [FULLSYNC, READ-ONLY] Report which surfaces get a full upload from guest memory, and - // whether that memory is empty. Gated; does nothing to the data or the upload. MvppFullSyncProbe.OnFullSync(Info, data, IsView); - // [MAP64, READ-ONLY] Same question, filtered on the small DoF maps (gated). MvppMap64Probe.OnFullSync(this, data); - // [MIPPROBE, READ-ONLY] Measure mip alpha coverage once per texture. Gated; does nothing to data/upload. if (_mipProbe && !_mipProbed) { _mipProbed = true; @@ -1114,10 +1008,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - // Large 4K texture-pack array textures (e.g. a 2048x2048 ASTC 2D array with 121 layers) decode to - // more than 2 GiB of RGBA8, which overflows Int32 and cannot fit in a single array. Decode and - // upload them one slice at a time instead (each slice is only a few MiB), reusing the same - // single-slice path that partial synchronization already uses. if (Info.FormatInfo.Format.IsAstc && !_context.Capabilities.SupportsAstcCompression && _depth == 1 && @@ -1158,7 +1048,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_forcedMipLevels > 1) { - _forcedMipsDirty = true; // [FORCEMIPS] level 0 changed; regenerated lazily at the next sampling bind + _forcedMipsDirty = true; _forceMipsMarks++; } @@ -1166,9 +1056,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [MIPPROBE / Mesure H1, READ-ONLY] Decodes each mip level to a TEMP buffer, measures the alpha - /// coverage (fraction of texels with alpha > 0.5) per level for cutout (binary-alpha) textures, and - /// logs the coverage loss down the chain. Never modifies the texture data or the upload path. /// private void MipCoverageProbe(ReadOnlySpan data) { @@ -1186,7 +1073,6 @@ namespace Ryujinx.Graphics.Gpu.Image if (!astc && !rgba8) { - // Mipped texture whose alpha we can't read cheaply (BCn native, etc.) -- log the landscape. System.Threading.Interlocked.Increment(ref _mipProbeUnreadable); return; } @@ -1203,7 +1089,7 @@ namespace Ryujinx.Graphics.Gpu.Image for (int level = 0; level < levels; level++) { - int off = _sizeInfo.AllOffsets[level]; // layer 0 + int off = _sizeInfo.AllOffsets[level]; if (off < 0 || off >= data.Length) { return; @@ -1246,7 +1132,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (level == 0 && !binary) { - return; // not a cutout texture -> skip + return; } } @@ -1299,7 +1185,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_forcedMipLevels > 1) { - _forcedMipsDirty = true; // [FORCEMIPS] + _forcedMipsDirty = true; _forceMipsMarks++; } @@ -1320,7 +1206,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_forcedMipLevels > 1 && level == 0) { - _forcedMipsDirty = true; // [FORCEMIPS] + _forcedMipsDirty = true; _forceMipsMarks++; } @@ -1344,7 +1230,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_forcedMipLevels > 1 && level == 0) { - _forcedMipsDirty = true; // [FORCEMIPS] tile streamed into the pool + _forcedMipsDirty = true; _forceMipsMarks++; } @@ -2086,12 +1972,12 @@ namespace Ryujinx.Graphics.Gpu.Image { if (MvppDestProbe.Enabled) { - MvppDestProbe.OnModify(this, viaModifying: false, bound: true); // read-only (gated) + MvppDestProbe.OnModify(this, viaModifying: false, bound: true); } if (_forcedMipLevels > 1) { - _forcedMipsDirty = true; // [FORCEMIPS] GPU-side write (copy/RT); regenerated lazily if sampled + _forcedMipsDirty = true; _forceMipsMarks++; } @@ -2115,7 +2001,7 @@ namespace Ryujinx.Graphics.Gpu.Image { if (MvppDestProbe.Enabled) { - MvppDestProbe.OnModify(this, viaModifying: true, bound: bound); // read-only (gated) + MvppDestProbe.OnModify(this, viaModifying: true, bound: bound); } if (bound) @@ -2149,9 +2035,6 @@ namespace Ryujinx.Graphics.Gpu.Image { DisposeTextures(); - // [FORCEMIPS] Replacement storages are allocated by other paths (scale change, size - // change) without the level boost; drop the forced-mips state so the regeneration - // never blits into levels the new storage does not have. if (_forcedMipLevels > 1) { _forceMipsLost++; diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs index c3ebd18c7..51866861d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs @@ -898,10 +898,10 @@ namespace Ryujinx.Graphics.Gpu.Image if (MvppPairProbe.Enabled) { - MvppPairProbe.OnPair(texture, sampler); // read-only (gated) + MvppPairProbe.OnPair(texture, sampler); } - texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty + texture?.EnsureForcedMips(); hostSampler = sampler?.GetHostSampler(texture); } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs index 421244d4d..2459d4444 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs @@ -111,9 +111,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [MV++ UI audit, read-only] Enumerates the currently-bound SAMPLED input textures across ALL - /// stages (compute has 1 stage, graphics has several). No effect on rendering state. Used by the - /// gated composite-locator probe to test whether a draw samples the captured scene texture. /// public void MvppEnumerateInputs(System.Action report) { @@ -137,10 +134,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// [MV++ UI audit, read-only] Like MvppEnumerateInputs but also reports the shader stage. - /// [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. public void MvppEnumerateInputsWithSampler(System.Action report) { for (int stage = 0; stage < _textureBindings.Length; stage++) @@ -186,9 +179,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [MV++ UI audit, read-only] Enumerates the currently-bound STORAGE IMAGES across all stages - /// (image load/store), the blind spot of MvppEnumerateInputs which only sees sampled textures. A - /// fullscreen upscale that reads its source via imageLoad shows up here, not as a sampled input. /// public void MvppEnumerateImages(System.Action report) { @@ -213,9 +203,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [MV++ UI audit, read-only] Counts the CURRENT texture bindings that are arrays/bindless - /// (ArrayLength > 1), across all stages. Lets the gated probe know whether a scene sample could be - /// hidden in an array binding that MvppEnumerateInputs cannot resolve. /// public int MvppArrayBindingCount() { @@ -236,10 +223,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [MV++ UI audit, read-only] Enumerates the CURRENT compute bindings so a gated diagnostic can - /// see what a DispatchCompute reads and writes. No effect on rendering state. The callback gets - /// (isImage, isStore, texture): isImage=false is a sampled input texture; isImage=true is a - /// storage image, and isStore=true means the shader WRITES it (the output candidate). /// public void MvppEnumerateCompute(System.Action report) { @@ -364,10 +347,10 @@ namespace Ryujinx.Graphics.Gpu.Image if (MvppPairProbe.Enabled) { - MvppPairProbe.OnPair(texture, sampler); // read-only (gated) + MvppPairProbe.OnPair(texture, sampler); } - texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty + texture?.EnsureForcedMips(); return (texture, sampler); } @@ -397,13 +380,6 @@ namespace Ryujinx.Graphics.Gpu.Image switch (stage) { case ShaderStage.Fragment: - // The exact (quantized) host/guest ratio, not the nominal scale: - // shader coordinate compensation must land on the real content size. - // KNOWN LIMIT: this support-buffer slot is a single scalar, so Y - // compensates with X's quantized ratio (they can differ by the - // size-dependent mip pad). Sub-texel error; separating them means a - // support-buffer layout + shader codegen change (invalidates every - // shader cache) -> deliberately parked as its own dossier. float scale = texture.EffectiveScaleX; if (scale != 1) @@ -633,11 +609,11 @@ namespace Ryujinx.Graphics.Gpu.Image return true; } - MvppDofProbe.AnnounceOnce(); // [DOFPROBE] one-shot gate-state line on the real draw path, even when OFF - MvppFeedbackProbe.AnnounceOnce(); // [FEEDBACKPROBE] one-shot gate-state line, even when OFF - MvppTraceProbe.AnnounceOnce(); // [TRACEPROBE] one-shot gate-state line, even when OFF - MvppScenePassProbe.AnnounceOnce(); // [SCENEPROBE] one-shot gate-state line, even when OFF - MvppTaaProbe.AnnounceOnce(); // [TAAPROBE] one-shot gate-state line, even when OFF + MvppDofProbe.AnnounceOnce(); + MvppFeedbackProbe.AnnounceOnce(); + MvppTraceProbe.AnnounceOnce(); + MvppScenePassProbe.AnnounceOnce(); + MvppTaaProbe.AnnounceOnce(); bool specStateMatches = true; @@ -683,45 +659,42 @@ namespace Ryujinx.Graphics.Gpu.Image state.CachedTexture != null && state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence && state.CachedSampler?.IsDisposed != true && - !MvppTwinFixProbe.NeedsRebind(state.CachedTexture)) // [TWINFIX] one forced re-resolve once classified + !MvppTwinFixProbe.NeedsRebind(state.CachedTexture)) { // The texture is already bound. state.CachedTexture.SynchronizeMemory(); if (MvppDofProbe.Enabled) { - // [DOFPROBE] cached (fast) path: a stable input re-bound this draw. MvppDofProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage); } if (MvppTaaProbe.Enabled) { - // [TAAPROBE] meme point d'observation, sans verrou : deux comparaisons - // d'entiers eliminent tout le trafic qui n'est pas pleine resolution. MvppTaaProbe.OnInput(state.CachedTexture, _channel.TextureManager.GetAnyRenderTarget(), stage); } if (MvppFeedbackProbe.Enabled) { - MvppFeedbackProbe.OnRead(state.CachedTexture); // [FEEDBACKPROBE] input read (fast path) + MvppFeedbackProbe.OnRead(state.CachedTexture); } if (MvppTraceProbe.Enabled) { - MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), state.CachedTexture); // [TRACEPROBE] dep edge (fast path) + MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), state.CachedTexture); } if (MvppScenePassProbe.Enabled) { - MvppScenePassProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); // [SCENEPROBE] per-draw input (fast path) + MvppScenePassProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); } - MvppBuilderInProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); // [BUILDERIN] per-draw input (fast path, self-gated) - MvppMap64Probe.OnInput(state.CachedTexture, stage, bindingInfo.Handle); // [MAP64] per-draw input (fast path, self-gated) - MvppMvBufProbe.OnSampled(state.CachedTexture); // [MVBUF] identity of the sampled MV buffer (self-gated) - MvppTwinMapProbe.OnRead(state.CachedTexture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated) + MvppBuilderInProbe.OnInput(state.CachedTexture, stage, bindingInfo.Handle, bindingInfo.Binding, state.CachedSampler); + MvppMap64Probe.OnInput(state.CachedTexture, stage, bindingInfo.Handle); + MvppMvBufProbe.OnSampled(state.CachedTexture); + MvppTwinMapProbe.OnRead(state.CachedTexture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); - state.CachedTexture.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty + state.CachedTexture.EnsureForcedMips(); if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && UpdateScale(state.CachedTexture, usageFlags, index, stage)) @@ -745,48 +718,42 @@ namespace Ryujinx.Graphics.Gpu.Image if (MvppPairProbe.Enabled) { - MvppPairProbe.OnPair(texture, sampler); // read-only (gated) + MvppPairProbe.OnPair(texture, sampler); } if (MvppDofProbe.Enabled) { - // [DOFPROBE] resolve (cache-miss) path: an input re-fetched this draw. The DoF - // feedback/history buffer is re-invalidated every frame, so it lands here often. MvppDofProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage); } if (MvppTaaProbe.Enabled) { - // [TAAPROBE] chemin de resolution (defaut de cache) : c'est celui ou un - // historique temporel, re-invalide a chaque image, atterrit le plus souvent. MvppTaaProbe.OnInput(texture, _channel.TextureManager.GetAnyRenderTarget(), stage); } if (MvppFeedbackProbe.Enabled) { - MvppFeedbackProbe.OnRead(texture); // [FEEDBACKPROBE] input read (resolve path) + MvppFeedbackProbe.OnRead(texture); } if (MvppTraceProbe.Enabled) { - MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), texture); // [TRACEPROBE] dep edge (resolve path) + MvppTraceProbe.OnEdge(_channel.TextureManager.GetAnyRenderTarget(), texture); } if (MvppScenePassProbe.Enabled) { - MvppScenePassProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); // [SCENEPROBE] per-draw input (resolve path) + MvppScenePassProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); } - MvppBuilderInProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); // [BUILDERIN] per-draw input (resolve path, self-gated) - MvppMap64Probe.OnInput(texture, stage, bindingInfo.Handle); // [MAP64] per-draw input (resolve path, self-gated) - MvppMvBufProbe.OnSampled(texture); // [MVBUF] identity of the sampled MV buffer (self-gated) - MvppTwinMapProbe.OnRead(texture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); // [TWINMAP] reader census + pool truth (self-gated) + MvppBuilderInProbe.OnInput(texture, stage, bindingInfo.Handle, bindingInfo.Binding, sampler); + MvppMap64Probe.OnInput(texture, stage, bindingInfo.Handle); + MvppMvBufProbe.OnSampled(texture); + MvppTwinMapProbe.OnRead(texture, stage, bindingInfo.Handle, texturePool, textureId, _channel.MemoryManager); - // [TWINFIX] (EXP 21) read-side redirect: reads of the sky-only twin are served by - // the material twin. Self-gated; returns the input texture unless armed+matched. texture = MvppTwinFixProbe.MaybeRedirect(texture, stage, bindingInfo.Handle); - texture?.EnsureForcedMips(); // [FORCEMIPS] no-op unless boosted and dirty + texture?.EnsureForcedMips(); specStateMatches &= specState.MatchesTexture(stage, index, descriptor); @@ -901,7 +868,7 @@ namespace Ryujinx.Graphics.Gpu.Image // The texture is already bound. cachedTexture.SynchronizeMemory(); - MvppTwinXferProbe.OnImageBind(cachedTexture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated + MvppTwinXferProbe.OnImageBind(cachedTexture, stage, bindingInfo.Handle, isStore); if (isStore) { @@ -942,7 +909,7 @@ namespace Ryujinx.Graphics.Gpu.Image } else { - MvppTwinXferProbe.OnImageBind(texture, stage, bindingInfo.Handle, isStore); // [TWINXFER] self-gated + MvppTwinXferProbe.OnImageBind(texture, stage, bindingInfo.Handle, isStore); if (isStore) { diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs index 6816d630e..5c7678af0 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -66,7 +66,6 @@ namespace Ryujinx.Graphics.Gpu.Image _textureOverlaps = new Texture[OverlapsBufferInitialCapacity]; _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity]; - // [VIEWALIAS] / [FULLSYNC] / [MAP64] Positive controls: make the probes' silence interpretable. MvppViewAliasProbe.ReportArmed(); MvppFullSyncProbe.ReportArmed(); MvppMap64Probe.ReportArmed(); @@ -878,7 +877,6 @@ namespace Ryujinx.Graphics.Gpu.Image info = info.CreateInfoWithFormat(overlap.Info.FormatInfo); } - // [VIEWALIAS] Read-only: report views created over a parent of a DIFFERENT format. MvppViewAliasProbe.OnViewCreated(info, overlap, oInfo.FirstLayer, oInfo.FirstLevel, oInfo.Compatibility.ToString()); texture = overlap.CreateView(info, sizeInfo, range.Value, oInfo.FirstLayer, oInfo.FirstLevel); @@ -1287,9 +1285,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// GPU capabilities /// Texture scale factor, to be applied to the texture size /// The texture creation information - // E3 (DLSS-SR) gate: fractional resolution scale support. Off by default; the daily - // driver is never affected. Shared by every site that must agree on the quantized - // per-texture effective scale (texture sizes, viewport/scissor, shader compensation). internal static readonly bool DlssSrSpike = Environment.GetEnvironmentVariable("RYUJINX_DLSS_SR") == "1"; @@ -1336,13 +1331,6 @@ namespace Ryujinx.Graphics.Gpu.Image { if (DlssSrSpike) { - // Fractional scale, NO padding: alloc == active == ceil(size * scale). - // Any padding (mip alignment) leaves a dead border that later passes sample - // (the BOTW terrain hole), and it CANNOT be covered by stretching the - // viewport: the framebuffer is bounded by the SMALLEST attachment (a - // single-mip depth bound with a multi-mip color kills the stretch). Mip - // consistency (VUID-04533: fb larger than a mip attachment) is instead - // guaranteed by ClampViewToStorage at view-creation time. width = (int)MathF.Ceiling(width * scale); height = (int)MathF.Ceiling(height * scale); activeWidth = width; @@ -1376,18 +1364,7 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// Clamps a view's host size to the physical dimensions of the parent storage at the - /// level the view starts on. With fractional scaling, host storage mips halve by floor - /// while scaled guest mip sizes round up (ceil), so a view's computed size can exceed - /// the storage mip by a pixel; a framebuffer sized from that view is then larger than - /// the attachment (VUID-04533, device lost when rendering to low mips). This clamp - /// replaces the old mip-alignment padding, which created the dead borders instead. /// - /// The computed view creation info - /// The parent host storage the view lives in - /// First mip level of the view on the parent storage - /// Scale factor of the texture (no-op at 1.0) - /// The creation info, size-clamped to the storage if needed public static TextureCreateInfo ClampViewToStorage(TextureCreateInfo createInfo, ITexture storage, int firstLevel, float scale) { if (!DlssSrSpike || scale == 1f || storage == null || firstLevel <= 0) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index 19b701c26..4e199803d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -67,22 +67,12 @@ namespace Ryujinx.Graphics.Gpu.Image /// Texture information /// Host GPU capabilities /// A host compatible format - // [FORCEMIPS_MASKS, default OFF] Companion knob of RYUJINX_MVPP_FORCEMIPS: cover the - // single-channel MASK textures (PAIRPROBE band census 11/07: BC4 = 20 mip-capable pairs = - // the biggest mip-less population left, classic foliage-alpha/detail masks; R8 = 9 more). - // Eligible mip-less BC4 textures are routed through the STOCK unsupported-BC path - // (decode to R8 host at upload) so the generated-mips blit chain can run on them. internal static readonly bool ForceMipsMasks = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS_MASKS") == "1"; /// - /// [FORCEMIPS_MASKS] True when this texture should take the software BC-decode path even - /// though the host supports BC natively: mip-less 2D BC4 mask of forced-mips size, while - /// the masks knob is set. Mirrors the forced-mips eligibility so host format and level - /// boost always agree. /// /// Texture information - /// True if the texture must be decoded to an uncompressed host format public static bool ForceMipsWantsUncompressed(in TextureInfo info) { return ForceMipsMasks && @@ -107,7 +97,7 @@ namespace Ryujinx.Graphics.Gpu.Image ? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4) : new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); } - + if (info.FormatInfo.Format.IsAstcSrgb) { return GraphicsConfig.EnableTextureRecompression diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index 0816abce7..48d2ac420 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -266,11 +266,11 @@ namespace Ryujinx.Graphics.Gpu.Image return dirty; } - + bool CheckDirtyCallback(int baseHandle, int regionCount, bool split, bool consume) { bool dirty = false; - + for (int i = 0; i < regionCount; i++) { TextureGroupHandle group = _handles[baseHandle + i]; @@ -301,7 +301,7 @@ namespace Ryujinx.Graphics.Gpu.Image { EvaluateRelevantHandles(texture, _discardDataCallback, out _); } - + bool DiscardDataCallback(int baseHandle, int regionCount, bool split, bool bound) { for (int i = 0; i < regionCount; i++) @@ -363,7 +363,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (group.NeedsCopy && group.Copy(_context)) { - MvppMvSyncProbe.OnCopyIn(Storage); // [MVSYNC] read-only, self-gated + MvppMvSyncProbe.OnCopyIn(Storage); anyModified |= true; // The copy target has been modified. handleDirty = false; } @@ -389,7 +389,7 @@ namespace Ryujinx.Graphics.Gpu.Image { bool partialPath = anyNotDirty || (_handles.Length > 1 && (anyModified || split)); - MvppMvSyncProbe.OnSyncDecision(Storage, partialPath, regionCount, anyModified); // [MVSYNC] read-only, self-gated + MvppMvSyncProbe.OnSyncDecision(Storage, partialPath, regionCount, anyModified); if (partialPath) { @@ -728,7 +728,7 @@ namespace Ryujinx.Graphics.Gpu.Image group.SignalModified(_context); } - + return true; }, out _); } @@ -743,10 +743,10 @@ namespace Ryujinx.Graphics.Gpu.Image ModifiedSequence = _context.GetModifiedSequence(); ClearIncompatibleOverlaps(texture); - + EvaluateRelevantHandles(texture, _signalModifyingCallback, out _, bound); } - + bool SignalModifyingCallback(int baseHandle, int regionCount, bool split, bool bound) { for (int i = 0; i < regionCount; i++) @@ -755,7 +755,7 @@ namespace Ryujinx.Graphics.Gpu.Image group.SignalModifying(bound, _context); } - + return true; } @@ -830,7 +830,7 @@ namespace Ryujinx.Graphics.Gpu.Image int targetLevelHandles = _hasMipViews ? levels : 1; result = false; - + if (_isBuffer) { return; @@ -1743,4 +1743,3 @@ namespace Ryujinx.Graphics.Gpu.Image } } } - diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index 66dccabfa..b4b2e8162 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -35,18 +35,13 @@ namespace Ryujinx.Graphics.Gpu.Image public float RenderTargetScale { get; private set; } = 1f; /// - /// Exact horizontal scale (host/guest ratio) of the currently bound render targets. - /// Matches unless quantized fractional scaling snapped - /// the target sizes up to the mip granularity. /// public float RenderTargetScaleX { get; private set; } = 1f; /// - /// Exact vertical scale (host/guest ratio) of the currently bound render targets. /// public float RenderTargetScaleY { get; private set; } = 1f; - // [COUTURE PROBE -- TEMPORARY, DISPOSABLE] throttle for the edge-seam hunt log. private long _coutureLogMs; /// @@ -72,7 +67,7 @@ namespace Ryujinx.Graphics.Gpu.Image _rtHostColors = new ITexture[Constants.TotalRenderTargets]; _rtColorsBound = new bool[Constants.TotalRenderTargets]; - MvppSeamProbe.Announce(); // one status line per boot, even if no call site ever fires + MvppSeamProbe.Announce(); } /// @@ -84,43 +79,36 @@ namespace Ryujinx.Graphics.Gpu.Image _cpBindingsManager.SetBindings(bindings); } - /// [MV++ UI audit, read-only] Enumerates the current compute bindings for the gated probe. public void MvppEnumerateComputeBindings(System.Action report) { _cpBindingsManager.MvppEnumerateCompute(report); } - /// [MV++ UI audit, read-only] Enumerates the graphics draw's sampled input textures. public void MvppEnumerateGraphicsInputs(System.Action report) { _gpBindingsManager.MvppEnumerateInputs(report); } - /// [MV++ UI audit, read-only] Count of array/bindless texture bindings in the current graphics draw. public int MvppGraphicsArrayBindingCount() { return _gpBindingsManager.MvppArrayBindingCount(); } - /// [MV++ UI audit, read-only] Enumerates the graphics draw's bound storage images (imageLoad/Store). public void MvppEnumerateGraphicsImages(System.Action report) { _gpBindingsManager.MvppEnumerateImages(report); } - /// [MV++ UI audit, read-only] Enumerates the graphics draw's sampled inputs with shader stage. public void MvppEnumerateGraphicsInputsStage(System.Action report) { _gpBindingsManager.MvppEnumerateInputsStage(report); } - /// [GLOWPROBE, read-only] Bound textures WITH their sampler. public void MvppEnumerateGraphicsInputsWithSampler(System.Action report) { _gpBindingsManager.MvppEnumerateInputsWithSampler(report); } - /// [MV++ UI audit, read-only] Enumerates the currently-bound colour render targets (all 8 slots). public void MvppEnumerateRenderTargets(System.Action report) { for (int i = 0; i < _rtColors.Length; i++) @@ -212,14 +200,7 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// [DLSS-SR fractional] Nominal equality is not enough to skip the scale update: two - /// targets can share the nominal scale yet carry different quantized host/guest ratios - /// (the mip-align pad is size-dependent, e.g. a 1600x900 and a 1280x720 pass). Switching - /// between them without re-deriving the group ratio leaves viewport/scissor covering the - /// OTHER pass's allocation: the last column/row is never rasterized (edge flicker). /// - /// The texture being bound as a render target - /// True if the texture's exact ratio disagrees with the current group ratio private bool EffectiveScaleMismatch(Texture texture) { return TextureCache.DlssSrSpike && @@ -236,9 +217,6 @@ namespace Ryujinx.Graphics.Gpu.Image public bool SetRenderTargetColor(int index, Texture color) { bool hasValue = color != null; - // [P1.5 RE-APPLIED 12/07 soir, A/B user] First in-game read blamed this for the seam - // flicker, but the pre-v1.2.1 binaries flicker too (user A/B) -- verdict reopened. - // User hypothesis: WITH this fix, Quality mode no longer showed the seam. Testing. bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && (RenderTargetScale != color.ScaleFactor || EffectiveScaleMismatch(color))); if (_rtColors[index] != color) @@ -256,12 +234,12 @@ namespace Ryujinx.Graphics.Gpu.Image { color.SynchronizeMemory(); color.SignalModifying(true); - color.DemoteForcedMipsOnRenderTargetBind(); // [MIPS-RT fix] no-op hors boost (2 tests de champ) + color.DemoteForcedMipsOnRenderTargetBind(); } if (MvppFeedbackProbe.Enabled && color != null) { - MvppFeedbackProbe.OnWrite(color); // [FEEDBACKPROBE] colour target bound = write (gated) + MvppFeedbackProbe.OnWrite(color); } _rtColors[index] = color; @@ -278,7 +256,7 @@ namespace Ryujinx.Graphics.Gpu.Image public bool SetRenderTargetDepthStencil(Texture depthStencil) { bool hasValue = depthStencil != null; - bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && (RenderTargetScale != depthStencil.ScaleFactor || EffectiveScaleMismatch(depthStencil))); // [P1.5 RE-APPLIED, A/B user] see SetRenderTargetColor + bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && (RenderTargetScale != depthStencil.ScaleFactor || EffectiveScaleMismatch(depthStencil))); if (_rtDepthStencil != depthStencil) { @@ -300,8 +278,6 @@ namespace Ryujinx.Graphics.Gpu.Image _rtDepthStencil = depthStencil; } - // Publish the last bound depth so the presentation path can forward a real depth buffer to - // DLSS. Only non-null binds (a real pass) update it; consumers validate dimensions. if (depthStencil != null) { _context.LastPresentDepthStencil = depthStencil; @@ -311,26 +287,18 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// Currently bound depth-stencil render target (if any). /// public Texture RenderTargetDepthStencil => _rtDepthStencil; /// - /// [COLORCAP probe] Currently bound color render target 0 (if any). Read-only accessor added - /// to let the native-color capture prototype read the scene-pass color attachment the same way - /// the depth capture reads . No behavior change. /// public Texture RenderTargetColor0 => _rtColors[0]; /// - /// [E0/SCENEPROBE read-only] Number of colour render-target (MRT) slots. Lets the scene probe - /// enumerate every out_attr_N attachment. No behaviour change. /// public int ColorTargetsLength => _rtColors.Length; /// - /// [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). /// public Texture GetColorTarget(int index) => (uint)index < (uint)_rtColors.Length ? _rtColors[index] : null; @@ -446,10 +414,6 @@ namespace Ryujinx.Graphics.Gpu.Image RenderTargetScale = targetScale; - // With quantized fractional scaling each target's real (host/guest) ratio can sit a - // hair above the nominal scale. Viewport/scissor must cover the full allocation, so - // expose the exact group ratio; the smallest one wins if targets disagree (the - // framebuffer is sized by the smallest attachment). float scaleX = targetScale; float scaleY = targetScale; @@ -498,11 +462,6 @@ namespace Ryujinx.Graphics.Gpu.Image RenderTargetScaleX = scaleX; RenderTargetScaleY = scaleY; - // [COUTURE PROBE -- TEMPORARY, DISPOSABLE] Edge-seam hunt (12/07): whenever the chosen - // group ratio disagrees with a bound target's exact ratio, the scissor covers - // ceil(guestW x group) while that target really spans guestW x effective -- the - // uncovered last columns ARE the black line. Print the exact numbers, throttled ~1/s. - // covX/covY = covered/needed host pixels; the difference is the line's width. if (TextureCache.DlssSrSpike && targetScale != 1f && Environment.TickCount64 - _coutureLogMs >= 1000) { @@ -705,12 +664,6 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// MV++ velocity pass: the graft binds its own framebuffer DIRECTLY at the GAL level, - /// bypassing this class's host-binding cache -- without this invalidation, the next - /// UpdateRenderTargets() compares against stale 'still bound' entries, skips the - /// re-issue, and the game keeps rendering into the graft's framebuffer (the measured - /// black-screen bug of 2026-07-02). Clearing the cache forces the next commit to - /// re-send the guest bindings to the backend. /// public void MvppInvalidateHostRenderTargets() { diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs index e519b2ef9..0e1ddfac1 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs @@ -598,16 +598,6 @@ namespace Ryujinx.Graphics.Gpu.Image int gobBlocksInZ = descriptor.UnpackGobBlocksInZ(); if (target != Target.Texture3D && gobBlocksInZ > 1 && depthOrLayers > 1) { - // [GOBPROBE, 21/07] This clamp is a FORK addition (it was put in for the BOTW/TOTK - // 4K packs); upstream honours the descriptor. It overrides the block-linear stride - // the game declared, and getting that stride wrong is precisely what produces - // RECTANGLES OF DISPLACED-BUT-REAL CONTENT -- the shape of the Xenoblade 2 artefact - // (blocky rows, worse indoors where more array textures are in flight, appearing as - // the camera turns and brings new ones in). - // - // Read-only for now: the clamp still applies exactly as before, we only COUNT it and - // describe what it hit. If it never fires on XC2 the lead dies here; if it fires - // constantly we have the suspect, and RYUJINX_NO_GOBZ_CLAMP=1 then tests it for real. if (MvppGobProbe.Enabled) { MvppGobProbe.NoteClamp(width, height, depthOrLayers, gobBlocksInZ, target.ToString(), formatInfo.Format.ToString()); diff --git a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs index 05462ada5..0b233815c 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs @@ -41,7 +41,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// End address of the buffer in guest memory. /// public ulong EndAddress => Address + Size; - + public Buffer Next { get; set; } public Buffer Previous { get; set; } @@ -283,14 +283,6 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Start address of the range to synchronize /// Size in bytes of the range to synchronize - // [Beast Roofer diag] RYUJINX_BUFSEQ=1 (EXP 13, gated OFF by default): defeat the - // sequence-number short-circuit of buffer synchronization. Within one sequence, a buffer - // region is checked for CPU writes at most once -- so a game writing constants MID-frame - // (CPU running ahead, standard) has those writes served only NEXT sequence: late-frame - // draws read one-generation-stale data. That is the LAST standing family for the XC2 - // stale-motion artifact (journal 145): patchy per draw, motion-only, barrier-insensitive. - // With the flag on, every query runs with a fresh sequence number (full recheck; slower, - // cannot be wrong). Artifact gone => root found; see the [BUFSEQ] witness counter. private static readonly bool _bufSeqForce = Environment.GetEnvironmentVariable("RYUJINX_BUFSEQ") == "1"; @@ -298,17 +290,6 @@ namespace Ryujinx.Graphics.Gpu.Memory private static long _bufSeqCalls; private static long _bufSeqLogMs; - // [BUFCOAL 02/08, journal (364)-(365)] Coalescence a trou tolere des uploads invite->hote - // (RYUJINX_BUFCOAL=1, OFF par defaut = chemin stock a l'octet pres). Mesure (362)-(363) : - // en rotation BOTW le fil GPU emet ~17 000 SetBufferData par vraie image (miettes de - // 160 o a 4 Ko), et A ~50 000 appels/s la machinerie de la file threadee par commande - // coute autant que les octets. Ce gate fusionne les regions sales d'un MEME buffer - // distantes de moins de RYUJINX_BUFCOAL_GAP octets (defaut 4096, borne 64 Ko) en UN - // LoadRegion, a l'interieur d'UN SEUL SynchronizeMemory (jamais de report inter-appel). - // ⚠️ SURETE : re-uploader des octets PROPRES n'est correct que si l'hote n'a pas de - // donnees que l'invite n'a pas — donc coalescence UNIQUEMENT quand _modifiedRanges est - // null (aucune plage ecrite par le GPU) ; sinon repli immediat sur le chemin stock - // (ExcludeModifiedRegions doit continuer de decouper autour des plages GPU). private static readonly bool _bufCoalEnabled = Environment.GetEnvironmentVariable("RYUJINX_BUFCOAL") == "1"; @@ -323,10 +304,6 @@ namespace Ryujinx.Graphics.Gpu.Memory private ulong _coalEnd; /// - /// Gated replacement for the load delegate: merges dirty regions of this buffer that are - /// within the tolerated gap into one pending window, flushed by - /// 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). /// private void LoadRegionCoalesced(ulong mAddress, ulong mSize) { @@ -365,8 +342,6 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - /// Uploads the pending coalesced window, if any. Must run before leaving the - /// synchronization that produced it (the window never survives across calls). private void FlushCoalesced() { if (_coalStart != ulong.MaxValue) @@ -416,7 +391,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { BackingState.RecordSet(); _context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size)); - MvppPalProbe.OnUpload(Address, Size, site: 0, _physicalMemory); // [PAL/UPVOL] read-only (gated): full guest->host upload + MvppPalProbe.OnUpload(Address, Size, site: 0, _physicalMemory); CopyToDependantVirtualBuffers(); } @@ -446,7 +421,6 @@ namespace Ryujinx.Graphics.Gpu.Memory if (_bufCoalEnabled) { - // [BUFCOAL] la fenetre en attente ne survit jamais a la synchro qui l'a produite. FlushCoalesced(); } } @@ -518,7 +492,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { return true; } - + if (_referenceCount == 0) { return false; @@ -541,7 +515,7 @@ namespace Ryujinx.Graphics.Gpu.Memory return false; } - + void SyncPreRangeAction(ulong address, ulong size) { _preFlush.CopyModified(address, size); @@ -573,7 +547,7 @@ namespace Ryujinx.Graphics.Gpu.Memory return true; } - + void SyncRangeAction(ulong address, ulong size) { _memoryTrackingGranular.RegisterAction(address, size, _externalFlushDelegate); @@ -587,7 +561,7 @@ namespace Ryujinx.Graphics.Gpu.Memory public void InheritModifiedRanges(Buffer from) { from._bufferInherited = true; - + if (from._modifiedRanges is { HasRanges: true }) { if (from._syncActionRegistered && !_syncActionRegistered) @@ -710,7 +684,7 @@ namespace Ryujinx.Graphics.Gpu.Memory _context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize)); - MvppPalProbe.OnUpload(mAddress, mSize, site: 1, _physicalMemory); // [PAL/UPVOL] read-only (gated): dirty-region upload (LoadRegion) + MvppPalProbe.OnUpload(mAddress, mSize, site: 1, _physicalMemory); CopyToDependantVirtualBuffers(mAddress, mSize); } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs index ae8351340..60fd4dea5 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs @@ -523,19 +523,19 @@ namespace Ryujinx.Graphics.Gpu.Memory overlaps = _buffers.FindOverlapsAsSpan(address, size); } - + address = Math.Min(address, overlaps[0].Address); endAddress = Math.Max(endAddress, overlaps[^1].EndAddress); - + for (int i = 0; i < overlaps.Length; i++) { anySparseCompatible |= overlaps[i].SparseCompatible; } Buffer[] overlapsArray = overlaps.ToArray(); - + _buffers.RemoveRange(overlaps[0], overlaps[^1]); - + ulong newSize = endAddress - address; _buffers.Add(CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlapsArray)); @@ -560,7 +560,7 @@ namespace Ryujinx.Graphics.Gpu.Memory private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment) { bool sparseAligned = alignment >= SparseBufferAlignmentSize; - + ReadOnlySpan overlaps = _buffers.FindOverlapsAsSpan(address, size); if (overlaps.Length != 0) @@ -582,7 +582,7 @@ namespace Ryujinx.Graphics.Gpu.Memory endAddress = Math.Max(endAddress, overlaps[^1].EndAddress); int oldOverlapCount; - + do { address = Math.Min(address, overlaps[0].Address); @@ -596,11 +596,11 @@ namespace Ryujinx.Graphics.Gpu.Memory while (oldOverlapCount != overlaps.Length); ulong newSize = endAddress - address; - + Buffer[] overlapsArray = overlaps.ToArray(); - + _buffers.RemoveRange(overlaps[0], overlaps[^1]); - + _buffers.Add(CreateBufferAligned(address, newSize, stage, sparseAligned, overlapsArray)); } } @@ -608,7 +608,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { // No overlap, just create a new buffer. _buffers.Add(new(_context, _physicalMemory, address, size, stage, sparseAligned, [])); - } + } } /// @@ -643,7 +643,7 @@ namespace Ryujinx.Graphics.Gpu.Memory NotifyBuffersModified?.Invoke(); RecreateMultiRangeBuffers(address, size); - + return newBuffer; } @@ -690,7 +690,6 @@ namespace Ryujinx.Graphics.Gpu.Memory MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy); MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size, BufferStage.Copy); - // [PAL] Read-only (gated): does a GPU copy write into the tracked palettes? MvppPalProbe.OnGpuCopy(dstRange.GetSubRange(0).Address, size); if (srcRange.Count == 1 && dstRange.Count == 1) diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs index 4bc5be7b9..0a4ff3b4f 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs @@ -215,7 +215,6 @@ namespace Ryujinx.Graphics.Gpu.Memory { MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStage.TransformFeedback); - // [PAL] Read-only (gated): does transform feedback write into the tracked palettes? MvppPalProbe.OnXfbBind(index, range); _transformFeedbackBuffers[index] = new BufferBounds(range); @@ -265,7 +264,6 @@ namespace Ryujinx.Graphics.Gpu.Memory MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags)); - // [PAL] Read-only (gated): which compute dispatches touch the tracked palettes. MvppPalProbe.OnComputeBind(index, range, (int)flags); _cpStorageBuffers.SetBounds(index, range, flags); @@ -292,7 +290,6 @@ namespace Ryujinx.Graphics.Gpu.Memory MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags)); - // [PAL] Read-only (gated): guest-hash the XC2 velocity palettes (vertex SSBO slots 0/1). MvppPalProbe.OnBind(stage, index, range, _channel.MemoryManager.Physical); if (!buffers.Buffers[index].Range.Equals(range)) diff --git a/src/Ryujinx.Graphics.Gpu/Memory/MvppPalProbe.cs b/src/Ryujinx.Graphics.Gpu/Memory/MvppPalProbe.cs index f20348244..df582e1a6 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/MvppPalProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/MvppPalProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Memory.Range; @@ -9,32 +6,12 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Memory { - /// - /// [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. - /// static class MvppPalProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_PAL_PROBE") == "1"; - private const ulong MinTrackedSize = 0x1000; // v5: >=4KiB -- catch skinned-character palettes too + private const ulong MinTrackedSize = 0x1000; private const int MaxTracked = 32; private const int HashStride = 4093; private const int AnomalyLogCap = 24; @@ -62,10 +39,9 @@ namespace Ryujinx.Graphics.Gpu.Memory private static long _anomaliesLogged; private static long _summaryMs; - /// Vertex-stage storage buffer bound (BufferManager.SetGraphicsStorageBuffer). Self-gated. public static void OnBind(int stage, int index, MultiRange range, PhysicalMemory pm) { - if (!Enabled || stage != 0 || index > 5 || pm == null) // v5: slots 0..5 + if (!Enabled || stage != 0 || index > 5 || pm == null) { return; } @@ -100,7 +76,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (rec.SeenThisFrame) { - return; // one hash per buffer per frame + return; } rec.SeenThisFrame = true; @@ -120,13 +96,8 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - // v3: identify the PRODUCER. v2 measured that no tracked palette is ever CPU-written - // (guest hash frozen over 3600 frames with NPCs moving) => they are GPU-written. This - // hook logs which COMPUTE dispatches bind a tracked range as storage, with the usage - // flags -- write-usage binds name the producer pass. private static readonly HashSet<(ulong, int, int)> _computeBinds = new(); - /// Compute storage buffer bound (BufferManager.SetComputeStorageBuffer). Self-gated. public static void OnComputeBind(int index, MultiRange range, int flags) { if (!Enabled) @@ -153,13 +124,9 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - // v4: v3 measured ZERO compute binds on the tracked palettes -- the producer is not a - // compute SSBO write. Remaining GPU write paths: TRANSFORM FEEDBACK (vertex-shader - // skinning into buffers, classic for this engine generation) and DMA buffer copies. private static readonly HashSet<(ulong, int)> _xfbBinds = new(); private static readonly HashSet<(ulong, ulong)> _copyHits = new(); - /// Transform feedback buffer bound (BufferManager.SetTransformFeedbackBuffer). Self-gated. public static void OnXfbBind(int index, MultiRange range) { if (!Enabled) @@ -186,7 +153,6 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - /// GPU buffer copy (BufferCache.CopyBuffer). Self-gated; physical addresses. public static void OnGpuCopy(ulong dstAddress, ulong size) { if (!Enabled) @@ -211,13 +177,8 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - /// 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). public static void OnUpload(ulong address, ulong size, int site = 0, PhysicalMemory pm = null) { - // [UPVOL] compteur de volume independant (gate RYUJINX_UPVOL, inerte sinon) -- - // partage ces sites d'appel pour ne pas toucher une 2e fois au chemin chaud. MvppUpVolProbe.OnUpload(address, size, site, pm); if (!Enabled) @@ -239,7 +200,6 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - /// Guest frame boundary (Window present). Folds per-frame flags into the verdict counters. public static void OnPresent() { if (!Enabled) diff --git a/src/Ryujinx.Graphics.Gpu/Memory/MvppUpVolProbe.cs b/src/Ryujinx.Graphics.Gpu/Memory/MvppUpVolProbe.cs index 650d1010a..170bc1f7e 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/MvppUpVolProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/MvppUpVolProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,18 +5,6 @@ using System.Threading; namespace Ryujinx.Graphics.Gpu.Memory { - /// - /// [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. - /// static class MvppUpVolProbe { public static readonly bool Enabled = @@ -34,15 +19,9 @@ namespace Ryujinx.Graphics.Gpu.Memory private static long _site0Bytes; private static long _site1Calls; private static long _site1Bytes; - // Buckets: <4K, 4K-64K, 64K-1M, >=1M (nombre, octets) private static readonly long[] _bucketCalls = new long[4]; private static readonly long[] _bucketBytes = new long[4]; - // [MIRBENCH v3] micro-banc de vitesse de lecture du MIROIR invite (l'hypothese (366) : - // la copie productrice est ~35x plus lente PAR OCTET que la meme copie depuis le pool). - // A chaque fenetre : retenir le PLUS GROS upload ; au flush, relire CE range depuis le - // miroir (pm.GetSpan -> copie vers un scratch) et copier la MEME taille depuis un tableau - // gere -> deux debits Mo/s compares, meme taille, meme scratch. Lecture seule. private static ulong _benchAddr; private static long _benchSize; private static PhysicalMemory _benchPm; @@ -129,8 +108,6 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - /// [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. private static void RunMirrorBench() { long benchSize = Interlocked.Exchange(ref _benchSize, 0); @@ -139,7 +116,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (pm == null || benchSize < 0x4000) { - return; // rien d'assez gros dans la fenetre pour un chrono fiable + return; } int sz = (int)Math.Min(benchSize, 0x40000); @@ -152,10 +129,6 @@ namespace Ryujinx.Graphics.Gpu.Memory try { - // Lecture MIROIR, DEUX passes chronometrees : la 1re est FROIDE (TLB/defauts de - // premiere lecture = exactement le cout que subit la copie de production, qui ne - // lit chaque region qu'une fois), la 2e est chaude (reference haute). Le scratch - // est reutilise entre fenetres => sa premiere-touche ne pollue que la fenetre 1. ReadOnlySpan src = pm.GetSpan(benchAddr, sz); long t0 = System.Diagnostics.Stopwatch.GetTimestamp(); src.CopyTo(_benchScratch); @@ -163,7 +136,6 @@ namespace Ryujinx.Graphics.Gpu.Memory src.CopyTo(_benchScratch); long t2 = System.Diagnostics.Stopwatch.GetTimestamp(); - // Meme taille, source = tableau gere ordinaire (chaud) _benchManaged.AsSpan(0, sz).CopyTo(_benchScratch); long t3 = System.Diagnostics.Stopwatch.GetTimestamp(); _benchManaged.AsSpan(0, sz).CopyTo(_benchScratch); diff --git a/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs b/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs index c6a3d9186..912699830 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/SupportBufferUpdater.cs @@ -23,12 +23,6 @@ namespace Ryujinx.Graphics.Gpu.Memory _data.RenderScale.AsSpan().Fill(defaultScale); DirtyRenderScale(0, SupportBuffer.RenderScaleMaxCount); - // [JITTERINIT 02/08, journal (380)] Le bloc jitter est emis dans CHAQUE vertex shader - // et LIT JitterOffset a chaque image — mais le champ n'etait marque dirty que quand sa - // valeur CHANGEAIT : jitter jamais arme ⇒ zone jamais televersee ⇒ le shader lisait de - // la memoire GPU non initialisee (suspect n°1 du dossier ecran-vert, 4 jeux). Marquer - // le zero initial dirty garantit un (0,0) reellement present sur le GPU des la - // premiere image, exactement comme RenderScale ci-dessus. _data.JitterOffset.X = 0f; _data.JitterOffset.Y = 0f; MarkDirty(SupportBuffer.JitterOffsetOffset, SupportBuffer.FieldSize); @@ -99,11 +93,7 @@ namespace Ryujinx.Graphics.Gpu.Memory } /// - /// Sets the DLSS Mode B clip-space jitter offset (in NDC) the shader adds to vertex positions - /// (scaled by w). Zero unless jitter is enabled, so the default path is unaffected. /// - /// Jitter X in normalized device coordinates - /// Jitter Y in normalized device coordinates public void SetJitter(float x, float y) { if (_data.JitterOffset.X != x || _data.JitterOffset.Y != y) @@ -133,7 +123,7 @@ namespace Ryujinx.Graphics.Gpu.Memory public void UpdateRenderScale(int index, float scale) { Span> renderScaleSpan = _data.RenderScale.AsSpan(); - + if (renderScaleSpan[1 + index].X != scale) { renderScaleSpan[1 + index].X = scale; @@ -164,7 +154,7 @@ namespace Ryujinx.Graphics.Gpu.Memory public void SetRenderTargetIsBgra(int index, bool isBgra) { Span> fragmentIsBgraSpan = _data.FragmentIsBgra.AsSpan(); - + bool isBgraChanged = fragmentIsBgraSpan[index].X != 0 != isBgra; if (isBgraChanged) diff --git a/src/Ryujinx.Graphics.Gpu/MvppDestProbe.cs b/src/Ryujinx.Graphics.Gpu/MvppDestProbe.cs index f96598a5a..d57932d33 100644 --- a/src/Ryujinx.Graphics.Gpu/MvppDestProbe.cs +++ b/src/Ryujinx.Graphics.Gpu/MvppDestProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -9,24 +6,13 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu { - /// - /// MV++ destination-anchored framebuffer-writer probe (RYUJINX_MVPP_DESTPROBE=1). READ-ONLY, default - /// off, nothing in the render path branches on it. Instead of eliminating engines one by one (each - /// un-hooked engine = a blind spot), it anchors on the OUTPUT: it records the GPU address of the - /// surface handed to Present, then logs every texture-modification event that hits that address. The - /// two modification choke points every writer passes through are Texture.SignalModifying (render-target - /// path = Maxwell3D) and Texture.SignalModified (explicit copy/store path = DMA / Fermi2D / compute - /// image store / inline-to-memory). The Modifying-vs-Modified bit alone classifies the final writer as - /// a 3D draw vs an explicit copy, with no per-engine or array/bindless blind spot. Per frame the ordered - /// write chain shows the composite first and the HUD writes after. - /// static class MvppDestProbe { private static bool _enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DESTPROBE") == "1"; - private const int MaxWritesLogged = 400; // hard cap on DEST-WRITE lines - private const int StackSampleWrites = 60; // capture a caller stack only for the first N matches (cost control) + private const int MaxWritesLogged = 400; + private const int StackSampleWrites = 60; private static readonly HashSet _presentedAddrs = new(); private static int _frame; @@ -34,18 +20,14 @@ namespace Ryujinx.Graphics.Gpu private static int _writesLogged; private static int _stackSamples; - // [chain trace] extra intermediate addresses to trace ALL writers of (compute/DMA/2D/3D), fed by - // the final-draw probe when it finds an HDR intermediate whose producer isn't a sampled-texture draw. private static readonly HashSet _trackedAddrs = new(); private static int _trackedLogged; private const int MaxTrackedLogged = 200; public static bool Enabled => _enabled; - /// [read-only] True if the address is a known presented-framebuffer backbuffer. public static bool IsPresentedAddress(ulong addr) => _presentedAddrs.Count != 0 && _presentedAddrs.Contains(addr); - /// [chain trace] Add an intermediate buffer address to trace every writer of. public static void AddTracked(ulong addr) { if (_enabled && addr != 0 && _trackedAddrs.Count < 32) @@ -54,10 +36,8 @@ namespace Ryujinx.Graphics.Gpu } } - /// [read-only] Monotonic present frame counter (for the final-draw probe's frame boundary). public static int Frame => _frame; - /// Called at Present with the resolved presented surface; records its address and starts a frame. public static void RegisterPresented(Image.Texture tex) { if (!_enabled || tex == null) @@ -88,7 +68,6 @@ namespace Ryujinx.Graphics.Gpu } } - /// Called from Texture.SignalModified (viaModifying=false) and SignalModifying (viaModifying=true). public static void OnModify(Image.Texture tex, bool viaModifying, bool bound) { if (!_enabled || tex == null || (_presentedAddrs.Count == 0 && _trackedAddrs.Count == 0)) @@ -96,7 +75,6 @@ namespace Ryujinx.Graphics.Gpu return; } - // SignalModifying(false) is an UNBIND, not a write -- ignore it. if (viaModifying && !bound) { return; @@ -112,8 +90,6 @@ namespace Ryujinx.Graphics.Gpu return; } - // Intermediate (tracked) writers get their own budget; this is the key question -- who - // writes the HDR intermediate, and via which engine (Modifying=3D RT vs Modified=copy/store). if (isTracked && !isPres) { if (_trackedLogged >= MaxTrackedLogged) @@ -159,9 +135,6 @@ namespace Ryujinx.Graphics.Gpu } } - // Best-effort engine identification: on a rare match, walk the stack past the Texture/Group frames to - // the first engine method (e.g. DmaClass.*, TwodClass.*, TextureBindingsManager.*, TextureManager.*). - // May collapse under Release inlining; the Modifying/Modified bit above is the robust discriminator. private static string Caller() { try diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index d74c9eb66..ad13ffd9a 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,10 +22,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - // [JITTEREMIT 03/08] Le bloc de jitter clip-space n'est plus emis dans les vertex shaders - // quand le jitter est eteint (il lisait Position en sortie avant de l'ecrire = indefini). - // La TRADUCTION change => les caches de shaders existants doivent etre invalides, sinon - // les utilisateurs continuent d'executer l'ancien code et le correctif ne les atteint pas. private const uint CodeGenVersion = 7355; private const string SharedTocFileName = "shared.toc"; @@ -326,11 +322,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache throw new DiskCacheLoadException(DiskCacheLoadResult.IncompatibleVersion); } - // MV++ shader-codegen probes: any flag that changes or instruments the generated - // host code must take the guest-recompile path (the same one every codegen version - // bump takes). Loading would serve shaders built with the OTHER setting when the - // flag was toggled between launches -- the stale-instrumentation trap. The write - // side is guarded in AddShader for the same reason. bool loadHostCache = header.CodeGenVersion == CodeGenVersion && !_mvppCodegenProbeActive; int programIndex = 0; @@ -566,9 +557,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// Cached program /// Optional host binary code /// Output streams to use - // MV++ shader-codegen probes that alter or instrument the generated host code. While any - // of them is active the shared host cache must be neither loaded (stale code from the - // other setting) nor written (instrumented code would poison the next probe-less launch). private static readonly bool _mvppCodegenProbeActive = Ryujinx.Graphics.Shader.Translation.MvppPositionProbe.Enabled || Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.Enabled || @@ -596,7 +584,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache public void AddShader(GpuContext context, CachedShaderProgram program, ReadOnlySpan hostCode, DiskCacheOutputStreams streams = null) { - // Probe builds never persist: their generated code only matches while the flag is on. if (_mvppCodegenProbeActive) { return; @@ -849,9 +836,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache dataInfo.UsesInstanceId, dataInfo.UsesDrawParameters, dataInfo.UsesRtLayer, - // [MVPP CUTOUT PROBE] UsesDiscard is NOT serialized (kept out of the cache format on purpose: - // no version bump, no forced recompile). Disk-cache-loaded shaders read false here; the probe - // is meant to run with the shader cache disabled so every shader is freshly translated. false, dataInfo.ClipDistancesWritten, dataInfo.FragmentOutputMap); diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index 6df4e3c3e..74b71df4c 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs @@ -59,20 +59,12 @@ namespace Ryujinx.Graphics.Gpu.Shader private readonly Dictionary _cpPrograms; private readonly Dictionary _gpPrograms; - // MV++ double-transform (b1): separate cache for the MvppVariant program (same guest - // shaders re-translated with TranslationFlags.MvppVariant). Kept apart from _gpPrograms - // and NOT persisted to the host disk cache, so the base program is never confused with - // the variant (invariant: the flag rides the translation, not just the storage). private readonly Dictionary _gpVariantPrograms; - // (b1) 5B3 diag: variant SPIR-V dump dir (RYUJINX_MVPP_VEL_DUMPSPV=), write-only. private static readonly string _mvppDumpDir = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DUMPSPV"); private static int _mvppDumpCount; - // [DUMPMAP] Read-only bridge: when RYUJINX_DUMP_MAP=1, log each dumped shader's guest - // address <-> ShaderDumper file paths, so a guest VA (e.g. fs=0x100082C30) maps to its - // ShaderNNNN.bin. Gated OFF by default; does not change dumping or translation. private static readonly bool _dumpMap = Environment.GetEnvironmentVariable("RYUJINX_DUMP_MAP") == "1"; @@ -331,9 +323,6 @@ namespace Ryujinx.Graphics.Gpu.Shader ShaderAddresses addresses, TranslationFlags extraFlags = TranslationFlags.None) { - // MV++ (b1): the MvppVariant program lives in its own cache and skips the host disk - // cache (which is keyed on guest code, not the flag) so the base is never returned - // for a variant request. Everything else is the identical translation path. bool isVariant = extraFlags != TranslationFlags.None; Dictionary gpCache = isVariant ? _gpVariantPrograms : _gpPrograms; @@ -342,9 +331,6 @@ namespace Ryujinx.Graphics.Gpu.Shader return gpShaders; } - // TryFind reads the guest bytecode into cachedGuestCode on hit AND miss (that's why - // a cold base translates below). A base HIT returns the disk-cached program; a - // VARIANT ignores the hit and always re-translates with MvppVariant flag. bool diskHit = _graphicsShaderCache.TryFind(channel, ref poolState, ref graphicsState, addresses, out gpShaders, out CachedGraphicsGuestCode cachedGuestCode); if (diskHit && !isVariant) @@ -497,9 +483,6 @@ namespace Ryujinx.Graphics.Gpu.Shader if (isVariant) { - // (b1) 5B3 diag (RYUJINX_MVPP_VEL_DUMPSPV=): dump the variant host SPIR-V - // at generation -- every survival "proof" so far lived at INJECTION time (IR); - // nobody ever read the COMPILED module. Write-only diagnostic, capped. if (_mvppDumpDir != null && _mvppDumpCount < 2048) { try @@ -521,8 +504,6 @@ namespace Ryujinx.Graphics.Gpu.Shader } } - // The MvppVariant lives ONLY in its runtime cache -- never added to the host - // hash table nor persisted to disk (both are keyed on guest code, not the flag). _gpVariantPrograms[addresses] = gpShaders; Logger.Info?.Print(LogClass.Gpu, @@ -653,7 +634,7 @@ namespace Ryujinx.Graphics.Gpu.Shader Span tfStateSpan = state.TfState.AsSpan(); Span> tfVaryingLocationsSpan = state.TfVaryingLocations.AsSpan(); - + for (int i = 0; i < Constants.TotalTransformFeedbackBuffers; i++) { TfState tf = tfStateSpan[i]; @@ -854,8 +835,6 @@ namespace Ryujinx.Graphics.Gpu.Shader ShaderDumpPaths paths = dumper?.Dump(code, context.Stage == ShaderStage.Compute) ?? default; - // [DUMPMAP] Read-only VA<->file bridge (gated RYUJINX_DUMP_MAP=1). Only fires when a dump - // actually happened (paths.HasPath). Does not alter dumping or translation. if (_dumpMap && paths.HasPath) { Logger.Info?.Print(LogClass.Gpu, diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs index 5202998c1..2b3dfd4c4 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs @@ -150,11 +150,6 @@ namespace Ryujinx.Graphics.Gpu.Shader AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, textureSetIndex, textureBinding, texturesPerStage); AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, imageSetIndex, imageBinding, imagesPerStage); - // MV++ (b1) sub-step 5a fix @91: the MVPP variant VS declares its N-1 palette cbuf at - // the fixed binding 91 -- OUTSIDE the per-stage windows above, so no window covers it. - // Add the one descriptor when (and only when) this stage's program carries it; the - // usage is added by the AddUsage(info.CBuffers) call below like any other cbuf. Base - // programs never carry binding 91 -> their layout is byte-identical to stock. foreach (BufferDescriptor cb in info.CBuffers) { if (cb.Binding == Ryujinx.Graphics.Shader.Translation.MvppVariantInjector.MvppN1HostBinding) @@ -164,12 +159,6 @@ namespace Ryujinx.Graphics.Gpu.Shader } } - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] The survivor-map SSBO lives at the fixed - // out-of-window binding 80 (declared by every fragment shader when the probe is ON, see - // TranslatorContext.CreateResourceManager). Reserved-style buffers are NOT part of - // info.SBuffers, so add its descriptor + write usage here explicitly, Fragment stage only - // -- same special-case pattern as the N-1 cbuf above. Default OFF => layout is - // byte-identical to stock. if (Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled && info.Stage == ShaderStage.Fragment) { diff --git a/src/Ryujinx.Graphics.Gpu/Window.cs b/src/Ryujinx.Graphics.Gpu/Window.cs index 26214870e..a9bb05900 100644 --- a/src/Ryujinx.Graphics.Gpu/Window.cs +++ b/src/Ryujinx.Graphics.Gpu/Window.cs @@ -58,19 +58,13 @@ namespace Ryujinx.Graphics.Gpu public object UserObj { get; } /// - /// DLSS Mode B sub-pixel jitter offset this frame was rendered with, snapshotted at enqueue and - /// carried through the queue so the present can hand the matching offset to DLSS. /// public float JitterX { get; } public float JitterY { get; } - /// [JITTERVAL probe] The DlssJitterState.FrameId this frame was tagged with at enqueue, - /// carried through the queue so the DLSS-side probe logs the same id the geometry-side probe did. public long JitterFrameId { get; } /// - /// MV++ camera view-projection this frame was rendered with, snapshotted at enqueue - /// like the jitter so the present hands DLSS the matrix matching the frame on screen. /// public System.Numerics.Matrix4x4 CameraVp { get; } public bool CameraVpValid { get; } @@ -119,12 +113,12 @@ namespace Ryujinx.Graphics.Gpu private int _framesAvailable; private long _mvppDepthLogMs; private long _mvppFifoLogMs; - private long _jitTraceMs; // [JITTRACE] cadence de l'etape B + private long _jitTraceMs; private int _mvppTakes; private int _mvppTakeHits; private Image.Texture _mvppHeldSceneDepth; private int _mvppHeldSceneDepthSeq; - private long _colorPubDiagMs; // [COLORCAP snapshot] throttled publish diagnostic + private long _colorPubDiagMs; public bool IsFrameAvailable => _framesAvailable != 0; @@ -213,16 +207,10 @@ namespace Ryujinx.Graphics.Gpu MultiRange range = new(address, (ulong)size); - // DLSS Mode B: snapshot the jitter offset this frame was rendered with (the value StateUpdater - // last applied to the scaled viewport) so it rides through the queue with the frame and reaches - // DLSS at present, instead of being looked up by a texture reference that does not match. float jitterX = DlssJitterState.Enabled ? DlssJitterState.OffsetX : 0f; float jitterY = DlssJitterState.Enabled ? DlssJitterState.OffsetY : 0f; - // [JITTERVAL probe] tag this frame with the current jitter frame id, carried to the present. long jitterFrameId = DlssJitterState.Enabled ? DlssJitterState.FrameId : 0L; - // MV++: snapshot the camera view-projection this frame was rendered with, and re-arm - // the once-per-frame capture for the next one. System.Numerics.Matrix4x4 cameraVp = default; bool cameraVpValid = DlssCameraState.Enabled && DlssCameraState.SnapshotCurrent(out cameraVp); if (DlssCameraState.Enabled) @@ -230,10 +218,8 @@ namespace Ryujinx.Graphics.Gpu Engine.Threed.MvppCameraCapture.OnFrameEnqueued(); } - // MV++ Phase 2.0: frame boundary for the draw-identity matching probe (no-op flag off). Engine.Threed.MvppP2Probe.OnFrameEnqueued(); - // MV++ Phase 2.2: frame boundary for the velocity pass (coverage clear). Engine.Threed.MvppVelocityPass.OnFrameEnqueued(); _frameQueue.Enqueue(new PresentationTexture( @@ -270,55 +256,38 @@ namespace Ryujinx.Graphics.Gpu pt.Cache.Tick(); - // [PRESYNC, 21/07] LE test décisif du dossier XC2. Tous les étages de rendu sont propres au - // dernier draw, l'image affichée est détruite, et rien entre les deux ne montrait d'anomalie - // -- SAUF cette resynchronisation, jamais instrumentée. Si le render target composé sur GPU - // est aussi suivi comme mémoire invitée et marqué sale, SynchronizeMemory recharge la mémoire - // invitée (périmée, car composée côté GPU) PAR-DESSUS le rendu propre. On capture avant/après. Engine.Threed.MvppPreSyncProbe.Before(texture); texture.SynchronizeMemory(); Engine.Threed.MvppPreSyncProbe.After(texture); - // [LAYOUT, 21/07] La texture présentée arrive DÉJÀ corrompue (mosaïque en blocs = détuilage). - // On compare ce que le JEU a demandé (pt.Info, issu de EnqueueFrameThreadSafe) à ce que le - // cache a réellement fourni (texture.Info). Un désaccord de tuilage (isLinear / gobBlocksInY / - // stride) est le suspect exact. Engine.Threed.MvppLayoutProbe.Compare(pt.Info, texture); Engine.Threed.MvppPreSyncProbe.DumpGuest(texture); - MvppDestProbe.RegisterPresented(texture); // read-only destination-anchored probe (gated) + MvppDestProbe.RegisterPresented(texture); - // [RTDUMP, 21/07] The frame as it actually reaches the screen. Everything captured - // so far was an INTERMEDIATE buffer, so "this one looks odd" could never be tied to - // "that block, right there, is what I see". Capturing the presented image in the - // same burst as the intermediates turns the whole investigation around: point at - // the defect, then find which buffer already carries it. Engine.Threed.MvppRtDumpProbe.NotePresented(texture); - // Vraie frontière d'image pour la sonde UI : sa détection d'origine (montée du facteur de - // résolution) est muette sur tout jeu rendu à l'échelle native, XC2 compris. Engine.Threed.MvppUiProbe.OnPresent(); Engine.Threed.MvppDrawStepProbe.OnPresent(texture); Engine.Twod.MvppTwodProbe.OnPresent(); Image.MvppCacheProbe.OnPresent(); - Image.MvppMvBufProbe.OnPresent(); // [MVBUF] frame boundary (self-gated) - Image.MvppMvSyncProbe.OnPresent(); // [MVSYNC] arming witness + heartbeat (self-gated) - Image.MvppWriterCensusProbe.OnPresent(); // [CENSUS] arming witness + heartbeat (self-gated) - Image.MvppTwinXferProbe.OnPresent(); // [TWINXFER] arming witness + heartbeat (self-gated) - Memory.MvppPalProbe.OnPresent(); // [PAL] frame boundary (self-gated) - Image.MvppTaaProbe.OnPresent(); // [TAAPROBE] frontiere d'image (auto-gardee) - // [JITTRACE] cadence de l'etape B, voir plus bas. + Image.MvppMvBufProbe.OnPresent(); + Image.MvppMvSyncProbe.OnPresent(); + Image.MvppWriterCensusProbe.OnPresent(); + Image.MvppTwinXferProbe.OnPresent(); + Memory.MvppPalProbe.OnPresent(); + Image.MvppTaaProbe.OnPresent(); if (Image.MvppFeedbackProbe.Enabled) { - Image.MvppFeedbackProbe.OnFrameBoundary(); // [FEEDBACKPROBE] guest frame boundary (gated) + Image.MvppFeedbackProbe.OnFrameBoundary(); } if (Image.MvppTraceProbe.Enabled) { - Image.MvppTraceProbe.OnFrameBoundary(); // [TRACEPROBE] dependency-graph report (gated) + Image.MvppTraceProbe.OnFrameBoundary(); } float cropScaleX = texture.EffectiveScaleX; @@ -355,76 +324,32 @@ namespace Ryujinx.Graphics.Gpu crop = new ImageCrop(left, right, top, bottom, crop.FlipX, crop.FlipY, crop.IsStretched, crop.AspectRatioX, crop.AspectRatioY); } - // DLSS Mode B: publish the jitter offset THIS exact frame was rendered with (carried through - // the queue), so the DLSS backend applies the matching sub-pixel offset. This is the carrier - // that replaces the old texture-reference lookup, which missed because the presented texture - // is not the jittered render target. DlssJitterState.PresentX = pt.JitterX; DlssJitterState.PresentY = pt.JitterY; DlssJitterState.PresentFrameId = pt.JitterFrameId; - // MV++: republish the camera matrix for this exact frame. Ordered mode consumes - // the oldest unconsumed camera the GPU thread saw (order does the pairing -- - // immune to the enqueue-snapshot race); snapshot mode keeps the value that rode - // the queue with the frame. if (DlssCameraState.FifoEnabled) { - // [GAMEJITTER 28/07] Le decalage sous-pixel du jeu sort de la file AVEC sa - // matrice et entre dans l'anneau de presentation AVEC elle : apparie a la meme - // image d'un bout a l'autre, sans champ statique qui pourrait deriver d'une - // image. Sur un cycle de 8 phases, declarer la mauvaise phase serait aussi faux - // que declarer zero. bool orderedValid = DlssCameraState.TryConsumeOrdered(out System.Numerics.Matrix4x4 orderedVp); - // [GAMEJITTER 28/07, v2] Le decalage ne passe PLUS par la file ordonnee -- y - // ajouter deux flottants la faisait rendre du vide (voir le commentaire de - // _fifo). Il entre ici, dans l'anneau de presentation, qui est une file - // concurrente donc sure. La valeur est celle de la derniere lecture ACCEPTEE : - // elle a ete prise pendant les dessins de cette image, c'est donc la bonne - // phase pour l'image qu'on presente. float orderedJx = Engine.Threed.MvppSoloCamera.LastJitterX; float orderedJy = Engine.Threed.MvppSoloCamera.LastJitterY; - // ⛔ [28/07 17h35] APPEL A 4 PARAMETRES NEUTRALISE — DETTE DE GAMEJITTER. - // La surcharge (vp, valid, jx, jy) a ete ajoutee a DlssCameraState le 28/07 a - // 11:46, mais le GAL.dll de l'installation date de 10:07 et ne la contient pas. - // Tant que le projet Gpu n'est pas recompile, personne ne s'en apercoit ; des - // qu'il l'est, l'appel part dans le vide : - // MissingMethodException: Method not found: 'Void - // DlssCameraState.PublishPresent(Matrix4x4 ByRef, Boolean, Single, Single)' - // (plantage constate 28/07 17h30 en deployant MULTIROT ; binaire restaure). - // - // Reconstruire le GAL reglerait la signature mais REINJECTERAIT GAMEJITTER et - // 18 jours de changements dans l'etat valide par Alex -- c'est exactement la - // casse du matin. Or GAMEJITTER est MORT : mesure du 28/07, 8 valeurs cote - // camera et `brut (0,000;0,000)` cote DLSS, le canal ne transporte rien. On - // perd donc zero fonctionnalite en appelant la surcharge a 2 parametres. - // - // A RETABLIR le jour ou le GAL sera reconstruit volontairement (une seule - // ligne, les deux valeurs sont deja calculees juste au-dessus). _ = orderedJx; _ = orderedJy; DlssCameraState.PublishPresent(in orderedVp, orderedValid); - // [FAMHOLD 01/08] Sonde R1/R2 du mecanisme (290), lecture pure : le hold de - // la file ordonnee (LastConsumeFresh == false) EST l'evenement mesure, et il - // vient d'etre etabli trois lignes plus haut. Voir MvppFamHold. if (Engine.Threed.MvppFamHold.Enabled) { Engine.Threed.MvppFamHold.OnPresent(DlssCameraState.LastConsumeFresh, in orderedVp); } - // [EXTPROBE 01/08] Phase T du design E : prediction en memoire + comparaison - // au reel suivant, lecture pure. Voir MvppExtProbe. if (Engine.Threed.MvppExtProbe.Enabled) { Engine.Threed.MvppExtProbe.OnPresent( DlssCameraState.LastConsumeFresh, in orderedVp, DlssCameraState.TeleportSeq); } - // [JITTRACE 28/07] ETAPE B : ce qui SORT de la file et entre dans l'anneau. - // Si A est non nul et B vaut zero, la perte est dans la file ; si B est non nul - // et C vaut zero, elle est dans l'anneau ou apres. if (Engine.Threed.MvppCameraCapture.JitTrace && Environment.TickCount64 - _jitTraceMs >= 1000) { @@ -452,19 +377,11 @@ namespace Ryujinx.Graphics.Gpu DlssCameraState.PublishPresent(pt.CameraVp, pt.CameraVpValid); } - // MV++: prefer the depth pinned at the scaled main pass (deterministic) over the - // "last depth bound by any pass" publication (a timing roulette that flapped - // between scene depth, shadow maps and bloom buffers -- the mode 2 tremble). Image.Texture presentDepth = null; if (DlssCameraState.Enabled) { Image.Texture scene = Engine.Threed.MvppCameraCapture.TakeSceneDepth(); - // ~1-2% of presents have no scene draw in their interval (double-present, - // pacing hiccup). Falling back to the last-bind roulette there re-injects - // shadow maps / bloom buffers 1-2x per second -- enough to keep the MV - // source flapping. Hold the last pinned scene depth instead; the texture - // object persists across frames and holds the latest rendered content. if (scene != null) { _mvppHeldSceneDepth = scene; @@ -472,9 +389,6 @@ namespace Ryujinx.Graphics.Gpu } else if (_mvppHeldSceneDepth != null && _mvppHeldSceneDepth.InvalidatedSequence != _mvppHeldSceneDepthSeq) { - // Drop the stale hold. HostTexture is never nulled on dispose (stock keeps the - // dead reference), so testing it can NOT detect destruction; InvalidatedSequence - // is the stock validity idiom and bumps on dispose, unmap AND host replacement. _mvppHeldSceneDepth = null; } @@ -489,7 +403,6 @@ namespace Ryujinx.Graphics.Gpu _mvppTakeHits++; } - // MV++ diag: what depth is published at present time? (heartbeat, capture runs only) if (MvppDev.Enabled && DlssCameraState.Enabled && Environment.TickCount64 - _mvppDepthLogMs >= 5000) { _mvppDepthLogMs = Environment.TickCount64; @@ -506,9 +419,6 @@ namespace Ryujinx.Graphics.Gpu Engine.Threed.MvppCameraCapture.StatSquareRejects = 0; } - // [COLORCAP snapshot] publish this frame's pinned HDR scene color host to the cross-layer - // bridge so the Vulkan present can snapshot it (parallel to the depth held path). Gap - // frames (no scene color pinned) keep the last good value. Gated; nothing wired to DLSS. if (GAL.MvppColorSnapshot.Enabled) { Image.Texture sceneColor = Engine.Threed.MvppCameraCapture.TakeSceneColor(); diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs index 5c9e3989b..65a7c82dc 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs @@ -116,6 +116,44 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg } } + private static readonly bool _vidProbe = + Environment.GetEnvironmentVariable("RYUJINX_VIDPROBE") == "1"; + private static long _vpWindowMs; + private static int _vpSubmitted, _vpDelivered, _vpDelayed, _vpFailed, _vpLastErr; + private static long _vpTotalSubmitted, _vpTotalDelivered; + + private static void VidProbeTick(int submitted, int delivered, int delayed, int failed, int lastErr) + { + _vpSubmitted += submitted; + _vpDelivered += delivered; + _vpDelayed += delayed; + _vpFailed += failed; + _vpTotalSubmitted += submitted; + _vpTotalDelivered += delivered; + + if (lastErr != 0) + { + _vpLastErr = lastErr; + } + + long now = Environment.TickCount64; + + if (now - _vpWindowMs < 1000) + { + return; + } + + if (_vpWindowMs != 0) + { + Logger.Info?.Print(LogClass.FFmpeg, + $"VIDPROBE 1s : soumis={_vpSubmitted} rendus={_vpDelivered} retardes={_vpDelayed} " + + $"echecs={_vpFailed} dernier-code={_vpLastErr} | cumul soumis={_vpTotalSubmitted} rendus={_vpTotalDelivered}"); + } + + _vpWindowMs = now; + _vpSubmitted = _vpDelivered = _vpDelayed = _vpFailed = 0; + } + public int DecodeFrame(Surface output, ReadOnlySpan bitstream) { FFmpegApi.av_frame_unref(output.Frame); @@ -130,8 +168,11 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg result = _decodeFrame(_context, output.Frame, &gotFrame, _packet); } + bool delayedTried = false; + if (gotFrame == 0) { + delayedTried = true; FFmpegApi.av_frame_unref(output.Frame); // If the frame was not delivered, it was probably delayed. @@ -151,9 +192,19 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg { FFmpegApi.av_frame_unref(output.Frame); + if (_vidProbe) + { + VidProbeTick(1, 0, delayedTried ? 1 : 0, 1, result); + } + return -1; } + if (_vidProbe) + { + VidProbeTick(1, 1, delayedTried ? 1 : 0, result < 0 ? 1 : 0, result < 0 ? result : 0); + } + return result < 0 ? result : 0; } diff --git a/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs b/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs index 7fb2a02e1..0319b0fae 100644 --- a/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs +++ b/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs @@ -35,7 +35,6 @@ 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; diff --git a/src/Ryujinx.Graphics.OpenGL/MvppGlDumpProbe.cs b/src/Ryujinx.Graphics.OpenGL/MvppGlDumpProbe.cs index ad4cd4e0e..020dfc77a 100644 --- a/src/Ryujinx.Graphics.OpenGL/MvppGlDumpProbe.cs +++ b/src/Ryujinx.Graphics.OpenGL/MvppGlDumpProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -11,14 +8,6 @@ using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.OpenGL { - /// - /// [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. - /// static class MvppGlDumpProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index c2f68d190..fba99062d 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -924,7 +924,7 @@ namespace Ryujinx.Graphics.OpenGL GL.Disable(EnableCap.CullFace); return; } - + GL.CullFace((TriangleFace) face.Convert()); GL.Enable(EnableCap.CullFace); @@ -1168,7 +1168,7 @@ namespace Ryujinx.Graphics.OpenGL public void SetRenderTargets(Span colors, ITexture depthStencil) { - MvppGlDumpProbe.OnRenderTargetsChange(_framebuffer); // [GLDUMP] read-only, self-gated + MvppGlDumpProbe.OnRenderTargetsChange(_framebuffer); EnsureFramebuffer(); for (int index = 0; index < colors.Length; index++) diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index c597e2d30..ff1de363d 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -40,7 +40,6 @@ namespace Ryujinx.Graphics.OpenGL public void Present(ITexture texture, ITexture depthTexture, ImageCrop crop, Action swapBuffersCallback) { - // depthTexture is unused by the OpenGL backend (DLSS is Vulkan-only). GL.Disable(EnableCap.FramebufferSrgb); (int oldDrawFramebufferHandle, int oldReadFramebufferHandle) = ((Pipeline)_renderer.Pipeline).GetBoundFramebuffers(); @@ -324,7 +323,7 @@ namespace Ryujinx.Graphics.OpenGL _antiAliasing = new FxaaPostProcessingEffect(_renderer); break; case AntiAliasing.None: - case AntiAliasing.Taa: // TAA is a Vulkan-only effect; on the GL backend it falls back to no AA. + case AntiAliasing.Taa: _antiAliasing?.Dispose(); _antiAliasing = null; break; @@ -428,7 +427,6 @@ namespace Ryujinx.Graphics.OpenGL public void SetHdrMode(bool enabled, float paperWhite, float peak, float curve, float gamma, float blend, float whiten) { - // HDR output (scRGB inverse tone mapping) is only implemented on the Vulkan backend. } } } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs index f50ca51c7..f17684190 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs @@ -333,11 +333,6 @@ 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); @@ -965,8 +960,6 @@ 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); @@ -990,7 +983,6 @@ 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); diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs index ce5026018..710897fb0 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs @@ -64,8 +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 GlslNMax; + public readonly FuncBinaryInstruction GlslNMin; public readonly FuncBinaryInstruction GlslSMax; public readonly FuncBinaryInstruction GlslFMin; public readonly FuncBinaryInstruction GlslSMin; @@ -105,7 +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 GlslNClamp; public readonly FuncTernaryInstruction GlslSClamp; public readonly FuncTernaryInstruction GlslFma; @@ -175,8 +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] + GlslNMax = context.GlslNMax; + GlslNMin = context.GlslNMin; GlslSMax = context.GlslSMax; GlslFMin = context.GlslFMin; GlslSMin = context.GlslSMin; @@ -216,7 +216,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv // Ternary GlslFClamp = context.GlslFClamp; - GlslNClamp = context.GlslNClamp; // [NCLAMP] + GlslNClamp = context.GlslNClamp; GlslSClamp = context.GlslSClamp; GlslFma = context.GlslFma; diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitConversion.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitConversion.cs index f20624711..247350dfc 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitConversion.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitConversion.cs @@ -184,8 +184,6 @@ 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) diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFloatArithmetic.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFloatArithmetic.cs index 6d497d0e2..88f0264c7 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFloatArithmetic.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFloatArithmetic.cs @@ -145,12 +145,6 @@ 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) @@ -257,18 +251,10 @@ 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 ulong HashTestFsAddress = 0x1000AE430UL; + private const float HashMagicImmediate = 43758.5469f; 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; @@ -277,10 +263,6 @@ namespace Ryujinx.Graphics.Shader.Instructions 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) @@ -298,16 +280,10 @@ namespace Ryujinx.Graphics.Shader.Instructions $"[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( diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMultifunction.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMultifunction.cs index 49683fc2f..3d673a579 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMultifunction.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMultifunction.cs @@ -95,13 +95,6 @@ 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) @@ -117,40 +110,17 @@ namespace Ryujinx.Graphics.Shader.Instructions 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 const ulong MufuPrecFsAddress = 0x1000AE430UL; private static readonly HashSet _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 + ulong target = MufuPrecProbe.AddressOverride ?? MufuPrecFsAddress; if (!MufuPrecProbe.Enabled || context.TranslatorContext.Address != target) { @@ -162,11 +132,9 @@ namespace Ryujinx.Graphics.Shader.Instructions if (dropped <= 0) { - return res; // 23 bits kept = the exact host result, nothing to drop. + return res; } - // 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)) diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs index c7a6ec5d8..2612eb84f 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs @@ -1034,7 +1034,7 @@ namespace Ryujinx.Graphics.Shader.Instructions EmitTextureSample(context, type, flags, imm, componentMask, dests, sources); return; - + Operand Ra() { if (srcA > RegisterConsts.RegisterZeroIndex) @@ -1197,9 +1197,6 @@ 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) && @@ -1235,11 +1232,8 @@ namespace Ryujinx.Graphics.Shader.Instructions 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 const ulong Periscope2FsAddress = 0x100082C30UL; + private const int Periscope2Handle = 0x10; private static void CapturePeriscope2(EmitterContext context, Operand[] dests, int handle) { @@ -1253,9 +1247,6 @@ namespace Ryujinx.Graphics.Shader.Instructions 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]), @@ -1266,15 +1257,7 @@ namespace Ryujinx.Graphics.Shader.Instructions $"[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= (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 const ulong CocConstGsAddress = 0x1000ADF30UL; private static bool _cocConstLogged; @@ -1287,9 +1270,6 @@ namespace Ryujinx.Graphics.Shader.Instructions 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; @@ -1307,23 +1287,13 @@ namespace Ryujinx.Graphics.Shader.Instructions } } - // [Beast Roofer diag] RYUJINX_TILEMAP_CONST= (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 const ulong TileMapFsAddress = 0x1000AE430UL; + private const int TileMapHandle = 0xE; private static readonly HashSet _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; @@ -1331,11 +1301,8 @@ namespace Ryujinx.Graphics.Shader.Instructions 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]), @@ -1346,14 +1313,9 @@ namespace Ryujinx.Graphics.Shader.Instructions $"[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 + return; } lock (_tileMapLoggedHandles) @@ -1384,13 +1346,8 @@ namespace Ryujinx.Graphics.Shader.Instructions } } - // [Beast Roofer diag] RYUJINX_MVKILL_CONST= (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 const ulong MvKillFsAddress = 0x1000AD730UL; + private const int MvKillHandle = 0x8; private static readonly HashSet _mvKillLoggedHandles = new(); @@ -1403,8 +1360,6 @@ namespace Ryujinx.Graphics.Shader.Instructions 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)) @@ -1433,21 +1388,11 @@ namespace Ryujinx.Graphics.Shader.Instructions } } - // [Beast Roofer diag] RYUJINX_MVSIGN_CONST= (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= (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 const ulong ResolveMvFsAddress = 0x100082C30UL; + private const int ResolveMvHandle = 0x10; private static readonly HashSet _resolveMvLoggedHandles = new(); - // [Beast Roofer diag] RYUJINX_TILECAP= (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 _tileCapLoggedHandles = new(); private static void CapTileMap(EmitterContext context, Operand[] dests, int handle) @@ -1487,7 +1432,6 @@ namespace Ryujinx.Graphics.Shader.Instructions continue; } - // [v2] channel bisection (journal 209). if (MvppTileCapProbe.Channel == "xy" && i == 2) { continue; @@ -1500,14 +1444,12 @@ namespace Ryujinx.Graphics.Shader.Instructions 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))); } } @@ -1578,7 +1520,6 @@ namespace Ryujinx.Graphics.Shader.Instructions 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)); diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs index d380d4a79..ced670b4d 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs @@ -66,9 +66,6 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation Flags |= TextureFlags.LodLevel; } - // [MVPP alpha LOD-bias] Mirror of SetLodLevelFlag for the bias flag; the caller appends - // the bias operand in tail position (codegen reads it after the offsets). No behavior - // change for existing code paths. public void SetLodBiasFlag() { Flags |= TextureFlags.LodBias; diff --git a/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs b/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs index add933767..b24eaa693 100644 --- a/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs +++ b/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs @@ -18,14 +18,7 @@ namespace Ryujinx.Graphics.Shader public bool UsesInstanceId { get; } public bool UsesDrawParameters { get; } public bool UsesRtLayer { get; } - // [MVPP CUTOUT PROBE] True if this (fragment) shader emits Instruction.Discard (KIL / alpha-test cutout). - // 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; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/AttributeUsage.cs b/src/Ryujinx.Graphics.Shader/Translation/AttributeUsage.cs index 49f2219b0..a9e567692 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/AttributeUsage.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/AttributeUsage.cs @@ -14,9 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation public HashSet NextUsedInputAttributesPerPatch { get; private set; } public int PassthroughAttributes { get; private set; } - // MV++ double-transform (b1) sub-step 4: coordinated locations of the injected clip_N / - // clip_{N-1} varyings, agreed across VS->FS (varyings link by location NUMBER). -1 = unset - // (not a VS->FS variant program, or fewer than 2 free user locations in [0, MaxAttributes-1]). public int MvppClipLocN { get; private set; } = -1; public int MvppClipLocPrev { get; private set; } = -1; @@ -95,12 +92,6 @@ namespace Ryujinx.Graphics.Shader.Translation } } - // MV++ (b1) sub-step 4: reserve the two highest free user-attribute locations in the UNION - // (this.UsedOutputAttributes already merged the next stage's inputs in MergeFromtNextStage) - // and publish them to BOTH stages -- exactly like the per-patch locationsMap above - // ("both stages must agree on the locations"). Bounded to valid user locations - // [0, MaxAttributes-1]; the old 31->0 scan produced out-of-range loc 31 (MaxAttributes = 16). - // Either loc staying -1 (fewer than 2 free) makes the injector bail (SKIPPED). public void AssignMvppClipLocs(AttributeUsage nextStage) { int used = UsedOutputAttributes | nextStage.UsedInputAttributes; diff --git a/src/Ryujinx.Graphics.Shader/Translation/CocConstProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/CocConstProbe.cs index e74231081..c3252cac5 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/CocConstProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/CocConstProbe.cs @@ -1,38 +1,9 @@ -// 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 { - /// - /// [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. - /// public static class CocConstProbe { public static readonly float? Value = Parse(); @@ -43,9 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs index 9f9301f2b..40530bd18 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs @@ -52,12 +52,6 @@ namespace Ryujinx.Graphics.Shader.Translation private readonly List _operations; private readonly Dictionary _labels; - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] Lazy cutout detection: does the DECODED - // program (all functions, subroutines included) contain a KIL? Computed once per context - // on first epilogue emission (multiple EXITs share the cache). Scanning the decoded program - // is the only sound point: emission order follows decoded block/function order (main first), - // so tracking Discard() during emission would miss KILs in later blocks or subroutines; - // ShaderProgramInfo.UsesDiscard is computed after emission = too late to gate it. private bool? _programHasKil; private bool ProgramHasKil() @@ -66,8 +60,6 @@ namespace Ryujinx.Graphics.Shader.Translation { bool found = false; - // DecodedProgram is a struct: guard against the parameterless EmitterContext() - // path (default Program) via its MainFunction reference. if (Program.MainFunction is not null) { foreach (DecodedFunction function in Program) @@ -342,18 +334,6 @@ namespace Ryujinx.Graphics.Shader.Translation } } - // DLSS Mode B clip-space jitter: offset the vertex position by the sub-pixel jitter (in NDC), - // 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)); @@ -415,7 +395,6 @@ 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() @@ -569,10 +548,6 @@ namespace Ryujinx.Graphics.Shader.Translation Operand src = Register(regIndexBase + component, RegisterType.Gpr); - // [Beast Roofer diag] RYUJINX_SKYMV_CONST= (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) @@ -586,31 +561,17 @@ namespace Ryujinx.Graphics.Shader.Translation 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)) { @@ -650,18 +611,6 @@ namespace Ryujinx.Graphics.Shader.Translation } } - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] Phase 2: mark this pixel in the - // private survivor-map SSBO (binding 80, declared in CreateResourceManager). This - // point is reached ONLY by fragments that were not killed (guest KIL emits OpKill, - // which terminates the invocation before this epilogue), and sits AFTER all guest - // color stores above -- observing survivors without touching any guest output. - // Out-of-map coordinates are skipped so the store can never go out of bounds. - // OmapTargets != 0 restricts the store to COLOR-writing shaders: depth-only passes - // (shadow maps) rasterize the whole sun-view terrain inside 0..4095x0..2047 and were - // measured saturating the map to 100% -- excluding them statically at translation - // keeps the map geometric. ProgramHasKil() further restricts it to CUTOUT shaders - // (decoded program contains a KIL) so the map becomes the alpha-test silhouette - // only. (Generic: pipeline state + decoded opcodes only, no game specifics.) if (TranslatorContext.Definitions.OmapTargets != 0 && ((MvppSurviveProbe.Enabled && ProgramHasKil()) || MvppSurviveProbe.MipMarkEnabled)) { @@ -675,10 +624,6 @@ namespace Ryujinx.Graphics.Shader.Translation Operand probeIndex = this.IAdd(this.IMultiply(probeY, Const(MvppSurviveProbe.MapStride)), probeX); - // Store the current viewport size (from the support buffer, packed W<<16|H) - // instead of a plain 1: the offline histogram then tells exactly WHICH passes - // wrote each pixel, with zero game-specific selection baked in. Viewport sizes - // are exact integers in float, so plain conversion needs no rounding. Operand probeVpW = this.FP32ConvertToU32( this.Load(StorageKind.ConstantBuffer, SupportBuffer.Binding, Const((int)SupportBufferField.ViewportSize), Const(0))); Operand probeVpH = this.FP32ConvertToU32( diff --git a/src/Ryujinx.Graphics.Shader/Translation/HashTestProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/HashTestProbe.cs index 856cc1433..f897c7609 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/HashTestProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/HashTestProbe.cs @@ -1,28 +1,9 @@ -// 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 { - /// - /// [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. - /// public static class HashTestProbe { public static readonly float? ConstValue = Parse(); @@ -33,7 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/MufuPrecProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MufuPrecProbe.cs index f5ec31b1e..2a2a96926 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MufuPrecProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MufuPrecProbe.cs @@ -1,49 +1,17 @@ -// 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 { - /// - /// [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. - /// public static class MufuPrecProbe { - /// Widest mantissa a binary32 result can carry. public const int MantissaBits = 23; - /// Mantissa bits to keep, or null when the probe is off. public static readonly int? BitsKept = Parse(); public static bool Enabled => BitsKept.HasValue; - /// [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. public static readonly ulong? AddressOverride = ParseAddr(); private static ulong? ParseAddr() @@ -62,9 +30,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvKillProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvKillProbe.cs index 3e2933365..bd9821482 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvKillProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvKillProbe.cs @@ -1,35 +1,9 @@ -// 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 { - /// - /// [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. - /// public static class MvKillProbe { public static readonly float? Value = Parse(); @@ -40,7 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvResolveMvProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvResolveMvProbe.cs index e4fed27f1..3f2dc5535 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvResolveMvProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvResolveMvProbe.cs @@ -1,27 +1,9 @@ -// 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 { - /// - /// [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). - /// public static class MvResolveMvProbe { public static readonly float? Value = Parse(); @@ -32,7 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvSignProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvSignProbe.cs index 238ca9008..b99d15124 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvSignProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvSignProbe.cs @@ -1,32 +1,9 @@ -// 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 { - /// - /// [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. - /// public static class MvSignProbe { public static readonly float? Value = Parse(); @@ -37,7 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppForceLod0Probe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppForceLod0Probe.cs index 88b8356f5..6ccb19d1f 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppForceLod0Probe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppForceLod0Probe.cs @@ -1,26 +1,9 @@ -// 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 { - /// - /// [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). - /// public static class MvppForceLod0Probe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppHashedAlpha.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppHashedAlpha.cs index bf8748ddf..fa6fb9842 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppHashedAlpha.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppHashedAlpha.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader.IntermediateRepresentation; @@ -10,41 +7,14 @@ using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper; namespace Ryujinx.Graphics.Shader.Translation { - /// - /// [MVPP HASHED-ALPHA Phase 1 -- READ-ONLY MATCHER] (RYUJINX_MVPP_HASHPROBE=1) - /// At fragment-shader translation time, on the post-SSA/optimized IR, finds every Discard and - /// traces the branch guarding it back through def-use (Operand.AsgOp) to decide whether it is - /// a recognizable ALPHA-TEST pattern: a FP compare against a constant/cbuf threshold whose - /// other operand reaches a TextureSample (the alpha channel of the cutout texture). - /// Pure diagnostics: counts + deduplicated per-shader pattern lines + periodic summary. - /// NOTHING is rewritten; default OFF => never called, byte-identical. Run with the shader - /// cache disabled so every fragment shader is freshly translated and analyzed. - /// Same analysis-pass pattern as MvppPositionProbe.Analyze. - /// public static class MvppHashedAlpha { public static readonly bool ProbeEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_HASHPROBE") == "1"; - // [Phase 2 -- REWRITE] (RYUJINX_MVPP_HASHED_ALPHA=1) Replaces the alpha-test THRESHOLD of - // the validated pattern with a per-pixel Wang hash in [0,1): "discard if alpha < T" - // becomes hashed alpha testing "discard if alpha < hash(pixel)" (Wyman-McGuire). The hash - // keys on floor(gl_FragCoord.xy) -- the pixel grid, which jitter does NOT move -- so with - // a still camera the stencil pattern is temporally STABLE by construction, and in motion - // DLSS averages the (camera-reprojected) dither into soft coverage. STRICT gating: only - // Less/LessOrEqual FP32 compares, discard-when-true, threshold as source 1 (const [0,1] - // or cbuf), alpha side tracing to a TextureSample, and a SINGLE-USE predicate (rewriting - // a shared predicate would change other consumers). Everything else is left untouched. - // CFG is never modified (linear op insertion + one operand swap). Default OFF = pass not - // even called, byte-identical. public static readonly bool RewriteEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_HASHED_ALPHA") == "1"; - // Width of the hashed band around the ORIGINAL threshold: T' = T + (hash-0.5)*Band. - // Full-range [0,1) hashing was measured breaking soft-alpha surfaces (TotK clouds: - // stippling over the whole translucent fringe) -- banding keeps alpha values far from - // the threshold on their original binary behavior and dithers only the transition zone - // (the foliage edge). Tunable without rebuild via RYUJINX_MVPP_HASHED_BAND. private static readonly float HashBand = ParseBand(); private static float ParseBand() @@ -57,35 +27,11 @@ namespace Ryujinx.Graphics.Shader.Translation : 0.3f; } - // A near-zero threshold is NOT a cutout: it is a "kill the fully invisible" optimization - // on soft-alpha surfaces (clouds, smoke). Hashing it dithers the whole translucent fringe - // (measured on TotK clouds) -- skip those, keep original behavior. private const float MinCutoutThreshold = 0.05f; - // [M3 ALPHA-AT-KIL -- READ-ONLY CAPTURE] (RYUJINX_MVPP_ALPHA_HISTO=1) At every alpha-test - // discard the strict matcher validates, insert a linear op sequence BEFORE the compare that - // packs (alpha 8b | alpha-threshold delta 8b biased at 128 | FragCoord.z 16b) and stores it - // per-pixel into the survivor-map SSBO (binding 80, plumbing reused wholesale from - // MvppSurviveProbe: declaration, descriptor, P3 scene bind-swap, periodic readback+dump). - // The compare and its operands are NOT touched -- rendering is byte-identical, only the - // side-effect store is added. Killed fragments write too (the store precedes the KIL): - // the offline histogram sees the FULL alpha population around the material threshold -- - // the measurement that proves/refutes the KIL root-cause's central link ("sub-pixel crowns - // live in the threshold zone"). Uses the same tinyThr skip as the rewrite: near-zero - // thresholds are soft-alpha invisibility kills, not the cutout population under test. public static readonly bool CaptureEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_ALPHA_HISTO") == "1"; - // [V1 alpha LOD-bias] (RYUJINX_MVPP_ALPHA_LODBIAS=) Stabilizes the DISCARD's alpha - // SAMPLE: sub-pixel foliage flickers because the sampled alpha itself oscillates under - // jitter (fine mips, high-frequency content) -- no stable threshold can fix that. Biasing - // the alpha lookup to a coarser mip makes it a stable local average. Two safe paths only: - // (a) DIRECT chain: duplicate the sample with the bias, route only the compare to it (the - // original sample and the color path are untouched); - // (b) ALPHA-ONLY sample (component mask == 8): bias it in place -- it produces no color - // component by construction. (Named risk: if that alpha also feeds blending output, - // the modulation gets blurred -- local, gated, reversible.) - // Everything else (arithmetic chains on multi-component samples) falls back untouched (V2). public static readonly float AlphaLodBias = ParseLodBias(); public static bool LodBiasEnabled => !float.IsNaN(AlphaLodBias); @@ -161,7 +107,7 @@ namespace Ryujinx.Graphics.Shader.Translation if (discards == 0) { - return; // not a cutout shader + return; } string pattern = $"kil={discards} match={matched} [{string.Join(" ; ", forms)}]"; @@ -180,7 +126,6 @@ namespace Ryujinx.Graphics.Shader.Translation { _patterns[pattern] = 1; - // New distinct shape: log it once with an example shader address. Logger.Info?.Print(LogClass.Gpu, $"MVPP-HASHPROBE NEW: {pattern} (fs @0x{address:X})"); } @@ -198,18 +143,10 @@ namespace Ryujinx.Graphics.Shader.Translation } } - /// - /// Finds the conditional branch guarding entry into the Discard's block, then classifies - /// the predicate's defining operation. Returns a compact shape string; ok = the shape is a - /// rewritable alpha-test (FP compare, const/cbuf threshold, alpha side reaches a texture). - /// private static string MatchDiscard(BasicBlock block, out bool ok) { ok = false; - // The guest KIL emission shape is: pred -> BranchIf{True,False}(skip) ; Discard. - // After CFG building the Discard terminates its block; entry into that block is - // decided by the (single) predecessor's conditional branch. if (block.Predecessors.Count != 1) { return $"preds={block.Predecessors.Count}"; @@ -223,7 +160,6 @@ namespace Ryujinx.Graphics.Shader.Translation return "uncond-entry"; } - // Which way enters the discard block: taken edge (pred.Branch) or fallthrough (pred.Next). bool discardOnTaken = pred.Branch == block; bool discardWhenCondTrue = discardOnTaken == (branch.Inst == Instruction.BranchIfTrue); @@ -252,8 +188,6 @@ namespace Ryujinx.Graphics.Shader.Translation Operand srcA = cmp.GetSource(0); Operand srcB = cmp.GetSource(1); - // One side should be the threshold (constant in [0,1] or a cbuf value), the other the - // alpha value (traced to a TextureSample). Try both orders. (string thr, bool thrOk) = ClassifyThreshold(srcB); (string alpha, bool alphaOk) = TraceToTexture(srcA, out string chain); @@ -271,9 +205,6 @@ namespace Ryujinx.Graphics.Shader.Translation ok = thrOk && alphaOk; - // [LOD-bias step 1] V1 feasibility census: a "direct" chain (compare consumes the - // sample's dest itself) is rewritable by the V1 alpha LOD-bias; anything else needs - // the V2 chain duplication. Counted only for matched alpha-tests. if (ok) { if (chain == "direct") @@ -329,13 +260,6 @@ namespace Ryujinx.Graphics.Shader.Translation return TraceToTexture(start, out _); } - /// - /// Bounded def-use walk from the candidate alpha operand, through arithmetic/moves/phis, - /// looking for a TextureSample source. Reports the sampled component mask and the CHAIN of - /// operations crossed on the winning path: "direct" = the compare consumes the sample's - /// dest with nothing in between (the V1 LOD-bias rewrite case); otherwise the op names - /// joined by '>' (the V2 cases, e.g. "Multiply>tex"). - /// private static (string, bool) TraceToTexture(Operand start, out string chain) { chain = "none"; @@ -345,7 +269,6 @@ namespace Ryujinx.Graphics.Shader.Translation return ("null", false); } - // DFS carrying the path (bounded depth; shared subtrees cut by the visited set). Stack<(Operand Oper, string Path)> stack = new(); HashSet visited = new(); stack.Push((start, "")); @@ -397,7 +320,6 @@ namespace Ryujinx.Graphics.Shader.Translation static string Extend(string path, string op) { - // Bound the path description; beyond 4 hops just mark it deep. if (path.Length > 60) { return path.EndsWith(">...") ? path : path + ">..."; @@ -407,8 +329,6 @@ namespace Ryujinx.Graphics.Shader.Translation } } - // ==================== Phase 2: rewrite ==================== - private static int _rwShaders; private static int _rwRewritten; private static int _rwSkipShape; @@ -456,10 +376,6 @@ namespace Ryujinx.Graphics.Shader.Translation continue; } - // [M3] Capture-only runs relax the two safety skips that only matter when - // the compare gets REWRITTEN: multiUse (we never touch the predicate) and - // tinyThr (soft-alpha kill-invisible sites are a population under test -- - // offline segmentation by the reconstructed threshold separates families). bool forCapture = CaptureEnabled && !RewriteEnabled && !LodBiasEnabled; if (!TryGetRewritableCompare(block, out Operation cmp, out string skipReason, forCapture)) @@ -472,11 +388,9 @@ namespace Ryujinx.Graphics.Shader.Translation if (!doneCmps.Add(cmp)) { - continue; // this compare already got the hash (shared guard) + continue; } - // Index this function's linked-list nodes once, to find the compare's node - // for the AddBefore insertions. if (nodeIndex == null) { nodeIndex = new Dictionary>(); @@ -501,8 +415,6 @@ namespace Ryujinx.Graphics.Shader.Translation continue; } - // [M3 ALPHA-AT-KIL] Capture first: it must read the ORIGINAL threshold - // operand, before any hash rewrite swaps source 1. if (CaptureEnabled) { InsertAlphaCapture(cmpNode, cmp); @@ -515,8 +427,6 @@ namespace Ryujinx.Graphics.Shader.Translation rewritten++; } - // [V1 alpha LOD-bias] Independent second stage on the same validated - // compare: stabilize the alpha SAMPLE feeding it. if (LodBiasEnabled) { TryApplyLodBias(cmp, nodeIndex); @@ -527,7 +437,7 @@ namespace Ryujinx.Graphics.Shader.Translation if (rewritten == 0 && skipped == 0 && captured == 0) { - return; // not a cutout shader + return; } lock (_sync) @@ -573,12 +483,6 @@ namespace Ryujinx.Graphics.Shader.Translation } } - /// - /// STRICT variant of the probe matcher, deciding rewritability: single predecessor, - /// conditional guard, discard-when-TRUE, FP32 Less/LessOrEqual, threshold as SOURCE 1 - /// (const in [0,1] or cbuf -- "discard if alpha < T"), alpha side reaching a - /// TextureSample, and the predicate consumed ONLY by the guarding branch. - /// private static bool TryGetRewritableCompare(BasicBlock block, out Operation cmp, out string skipReason, bool forCapture = false) { cmp = null; @@ -612,8 +516,6 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - // Rewriting the compare changes its predicate for EVERY consumer; only safe when the - // guarding branch is the single use. Irrelevant for capture (read-only). if (!forCapture && cond.UseOps.Count > 1) { skipReason = "multiUse"; @@ -630,8 +532,6 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - // Threshold must be source 1: "alpha < T". The mirrored form ("T < alpha") kills - // ABOVE the threshold -- not the foliage alpha-test; left untouched. Operand thrOper = candidate.GetSource(1); (_, bool thrOk) = ClassifyThreshold(thrOper); @@ -643,10 +543,6 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - // Near-zero constant threshold = invisible-pixel culling on soft alpha (clouds), - // not a cutout -- hashing it stipples the whole translucent fringe. Skip for the - // rewrite; CAPTURED anyway (M3 measures every family, segmented offline by the - // reconstructed threshold). if (!forCapture && thrOper.Type == OperandType.Constant && BitConverter.Int32BitsToSingle(thrOper.Value) < MinCutoutThreshold) { @@ -668,11 +564,6 @@ namespace Ryujinx.Graphics.Shader.Translation return true; } - /// - /// Inserts the Wang-hash op sequence right before the compare and swaps its threshold - /// (source 1) for hash01. Linear insertion in the compare's own block: SSA dominance is - /// trivially preserved, the CFG is untouched. - /// private static void InsertHashThreshold(LinkedListNode cmpNode, Operation cmp) { LinkedList list = cmpNode.List; @@ -693,17 +584,14 @@ namespace Ryujinx.Graphics.Shader.Translation return dest; } - // floor(gl_FragCoord.xy) -- host pixel grid, invariant under jitter. Operand fx = Emit(Instruction.Load, StorageKind.Input, Const((int)IoVariable.FragmentCoord), Const(0)); Operand fy = Emit(Instruction.Load, StorageKind.Input, Const((int)IoVariable.FragmentCoord), Const(1)); Operand ux = EmitOp(Instruction.ConvertFP32ToU32, fx); Operand uy = EmitOp(Instruction.ConvertFP32ToU32, fy); - // seed = ux ^ (uy * goldenRatio32) Operand uyG = EmitOp(Instruction.Multiply, uy, Const(unchecked((int)0x9E3779B9))); Operand seed = EmitOp(Instruction.BitwiseExclusiveOr, ux, uyG); - // Wang hash. Operand a = EmitOp(Instruction.BitwiseExclusiveOr, seed, Const(61)); Operand b = EmitOp(Instruction.ShiftRightU32, seed, Const(16)); Operand h1 = EmitOp(Instruction.BitwiseExclusiveOr, a, b); @@ -714,14 +602,10 @@ namespace Ryujinx.Graphics.Shader.Translation Operand d = EmitOp(Instruction.ShiftRightU32, h4, Const(15)); Operand h5 = EmitOp(Instruction.BitwiseExclusiveOr, h4, d); - // hash01 = float(h >> 8) / 2^24 in [0,1). Operand hTop = EmitOp(Instruction.ShiftRightU32, h5, Const(8)); Operand hf = EmitOp(Instruction.ConvertU32ToFP32, hTop); Operand hash01 = EmitOp(Instruction.FP32 | Instruction.Multiply, hf, ConstF(1f / 16777216f)); - // Banded threshold: T' = T + (hash01 - 0.5) * Band, keeping the ORIGINAL threshold - // operand. Values far from T keep their binary behavior (protects soft alpha); - // only the transition band dithers. Operand originalThr = cmp.GetSource(1); Operand centered = EmitOp(Instruction.FP32 | Instruction.Add, hash01, ConstF(-0.5f)); Operand scaled = EmitOp(Instruction.FP32 | Instruction.Multiply, centered, ConstF(HashBand)); @@ -730,21 +614,8 @@ namespace Ryujinx.Graphics.Shader.Translation cmp.SetSource(1, banded); } - // ==================== M3 alpha-at-KIL capture ==================== - private static int _capInserted; - /// - /// [M3 -- READ-ONLY] Inserts, right before the validated alpha-test compare, a linear op - /// sequence that packs (alpha | delta-vs-threshold | fragment depth) and stores it into the - /// survivor-map SSBO at this pixel. Neither the compare nor any of its operands is modified; - /// the CFG is untouched (coordinates are CLAMPED into the fixed map, not branch-guarded, - /// because inserting branches post-SSA would rebuild the CFG). Every fragment evaluating - /// the alpha test writes -- including the ones the KIL then discards -- so the map holds - /// the full pre-discard alpha population. Packing (u32, never zero by construction: - /// the delta byte is >= 1): bits 0-7 alpha*255, bits 8-15 clamp(alpha-thr,-1,1)*127+128, - /// bits 16-31 FragCoord.z*65535 (near/far stratification key). - /// private static void InsertAlphaCapture(LinkedListNode cmpNode, Operation cmp) { LinkedList list = cmpNode.List; @@ -771,7 +642,6 @@ namespace Ryujinx.Graphics.Shader.Translation EmitOp(Instruction.FP32 | Instruction.Maximum, v, ConstF(0f)), ConstF(1f)); } - // Map index from gl_FragCoord (host pixels), clamped into the 4096x2048 map. Operand fx = Emit(Instruction.Load, StorageKind.Input, Const((int)IoVariable.FragmentCoord), Const(0)); Operand fy = Emit(Instruction.Load, StorageKind.Input, Const((int)IoVariable.FragmentCoord), Const(1)); Operand cx = EmitOp(Instruction.MinimumU32, @@ -781,13 +651,10 @@ namespace Ryujinx.Graphics.Shader.Translation Operand index = EmitOp(Instruction.Add, EmitOp(Instruction.Multiply, cy, Const(MvppSurviveProbe.MapStride)), cx); - // alpha (compare source 0) -> bits 0-7. Operand alpha = cmp.GetSource(0); Operand a8 = EmitOp(Instruction.ConvertFP32ToU32, EmitOp(Instruction.FP32 | Instruction.Multiply, Clamp01(alpha), ConstF(255f))); - // delta = alpha - threshold (compare source 1, the ORIGINAL material threshold), - // clamped to [-1,1], biased at 128 -> bits 8-15 (resolution 1/127 around the threshold). Operand delta = EmitOp(Instruction.FP32 | Instruction.Subtract, alpha, cmp.GetSource(1)); Operand dClamped = EmitOp(Instruction.FP32 | Instruction.Minimum, EmitOp(Instruction.FP32 | Instruction.Maximum, delta, ConstF(-1f)), ConstF(1f)); @@ -795,7 +662,6 @@ namespace Ryujinx.Graphics.Shader.Translation EmitOp(Instruction.FP32 | Instruction.Add, EmitOp(Instruction.FP32 | Instruction.Multiply, dClamped, ConstF(127f)), ConstF(128f))); - // FragCoord.z in [0,1] -> bits 16-31 (near/far stratification). Operand fz = Emit(Instruction.Load, StorageKind.Input, Const((int)IoVariable.FragmentCoord), Const(2)); Operand z16 = EmitOp(Instruction.ConvertFP32ToU32, EmitOp(Instruction.FP32 | Instruction.Multiply, Clamp01(fz), ConstF(65535f))); @@ -804,36 +670,19 @@ namespace Ryujinx.Graphics.Shader.Translation EmitOp(Instruction.BitwiseOr, a8, EmitOp(Instruction.ShiftLeft, d8, Const(8))), EmitOp(Instruction.ShiftLeft, z16, Const(16))); - // Per-pixel store into the survivor map (binding 80). StateUpdater's P3 bind-swap - // routes it to the REAL buffer only on the pinned scene depth (dummy sink elsewhere), - // exactly like the survivor store. Same source layout as the epilogue Store helper: - // binding, field 0 ("data"), element index, value. list.AddBefore(cmpNode, new Operation(Instruction.Store, StorageKind.StorageBuffer, (Operand)null, Const(MvppSurviveProbe.HostBinding), Const(0), index, packed)); } - // ==================== V1 alpha LOD-bias ==================== - private static int _lbDirect; private static int _lbInPlace; private static int _lbSkipped; private static readonly HashSet _lbDone = new(); - // Flags that make either path unsafe or meaningless: bindless (handle source layout), - // an existing explicit LOD or bias, gather (extra tail operand), derivatives (explicit - // gradients override bias), integer coords (texelFetch has no filtering). private const TextureFlags LodBiasExcludedFlags = TextureFlags.Bindless | TextureFlags.LodLevel | TextureFlags.LodBias | TextureFlags.Gather | TextureFlags.Derivatives | TextureFlags.IntCoords; - /// - /// Applies the V1 alpha LOD-bias to the compare's alpha side when one of the two safe - /// patterns holds. Path (a): the compare consumes a TextureSample dest directly -> emit a - /// BIASED DUPLICATE of that sample (single component, new dest) right after the original - /// and route only the compare to it. Path (b): the chain ends at an ALPHA-ONLY sample - /// (component mask 8) -> append the bias to the original in place (it produces no color). - /// Anything else falls back untouched. - /// private static void TryApplyLodBias(Operation cmp, Dictionary> nodeIndex) { Operand alphaSrc = cmp.GetSource(0); @@ -843,7 +692,6 @@ namespace Ryujinx.Graphics.Shader.Translation return; } - // Path (a): direct consumption of the sample's dest. if (alphaSrc.AsgOp is TextureOperation directTex && directTex.Inst == Instruction.TextureSample) { @@ -857,8 +705,6 @@ namespace Ryujinx.Graphics.Shader.Translation return; } - // Which component of the original does the compare read? dests[i] maps to the - // i-th set bit of the component mask. int destIndex = -1; for (int i = 0; i < directTex.DestsCount; i++) @@ -916,7 +762,6 @@ namespace Ryujinx.Graphics.Shader.Translation return; } - // Path (b): indirect chain ending at an alpha-only sample (mask 8, single dest). if (TraceToTextureOp(alphaSrc) is TextureOperation chainTex && chainTex.Index == 8 && chainTex.DestsCount == 1 && @@ -927,7 +772,7 @@ namespace Ryujinx.Graphics.Shader.Translation { if (!_lbDone.Add(chainTex)) { - return; // already biased (shared by several discard chains) + return; } _lbInPlace++; @@ -942,7 +787,6 @@ namespace Ryujinx.Graphics.Shader.Translation lock (_sync) { _lbSkipped++; } } - /// Returns the bit position of the n-th set bit of a mask, or -1. private static int NthSetBit(int mask, int n) { for (int bit = 0; bit < 32; bit++) @@ -956,7 +800,6 @@ namespace Ryujinx.Graphics.Shader.Translation return -1; } - /// Bounded def-use walk returning the first TextureSample operation reached. private static TextureOperation TraceToTextureOp(Operand start) { Stack stack = new(); diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppJitterEmit.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppJitterEmit.cs index 637826e7d..b8cc33bfb 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppJitterEmit.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppJitterEmit.cs @@ -1,21 +1,6 @@ -// 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 { - /// - /// [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. - /// public static class MvppJitterEmit { private static readonly bool _forceOff = @@ -24,7 +9,6 @@ namespace Ryujinx.Graphics.Shader.Translation private static readonly bool _jitterOn = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER") is "1" or "true" or "on"; - /// True quand le bloc ne doit PAS etre emis (defaut : jitter eteint). public static bool Disabled => _forceOff || !_jitterOn; } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppNClampProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppNClampProbe.cs index 422f64f9b..031861f14 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppNClampProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppNClampProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Graphics.Shader.CodeGen; using System; @@ -8,22 +5,6 @@ using System.Threading; namespace Ryujinx.Graphics.Shader.Translation { - /// - /// [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. - /// public static class MvppNClampProbe { public static readonly bool Enabled = @@ -31,9 +12,6 @@ namespace Ryujinx.Graphics.Shader.Translation private static long _applied; - /// 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. public static void OnApplied(ILogger logger) { if (Interlocked.Increment(ref _applied) == 1) diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppNMinMaxProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppNMinMaxProbe.cs index aeb37fafe..f6c619c0f 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppNMinMaxProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppNMinMaxProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Graphics.Shader.CodeGen; using System; @@ -8,20 +5,6 @@ using System.Threading; namespace Ryujinx.Graphics.Shader.Translation { - /// - /// [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. - /// public static class MvppNMinMaxProbe { public static readonly bool Enabled = @@ -29,8 +12,6 @@ namespace Ryujinx.Graphics.Shader.Translation private static long _applied; - /// First call prints the armed witness (translation-time): no line in the - /// log = gate off OR nothing re-translated (shader cache still on) = VOID. public static void OnApplied(ILogger logger) { if (Interlocked.Increment(ref _applied) == 1) diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppPositionProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppPositionProbe.cs index 0c86276e5..191df7cb0 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppPositionProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppPositionProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader.IntermediateRepresentation; @@ -10,17 +7,6 @@ using System.Text; namespace Ryujinx.Graphics.Shader.Translation { - /// - /// MV++ Phase 2.1 run A (RYUJINX_MVPP_P21_SHADER=1): at vertex-shader translation time, - /// backward-slice the post-optimization IR from the gl_Position stores and report which - /// constant-buffer locations feed the position: static reads become Cbuf(slot, offset) - /// operands after constant folding; dynamic reads stay Load(ConstantBuffer) with a - /// non-constant vector index, whose SOURCE (instance id, vertex attribute = skinning - /// indices, scalar from another cbuf...) is classified by a bounded secondary slice. - /// Translation-time only, zero rendering change; the same flag also bypasses the host - /// shader cache at boot (standard guest-recompile path) so every shader is re-translated - /// and analyzed once. Metric targets live in docs/MVPP-JOURNAL.md (Phase 2.1 entry). - /// public static class MvppPositionProbe { public static readonly bool Enabled = @@ -29,8 +15,6 @@ namespace Ryujinx.Graphics.Shader.Translation private const int MaxSliceSteps = 20000; private const int MaxIndexSliceSteps = 512; - // Distinct result payloads only: thousands of shaders share a handful of layouts and - // repeating identical lines would drown the signal. Progress is logged periodically. private static readonly object _sync = new(); private static readonly Dictionary _patterns = new(); private static int _analyzed; @@ -88,8 +72,6 @@ namespace Ryujinx.Graphics.Shader.Translation sawPosition = true; - // Slice from everything but the IoVariable selector (component index - // constants are leaves and cost nothing). for (int i = 1; i < op.SourcesCount; i++) { stack.Push(op.GetSource(i)); @@ -150,8 +132,6 @@ namespace Ryujinx.Graphics.Shader.Translation if (defOp.Inst == Instruction.Call) { - // The slice does not cross function calls (assumed limitation, journal); - // arguments are still followed so nothing upstream is silently dropped. hasCalls = true; } @@ -179,10 +159,6 @@ namespace Ryujinx.Graphics.Shader.Translation if (slot < 0) { - // Binding that maps to no guest cbuf slot = Ryujinx's own injected support - // buffer read (y-flip/render-scale), present in every vertex shader. It is - // instrumentation noise, not game evidence -- labeled, not chased (run A - // initially reported it as cb-1[idx<-unresolved] on 100% of shaders). dynamicReads.Add("rxsup"); return; @@ -199,12 +175,9 @@ namespace Ryujinx.Graphics.Shader.Translation return; } - // Dynamically indexed: the index source is the actual object of the hunt -- - // it names the mechanism that picks the object transform out of the pool. dynamicReads.Add($"cb{slot}[idx<-{ClassifyIndex(vecIndex, resourceManager)}]"); } - /// Bounded secondary slice naming what feeds a dynamic cbuf index. private static string ClassifyIndex(Operand start, ResourceManager resourceManager) { if (start == null) @@ -298,8 +271,6 @@ namespace Ryujinx.Graphics.Shader.Translation IoVariable v = (IoVariable)io.Value; if (v == IoVariable.UserDefined) { - // Vertex attribute: for a dynamic transform index this is the skinning-index - // signature (bone indices ride in per-vertex attributes). Operand loc = load.SourcesCount > 1 ? load.GetSource(1) : null; return loc != null && loc.Type == OperandType.Constant ? $"vtx-attr{loc.Value}" : "vtx-attr?"; @@ -317,7 +288,6 @@ namespace Ryujinx.Graphics.Shader.Translation bool hasCalls, bool truncated) { - // Compact per-slot summary of the static reads, in BYTE offsets. Dictionary> yPerSlot = new(); foreach ((int slot, int wordOffset) in staticReads) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppSurviveProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppSurviveProbe.cs index 82e507c0a..6a076388c 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppSurviveProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppSurviveProbe.cs @@ -1,36 +1,11 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. namespace Ryujinx.Graphics.Shader.Translation { - /// - /// [MVPP SURVIVE PROBE -- TEMPORARY DIAGNOSTIC, DISPOSABLE] Shared configuration for the - /// fragment "survivor" probe: a private SSBO written from the fragment epilogue - /// (PrepareForReturn) by fragments that survive discard/KIL (OpKill terminates the - /// invocation before the epilogue, so only surviving fragments can reach the store). - /// - /// Single source of truth for the host binding/stride, referenced by BOTH the shader - /// translator (declaration + store emission) and Ryujinx.Graphics.Gpu (descriptor layout - /// + runtime binding), mirroring the MvppVariantInjector.MvppN1HostBinding pattern. - /// - /// Gated by RYUJINX_MVPP_SURVIVE_PROBE; default OFF => no declaration, no descriptor, - /// no binding, no shader change (build is byte-identical). Run with the shader cache - /// DISABLED (per-game config) so every fragment shader is freshly translated. - /// public static class MvppSurviveProbe { public static readonly bool Enabled = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_SURVIVE_PROBE") == "1"; - // [M3 ALPHA-AT-KIL] The alpha-capture probe (MvppHashedAlpha.CaptureEnabled) reuses this - // buffer chain wholesale. The PLUMBING (SSBO declaration, descriptor, P3 scene bind-swap, - // periodic readback+dump) activates for EITHER gate; the survivor EPILOGUE STORE itself - // stays on Enabled only, so the two probes never write the map in the same run. - // [MIPMARK, 11/07] Preuve pixel « texture sans mip -> petits arbres » : le store d'épilogue - // est émis dans TOUS les shaders couleur (pas seulement KIL) ; le ROUTAGE réel-vs-dummy par - // draw (StateUpdater) ne laisse écrire que les draws de scène qui lient une texture 2D - // levels=1 minifiable. Lecture seule, défaut OFF. public static readonly bool MipMarkEnabled = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPMARK") == "1"; @@ -38,25 +13,12 @@ namespace Ryujinx.Graphics.Shader.Translation Enabled || MipMarkEnabled || System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_ALPHA_HISTO") == "1"; - // Guest storage buffer bindings on Vulkan are GetStageIndex(stage) * 16 + slot - // (slot < MaxStorageBuffersPerStage = 16, 5 stages) = 0..79. Binding 80 can never be - // produced by the guest formula, so it never aliases a guest SSBO. Vulkan-side - // Constants.MaxStorageBufferBindings is raised (+2) to size the descriptor arrays. public const int HostBinding = 80; - // Backend-mirror slot: the REAL mask buffer is also bound here once (never swapped to - // the dummy, never part of any pipeline layout). The Vulkan DescriptorSetUpdater captures - // the backend-mapped range passing through this index so DlssUpscaler can feed the mask - // to its conversion compute -- in-order via the normal command queue, no side-channel race. public const int BackendMirrorBinding = 81; - // Storage buffer descriptor set (same hardcoded set the reserved TFE/VtG storage - // buffers use, see TranslatorContext.CreateResourceManager). public const int SetIndex = 1; - // Fixed row stride and map height, in pixels. gl_FragCoord is in HOST pixels - // (render targets are host-scaled), so the map must cover the host resolution - // (2666x1500 at res_scale 2 fits 4096x2048). One u32 per pixel. public const int MapStride = 4096; public const int MapHeight = 2048; diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppTileCapProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppTileCapProbe.cs index f558828b0..36d50682b 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppTileCapProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppTileCapProbe.cs @@ -1,38 +1,15 @@ -// 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 { - /// - /// [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). - /// public static class MvppTileCapProbe { public static readonly float? Value = Parse(); public static bool Enabled => Value.HasValue; - /// [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. public static readonly string Channel = Environment.GetEnvironmentVariable("RYUJINX_TILECAP_CH"); diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppTruncEpsProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppTruncEpsProbe.cs index cd0dc8627..986952055 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppTruncEpsProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppTruncEpsProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using System; using System.Globalization; @@ -8,21 +5,6 @@ using System.Threading; namespace Ryujinx.Graphics.Shader.Translation { - /// - /// [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). - /// public static class MvppTruncEpsProbe { public static readonly float? Value = Parse(); diff --git a/src/Ryujinx.Graphics.Shader/Translation/MvppVariantInjector.cs b/src/Ryujinx.Graphics.Shader/Translation/MvppVariantInjector.cs index c40e758a0..cef5cd4a6 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/MvppVariantInjector.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/MvppVariantInjector.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader.IntermediateRepresentation; @@ -10,57 +7,15 @@ using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper; namespace Ryujinx.Graphics.Shader.Translation { - /// - /// MV++ double-transform (b1) sub-steps 3-4. On the POST-optimization IR of a variant program - /// (hook next to , so Ryujinx's DCE has already run), wire two - /// clip-space output varyings whose locations are agreed across VS->FS by - /// (Option B; varyings link by location NUMBER): - /// - clip_N (loc MvppClipLocN) = the CURRENT gl_Position value, SHARED (no clone). - /// - clip_N-1 (loc MvppClipLocPrev) = a CLONE of the position backward slice. - /// The fragment shader reads both and writes MV = ndc(clip_N) - ndc(clip_{N-1}) to RT0.xy. At - /// this stage the clone reads the CURRENT constant buffers, so clip_{N-1} == clip_N bit-for-bit - /// -> MV == 0 exactly ("by construction"), image identical; the real past (ring N-1) is sub-step 5. - /// - /// Because a real FS reads the varyings, the host driver can no longer prune them: sub-step 4 - /// delivers BINARY SURVIVAL. (Sub-step 3 only proved the IR-level clone; its "loc 31" was an - /// out-of-range artifact -- MaxAttributes = 16, valid user locations are 0-15, now enforced by - /// AssignMvppClipLocs which also guarantees the VS output loc == the FS input loc.) - /// - /// SKIP-LIST (crash fix): a position slice can contain an Operation SUBCLASS (e.g. - /// = vertex texture fetch). Cloning it as a plain Operation loses - /// the subclass data and crashes SPIR-V codegen -> such shaders BAIL (variant stays byte-identical - /// to base). The VS also bails if fewer than 2 free user locations exist in [0,15] or the program - /// is not a direct VS->FS variant (both coordinated locs are then -1). - /// public static class MvppVariantInjector { - // (b1) sub-step 5a fix @91 (non-global): host binding of the N-1 palette cbuf, declared - // by the VARIANT programs ONLY. 91 = the first binding no guest formula can produce - // (Vulkan guest bindings are GetStageIndex(stage)*18 + slot + 1, max = 4*18+17+1 = 90; - // binding 0 = support buffer), so it can never alias a guest cbuf and the graft's direct - // SetUniformBuffers(91) can never pollute a non-classified draw -- WITHOUT shifting any - // guest binding (the global-reservation fix B crashed on the mixed-layout host disk - // cache). Vulkan's Constants.MaxUniformBufferBindings is sized to cover it (= 92). - // Consumers: this injector (declares + redirects the clone), StateUpdater (publishes to - // the graft), both log the value -> the run's BINDING MATCH catches any drift. public const int MvppN1HostBinding = 91; - // Guest constant-buffer slot of the bone palette (cb5). The cloned clip_{N-1} slice reads - // this today; sub-step 5a redirects those reads to a fresh N-1 cbuf when enabled. private const int Cb5Slot = 5; - // (b1) sub-step 5a: redirect the cloned clip_{N-1} cb5 reads to a NEW cbuf (the N-1 palette). - // The graft binds that cbuf to the ring at re-draw. Gated OFF -> sub-step 4 behaviour. private static readonly bool _n1Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_N1") == "1"; - // (b1) 5B3 CANARY diag (RYUJINX_MVPP_VEL_CANARY=1, debug-only discriminant): an - // UNCONDITIONAL RT0.xy store at the very ENTRY of the variant FS main() -- executes - // for EVERY rasterized fragment of the re-draw, before any guest control flow (an - // anchor-placed canary would inherit the dead branch it is meant to expose). Guest - // stores overwrite it on paths that DO run, so surviving magenta maps the dead-branch - // draws exactly. No cache bypass needed: the injector runs at every boot (699 - // 'varyings SURVIVE' lines per boot prove the variants always retranslate). private static readonly bool _canaryEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_CANARY") == "1"; private static bool _canaryLogged; @@ -89,12 +44,6 @@ namespace Ryujinx.Graphics.Shader.Translation return; } - // (b1) sub-step 5a fix @91 (non-global): the N-1 palette cbuf lives at the FIXED host - // binding 91 -- outside every guest window (1..90), declared by THIS variant only -> - // no direct-bind pollution possible, no cleanup, and NO guest binding shift (the - // global reservation of fix B broke the mixed-layout host disk cache). Resolve cb5's - // host binding + declare the N-1 cbuf at 91, then redirect the clone's cb5 reads to - // it. Only when the guest uses cb5. cb5Binding < 0 -> no redirect (clone reads cb5). int cb5Binding = -1; int n1Binding = -1; @@ -106,8 +55,6 @@ namespace Ryujinx.Graphics.Shader.Translation resourceManager.DeclareMvppN1ConstantBuffer(n1Binding); } - // Build ALL clones into a pending list FIRST; a single unclonable op anywhere in any - // position slice bails the WHOLE shader (no partial injection, no inserted ops). List<(LinkedListNode StoreNode, List NewOps)> pending = new(); int shared = 0; bool bail = false; @@ -162,7 +109,6 @@ namespace Ryujinx.Graphics.Shader.Translation break; } - // clip_N (loc N): the CURRENT position value, SHARED (no clone). newOps.Add(new Operation( Instruction.Store, StorageKind.Output, @@ -172,7 +118,6 @@ namespace Ryujinx.Graphics.Shader.Translation component, value)); - // clip_{N-1} (loc Prev): the CLONE (== clip_N at N-1 == N). newOps.Add(new Operation( Instruction.Store, StorageKind.Output, @@ -196,7 +141,7 @@ namespace Ryujinx.Graphics.Shader.Translation if (pending.Count == 0) { - return; // no position store -> nothing to do (not an error) + return; } foreach ((LinkedListNode storeNode, List newOps) in pending) @@ -228,12 +173,9 @@ namespace Ryujinx.Graphics.Shader.Translation if (locN < 0 || locPrev < 0) { - return; // not a paired VS->FS variant -> nothing to read + return; } - // (b1) CANARY: unconditional RT0.xy store at the ENTRY block of main (funcs[0]), - // before any guest control flow. Value = mv (-0.5, 0.866) -> decoded as vivid - // MAGENTA at full brightness by the mode-2 debug view. if (_canaryEnabled) { foreach (BasicBlock block in funcs[0].Blocks) @@ -255,7 +197,7 @@ namespace Ryujinx.Graphics.Shader.Translation block.Operations.AddLast(c1); } - break; // entry block only + break; } if (!_canaryLogged) @@ -266,7 +208,6 @@ namespace Ryujinx.Graphics.Shader.Translation } } - // Insert AFTER the guest's LAST fragment color store so our MV wins RT0.xy. LinkedListNode anchor = null; foreach (Function func in funcs) @@ -295,14 +236,12 @@ namespace Ryujinx.Graphics.Shader.Translation return; } - // Register the FS inputs we read (x, y, w of both varyings). Must match the VS outputs. foreach (int c in new[] { 0, 1, 3 }) { attributeUsage.SetInputUserAttribute(locN, c); attributeUsage.SetInputUserAttribute(locPrev, c); } - // Sequentially chain the new ops right after the guest's last color store. Operand LoadIn(int loc, int comp) { Operand dst = Local(); @@ -342,7 +281,6 @@ namespace Ryujinx.Graphics.Shader.Translation Operand py = LoadIn(locPrev, 1); Operand pw = LoadIn(locPrev, 3); - // ndc = clip.xy / clip.w for each; MV = ndc(clip_N) - ndc(clip_{N-1}) (== 0 at N-1 == N). Operand ndcNx = Fp(Instruction.FP32 | Instruction.Divide, nx, nw); Operand ndcNy = Fp(Instruction.FP32 | Instruction.Divide, ny, nw); Operand ndcPx = Fp(Instruction.FP32 | Instruction.Divide, px, pw); @@ -351,12 +289,6 @@ namespace Ryujinx.Graphics.Shader.Translation Operand mvx = Fp(Instruction.FP32 | Instruction.Subtract, ndcNx, ndcPx); Operand mvy = Fp(Instruction.FP32 | Instruction.Subtract, ndcNy, ndcPy); - // (b1) FIX-RACINE F1 -- PERMANENT INVARIANT, not a setting: no non-finite value - // ever reaches the MV target (the final wall before DLSS at J3). NaN detected via - // v != v, Inf via |v| > 1e30; either -> clamp to 0 (= the MV=0 baseline fallback). - // Run-13 proof: degenerate clip_(N-1) (bad N-1 palette -> w through 0) wrote NaN, - // invisible to every instrument -- F2 (per-draw plausibility gate) fixes the cause, - // THIS guarantees the symptom class can never escape regardless. Operand Guard(Operand v) { Operand nan = Fp(Instruction.FP32 | Instruction.CompareNotEqual, v, v); @@ -378,11 +310,6 @@ namespace Ryujinx.Graphics.Shader.Translation $"(== VS output locs -> LOC MATCH), writes MV=ndc(N)-ndc(N-1) to RT0 -- varyings SURVIVE."); } - /// - /// Clones the SSA cone that computes . Leaves (constants, cbuf reads, - /// input results, Phi results) are SHARED (N-1 == N). An Operation SUBCLASS (texture etc.) - /// cannot be safely cloned as a plain Operation -> sets . - /// private static Operand CloneCone(Operand o, Dictionary map, List newOps, int cb5Binding, int n1Binding, ref int shared, ref bool bail) { if (bail || o == null || o.Type != OperandType.LocalVariable || o.AsgOp == null) @@ -398,12 +325,12 @@ namespace Ryujinx.Graphics.Shader.Translation if (o.AsgOp is not Operation defOp) { shared++; - return o; // Phi (or non-Operation def) -> share it (transparent at N-1 == N) + return o; } if (defOp.GetType() != typeof(Operation)) { - bail = true; // TextureOperation / CommentNode / any subclass -> not clonable here + bail = true; return o; } @@ -418,10 +345,6 @@ namespace Ryujinx.Graphics.Shader.Translation return o; } - // (b1) sub-step 5a: in the CLONE only, redirect cb5 reads to the N-1 palette cbuf. A - // constant-buffer Load's source[0] is the host binding (Const); swap cb5's for the N-1's. - // clip_N reads the ORIGINAL (un-cloned) cone, so it keeps reading cb5 -> the two diverge - // only by which buffer they read (identical data at 5a's trivial offset -> MV == 0). if (n1Binding >= 0 && defOp.Inst == Instruction.Load && defOp.StorageKind == StorageKind.ConstantBuffer && diff --git a/src/Ryujinx.Graphics.Shader/Translation/NanScrubProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/NanScrubProbe.cs index 699412699..dd945fc7b 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/NanScrubProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/NanScrubProbe.cs @@ -1,56 +1,17 @@ -// 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 { - /// - /// [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. - /// 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() @@ -65,10 +26,6 @@ namespace Ryujinx.Graphics.Shader.Translation 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 _armedAddresses = new(); public static bool WasArmed(ulong address) diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs index aee5eaeda..1299ffdff 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs @@ -8,7 +8,6 @@ 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"; @@ -65,14 +64,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations TextureOperation texOp, LinkedListNode 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; diff --git a/src/Ryujinx.Graphics.Shader/Translation/PeriscopeProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/PeriscopeProbe.cs index b995deda9..4a4bca4d3 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/PeriscopeProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/PeriscopeProbe.cs @@ -1,37 +1,13 @@ -// 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 { - /// - /// [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. - /// 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"; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs index c5824e41a..733e0f530 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs @@ -145,18 +145,11 @@ namespace Ryujinx.Graphics.Shader.Translation return Properties.AddLocalMemory(new MemoryDefinition(name, type, arrayLength)); } - // MV++ (b1) sub-step 5a: a cbuf slot is "used" once it has a real host binding. Read-only - // probe (does NOT allocate, unlike GetConstantBufferBinding). public bool IsConstantBufferSlotUsed(int slot) { return slot >= 0 && slot < _cbSlotToBindingMap.Length && _cbSlotToBindingMap[slot].Binding >= 0; } - // MV++ (b1) sub-step 5a fix @91: declare the N-1 palette cbuf at its FIXED host binding - // (set 0, like the support buffer). Standard vec4-array layout so the cloned clip_{N-1} - // reads it exactly like cb5. The binding (MvppVariantInjector.MvppN1HostBinding = 91) is - // outside every guest window (1..90) -> it can never alias a guest cbuf -> no direct-bind - // pollution, no cleanup needed, and no guest binding is shifted. public void DeclareMvppN1ConstantBuffer(int binding) { AddNewConstantBuffer(0, binding, "mvpp_n1"); @@ -449,7 +442,7 @@ namespace Ryujinx.Graphics.Shader.Translation if (found) { selectedMeta.UsageFlags |= TextureUsageFlags.NeedsScaleValue; - + int dimensions = type.Dimensions; bool canScale = _stage.SupportsRenderScale && selectedInfo.ArrayLength == 1 && dimensions == 2; diff --git a/src/Ryujinx.Graphics.Shader/Translation/RroReduceProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/RroReduceProbe.cs index 0d0146160..10b21f6be 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/RroReduceProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/RroReduceProbe.cs @@ -1,24 +1,8 @@ -// 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 { - /// - /// [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. - /// public static class RroReduceProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Shader/Translation/SkyMvProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/SkyMvProbe.cs index 699a56a20..dc5673010 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/SkyMvProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/SkyMvProbe.cs @@ -1,37 +1,9 @@ -// 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 { - /// - /// [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. - /// public static class SkyMvProbe { public static readonly float? Value = Parse(); @@ -42,7 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/TileMapConstProbe.cs b/src/Ryujinx.Graphics.Shader/Translation/TileMapConstProbe.cs index 1af1dda53..7d6e2d9e6 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TileMapConstProbe.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TileMapConstProbe.cs @@ -1,38 +1,9 @@ -// 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 { - /// - /// [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. - /// public static class TileMapConstProbe { public static readonly float? Value = Parse(); @@ -43,9 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation { 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)) { diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslationFlags.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslationFlags.cs index e00d6f6ba..f576bee4a 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TranslationFlags.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TranslationFlags.cs @@ -11,11 +11,6 @@ namespace Ryujinx.Graphics.Shader.Translation Compute = 1 << 1, DebugMode = 1 << 2, - // MV++ double-transform (b1): re-translate a classified VS into a variant that also - // computes clip_{N-1} (position slice duplicated, cbuf 5/8 -> N-1 buffers) and stores - // it to a capture varying; a companion FS differences it to RG16F. Set only by the - // graft's variant request, so game shaders stay byte-identical. Unused until the - // cache-keying sub-step wires it -- this bit alone is a zero-behavior foundation. MvppVariant = 1 << 3, } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs index b923c94d7..1babb7dfa 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs @@ -20,27 +20,15 @@ namespace Ryujinx.Graphics.Shader.Translation private readonly int _localMemorySize; private IoUsage _vertexOutput; - // [MVPP CUTOUT PROBE] Gated IR scan for Instruction.Discard at translation, to detect alpha-cutout - // fragment shaders. Default OFF => scan skipped, ShaderProgramInfo.UsesDiscard stays false, translation - // byte-identical. Read-only diagnostic; never affects codegen or the render path. private static readonly bool _cutoutProbe = 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; @@ -202,9 +190,6 @@ namespace Ryujinx.Graphics.Shader.Translation nextStage._program.UsedFeatures.HasFlag(FeatureFlags.FixedFuncAttr), nextStage.AttributeUsage); - // MV++ double-transform (b1) sub-step 4: for a variant VS->FS program, reserve+publish - // the clip_N / clip_{N-1} varying locations so both stages agree (Option B). Gated on - // the flag; only a DIRECT VS->FS pairing (no geometry/tess in between). if ((Options.Flags & TranslationFlags.MvppVariant) != 0 && Definitions.Stage == ShaderStage.Vertex && nextStage.Definitions.Stage == ShaderStage.Fragment) @@ -337,31 +322,21 @@ namespace Ryujinx.Graphics.Shader.Translation funcs[i] = new Function(cfg.Blocks, $"fun{i}", false, inArgumentsCount, outArgumentsCount); } - // MV++ Phase 2.1 run A: position-slice analysis on the optimized IR (no-op flag off). if (MvppPositionProbe.Enabled && !asCompute) { MvppPositionProbe.Analyze(funcs, resourceManager, Definitions.Stage, Address); } - // [MVPP HASHED-ALPHA Phase 1] Read-only alpha-test pattern matcher on the post-SSA IR - // of fragment shaders containing Discard (no-op flag off, nothing rewritten). if (MvppHashedAlpha.ProbeEnabled && !asCompute) { MvppHashedAlpha.Analyze(funcs, Definitions.Stage, Address); } - // [MVPP HASHED-ALPHA Phase 2 / V1 alpha LOD-bias] Gated rewrites on the same post-SSA - // IR: hashed threshold (RYUJINX_MVPP_HASHED_ALPHA) and/or alpha-sample LOD bias - // (RYUJINX_MVPP_ALPHA_LODBIAS), each independently gated inside. Both flags off = - // pass never called, byte-identical. if ((MvppHashedAlpha.RewriteEnabled || MvppHashedAlpha.LodBiasEnabled || MvppHashedAlpha.CaptureEnabled) && !asCompute) { MvppHashedAlpha.Rewrite(funcs, Definitions.Stage, Address); } - // MV++ double-transform (b1) sub-step 3 (B): duplicate the position slice into - // clip_{N-1} and write it to a registered UserDefined output varying, on the - // MvppVariant's post-optimization IR (N-1 == N here -> gl_Position untouched). if (!asCompute && (Options.Flags & TranslationFlags.MvppVariant) != 0) { MvppVariantInjector.Inject(funcs, AttributeUsage, resourceManager, Definitions.Stage, Address); @@ -401,9 +376,6 @@ namespace Ryujinx.Graphics.Shader.Translation _ => 1 }; - // [MVPP CUTOUT PROBE] Read-only IR scan (gated): a fragment shader that emits Instruction.Discard - // is an alpha-cutout shader (in-shader KIL or fixed-func alpha test). Scans blocks/operations for - // the Discard op. Skipped entirely when the flag is off -> UsesDiscard=false, no cost, no change. bool usesDiscard = false; if (_cutoutProbe && originalDefinitions.Stage == ShaderStage.Fragment) { @@ -477,11 +449,6 @@ namespace Ryujinx.Graphics.Shader.Translation { ResourceManager resourceManager = new(Definitions.Stage, GpuAccessor, GetResourceReservations()); - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] Declare the private survivor-map SSBO in - // every fragment shader (gated, default OFF). Same declaration pattern as the reserved - // TFE buffers below (std430, u32[] "data"), at the fixed out-of-window binding 80 that no - // guest formula can produce. Phase 1: declaration only -- nothing stores to it yet, so the - // SPIR-V just carries an unused buffer and the rendered output is unchanged. if (MvppSurviveProbe.BufferEnabled && Definitions.Stage == ShaderStage.Fragment) { StructureType surviveProbeStruct = new(new StructureField[] diff --git a/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs b/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs index 3cfe36559..f33af527b 100644 --- a/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs +++ b/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs @@ -115,11 +115,6 @@ namespace Ryujinx.Graphics.Texture.Astc return size * 4; } - /// - /// Computes the fully-decoded (RGBA8) size in bytes using 64-bit arithmetic, so callers can detect - /// when the result would overflow (e.g. large 4K texture-pack array textures) and - /// decode slice-by-slice instead of allocating a single oversized buffer. - /// public static long QueryDecompressedSizeLong(int sizeX, int sizeY, int sizeZ, int levelCount, int layerCount) { long size = 0; diff --git a/src/Ryujinx.Graphics.Texture/LayoutConverter.cs b/src/Ryujinx.Graphics.Texture/LayoutConverter.cs index 66b4ee248..4e2432b69 100644 --- a/src/Ryujinx.Graphics.Texture/LayoutConverter.cs +++ b/src/Ryujinx.Graphics.Texture/LayoutConverter.cs @@ -11,15 +11,8 @@ namespace Ryujinx.Graphics.Texture { public const int HostStrideAlignment = 4; - /// - /// When true, multi-layer block-linear textures are deswizzled with one thread per layer - /// instead of a single thread. Set the environment variable RYU_DESWIZZLE_SERIAL=1 to force - /// the original single-threaded path (useful to measure the parallel speed-up A/B). - /// public static bool ParallelConversion = Environment.GetEnvironmentVariable("RYU_DESWIZZLE_SERIAL") != "1"; - // Only fan a level's layers across threads when there is enough work to amortize the - // scheduling overhead; tiny mip levels stay on the calling thread. private const long ParallelConversionThreshold = 2L * 1024 * 1024; public static void ConvertBlockLinearToLinear( @@ -183,14 +176,8 @@ namespace Ryujinx.Graphics.Texture int sd = Math.Max(1, sliceDepth >> level); - // In the original serial code the output pointer advances continuously from one - // layer to the next, so layer L always starts at this fixed, non-overlapping offset. - // That is what makes deswizzling the layers in parallel safe. long layerOutputSize = (long)stride * h * sd; - // Processes a single array layer of the current mip level into its own output range. - // Each call owns its BlockLinearLayout (it carries mutable SetZ/SetY cursor state), - // so concurrent layers never share mutable state nor overlapping writes. unsafe void ConvertLayer(byte* outputPtr, byte* dataPtr, int layer) where T : unmanaged { BlockLinearLayout layoutConverter = new( @@ -253,8 +240,6 @@ namespace Ryujinx.Graphics.Texture { if (ParallelConversion && layers > 1 && layerOutputSize * layers >= ParallelConversionThreshold) { - // Pointers are captured as nint (lambdas cannot close over raw pointers). - // The fixed block keeps the buffers pinned for the whole, blocking Parallel.For. nint outBase = (nint)outputPtr; nint inBase = (nint)dataPtr; diff --git a/src/Ryujinx.Graphics.Vulkan/Constants.cs b/src/Ryujinx.Graphics.Vulkan/Constants.cs index 2f9f4cec4..9cad14b03 100644 --- a/src/Ryujinx.Graphics.Vulkan/Constants.cs +++ b/src/Ryujinx.Graphics.Vulkan/Constants.cs @@ -12,18 +12,7 @@ namespace Ryujinx.Graphics.Vulkan public const int MaxStorageBuffersPerStage = 16; public const int MaxTexturesPerStage = 64; public const int MaxImagesPerStage = 16; - // MV++ (b1) sub-step 5a fix @91: +1 covers the support buffer at binding 0 (the guest - // windows are stage*18+slot+1, so the last valid guest binding is 90 -- the stock size - // of 90 was one short for stage-4 slot-17), +1 for the MVPP N-1 palette cbuf at binding - // 91 = the first binding NO guest formula can produce (variant programs only; see - // MvppVariantInjector.MvppN1HostBinding -- keep the two values in sync). public const int MaxUniformBufferBindings = MaxUniformBuffersPerStage * MaxShaderStages + 2; - // [MVPP SURVIVE PROBE -- TEMPORARY, DISPOSABLE] +2 for the survivor-map SSBO at fixed - // binding 80 (the first storage binding NO guest formula can produce; guest storage - // bindings are stage*16+slot = 0..79) and its backend-mirror slot at 81 (never in any - // pipeline layout -- pure pass-through so DlssUpscaler can find the mask buffer; see - // MvppSurviveProbe.HostBinding/BackendMirrorBinding -- keep in sync). Only sizes the - // DescriptorSetUpdater arrays; rendering behavior is unchanged. public const int MaxStorageBufferBindings = MaxStorageBuffersPerStage * MaxShaderStages + 2; public const int MaxTextureBindings = MaxTexturesPerStage * MaxShaderStages; public const int MaxImageBindings = MaxImagesPerStage * MaxShaderStages; diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs index 66af898f7..d71480d8b 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs @@ -452,10 +452,6 @@ namespace Ryujinx.Graphics.Vulkan currentBufferRef = newRef; } - // [MVPP SURVIVE PROBE -- cutout bias mask] The mirror binding (81) is never part - // of any pipeline layout; a range passing through it IS the survivor mask buffer, - // already backend-mapped by the threaded layer. Capture it for DlssUpscaler's - // conversion compute. In-order (normal replay), written once per session. if (index == Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BackendMirrorBinding && buffer.Handle != BufferHandle.Null) { @@ -498,9 +494,6 @@ namespace Ryujinx.Graphics.Vulkan SignalDirty(DirtyFlags.Storage); } - // [MVREADBAR] Read-only scan (gated at the call site): is any currently-bound sampled - // texture the XC2 object-MV buffer? Bound refs are in-order at the backend by construction, - // which is what makes this the correct place to detect the MV-sampling draws. public bool MvppAnySampledMvBuffer() { for (int i = 0; i < _textureRefs.Length; i++) @@ -532,8 +525,8 @@ namespace Ryujinx.Graphics.Vulkan } else if (texture is TextureView view) { - MvppVkImgProbe.OnSampled(view); // [VKIMG] read-only, self-gated - MvppSampStoreProbe.OnSampled(stage, binding, view); // [SAMPSTORE] read-only, self-gated + MvppVkImgProbe.OnSampled(view); + MvppSampStoreProbe.OnSampled(stage, binding, view); ref TextureRef iRef = ref _textureRefs[binding]; @@ -560,8 +553,8 @@ namespace Ryujinx.Graphics.Vulkan { if (texture is TextureView view) { - MvppVkImgProbe.OnSampled(view); // [VKIMG] read-only, self-gated - MvppSampStoreProbe.OnSampled(stage, binding, view); // [SAMPSTORE] read-only, self-gated + MvppVkImgProbe.OnSampled(view); + MvppSampStoreProbe.OnSampled(stage, binding, view); view.Storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stage.ConvertToPipelineStageFlags()); @@ -891,7 +884,7 @@ namespace Ryujinx.Graphics.Vulkan ref DescriptorImageInfo texture = ref textures[i]; ref TextureRef refs = ref _textureRefs[binding + i]; - MvppDescTruthProbe.OnDescriptorWrite(refs.View, refs.ImageView); // [DESCTRUTH] read-only, self-gated + MvppDescTruthProbe.OnDescriptorWrite(refs.View, refs.ImageView); texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default; texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default; @@ -1014,7 +1007,7 @@ namespace Ryujinx.Graphics.Vulkan ref DescriptorImageInfo texture = ref textures[i]; ref TextureRef refs = ref _textureRefs[binding + i]; - MvppDescTruthProbe.OnDescriptorWrite(refs.View, refs.ImageView); // [DESCTRUTH] read-only, self-gated + MvppDescTruthProbe.OnDescriptorWrite(refs.View, refs.ImageView); texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default; texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default; diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs index cad89b031..c29451198 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssBinaries.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using System; using System.Collections.Generic; @@ -9,38 +6,16 @@ using System.IO; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// Locates the "bring your own" (BYO) DLSS runtime DLLs. - /// - /// Ryujinx never ships the proprietary nvngx_dlss.dll: the user supplies their own copy, - /// which they already own via any DLSS game or their NVIDIA driver. This locator first - /// checks an explicit "dlss" folder (where the user can drop the DLL), and if nothing is - /// there it auto-scans common game/launcher locations and returns the newest copy found. - /// The NVIDIA Streamline DLLs (MIT) may be bundled, but are looked up the same way so they - /// can also be supplied externally. - /// - /// Nothing here downloads or redistributes the proprietary DLL - it only points at a copy - /// the user already legitimately has on their machine. - /// public static class DlssBinaries { public const string NgxDlssDll = "nvngx_dlss.dll"; public const string StreamlineInterposer = "sl.interposer.dll"; - // Frame generation (Porte A). The three sl.* plugins are MIT and bundlable; FG requires - // Reflex/PCL markers so all three are checked together. nvngx_dlssg.dll is proprietary - // and BYO, like nvngx_dlss.dll - but unlike it, the driver reliably ships it in the - // DriverStore, so that is the primary fallback. public const string NgxDlssGDll = "nvngx_dlssg.dll"; public const string StreamlineDlssG = "sl.dlss_g.dll"; public const string StreamlineReflex = "sl.reflex.dll"; public const string StreamlinePcl = "sl.pcl.dll"; - // Memoized auto-scan result. The library scan walks every game folder of every launcher - // on every drive - hundreds of directories on every single launch when the DLL is not in - // the BYO folder - and the winning path almost never moves. Remember it, validate it - // still exists, and only rescan when it vanished. The BYO folder is checked first every - // time, so dropping a DLL there always overrides the cache. private static string PathCacheFile { get @@ -72,7 +47,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } catch { - // Unreadable cache = no cache; the scan below rebuilds it. } return null; @@ -90,18 +64,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } catch { - // Not fatal: next launch just scans again. } } - /// - /// Returns the path to a usable nvngx_dlss.dll, or null if none could be found. - /// - /// Explicit folder the user can drop the DLL into (highest priority). - /// When true, scan common game/driver locations if the BYO folder is empty. public static string LocateNgxDlss(string byoFolder, bool autoScan = true) { - // 1. Explicit BYO folder always wins. if (!string.IsNullOrEmpty(byoFolder)) { string explicitPath = Path.Combine(byoFolder, NgxDlssDll); @@ -116,14 +83,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return null; } - // 2. A still-valid memoized result skips the library walk entirely. string cached = ReadCachedNgxPath(); if (cached != null) { return cached; } - // 3. Auto-scan known locations and pick the newest version found. string best = null; Version bestVersion = null; @@ -149,9 +114,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return best; } - /// - /// Returns the folder containing sl.interposer.dll (bundled or in the BYO folder), or null. - /// public static string LocateStreamlineFolder(string byoFolder, string bundledFolder) { foreach (string folder in new[] { byoFolder, bundledFolder }) @@ -165,12 +127,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return null; } - /// - /// Returns which of the FG-related Streamline plugin DLLs (sl.dlss_g, sl.reflex, sl.pcl) - /// are missing from . Empty array == all present. These are the - /// MIT-licensed, bundlable pieces - kept distinct from the proprietary NGX binary so a - /// failed FG probe is attributable to the right missing piece. - /// public static string[] MissingFrameGenPlugins(string folder) { if (string.IsNullOrEmpty(folder)) @@ -191,15 +147,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return missing.ToArray(); } - /// - /// Porte B.1: makes sure nvngx_dlssg.dll is INSIDE the plugin folder, which is where NGX - /// loads it from (the DLSS_G plugin does not search the DriverStore itself - confirmed by - /// the manual copy being required so far). If the folder has no copy (or an older one), - /// the newest DriverStore copy is staged in. Never downloads anything: the file already - /// sits on the user's machine, shipped by their NVIDIA driver. - /// describes the outcome for attributable logging. - /// Returns the usable path (in-folder when possible), or null if none exists anywhere. - /// public static string StageNgxDlssG(string dlssFolder, out string action) { if (string.IsNullOrEmpty(dlssFolder)) @@ -210,12 +157,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } string target = Path.Combine(dlssFolder, NgxDlssGDll); - string driverCopy = LocateNgxDlssG(null); // DriverStore only (byo folder skipped) + string driverCopy = LocateNgxDlssG(null); if (File.Exists(target)) { - // Refresh only if the driver ships a strictly newer build; otherwise leave the - // user's copy alone (they may have pinned a specific version on purpose). Version targetVersion = GetFileVersion(target); Version driverVersion = driverCopy != null ? GetFileVersion(driverCopy) : null; @@ -255,20 +200,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } catch (Exception) { - // Plugin folder not writable (e.g. Program Files install). The file exists but NGX - // will not find it - tell the user exactly what to copy where. action = $"found at \"{driverCopy}\" but could NOT be copied into the plugin folder - copy it there manually"; return driverCopy; } } - /// - /// Returns the path to a usable nvngx_dlssg.dll (proprietary, BYO), or null. Checks the - /// explicit BYO folder first, then the NVIDIA DriverStore (nv_dispi.inf_amd64_*), where - /// the driver reliably ships it. The DriverStore scan is targeted (top level of each - /// driver folder only), so it stays fast. Newest file version wins. - /// public static string LocateNgxDlssG(string byoFolder) { if (!string.IsNullOrEmpty(byoFolder)) @@ -319,8 +256,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static IEnumerable GetScanRoots() { - // Common launcher install roots, per drive. Kept curated so the scan stays fast - // instead of walking entire drives. string[] relativeRoots = { @"SteamLibrary\steamapps\common", @@ -344,8 +279,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // The NVIDIA driver ships NGX DLLs here (usually nvngx_dlssg/_dlssd; base dlss may - // be present depending on driver), so include it as a fallback. string driverStore = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\DriverStore\FileRepository"); @@ -389,8 +322,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // Recursive enumeration that swallows access errors on individual subtrees instead of - // aborting the whole scan (game folders often contain locked/protected directories). private static IEnumerable SafeEnumerate(string root, string fileName) { Queue pending = new(); @@ -407,7 +338,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } catch { - // ignored - unreadable directory } if (matches != null) @@ -425,7 +355,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } catch { - // ignored } if (subDirs != null) @@ -451,7 +380,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } catch { - // ignored - treat as unknown version } return null; diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssFrameGenHooks.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssFrameGenHooks.cs index 4f5685d01..e91060988 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssFrameGenHooks.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssFrameGenHooks.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Silk.NET.Core.Contexts; @@ -10,32 +7,8 @@ using System; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// DLSS-FG Porte B.1 - Streamline "manual hooking" for Vulkan (SL SDK 2.12, - /// docs/ProgrammingGuideManualHooking.md §1.1-Vulkan/§4.2). - /// - /// sl.interposer.dll exports its own vkGetDeviceProcAddr which returns vulkan-1.dll - /// addresses for the whole API EXCEPT the entry points SL intercepts (sl_hooks.h - /// FunctionHookID: eVulkan_Present, eVulkan_CreateSwapchainKHR, eVulkan_DestroySwapchainKHR, - /// eVulkan_GetSwapchainImagesKHR, eVulkan_AcquireNextImageKHR, eVulkan_DeviceWaitIdle, - /// eVulkan_CreateWin32SurfaceKHR, eVulkan_DestroySurfaceKHR) - for those it returns SL's - /// proxies. This class is an over that resolver, so the - /// (a Silk.NET KhrSwapchain built on it) routes exactly the - /// hooked swapchain/present calls through Streamline and nothing else. - /// - /// Instances only exist when RYUJINX_DLSS_FG=1 AND the DLSS_G probe reported supported; - /// when VulkanRenderer.FgHooks is null, Window.cs presents through the untouched - /// native SwapchainApi - the flag-off path is byte-for-byte today's behavior. - /// - /// KNOWN LIMITS (deliberate, logged, to be judged on sl.log): - /// - The VkSurface is created by the UI layer long before slInit, so the two surface - /// hooks are NOT routed. If sl.dlss_g needs them (HWND tracking) sl.log will say so. - /// - The first swapchain predates slInit; VulkanRenderer marks it dirty so the first - /// present after arming recreates it through the proxy. - /// class DlssFrameGenHooks : INativeContext { - // The sl_hooks.h-mandated Vulkan entry points we route (surface hooks excluded, see above). private static readonly string[] _hookedFunctions = { "vkCreateSwapchainKHR", @@ -51,10 +24,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private readonly IntPtr _devicePtr; private readonly IntPtr _deviceWaitIdleProxy; - /// - /// KhrSwapchain whose five swapchain/present entry points resolve to SL's proxies. - /// Use ONLY for swapchains created through it (SL tracks those handles internally). - /// public KhrSwapchain SwapchainProxy { get; } private DlssFrameGenHooks(Vk api, Device device, IntPtr deviceWaitIdleProxy) @@ -67,12 +36,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss SwapchainProxy = new KhrSwapchain(this); } - /// - /// Probes the interposer for every mandatory swapchain proxy and logs, per function, - /// whether SL actually intercepts it (proxy address differs from the native one) or - /// passes through. Returns null - and changes nothing - unless ALL five swapchain - /// entry points resolve, so a partially-hooked present path can never be armed. - /// public static DlssFrameGenHooks TryCreate(Vk api, Device device) { if (!Streamline.IsInitialized) @@ -91,8 +54,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss if (slPtr == IntPtr.Zero) { - // vkDeviceWaitIdle proxy missing is tolerable (we fall back to the native - // wait); a missing swapchain/present proxy is not - FG cannot inject frames. if (name == "vkDeviceWaitIdle") { Logger.Warning?.Print(LogClass.Gpu, @@ -113,8 +74,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss deviceWaitIdleProxy = slPtr; } - // Attribution: identical addresses mean the interposer is passing this function - // through (not intercepting it on this setup) - valid, but worth seeing per-run. Logger.Info?.Print(LogClass.Gpu, slPtr == (IntPtr)nativePtr ? $"DLSS-FG: {name} -> passthrough (interposer returned the native address)." : $"DLSS-FG: {name} -> SL proxy installed (0x{slPtr:X} vs native 0x{nativePtr:X})."); @@ -128,11 +87,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return new DlssFrameGenHooks(api, device, deviceWaitIdleProxy); } - /// - /// Routes vkDeviceWaitIdle through SL's proxy (eVulkan_DeviceWaitIdle) so Streamline can - /// retire in-flight frame-generation work before a swapchain teardown. Falls back to the - /// native wait if the proxy was unavailable - the wait itself must always happen. - /// public unsafe void DeviceWaitIdle() { if (_deviceWaitIdleProxy != IntPtr.Zero) @@ -145,11 +99,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // --- INativeContext: resolver behind SwapchainProxy --- - // SL's vkGetDeviceProcAddr already covers the full API (native addresses for anything it - // does not intercept), so the native-loader fallback below only triggers if the export - // itself failed for a given name. - public nint GetProcAddress(string proc, int? slot = default) { IntPtr addr = Streamline.GetVulkanDeviceProxy(_devicePtr, proc); @@ -170,7 +119,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public void Dispose() { - // Nothing owned: the interposer module belongs to Streamline, the device to the renderer. } } } diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssGpuTimer.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssGpuTimer.cs index 884f43c32..78271595e 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssGpuTimer.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssGpuTimer.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). Diagnostic only; no NVIDIA SDK code. using Ryujinx.Common.Logging; using Silk.NET.Vulkan; @@ -8,24 +5,16 @@ using System; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// Diagnostic GPU timer for the DLSS path (RYUJINX_DLSS_GPUTIME=1). Brackets the per-frame DLSS GPU - /// work (motion + depth + slEvaluateFeature) with a pair of Vulkan timestamps and reports the GPU - /// time, so a steady/creeping heavy load can be told apart from a sudden GPU hang -- the in-game - /// ErrorDeviceLost is a ~2s TDR, which looks very different here (a slow climb vs results that simply - /// stop arriving on the killer frame). Results are read back host-side a few frames late so the read - /// never stalls the GPU. Off by default; the build is byte-identical when unset. - /// internal sealed class DlssGpuTimer : IDisposable { private const int Ring = 4; - private const int Count = Ring * 2; // two timestamps (start, end) per ring slot - private const double SpikeMs = 100.0; // a single frame above this is logged immediately + private const int Count = Ring * 2; + private const double SpikeMs = 100.0; - private readonly string _label; // [18/07] plusieurs instances (global vs par-passe) -> logs distincts + private readonly string _label; private readonly Vk _api; private readonly Device _device; - private readonly double _periodNs; // nanoseconds per timestamp tick + private readonly double _periodNs; private readonly bool _supported; private QueryPool _pool; @@ -64,12 +53,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _slotFrame[i] = -1; } - // Unconditional: confirms the probe armed and surfaces the tick period. If this line is absent - // from a run, the RYUJINX_DLSS_GPUTIME flag never reached this process (wrong launcher/env). Logger.Info?.Print(LogClass.Gpu, $"{_label} GPU timer ARMED: timestampPeriod={_periodNs:0.###} ns, supported={_supported}."); } - /// Records the start timestamp for this frame and returns the ring slot (or -1). public void Begin(CommandBuffer cmd) { if (!_supported) @@ -83,7 +69,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _slotFrame[slot] = _frame; } - /// Records the end timestamp for this frame's slot. public void End(CommandBuffer cmd) { if (!_supported) @@ -96,7 +81,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _frame++; } - /// Reads back the slot written Ring frames ago (long since complete) host-side. public unsafe void Collect() { if (!_supported) diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs index 8bcedad6e..2f2802973 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssIntegration.cs @@ -1,198 +1,77 @@ -// 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.Vulkan.Dlss { - /// - /// Central on/off switch and shared constants for the experimental DLSS integration. - /// - /// Everything DLSS-related (loading Streamline, the extra Vulkan device extensions/features, - /// slSetVulkanInfo, the present-time evaluate) is gated on the RYUJINX_DLSS environment - /// variable. When it is unset the default code path is byte-identical to upstream, so a normal - /// launch never loads Streamline nor changes how the Vulkan device is created. - /// public static class DlssIntegration { private static readonly bool _enabled = IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_DLSS")); - // DLSS-SR fractional internal (RYUJINX_DLSS_SR): the mip-align padding in TextureCache makes a - // fractional internal resolution crash-safe, so keep the exact preset-fitting scale instead of - // snapping it to an integer. The integer snap forced the internal to a ratio that fell between - // DLSS presets -> "no preset fits" fallback -> DLSS-SR never actually ran. private static readonly bool _srFractional = IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_DLSS_SR")); - // One-shot auto Resolution Scale (RYUJINX_DLSS_AUTO_RES): on the first upscaling frame, DlssUpscaler - // nudges the guest resolution scale so the internal render lands on the chosen mode's optimal preset - // input for the ACTUAL display, then never touches it again -- every mode (Quality..UltraPerformance) - // hits its ideal preset with no blurry spatial blit, via a single slider-equivalent write (no runtime - // piloting). Requires DLSS-SR, whose mip-align padding makes the resulting fractional internal - // crash-safe; without it the fractional scale would fall between presets and be snapped away. public static readonly bool AutoResScale = _srFractional && IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_DLSS_AUTO_RES")); - /// True when the user opted into DLSS via RYUJINX_DLSS=1. public static bool IsEnabled => _enabled; - /// - /// True when the DLSS-SR fractional path (RYUJINX_DLSS_SR=1) is active. Read by - /// TextureStorage to zero fresh image allocations at birth: the memory suballocator - /// recycles freed device memory within the process, and on this path a render target - /// can reach DLSS (and the screen) before the game's own writes cover the quantized - /// allocation -- the recycled bytes are then a recognizable earlier frame (the - /// startup ghost image in Quality). Off this path the flag stays false and texture - /// creation is byte-identical to upstream. - /// public static bool SrFractional => _srFractional; - /// - /// RYUJINX_DLSS_NOBIRTHCLEAR=1: diagnostic kill-switch for the TEXTURE birth-clear only - /// (the swapchain birth-clear stays on -- it owns the validated FG startup-ghost fix). - /// A/B for the 13/07 question: does zeroing fresh allocations HARDEN the fractional-path - /// black line (the uncovered rim used to show soft recycled memory, now pure black)? - /// public static readonly bool BirthClearDisabled = IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_DLSS_NOBIRTHCLEAR")); - /// - /// RYUJINX_BIRTHCLEAR_ALL=1 (OFF by default): zero EVERY fresh texture allocation, not - /// only those on the DLSS-SR fractional path. - /// - /// Diagnostic for the 21/07 Xenoblade 2 artifact -- blocks of recognizable-but-wrong - /// content appearing when the camera TURNS. Measured on Alex's machine with DLSS OFF - /// (log: read mode=0, zero "using mode"), no mods, shader cache purged, native res: the - /// artifact is there, so it belongs to none of the DLSS/MV++/FG stack. What it does match - /// is the mechanism this file already documents above: the Vulkan backend never clears a - /// new image (Undefined -> General only) and the suballocator recycles freed device - /// memory, so a render target the game has not finished writing shows an EARLIER FRAME. - /// Turning the camera is exactly when a streaming game allocates new targets. - /// - /// The existing fix is gated to SrFractional on purpose, to keep texture creation - /// byte-identical to upstream everywhere else. This switch lifts that gate for the test - /// only: artifact gone => the cause is recycled memory and we know what to build; still - /// there => the mechanism is ruled out and we drop it. - /// public static readonly bool BirthClearAll = IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_BIRTHCLEAR_ALL")); - // Porte A - DLSS Frame Generation probe (RYUJINX_DLSS_FG=1, requires RYUJINX_DLSS=1): - // adds DLSS_G to slInit's featuresToLoad and probes slIsFeatureSupported(DLSS_G) after - // device registration. Diagnostic only: no frame is ever generated (present-injection is - // not wired), and with the flag off featuresToLoad stays {DLSS} - the daily-driver and - // SR paths are byte-identical. slInit is once-per-process, so rollback == unset the flag. public static readonly bool FrameGenRequested = _enabled && IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_DLSS_FG")); - /// - /// Whether DLSS is fed the median-filtered motion field (outliers removed) instead of the raw - /// optical-flow field. On by default; set RYUJINX_DLSS_MV_FILTER=0 to A/B against the raw field. - /// The instrumentation logs both raw and filtered metrics regardless of which one is used. - /// public static readonly bool MvFilterEnabled = Environment.GetEnvironmentVariable("RYUJINX_DLSS_MV_FILTER") is not ("0" or "false" or "off"); - /// - /// Whether the per-frame motion-field metrics (RMS/outliers) are accumulated and logged. OFF by - /// default: the stats use hundreds of thousands of atomic adds per frame, which is fine for a - /// measurement run but costs frame time, so it is a dev tool gated on RYUJINX_DLSS_METRICS=1. - /// public static readonly bool MetricsEnabled = Environment.GetEnvironmentVariable("RYUJINX_DLSS_METRICS") is "1" or "true" or "on"; - /// - /// Diagnostic flag (RYUJINX_DLSS_FORCE_DLSS=1): force pure DLSS every frame — never hand off to - /// the spatial (NIS) fallback and never reset history mid-scene. Lets a camera pan be judged on - /// DLSS's motion reconstruction in isolation, with no DLSS<->NIS pumping or history churn. - /// OFF by default (the build is byte-identical); intended only for A/B measurement runs. - /// public static readonly bool ForceDlss = Environment.GetEnvironmentVariable("RYUJINX_DLSS_FORCE_DLSS") is "1" or "true" or "on"; - /// - /// Diagnostic (RYUJINX_DLSS_GPUTIME=1): bracket the DLSS GPU work (motion + depth + evaluate) - /// with Vulkan timestamps and log the per-frame GPU time, to tell a heavy/creeping load apart - /// from a sudden GPU hang (the in-game ErrorDeviceLost is a ~2s TDR). Off by default. - /// public static readonly bool GpuTimingEnabled = Environment.GetEnvironmentVariable("RYUJINX_DLSS_GPUTIME") is "1" or "true" or "on"; - /// - /// (RYUJINX_DLSS_OUTPUT_BARRIER, default ON) Insert an explicit image barrier on the DLSS output - /// between the NGX evaluate and the blit that reads it. NGX writes that image through raw Vulkan - /// handles, outside Ryujinx's resource tracking, so without this neither the tracker nor the - /// validation layer adds a write->read dependency -- a candidate for the rare device-lost hang. - /// Correct on its own merits; gated so it can be A/B'd (set to 0/false/off to disable). - /// public static readonly bool OutputBarrierEnabled = Environment.GetEnvironmentVariable("RYUJINX_DLSS_OUTPUT_BARRIER") is not ("0" or "false" or "off"); - /// - /// Extra Vulkan device extensions NGX/DLSS needs, on top of what Ryujinx already enables - /// (VK_KHR_push_descriptor is already in the desirable list). Enabled only when supported. - /// public static readonly string[] DeviceExtensions = { "VK_NVX_binary_import", "VK_NVX_image_view_handle", "VK_KHR_buffer_device_address", - // NOTE: VK_EXT_buffer_device_address removed -- the Vulkan 1.2 core feature - // (featuresVk12.BufferDeviceAddress, VulkanInitialization.cs) already provides this, and the - // Vulkan spec FORBIDS enabling both the KHR/core path and the older EXT extension together - // (VUID-VkDeviceCreateInfo-ppEnabledExtensionNames-03328 / -04748). Listing both produced - // validation errors and an invalid device -- a credible cause of the random device-lost. - "VK_NV_optical_flow", // NVOFA (B2): hardware optical flow accelerator for accurate motion vectors - "VK_KHR_synchronization2", // required dependency of VK_NV_optical_flow - "VK_KHR_format_feature_flags2", // required dependency of VK_NV_optical_flow (was missing -> VUID-01387, invalid device) + "VK_NV_optical_flow", + "VK_KHR_synchronization2", + "VK_KHR_format_feature_flags2", }; - /// - /// Injected by the host (which owns GraphicsConfig.ResScale) so the Vulkan backend can read - /// and drive the guest resolution scale without referencing the GPU project. Used to make the - /// internal render resolution match the chosen DLSS mode. - /// public static Func ResolutionScaleGetter; public static Action ResolutionScaleSetter; - /// The user's configured resolution scale, captured before a mode preset is applied. public static float UserBaseResScale = 1f; private static bool _modeLogged; - /// - /// The selected DLSS quality mode (RYUJINX_DLSS_MODE), defaulting to Native. Under the - /// "no automatic piloting" design this only chooses the one-shot Resolution Scale preset - /// applied by ; DLSS itself picks the matching preset - /// from whatever internal resolution results, and never steers the resolution at runtime. - /// public static readonly DlssQualityMode Mode = ParseQualityMode(Environment.GetEnvironmentVariable("RYUJINX_DLSS_MODE")) ?? DlssQualityMode.Native; - /// - /// Render-resolution factor for a quality mode, relative to the user's configured scale. - /// Native = 1 (untouched); the upscale modes render below it so DLSS reconstructs upward. - /// public static float RenderScaleFactor(DlssQualityMode mode) { return mode switch { - DlssQualityMode.Quality => 0.667f, // 1 / 1.5 - DlssQualityMode.Balanced => 0.581f, // 1 / 1.72 - DlssQualityMode.Performance => 0.5f, // 1 / 2 - DlssQualityMode.UltraPerformance => 0.333f, // 1 / 3 - _ => 1f, // Native: leave the user's resolution alone. + DlssQualityMode.Quality => 0.667f, + DlssQualityMode.Balanced => 0.581f, + DlssQualityMode.Performance => 0.5f, + DlssQualityMode.UltraPerformance => 0.333f, + _ => 1f, }; } - /// - /// Applies the selected quality mode's render-scale preset to the guest resolution by writing - /// GraphicsConfig.ResScale through the host-injected setter -- equivalent to the user nudging - /// their Resolution Scale slider. The host calls this at config time (not per frame), right - /// after it resets the scale to the configured value, so it always derives from a clean base - /// and never compounds. This deliberately avoids the runtime resolution-driving that previously - /// destabilized the renderer. No-op for Native or when DLSS is off. - /// public static void ApplyModeResolutionScale() { if (!_enabled || ResolutionScaleGetter == null || ResolutionScaleSetter == null) @@ -216,12 +95,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss float scaled = baseScale * factor; - // DLSS device-lost investigation: snap the internal resolution scale to an INTEGER. A fractional - // scale (e.g. 2 x 0.667 = 1.33) makes Ryujinx round each render target's size independently, so a - // target's mip can end up 1px smaller than the framebuffer built from it -> VUID-04533/04534 -> - // a likely out-of-bounds write and the random device-lost. Integer scales keep mip sizes aligned. - // With RYUJINX_DLSS_SR on, the fractional internal is crash-safe (mip-align padding), so keep - // the exact scale so DLSS-SR lands inside a preset's dynamic range. Otherwise snap to integer. float aligned = _srFractional ? MathF.Max(0.1f, scaled) : MathF.Max(1f, MathF.Round(scaled)); ResolutionScaleSetter(aligned); @@ -253,12 +126,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// User-facing DLSS quality mode. Under the "no automatic piloting" design these act only as a - /// one-shot Resolution Scale preset (see ): - /// Native leaves the user's resolution alone (DLSS just anti-aliases it), the others render below - /// it so DLSS upscales for more FPS. DLSS never drives the resolution at runtime. - /// public enum DlssQualityMode { Native, diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs index 57d931fb6..67340a970 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssJitter.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Graphics.GAL; using System; @@ -8,15 +5,6 @@ using System.Globalization; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// Generates the deterministic Halton(2,3) sub-pixel jitter for DLSS "Mode B" and publishes the - /// NEXT frame's offset to the cross-layer , where the GPU layer reads - /// it to shift the resolution-scaled viewport. The value the present actually hands to DLSS is the - /// one carried alongside that frame through the present queue (DlssJitterState.PresentX/Y), not read - /// from here, so it always matches the jitter baked into that exact frame regardless of frame-queue depth. - /// - /// Gated on RYUJINX_DLSS_JITTER=1; off => DlssJitterState stays disabled and the path is identical. - /// public static class DlssJitter { private static readonly bool _flag = @@ -24,10 +12,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public static bool Enabled => _flag && DlssIntegration.IsEnabled; - // Jitter amplitude in render pixels: the Halton offset (+-0.5) is multiplied by this before it is - // published. 1.0 = a real +-0.5px sub-pixel jitter (what DLSS wants). Live-tunable via - // RYUJINX_DLSS_JITTER_SCALE so a large value (e.g. 10 = +-5px) can be used as a VISIBLE "reticle" - // test to confirm the clip-space gl_Position injection is actually moving the image. Read once. private static readonly float _scale = ParseScale(); public static float Scale => _scale; @@ -43,12 +27,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return scale; } - return 1.0f; // real +-0.5px sub-pixel jitter by default + return 1.0f; } - // Texture mip LOD bias applied to every guest sampler while jitter is on (NVIDIA's DLSS guidance: a - // negative bias forces a sharper, stable mip so sub-pixel jitter does not flicker the mip selection - // frame to frame). Live-tunable via RYUJINX_DLSS_JITTER_LOD_BIAS; defaults to -0.5. Read once. private static readonly float _lodBias = ParseLodBias(); public static float LodBias => _lodBias; @@ -66,14 +47,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return -0.5f; } - private const uint SequenceLength = 16; // a short Halton phase set, classic for DLSS + private const uint SequenceLength = 16; private static uint _index; - /// - /// Publishes the next frame's jitter offset to the GPU layer. Called once per present; the - /// offset takes effect on the next guest frame's scaled viewport. No-op (disabled) when off. - /// public static void Advance() { if (!Enabled) @@ -85,26 +62,18 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return; } - _index = _index % SequenceLength + 1; // 1..N (Halton is undefined at 0) + _index = _index % SequenceLength + 1; DlssJitterState.Enabled = true; - // RYUJINX_DLSS_JITTER_DECLONLY=1: declaration WITHOUT image injection (diagnostic split). DlssJitterState.Inject = Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_DECLONLY") != "1"; - // RYUJINX_DLSS_JITTER_UI=1: also jitter the full-screen scale-1 (UI) passes. DlssJitterState.JitterUi = Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_UI") == "1"; - // RYUJINX_DLSS_JITTER_NOSHADOW=1: exclude square scaled passes (shadow atlases). DlssJitterState.NoShadowJitter = Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_NOSHADOW") == "1"; - // Camera-pass-only injection is the DEFAULT since 05/07 (validated in game, run H): the - // NVIDIA rule -- jitter is applied at ONE place, the main camera geometry pass; shadows, - // post chains and UI are never jittered by an engine. RYUJINX_DLSS_JITTER_CAMPASS=0 - // restores the legacy inject-everything behavior (diagnostic only). DlssJitterState.CamPassOnly = Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_CAMPASS") != "0"; DlssJitterState.OffsetX = (Halton(_index, 2) - 0.5f) * _scale; DlssJitterState.OffsetY = (Halton(_index, 3) - 0.5f) * _scale; - // [JITTERVAL probe] one id per generated offset; rides the present queue with the frame. DlssJitterState.FrameId++; } diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssSharpenPass.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssSharpenPass.cs index 0d067a3e8..1ec324c5f 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssSharpenPass.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssSharpenPass.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). Reuses the fork's FSR RCAS sharpening shader (new LINEAR variant). using Ryujinx.Common; using Ryujinx.Common.Logging; @@ -13,17 +10,6 @@ using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// [SHARPEN 02/08, journal (372)] Nettete post-DLSS en PRE-PASSE : RCAS maison (variante - /// LINEAR_OUTPUT de FsrSharpening.glsl — float lineaire, AUCUN encodage) applique 1:1 sur la - /// sortie DLSS vers une texture temporaire ; le blit final habituel (tonemap scRGB/PQ, flip, - /// barres d'aspect) consomme ensuite la version nette. Universel : aucune contrainte de - /// cadre, contrairement au remplacement du blit (ecarte — fenetre 3840x2075 avec barres - /// mesuree au log). Meme mecanique RCAS-sur-lineaire que la variante HDR validee a l'oeil - /// a l'epoque du chantier HDR. - /// Gate RYUJINX_DLSS_SHARPEN=1 (OFF par defaut) ; force RYUJINX_DLSS_SHARPEN_LEVEL 0-100 - /// (defaut 25 ; mappee comme le slider FSR de l'UI : 1.5 - level*0.015). - /// class DlssSharpenPass : IDisposable { public static readonly bool Enabled = @@ -48,8 +34,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _pipeline = new PipelineHelperShader(renderer, device); _pipeline.Initialize(); - // Meme layout que la passe de nettete du FsrScalingFilter (bindings declares 2/3/4 ; - // seule la 4 est consommee par le shader RCAS). ResourceLayout layout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 3) @@ -67,8 +51,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ], layout); } - /// Nettete 1:1 de la sortie DLSS vers la texture temporaire ; rend la version - /// nette (ou null si la creation de la temporaire echoue — l'appelant blitte l'original). public TextureView Run(TextureView input, CommandBufferScoped cbs) { if (_sharpened == null || @@ -92,7 +74,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"DLSS nettete: ARMEE (SHARPEN=1, niveau {Level}/100) - RCAS lineaire maison en pre-passe sur la sortie DLSS."); } - // Meme encodage de force que le slider FSR de l'UI. ReadOnlySpan sharpening = [1.5f - (Level * 0.01f * 1.5f)]; using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, sizeof(float)); buffer.Holder.SetDataUnchecked(buffer.Offset, sharpening); diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs index 92fbe4de7..77b3fda57 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/DlssUpscaler.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common; using Ryujinx.Common.Logging; @@ -16,47 +13,25 @@ using VkFormat = Silk.NET.Vulkan.Format; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// Drives DLSS-SR at present time: upscales the game's framebuffer into a full-resolution target - /// via slEvaluateFeature, then blits that target to the swapchain with the usual scRGB/HDR - /// tone-map. Motion vectors are estimated with a compute-shader optical flow between the previous - /// and current frame (no dedicated optical-flow queue needed); depth is still a zeroed dummy. - /// Everything is gated by ; on any failure TryRun returns - /// false so the caller falls back to the normal blit/scaling path. - /// internal sealed class DlssUpscaler : IDisposable { - internal const uint ViewportId = 0; // shared with the FG present-time calls (Window.cs) - private const float MaxMotion = 32f; // motion-vector clamp, in render-resolution pixels - private const float SceneChangeHighFraction = 0.85f; // arm a reset only on a near-total change (real scene cut), not fast camera motion - private const float SceneChangeLowFraction = 0.55f; // ... and disarm (hysteresis) once it drops back below this - private const int ResetCooldownFrames = 45; // min frames between resets (~0.75s), so fast-pan blips don't reset repeatedly - private const float SceneCutMaxMotion = 0.02f; // a history reset only fires when motion is below this (real cuts have ~0 motion; gameplay has more) - private const int CounterCount = 9; // uints in the motion-pass SSBO (scene-change counters; the trailing stat slots are now always zero) + internal const uint ViewportId = 0; + private const float MaxMotion = 32f; + private const float SceneChangeHighFraction = 0.85f; + private const float SceneChangeLowFraction = 0.55f; + private const int ResetCooldownFrames = 45; + private const float SceneCutMaxMotion = 0.02f; + private const int CounterCount = 9; - // MV++ Phase 1: camera view-projection pair of the two most recent presented frames - // (curr = the frame being upscaled now) and the reprojection MV pass state. - // RYUJINX_MVPP_REPROJ: 0 = off, 1 = diagnostic (compute + compare against the optical - // flow, DLSS still consumes the flow), 2 = DLSS consumes the reprojection vectors. private static readonly int _mvppReprojMode = int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_MVPP_REPROJ"), out int mvppEnvMode) ? mvppEnvMode : 0; - // RYUJINX_MVPP_FLIPY: assume the flipped NDC y orientation in the reprojection shader -- - // the one genuinely ambiguous convention in the chain, settled empirically in mode 2 - // with DLSS as the judge (wrong y = massive smearing on vertical pans). private static readonly bool _mvppFlipY = Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLIPY") == "1"; - // RYUJINX_MVPP_VPDELAY: pair the presented frame with a one-present-older VP pair -- - // A/B probe for the enqueue-time snapshot's frame skew (the "slides briefly on a hard - // stop" artifact). Slide gone = the VP stream led the image by one present (ship the - // delay); slide longer = it lags (the fix belongs at the GPU-thread frame boundary). private static readonly bool _mvppVpDelay = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VPDELAY") == "1"; - // RYUJINX_MVPP_ZGL: window-depth -> NDC-z convention override for the reprojection - // shader (1 = GL-style (z+1)/2, 0 = direct). Unset = auto from the guest's viewport - // transform (DlssCameraState.GuestDepthMinusOneToOne). private static readonly string _mvppZglEnv = Environment.GetEnvironmentVariable("RYUJINX_MVPP_ZGL"); @@ -66,102 +41,38 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static readonly bool _mvppTrace = Environment.GetEnvironmentVariable("RYUJINX_MVPP_TRACE") == "1"; - // RYUJINX_MVPP_ZERO: mode 2 with the reprojection pass writing ZERO motion vectors -- - // the isolation test. Zero MVs = exactly what the stable flow baseline feeds, so: - // still trembles = the pass itself disturbs the game (depth sampling side effect); - // stable = the MV VALUES are the tremble, and the center-pixel mv log has the numbers. private static readonly bool _mvppZeroMv = Environment.GetEnvironmentVariable("RYUJINX_MVPP_ZERO") == "1"; - // RYUJINX_MVPP_DYNMASK=1 enables the dynamic-object/disocclusion mask (Phase 1.5): - // pixels whose depth disagrees with the previous frame's reprojected depth moved BY - // THEMSELVES (or were disoccluded) -- camera MVs are wrong for them, they read zero. - // OFF BY DEFAULT after in-game evaluation: vegetation is world-anchored (camera MVs - // are ~right for it) but its depth churns, so the mask zeroes whole grass fields and - // forward running ghosts radially. Needs a smarter classifier before it ships. private static readonly bool _mvppDynMask = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DYNMASK") == "1"; - // RYUJINX_MVPP_BORDERFIX=1 (dossier ligne noire, 18/07) : le jeu rendu à résolution - // scalée laisse un cadre de texels PUR NOIR au bord de sa compose finale (mesuré aux - // dumps : 2 px haut+gauche, = LE « cadre noir » vu par l'utilisateur en pan). Cette - // passe le remplace par le premier pixel valide vers l'intérieur, AVANT DLSS — - // l'image, DLSS et la FG le perdent d'un coup. Détection par balayage (cap _SCAN, - // défaut 8 : un bord noir épais type cinématique ne trouve rien dans le cap → intact). private static readonly bool _mvppBorderFix = Environment.GetEnvironmentVariable("RYUJINX_MVPP_BORDERFIX") == "1"; private static readonly float _mvppBorderFixScan = ParseEnvFloat("RYUJINX_MVPP_BORDERFIX_SCAN", 8f); private static readonly float _mvppBorderFixThresh = ParseEnvFloat("RYUJINX_MVPP_BORDERFIX_THRESH", 2f); - // RYUJINX_MVPP_DYNSONDE=1 (étape 2, 18/07) : le masque dynamique en mode SONDE — - // le compare-depth tourne et compte (population, zones, vitalité du flow, désaccord - // flow-vs-caméra) mais ne zérote RIEN : la sortie MV est celle du bat royal à - // l'octet. Cartographie AVANT toute fusion (sonde d'abord, bouton ensuite). private static readonly bool _mvppDynSonde = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DYNSONDE") == "1"; - // RYUJINX_MVPP_DEPTHSONDE=1 (garde-fou depth-sanity, 19/07) : sonde lecture-seule. - // Mesure, sur les statPixels du chemin normal, la contradiction « la depth dit LOIN - // mais le MV translate fort » (résidu |mv point-based − rotation-only| sur les pixels - // quasi-far 0.98<=d<1.0, + population contrôle mi-plage). Le sky% ne discrimine PAS - // (mesuré 19/07 : TOTK vit à 40-99 % de ciel, BOTW plafonne à 48 % — inversé) ; la - // vraie signature BOTW = décor entier lu quasi-far avec reconstruction folle (worldY - // −5263 m) → translations fantômes → ghost. Slots 226-229 (padding mort pré-256). - // Nécessite statPixel armé (DEV=1 au banc) ; défaut OFF, zéro effet sur la sortie. private static readonly bool _mvppDepthSonde = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHSONDE") == "1"; - // RYUJINX_MVPP_CHECKER=1 (28/07) : sonde lecture-seule de DISCONTINUITÉ DE CHEMIN. - // Hypothèse testée : le ciel/la brume lointaine de XC2 sont rendus en demi-résolution - // ALTERNÉE (damier). Le chemin de reprojection étant choisi par pixel sur un seuil de - // depth, un pixel sur deux du même nuage part dans l'autre famille de vecteurs, et le - // damier alternant d'une image à l'autre, un même point bascule -> « les nuages - // apparaissent et disparaissent » (Alex, 28/07, au loin, caméra en mouvement). - // Discriminant = chkDiag (orthogonaux opposés + diagonal concordant) : un bord d'objet - // ordinaire est une courbe et ne peut pas produire cette signature. Slots 233-236, - // réserve libre. Aucun paramètre ajouté au bloc d'uniformes (reste à 53/53). private static readonly bool _mvppChecker = Environment.GetEnvironmentVariable("RYUJINX_MVPP_CHECKER") == "1"; - // RYUJINX_MVPP_WARMUP= (28/07) : DELAI DE GRACE avant que la reprojection ecrive. - // - // Mesure du jour : les lignes noires autour des arbres apparaissent ou non SELON LA - // VITESSE DE CHARGEMENT. Meme code, trois lancements : au premier (chargement a froid, - // decor lent a arriver) AUCUNE ligne ; aux suivants (chargement a chaud) elles reviennent. - // Alex l'a note de lui-meme : « la fois ou ca a marche, les feuilles ne sont pas apparues - // tout de suite ». - // - // Lecture : quand le decor arrive TARD, MV++ a eu le temps de se stabiliser. Quand il - // arrive TOT, la passe ecrit ses tout premiers vecteurs -- les moins fiables, juste apres - // l'election -- pile pendant que le feuillage se dessine, et DLSS aligne son historique - // dessus. Le defaut n'est donc pas dans la logique mais dans le MOMENT. - // - // Ici on exige N images de paire de matrices valide AVANT de laisser la passe ecrire. Le - // comportement cesse de dependre de la vitesse du disque. 0 = desactive (comportement - // historique). 30 images ~ 1 seconde a 30 fps. private static readonly int _mvppWarmup = Math.Max(0, (int)ParseEnvFloat("RYUJINX_MVPP_WARMUP", 0f)); private int _mvppWarmupCount; - // RYUJINX_MVPP_DEPTHGUARD=1 (19/07) : garde-fou depth-sanity — décision d'Alex 18/07 - // (« générique prudent », le 1 du plan 1-puis-2). Détection : en pan, un résidu - // |mv point-based − rotation-pure| ÉGAL à toutes les profondeurs est non-physique - // (la rotation n'a pas besoin de depth ; seule une translation crée de la parallaxe, - // et elle est forcément plus forte en mi-plage qu'en quasi-far). Mesuré 19/07 : - // BOTW pans = min(farRes,midRes) 1,7-2,3 px ratio 0,97-1,00 (30 s consécutives) ; - // TOTK 225 s = 0 trip, pire seconde ratio 0,46 (marge ×1,5). Conséquence du trip : - // famille MV muette = fallback pré-mode-2 (MV flow/LK + dejitter, le comportement - // v1.2.1) — zéro régression possible par construction. Hystérésis : trip après - // 3 s mauvaises consécutives, récupère après 10 s saines consécutives (la sonde - // reste vivante, la passe reproj tourne toujours). Zéro nom de jeu, auto-mesure. private static readonly bool _mvppDepthGuard = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTHGUARD") == "1"; private static readonly float _depthGuardPx = ParseEnvFloat("RYUJINX_MVPP_DEPTHGUARD_PX", 1.2f); private static readonly float _depthGuardRatio = ParseEnvFloat("RYUJINX_MVPP_DEPTHGUARD_RATIO", 0.7f); - private const int DepthGuardTripSeconds = 3; // mauvaises consécutives avant de couper - private const int DepthGuardRecoverSeconds = 10; // saines consécutives avant de rétablir - private const float DepthGuardMinBinPct = 0.02f; // plancher de population par bin (menus exclus) + private const int DepthGuardTripSeconds = 3; + private const int DepthGuardRecoverSeconds = 10; + private const float DepthGuardMinBinPct = 0.02f; private bool _depthGuardMuted; private int _depthGuardBadStreak; private int _depthGuardGoodStreak; @@ -172,165 +83,57 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private bool _mvppHasPrevDepth; private TextureView _mvppBiasMask; - // RYUJINX_MVPP_BIAS=1 feeds the dynamic mask to DLSS as the official bias-current-color - // hint (favor the current frame on pixels whose motion our MVs cannot describe). - // OFF BY DEFAULT after in-game evaluation: with the current mask it spreads flicker - // onto static outlines instead of reducing object ghosting. Revisit with the mask. private static readonly bool _mvppBias = Environment.GetEnvironmentVariable("RYUJINX_MVPP_BIAS") == "1"; - // RYUJINX_MVPP_FG_RAWMV=1 hands frame generation the PRE-deadband reprojection MVs - // (journal problem #1, the settle slide with FG): the deadband answers a DLSS-SR need - // (the game's TAA already stabilized the presented image), but DLSS-G interpolates the - // REAL motion between two presented frames -- post-deadband "nothing moves" vectors - // make it hold a still image while the camera settle is still drifting. Opt-in for the - // one-variable A/B against the frozen tag; DLSS-SR keeps the deadbanded MVs either way. - // [RAW-MV-SR A/B, 11/07] RYUJINX_DLSS_RAW_MV_SR=1 : route le champ MV PRE-deadband - // (imgRawMv) vers l'Evaluate SR du chemin inject, au lieu du champ deadbande. Une seule - // variable ; jitter/depth/MvecScale/generation MV intouches. Defaut OFF = inchange. private static readonly bool _rawMvSr = Environment.GetEnvironmentVariable("RYUJINX_DLSS_RAW_MV_SR") == "1"; private static readonly bool _mvppFgRawMv = Environment.GetEnvironmentVariable("RYUJINX_MVPP_FG_RAWMV") == "1"; - // RYUJINX_MVPP_DUMP=1 (REPROJ, post-E9 method change): write the RAW MV field and the - // scene depth to disk every ~2 s (12 pairs max, ~0.5 GB) while the artifact is on - // screen -- the nested-squares mechanism gets identified by OFFLINE NUMERIC analysis - // of the actual data instead of another blind lever. Present-thread readback, same - // sync path as the F8 screenshot; the stall is the price of a diagnostic run. private static readonly bool _mvppDump = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUMP") == "1"; - // RYUJINX_MVPP_DUMP_DELAY= (dump v3 lesson): the 10 slots burn in the first - // ~20 s = title screen + save menu, the sweep never gets captured. Delay counts from - // DLSS start (title appearance), 0/unset = capture immediately (today's behavior). private static readonly float _mvppDumpDelay = ParseEnvFloat("RYUJINX_MVPP_DUMP_DELAY", 0f); - // RYUJINX_MVPP_DUMP_INTERVAL_MS= (28/07) : espacement entre deux captures. Le défaut - // historique de 2000 ms convient pour un artefact PERSISTANT, mais rend structurellement - // invisible un défaut d'ALTERNANCE (« les nuages apparaissent et disparaissent ») : deux - // instantanés à 2 s d'intervalle ne peuvent pas montrer un basculement image par image. - // Mettre 0 capture des images CONSÉCUTIVES. Coût assumé : readback synchrone à chaque - // image = saccade franche pendant les 10 slots, et ~2 Go sur disque. Défaut inchangé. private static readonly float _mvppDumpIntervalMs = ParseEnvFloat("RYUJINX_MVPP_DUMP_INTERVAL_MS", 2000f); - // RYUJINX_MVPP_DUMP_BURST= (28/07) : taille d'une RAFALE d'images consécutives. - // Le déclenchement à l'horloge seule est trop fragile pour un défaut qui n'existe que - // pendant un geste : la capture du 28/07 16h16 a photographié une caméra POSÉE (image - // qui ne varie que de 1,7/255 entre deux prises, champ MV à 0,005 px = correct pour une - // caméra immobile) et n'a donc rien pu observer. Avec n>1, on capture n images d'affilée - // puis on attend INTERVAL_MS avant la rafale suivante : plusieurs fenêtres de tir au - // lieu d'une seule, sans perdre la consécutivité dont l'analyse d'alternance a besoin. - // 1 = comportement historique. private static readonly int _mvppDumpBurst = Math.Max(1, (int)ParseEnvFloat("RYUJINX_MVPP_DUMP_BURST", 1f)); - // RYUJINX_MVPP_DUMP_ON_MOVE= (28/07) : ne DEMARRER une rafale que si la matrice camera - // a change de valeur sur les n derniers presents CONSECUTIFS. Supprime le probleme de - // timing : au lieu de tirer a l'aveugle en esperant que le joueur bouge, on tire quand il - // bouge REELLEMENT. Une fois la rafale commencee elle va jusqu'au bout (il faut des images - // consecutives, meme si le geste s'arrete au milieu). 0 = desactive (horloge seule). private static readonly int _mvppDumpOnMove = Math.Max(0, (int)ParseEnvFloat("RYUJINX_MVPP_DUMP_ON_MOVE", 0f)); - // [HOLDCAP 31/07] RYUJINX_MVPP_DUMP_ON_HOLD= : ne tire une rafale QUE sur un figement - // de paire (hold de la file) precede d'au moins presents de mouvement continu. - // 0 = OFF (defaut) => le declencheur existant est bit-pour-bit inchange. - // Raison d'etre : DUMP_ON_MOVE exige _mvppMoveStreak >= n AU moment du tir, donc il ne - // peut PAS capturer un figement (la serie vient d'etre remise a zero). Verifie sur les - // 10 dumps du 30/07 : curr==prev nulle part, prev==prev2 nulle part -- 20 occasions, - // zero capture. Ce declencheur-ci vise exactement le trou. private static readonly int _mvppDumpOnHold = Math.Max(0, (int)ParseEnvFloat("RYUJINX_MVPP_DUMP_ON_HOLD", 0f)); - // [INTERVALFIX 31/07] RYUJINX_MVPP_INTERVAL_FIX=1. OFF par defaut => _mvppIntervalFix reste - // faux, aucun saut de decalage n'a lieu, l'intervalle vaut 1 en permanence et le facteur - // envoye au shader vaut 1.0 : comportement bit-pour-bit celui d'avant. - // - // CE QU'IL CORRIGE, MESURE LE 31/07 SUR 4 EVENEMENTS CAPTURES. - // Quand la file de cameras est a sec, TryConsumeOrdered rend _fifoLast : la valeur qui - // arrive est IDENTIQUE a _mvppVpCurr. La laisser entrer fige la paire (curr == prev) donc - // la reprojection n'a aucun mouvement a decrire : - // image du figement + 1 : declare (+0.00, +0.00) sur 13 tuiles / 13, ecran a 19-56 px ; - // image suivante : declare x1,94 a x3,50 le mouvement reel (la paire couvre alors - // DEUX intervalles) ; - // image d'apres : juste a 1,02 px. - // Retenu -> lance au double -> remis en place = l'oscillation ("ca bouge un peu et ca - // revient"). Somme nette JUSTE sur deux images (0 + 2x = 2x), d'ou l'absence de derive. - // - // LE CORRECTIF EN DEUX MOITIES INDISSOCIABLES : sauter le decalage sur le doublon (la paire - // garde son intervalle precedent, non nul et plausible), puis diviser par l'ecart de - // tampons a l'image de rattrapage. Sauter SEUL = ELECTROT, rejete le 28/07 : le defaut est - // deplace, pas supprime. private static readonly bool _mvppIntervalFix = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INTERVAL_FIX") == "1"; - // See the field-side comment at _lastResetMs (zoom-at-startup dossier). private static readonly float _resetCooldownMs = ParseEnvFloat("RYUJINX_DLSS_RESET_COOLDOWN_MS", 0f); - // RYUJINX_DLSS_SCENECUT_MAX_MOTION (zoom dossier, 05/07): a history reset fires only when the - // motion fraction is below this. Default = the const (0.02). Measured on TOTK's load ramp: the - // FALSE resets (the "zoom breathing" in quality) carry 1.4-1.9% motion -- the exposure/fade ramp - // confuses the motion estimator -- while the genuine load->game cut is ~0.2%. Lowering to ~0.01 - // filters the ramp train while keeping the real cut. Default 0.02 = stock behavior. private static readonly float _sceneCutMaxMotion = ParseEnvFloat("RYUJINX_DLSS_SCENECUT_MAX_MOTION", SceneCutMaxMotion); - // [CUTONJUMP 27/07] Last camera-warp sequence number seen. See the use site near the reset - // decision: a change means the camera source detected a warp since the previous frame. private int _lastTeleportSeq; - // RYUJINX_DLSS_DEPTH=0: force the zeroed dummy depth (jitter-tremble discriminant, see the - // depthSource selection). Unset/other = real depth pipeline unchanged (E9b default). private static readonly bool _forceDummyDepth = Environment.GetEnvironmentVariable("RYUJINX_DLSS_DEPTH") == "0"; - // Jitter sign convention vs the CURRENT nvngx DLL. Defaults = (+1,-1), the combination - // validated IN GAME on 05/07 against the 310.6 transformer DLL (static AND motion clean, - // camera-pass-only injection). The old pre-DLSS4 calibration was (-1,-1); envs override - // for future DLL re-calibrations. private static readonly float _jitterSignX = ParseEnvFloat("RYUJINX_DLSS_JITTER_SIGNX", 1f); private static readonly float _jitterSignY = ParseEnvFloat("RYUJINX_DLSS_JITTER_SIGNY", -1f); - // [C4 convention knobs] Inject-path jitter/MV convention overrides. Defaults reproduce the present-path - // behaviour EXACTLY (byte-identical), so nothing changes unless a knob is set. Used to bisect the - // inject-path conventions ONE variable at a time -- the scene RT may not share the composite's - // orientation/timing the present path was tuned for. - // RYUJINX_DLSS_INJECT_JIT_OFFSET=1 : declare the CURRENT frame's jitter (DlssJitterState.Offset, - // what StateUpdater rendered the scene with) instead of the - // present-carried Present (which is 1 frame old at the inject hook). - // RYUJINX_DLSS_INJECT_JITSX / _JITSY : override the declared-jitter sign per axis. - // RYUJINX_DLSS_INJECT_MV_FLIPY = 0/1 : override the reprojection MV Y convention (else present default). - // RYUJINX_DLSS_INJECT_JIT_SCALE = : scale the DECLARED inject jitter offset ONLY (default 1.0 = - // byte-identical). Tests applied-vs-declared amplitude: JITTERVAL (07/07) measured the scene is rendered - // with jitter = declared x (vpW/InputWidth) ~= 2666/3200 ~= 0.833, so declaring 0.833x matches what the - // scene actually received. Touches ONLY declJitter below -- NOT Halton/StateUpdater/guest/MV/depth/present. private static readonly bool _injectJitUseOffset = Environment.GetEnvironmentVariable("RYUJINX_DLSS_INJECT_JIT_OFFSET") == "1"; private static readonly float _injectJitSignX = ParseEnvFloat("RYUJINX_DLSS_INJECT_JITSX", _jitterSignX); private static readonly float _injectJitSignY = ParseEnvFloat("RYUJINX_DLSS_INJECT_JITSY", _jitterSignY); private static readonly float _injectJitScale = ParseEnvFloat("RYUJINX_DLSS_INJECT_JIT_SCALE", 1f); - // [POSTCOLOR, 09/07] Feed the inject DLSS the native POST-TONEMAP colour (the same snapshot mode B - // uses) instead of the raw pre-tonemap mid-frame scene. User confirmed 09/07: the raw HDR makes DLSS - // smear the highlights ("les couleurs bavent"); the tonemapped image reconstructs clean/warm. The - // snapshot is captured AT PRESENT => one frame stale at the mid-frame inject => judge on STATIC first - // (colour lags geometry in motion). Default OFF = raw scene, BYTE-IDENTICAL. Requires COLORCAP_SNAP=1. private static readonly bool _injectPostColor = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_POSTCOLOR") == "1"; - // [TINTPROBE, 09/07] DECISIVE downstream-overwrite test. Feed the inject DLSS a texture we own, - // cleared to vivid HDR magenta, instead of the scene. Magenta on screen => the inject colour reaches - // the display (=> POSTCOLOR failed for another reason, not downstream). Normal screen => the game's - // downstream passes overwrite the inject output (=> the colour lever is structural = path B). One - // variable, touches ONLY the colour input of the inject evaluate. Default OFF = byte-identical. private static readonly bool _injectTintProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_TINTPROBE") == "1"; - // [SUBRECT, 10/07] Hypothesis C measurement: at a LOW dynamic-resolution step the game only - // fills a corner of the scene RT, but the inject Evaluate tags the FULL RT dims (no subrect - // is ever declared -- ActiveWidth exists only in the reproj shader). Dump the full inject - // input once at a low step and once at full res, log active-vs-tagged extents, and count - // low/full inject frames. Read-only, default OFF. private static readonly bool _dumpSubrect = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DUMP_SUBRECT") == "1"; private static long _subrectLastTryMs; @@ -340,68 +143,36 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static int _subrectFullFrames; private static long _subrectSummaryMs; - // [DUMPCOLOR, 09/07] One-shot: dump to PNG the raw scene src AND the post-tonemap snapshot that - // POSTCOLOR would feed, to SEE whether they actually differ (why did POSTCOLOR change nothing?). - // Read-only, no swap. Requires COLORCAP_SNAP=1 for the snapshot. Default OFF. private static readonly bool _injectDumpColor = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_DUMPCOLOR") == "1"; - // [HELDCOLOR / the REAL POSTCOLOR test, 09/07] Feed the inject the PRESENT-captured TONEMAPPED held - // copy (MvppColorSnapshot.SceneColorHeld) instead of the live raw mid-frame scene. Dumps 09/07 proved - // the present colour is tonemapped/balanced vs the mid-frame raw (crushed HDR shadows). 1-frame stale - // (judge STATIC). Default OFF = raw scene, byte-identical. Requires COLORCAP_SNAP=1. private static readonly bool _injectHeldColor = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_HELDCOLOR") == "1"; - // [RESET-COUNT, 09/07] Diagnostic: tally DLSS history resets by reason with a periodic summary, to - // SEE (by numbers, not by the user's eye) what causes the in-game flashes. Default OFF = zero cost. private static readonly bool _resetCount = Environment.GetEnvironmentVariable("RYUJINX_MVPP_RESET_COUNT") == "1"; - // [INJECT DYNRES, 09/07] When ON, a dyn-res INPUT-size change alone does NOT wipe DLSS history (DLSS - // handles dynamic resolution natively); only an OUTPUT-size change still resets. Targets the dyn-res - // flash. Default OFF = current behaviour (any dims change resets), byte-identical. private static readonly bool _injectDynRes = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_DYNRES") == "1"; - // [FLASHCAP / Mesure A, 09/07] Objectively detect flashes: each frame, downscale the DLSS output to a - // tiny texture, read it back (cheap), and measure how much it changed vs the previous frame. A sudden - // SPIKE (relative to the local baseline) = a flash -> auto-dump the FULL frames around it so we can SEE - // WHERE the artifact is (foliage/animated => supports the MV hypothesis; static/global => breaks it). - // Default OFF. Measures the DLSS output (pre-composite), isolating DLSS from HUD/present. private static readonly bool _flashCap = Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP") == "1"; - // RYUJINX_MVPP_INJECT_SKYROT=1: route far-plane/SKY pixels (d >= 1.0) through rotation-only MV in the - // INJECT reproj (shader skyRot param, p[47]). Default OFF = byte-identical (p[47]=0 -> useRot depends only - // on farRotStart, which is also 0 here). Touches ONLY no-depth sky pixels (MvppReproject.glsl:189/197); - // valid-depth geometry, distant foliage (d~0.98), and FARROT are untouched. private static readonly bool _injectSkyRot = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_SKYROT") == "1"; - // RYUJINX_MVPP_INJECT_QUIET=1: cut inject DIAGNOSTIC overhead ONLY (per-frame INJECT-B log, the ~1s GPU - // stats readback = a GPU sync stall, shader diagMode stat accumulation). Default OFF = current behaviour. - // Does NOT touch DLSS/jitter/skyRot/maxMotion/reset/injection pipeline -- render is byte-identical, only - // the diagnostics go silent. Perf-cleanup instrument. private static readonly bool _injectQuiet = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_QUIET") == "1"; - // RYUJINX_MVPP_INJECT_GAPMS: ms since the last inject that trips a history reset ("gap"). Default 50 = - // byte-identical. MEASURED (08/07): under ~20fps (frame > 50ms) EVERY frame trips this -> history wiped - // every frame during heavy motion = no temporal accumulation exactly when it matters (gaps cluster in - // dense bursts = an FPS dip, not a scene cut). Raise (e.g. 500) so only real cuts (menu/load = seconds) - // reset, not FPS dips. Real cuts stay caught (their gap is seconds) + dims-changed/mv-zero still fire. private static readonly float _injectGapMs = ParseEnvFloat("RYUJINX_MVPP_INJECT_GAPMS", 50f); private static readonly int _injectMvFlipY = ParseInjectFlipOverride("RYUJINX_DLSS_INJECT_MV_FLIPY"); private static int ParseInjectFlipOverride(string name) { string v = Environment.GetEnvironmentVariable(name); - return v == "0" ? 0 : v == "1" ? 1 : -1; // -1 = unset -> use the present-path default + return v == "0" ? 0 : v == "1" ? 1 : -1; } - // See the de-jitter comment at the assignment site (jitter dossier diagnostic). private static readonly bool _noDejitter = Environment.GetEnvironmentVariable("RYUJINX_DLSS_NODEJITTER") == "1"; - // See the phase comment at the declJitter assignment (jitter dossier diagnostic). private static readonly bool _jitterPhaseMinus1 = Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_PHASE") == "-1"; @@ -410,36 +181,17 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private readonly long _mvppDumpNotBeforeMs = Environment.TickCount64 + (long)(_mvppDumpDelay * 1000); - // RYUJINX_MVPP_EDGE (REPROJ/E5): leading-edge disocclusion policy for pixels whose - // history reprojects OFF-SCREEN during camera sweeps -- the black-lines exhibit - // (user-nailed geometry: lines at the leading edge in BOTH sweep directions, never - // on the sky = they need revealed WORLD content). 0 = zero MV (historic behavior, - // default), 1 = clamp prevUv to the border (standard TAA nearest-valid-history), - // 2 = raw off-screen MV (DLSS validates history itself; pre-M runs saw a dark band). private static readonly float _mvppEdgeMode = ParseEnvFloat("RYUJINX_MVPP_EDGE", 0f); - // RYUJINX_MVPP_FARROT= (REPROJ/E4): beyond this view distance the reprojection - // blends to ROTATION-ONLY (full at 2x). Kills the far depth-quantization MV banding - // (the black-lines exhibit: elimination tree E1/E2/E3 left only this mechanism) while - // translation is negligible at range. 0/unset = off, proven behavior untouched. + private static readonly bool _mvppEdgeAlways = ParseEnvFloat("RYUJINX_MVPP_EDGE_ALWAYS", 0f) > 0f; + private static readonly float _mvppFarRot = ParseEnvFloat("RYUJINX_MVPP_FARROT", 0f); - // [JITTERVAL probe, 07/07] DLSS side of the applied-vs-declared jitter proof (joins the - // StateUpdater JITTERVAL-GEO line by frame id). Gated, default OFF; reads only. private static readonly bool _jitterval = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTERVAL") == "1"; - // [JITTERVAL temporal probe, 10/07] Per-PRESENT (JITTERVAL-PRES, TryRun) and per-inject-EVALUATE - // (JITTERVAL-INJ, RunInjectMidFrame) event lines: the two clocks (present ~60 Hz, game ~30 fps) - // share the jitter sequence and the camera pair, so the chronology is the measurement. Same gate, - // log-only, reads only. private long _jvPres; - // [FLASHCAP OUT, 10/07] Read-only triplet dump of the PRESENT-path DLSS output (_output) -- the - // displayed image in ABMODE=B. Own timer, same TIMED/BURST/CAP knobs as the Window-side FLASHCAP. - // Reading _output during command recording yields the PREVIOUS present's result (same 1-frame skew - // as DumpField(_output)); a burst of consecutive presents therefore still dumps consecutive frames. - // Dump-arming only: no DLSS input, preset, history or jitter change. Default OFF. private static readonly bool _flashOut = Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP_OUT") == "1"; private static readonly int _flashOutSec = @@ -498,115 +250,58 @@ namespace Ryujinx.Graphics.Vulkan.Dlss d.M41 * d.M41 + d.M42 * d.M42 + d.M43 * d.M43 + d.M44 * d.M44); } - // [ALIGN0 probe, 07/07] Resource-alignment audit at the eval site (color/depth/MV frame ids, - // dims, formats, MvecScale, jitter, mode, HDR, MV_DEPTH_MATCH). Gated, default OFF; reads only, - // nothing wired to the eval path. Logs on signature change + a 2s heartbeat (dyn-res ruptures). private static readonly bool _align0 = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_ALIGN0") == "1"; private long _align0Ms; private long _align0Sig = -1; - // [A/B/C flag, 07/07] Experimental mode selector. DIAGNOSTIC ONLY at this step: read solely by the - // ALIGN0 log; NOTHING in the DLSS/Evaluate path branches on it. Default 'A' = current pipeline. - // B/C only change what ALIGN0 REPORTS (source label + native candidate), never the behavior. private static readonly char _abMode = (System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_ABMODE") ?? "A").ToUpperInvariant() is { Length: > 0 } s ? s[0] : 'A'; - // [B étape 1] state for the gated native-color feeder + its transition-reset detection. private bool _bWasActive; private long _align1Ms; private long _align1Sig = -1; - // [B étape 2, 07/07] Native-resolution LOCK. TOTK drops dyn-res to 1332x750 when the camera is - // still; the strict 2666 gate then bailed B->A every ~1 s, and every switch forces a DLSS history - // reset = the black flash the user saw at rest. Instead: keep a stable COPY of the last full 2666 - // native frame and re-feed it during the dip, staying in B with NO reset. The copy is written only - // on good (2666) frames and read only on dip frames -- never both the same frame, so there is no - // copy-then-sample hazard. The existing depth-hold reuses the matching 2666 depth automatically - // (a 1332 live depth is < half of 2666 -> "not usable"). Gated by RYUJINX_DLSS_BLOCKRES (default - // ON in B); set it to 0 to restore the old flap for an A/B measurement. - private TextureView _bNativeHeld; // stable copy of the last valid 2666x1500 native color + private TextureView _bNativeHeld; private bool _bHasNativeHeld; - private long _bNativeHeldFrame; // present-frame id captured into the held copy - private bool _bLockedThisFrame; // this frame re-fed the held copy during a dyn-res dip - private int _bLiveNatW, _bLiveNatH; // last observed LIVE native size (for logging) + private long _bNativeHeldFrame; + private bool _bLockedThisFrame; + private int _bLiveNatW, _bLiveNatH; private static readonly bool _blockRes = (Environment.GetEnvironmentVariable("RYUJINX_DLSS_BLOCKRES") ?? "1") == "1"; - // [B étape 2, 07/07] The scene-CHANGE detector (below) is calibrated for the presented LDR image; - // on the native HDR R11G11B10Float input it saturates to "changed=100% motion=0%" EVERY frame, so - // the cooldown-gated logic fires a FALSE DLSS history reset ~every 0.75 s = the black reconvergence - // flash the user saw at rest (measured: 50->80 false cuts, all changed=100%/motion=0%, cadence == - // the 45-frame cooldown). Mode B never carries a mid-stream hard cut (loading->game happens on A - // before B engages), so suppress the scene-cut reset while B feeds native color. Genuine first-frame - // and A->B entry resets are kept. Gated (default ON in B); =0 restores the resets for A/B. private static readonly bool _bNoReset = (Environment.GetEnvironmentVariable("RYUJINX_DLSS_BNORESET") ?? "1") == "1"; - private int _bSuppressedCuts; // count of false scene-cuts swallowed in B (log only) + private int _bSuppressedCuts; - // RYUJINX_MVPP_SKYROT=1 (06/07, "verite du lointain"): route ONLY the far-plane/sky - // pixels (d >= 1.0) through the rotation-only reprojection instead of zero MV, WITHOUT - // FARROT's distance blend on real geometry -- distant foliage (d~0.98) keeps its normal - // reproj MV, so the sky-ghost fix and the foliage-shimmer verdict stay separable. - // Default off: daily behaviour byte-identical when unset. private static readonly bool _mvppSkyRot = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SKYROT") == "1"; - // RYUJINX_MVPP_SKYFLOW=1 (17/07, étape 1 MV++): far-plane/sky pixels take the OPTICAL-FLOW - // sample (FlowMvTex, already bound for the diag stats) when it is credible (above deadLo, - // under maxMotion) -- the sky's content drifts on its own (cloud sea 4-10 px/min, night - // skybox; wander tiles 09/07) and rotation-only MV cannot describe it. Falls back to the - // SKYROT/zero path per pixel. Default off: behaviour byte-identical when unset. private static readonly bool _mvppSkyFlow = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SKYFLOW") == "1"; - // RYUJINX_MVPP_SKYDRIFT=1 (17/07, étape 1 MV++ v2) : nourrit le ciel avec la dérive de - // CONTENU que la sonde a prouvée extractible (moyenne signée du flow ciel : Y de signe - // stable ~-0.015 px/frame sur 35 s consécutives au repos). Boucle : sommes de la sonde - // lues à 1 Hz -> EMA -> params driftX/Y -> le shader l'ajoute aux pixels ciel (exempt - // du deadband) sauf quand l'échantillon de flow direct est crédible. Remis à zéro avec - // l'historique DLSS (coupures de scène). Défaut OFF = comportement byte-identique. private static readonly bool _mvppSkyDrift = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SKYDRIFT") == "1"; - // RYUJINX_MVPP_SKYCUT_COOLDOWN= (18/07, dossier "micro-glitch a l'entree en jeu") : - // pendant N frames apres un reset d'historique, la famille ciel/lointain (SKYROT, EDGE, - // SKYFLOW, SKYDRIFT, FARROT) est suspendue -> le dome ecrit des MV nulles, edge = politique - // par defaut = exactement le comportement pre-famille. Cause mesuree (run E2, sonde 01:28, - // cap 4 px sature) : au cut, matrice prev et flow decrivent l'ANCIENNE scene pendant 1-2 - // frames -> MV poubelle sur tout le dome -> accroc visible a la transition chargement->jeu - // (entonnoir 18/07 : dose-reponse par feature, DLL passive et env daily innocentes). - // 0 = garde-fou coupe (repro du glitch a la demande). Defaut 30 (~0.5 s a 60 fps) ; le - // cout d'un faux positif = le dome revient a des MV nulles une demi-seconde (invisible). private static readonly int _mvppSkyCutCooldown = (int)ParseEnvFloat("RYUJINX_MVPP_SKYCUT_COOLDOWN", 30f); - // RYUJINX_MVPP_STATS_STRIDE=N (18/07, dossier perf) : cadence des bandes de stats, - // 1 frame sur N. Voir le commentaire au site p[49]. private static readonly int _mvppStatsStride = (int)ParseEnvFloat("RYUJINX_MVPP_STATS_STRIDE", 1f); private float _skyDriftEmaX; private float _skyDriftEmaY; - // [SKYCALM 27/07] See the ingestion site: the drift estimate is only fed while the camera is - // calm, because a pan cannot inform it and can only poison it. private static readonly bool _mvppSkyCalm = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SKYCALM") == "1"; private const float SkyCalmMaxPxPerFrame = 0.1f; - // [SKYDEPTH 27/07] See the parameter site. Default 1.0 = historic behaviour. private static readonly float _skyDepth = ParseEnvFloat("RYUJINX_MVPP_SKYDEPTH", 1.0f); - // [SKYGRID clamp 27/07] See the clamp site. Default keeps the historic 0.15 exactly, so - // without the variable nothing changes on any game. private static readonly float _skyGridClamp = ParseEnvFloat("RYUJINX_MVPP_SKYGRID_CLAMP", 0.15f); - // [SKYGRID pop 27/07] See the population gate. Default keeps the historic 200 exactly. - // [SKYBAND 27/07] See the band fallback. Default OFF: without it the behaviour is the - // historic one, zone estimate or global average, nothing in between. private static readonly bool _skyBand = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SKYBAND") == "1"; @@ -617,101 +312,56 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static readonly uint _skyGridMinSamples = (uint)MathF.Max(1f, ParseEnvFloat("RYUJINX_MVPP_SKYGRID_MINSAMPLES", 200f)); - // RYUJINX_MVPP_SKYGRID=1 (18/07, SKYFLOW v2) : le dôme reçoit, EN PLUS de la dérive globale - // SKYDRIFT, un RÉSIDU local par zone (grille 8x4, EMA par zone, interpolé bilinéairement - // dans le shader = lisse par construction — le remède mesuré aux pointillés de SKYFLOW v1 - // (adoption pixel-par-pixel, sonde SKYCOH : bruit corrélé par blocs 4-8 px, 3-6x le signal). - // Justifié par la sonde SKYGRID (spread entre zones = 4-15x le bruit de moyenne, bandes - // verticales stables). Résidus transportés dans la QUEUE du buffer de stats (slots 124-187, - // écrits par le CPU avant dispatch — zéro nouveau binding) ; OFF => résidus à zéro dès la - // création du buffer => addition nulle, comportement identique. private static readonly bool _mvppSkyGrid = Environment.GetEnvironmentVariable("RYUJINX_MVPP_SKYGRID") == "1"; private readonly float[] _zoneDriftEmaX = new float[32]; private readonly float[] _zoneDriftEmaY = new float[32]; - // RYUJINX_DLSS_DYNRES=1 (06/07, zoom-at-startup dossier): with the DLSS output already fixed to - // the display, a dynamic-resolution INPUT change must NOT tear down the DLSS output target. - // The stock EnsureResources rebuilds everything (incl. _output) on any input-size change, and - // TOTK's startup dyn-res ramp changes the input every frame -> the DLSS output/history is - // recreated repeatedly -> the quality-mode "zoom breathing". When set, keep _output alive on an - // input-only change and rebuild only the input-sized helper buffers. Default off = stock. private static readonly bool _dlssDynRes = Environment.GetEnvironmentVariable("RYUJINX_DLSS_DYNRES") == "1"; - // RYUJINX_DLSS_PINOUT=1 (06/07, zoom dossier -- THE never-coded angle, named 04/07 "output follows - // input during the spawn ramp", pinned-output opened but never coded). Once a stable (mode, output= - // display) is picked, LOCK it and reuse it every frame -- the DLSS output can never again be derived - // from the (dyn-res) input, so it cannot follow the input during the startup ramp. Default off. private static readonly bool _dlssPinOut = Environment.GetEnvironmentVariable("RYUJINX_DLSS_PINOUT") == "1"; - // RYUJINX_DLSS_UPSCALE_PASS=1 (INJECT chantier, 08/07): "inject DLSS before the game composite". The - // GPU-layer detector identifies the scene-upscale pass generically and carries the source + real - // destination handles through the GAL command; the mid-frame hook runs DLSS in host space. Étape C3a - // writes the REAL destination RT (guest draw still kept for comparison). Default OFF => daily byte-identical. private static readonly bool _injectPass = MvppInjectState.Enabled; - private const uint InjectViewportId = 1; // separate DLSS feature instance -- never touches the present path's viewport 0 - // [C3b] Single rollback switch: true => the guest upscale draw is skipped when DLSS wrote the real dst - // (evaluate OK), making DLSS visible; false => guest draw always kept (C3a behaviour / instant rollback). + private const uint InjectViewportId = 1; private const bool SubstituteActive = true; private uint _injectFrame; - // Scene-resolution coherent inputs for the inject evaluate. DLSS derives its render resolution from - // the MOTION texture dims, so color/depth/motion must share the scene render res. The reproj/flow MV - // already computed lives at the COMPOSITE res (~1600), not the scene res (~1333), so it cannot be - // reused coherently here -- Étape B feeds a zeroed motion at scene res (valid image, no real motion); - // Étape C wires a real scene-res MV. Depth uses the real scene depth when it matches, else zeroed. private TextureView _injectDepthZero; private TextureView _injectMotionZero; private int _injectAuxW, _injectAuxH; private long _injectStatusMs; private long _injectStatusSig = -1; private bool _injectPickWarned; - private bool _injectDumped; // [INJECT étape B sous-étape 4] one-shot PNG dump guard (one per launch) - private bool _injectPendingDump; // [sous-étape 4] deferred-by-one-frame dump request (DumpField(_output) model) - private int _injectDiagCount; // [DIAG C2b vertical-offset] one-shot replay-side detail counter - private const int InjectDumpSettleFrames = 60; // [DIAG dump timing] OK-evaluate frames to skip after the first (load transient) before dumping - private int _injectOkCount; // [DIAG dump timing] evaluate=OK frames since the first - private long _injectFirstOkMs; // [DIAG dump timing] tick of the first evaluate=OK - private TextureView _injectLastDst; // [C3a] the real dst written last frame, for the deferred sanity dump - // [C4] Scene-res (2666x1500) camera-reprojection MV, GENERATED but NOT yet fed to DLSS (validation - // phase). Reuses the proven MvppReproject shader with all extra features OFF. The 3 extra images are - // required by the shader's static layout (written every pixel) even though only _injectMotion matters. - private TextureView _injectMotion; // rg16f, the reproj MV output we validate - private TextureView _injectMotionRaw; // rg16f, shader's pre-deadband MV (bound, unused by us) - private TextureView _injectReprojDepthSnap; // r32f, shader's depth snapshot (bound, unused by us) - private TextureView _injectReprojBiasMask; // r8, shader's bias mask (bound, unused by us) + private bool _injectDumped; + private bool _injectPendingDump; + private int _injectDiagCount; + private const int InjectDumpSettleFrames = 60; + private int _injectOkCount; + private long _injectFirstOkMs; + private TextureView _injectLastDst; + private TextureView _injectMotion; + private TextureView _injectMotionRaw; + private TextureView _injectReprojDepthSnap; + private TextureView _injectReprojBiasMask; private int _injectReprojW, _injectReprojH; private BufferHandle _injectReprojStats; private long _injectReprojLogMs; private bool _injectReprojWarned; - // [C4 wire] Temporal-accumulation state for viewport 1: reset only on discontinuities. private bool _injectHasHistory; private int _injectPrevRw, _injectPrevRh, _injectPrevOw, _injectPrevOh; private long _injectLastRunMs; - // [Option A] Set by RunInjectMidFrame when it substituted the scene upscale this frame; read (and - // reset) by the present TryRun so it SKIPS its redundant DLSS. Both on the render thread, inject - // (mid-frame) precedes present -> no race. Fallback: false => present DLSS runs as before. private bool _injectSubstitutedThisFrame; - // [POSTCOLOR] True on frames where the post-tonemap snapshot actually replaced the raw scene input. private bool _injectPostColorUsed; - // [HELDCOLOR] True on frames where the present-held tonemapped copy replaced the raw scene input. private bool _injectHeldColorUsed; - // [TINTPROBE] The magenta texture we feed as the DLSS colour input, and whether it was used this frame. private TextureView _injectTintTex; private int _injectTintW, _injectTintH; private bool _injectTintUsed; - // [DUMPCOLOR] One-shot guard so the src/snapshot PNGs are written exactly once per run. private bool _injectColorDumped; - // [DUMPCOLOR] Throttle: only attempt the (expensive) readback once per second while waiting for a - // bright gameplay frame, so the retries don't stall every frame. private long _injectDumpLastTryMs; - // [RESET-COUNT] Per-reason reset tallies over the current ~8s window + summary timer. private int _rcDims, _rcGap, _rcMvzero, _rcFirst, _rcFrames; private long _rcSummaryMs; - // [FLASHCAP] Tiny downscale target + per-frame luminance samples for the frame-to-frame diff metric, - // running baseline, spike/dump bookkeeping and periodic summary. private TextureView _flashTiny; private int[] _flashCur, _flashPrev; private bool _flashPrevValid; @@ -720,24 +370,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private long _flashLogMs; private long _presentSkipLogMs; - // RYUJINX_MVPP_DEPTH_LINEAR=1 (REPROJ/E2): sample the scene depth with a LINEAR filter - // instead of NEAREST in the reprojection pass. The dyn-res depth buffer is ~2.4x smaller - // than the input grid; at grazing angles (distant ground seen from above) the per-texel - // depth gradient is huge and the nearest-tap aliasing stripes the reconstructed world - // positions -> striped MV values (REPROJ exhibit #1, black lines). Linear smooths the - // grazing case; silhouettes (island rims, exhibit #3) may read WORSE (phantom in-between - // depths) -- the E2 run reads the two exhibits separately. Default OFF: proven behavior. private static readonly bool _mvppDepthLinear = Environment.GetEnvironmentVariable("RYUJINX_MVPP_DEPTH_LINEAR") == "1"; - // RYUJINX_MVPP_VEL_COMPOSE=1 (b1/J3): compose the per-object MV delta (the VEL - // coverage: NDC delta written by the variant FS against the REAL ring N-1 palette, - // camera factored out) on top of the camera-reprojection MVs -- DLSS finally - // consumes true object motion on actor pixels. The delta is added AFTER the - // deadband (the deadband answers a TAA need on STATIC geometry; object motion is - // real in the image). Freshness gate is MANDATORY, unlike the display-only FRESH - // flag: a stale coverage under a moving camera puts object MVs on the wrong - // pixels -- worse than baseline. Default OFF: daily drivers untouched. private static readonly bool _mvppVelCompose = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_COMPOSE") == "1"; @@ -745,10 +380,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private int _mvppComposeStale; private bool _mvppComposedThisFrame; - // Sub-pixel/slow-motion deadband thresholds in pixels (see the shader comment). - // Defaults settled by eye on TOTK (02/07): 5/12 = stop transient gone, textures clean - // (3/8 already stopped the tremble but left some texture shimmer; below that the - // whole-image tremble returns). Env-tunable for future games without rebuilds. private static readonly float _mvppDeadLo = ParseEnvFloat("RYUJINX_MVPP_DEAD_LO", 5.0f); private static readonly float _mvppDeadHi = ParseEnvFloat("RYUJINX_MVPP_DEAD_HI", 12.0f); @@ -760,11 +391,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss System.Globalization.CultureInfo.InvariantCulture, out float v) ? v : fallback; } - private const int MvppStatsCount = 258; // 20 base + skySum(20/21, x1024 cap 4px) + skyFlowSum(22/23, signés x16) + skyD1Count(24) + skyDbgDriftY(25) + skyNbDiff2/16(26/27, SKYCOH x16) + SKYGRID 8x4 : zoneFlowX[32]=28..59, zoneFlowY[32]=60..91 (signés x16), zoneCount[32]=92..123, zoneDriftX[32]=124..155 + zoneDriftY[32]=156..187 (CPU->shader, floatBits, résidus EMA px/frame) + DYNSONDE étape 2 : dynFlowX/Y(188/189, x16) + dynDiffX/Y(190/191, x16) + dynFlowAlive(192) + pad(193) + dynZoneCount[32]=194..225 + réserve 226..255 + BORDERFIX filled(256)+pad(257) — 256 = offset byte 1024, aligné 256 pour le bind-avec-offset de la passe BorderClamp (minStorageBufferOffsetAlignment max spec = 256) - private const int MvppBorderFilledIndex = 256; // [BORDERFIX] slot du compteur santé (voir ci-dessus) - // RYUJINX_MVPP_MAXMOTION: MV clamp in pixels (camera reprojection legitimately exceeds the flow's 32px - // clamp on fast pans). Default 128 = byte-identical. Raise (e.g. 512) to MEASURE fast-motion saturation: - // if the logged |reproj|/skyMV rises above 128 at a higher clamp, the 128 clamp was biting. + private const int MvppStatsCount = 258; + private const int MvppBorderFilledIndex = 256; private static readonly float MvppMaxMotion = ParseEnvFloat("RYUJINX_MVPP_MAXMOTION", 128f); private System.Numerics.Matrix4x4 _mvppVpCurr; private System.Numerics.Matrix4x4 _mvppVpPrev; @@ -773,96 +401,47 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private bool _mvppHasPair; private bool _mvppHasPrev2; - // [VPSTAMP 28/07] Numero de present auquel chaque matrice a ete ADOPTEE. Le decalage - // curr/prev n'a lieu que si une camera fraiche est disponible (PresentVpValid) : quand - // une image n'en a pas, la paire reste en place (le vecteur decrit l'intervalle - // PRECEDENT) et l'image suivante saute par-dessus (le vecteur CUMULE deux intervalles). - // Mesure 28/07 : image 5 = intervalle 3->4 a 0,59 px pres ; image 8 = cumul 6->8 a - // 1,13 px pres, contre 5,08 et 16,35 px sous l'hypothese d'un appariement correct. - // curr - prev DOIT valoir 1. Toute autre valeur est le defaut, lu directement. private long _mvppVpCurrStamp; private long _mvppVpPrevStamp; private long _mvppVpPrev2Stamp; - private int _mvppNoCamPresents; // presents arrives SANS camera fraiche (la cause racine) + private int _mvppNoCamPresents; - // [MOVESTREAK 28/07] Nombre de presents CONSECUTIFS ou la matrice camera a change de - // valeur. Sert a declencher les dumps SUR LE MOUVEMENT au lieu d'une horloge : un defaut - // qui n'existe que pendant un geste ne se capture pas a l'aveugle (echec du 16h16, et - // 2 rafales sur 4 perdues au 16h23). Remis a zero des qu'un present ne bouge plus. private int _mvppMoveStreak; - // [HOLDCAP 31/07] INSTRUMENT SEUL -- aucune decision de rendu ne lit ces trois champs. - // Quand la file de cameras est a sec, TryConsumeOrdered rend _fifoLast : la paire se fige - // (curr == prev) et _mvppMoveStreak est ecrase par 0. On garde donc sa valeur d'AVANT le - // figement, plus la duree du figement en cours, pour pouvoir declencher un dump SUR - // l'evenement que DUMP_ON_MOVE exclut structurellement (il exige du mouvement AU moment - // du tir, or ici le mouvement vient de s'arreter). Mesure 31/07 : 1 a 3 presents figes - // sur 150 en regime ordinaire, rafales jusqu'a 45. private int _mvppStreakBeforeFreeze; private int _mvppHoldRun; private bool _mvppFrozenPair; - // [INTERVALFIX 31/07] Vrai quand le decalage de paire vient d'etre saute. Sert a n'en - // sauter QU'UN de suite : au 2e figement consecutif on reprend le comportement actuel, - // sinon un ecran de chargement (145 figements consecutifs mesures le 31/07) figerait la - // paire pendant des centaines d'images et la reprojection deviendrait geometriquement - // absurde. Remis a faux des qu'un decalage a lieu. private bool _mvppShiftSkipped; - // [HOLDCLASS 31/07] Profondeur de la file camera apres consommation, snapshot au present. - // Instrument seul : ecrit dans vp_NN.txt, lu par personne d'autre. private int _mvppFifoDepth; - // [ROTDELTA 28/07] echelle du changement de ROTATION par present (voir le calcul). private float _mvppRotDeltaMax; private double _mvppRotDeltaSum; private int _mvppRotDeltaN; - // [JUMPDIST 28/07] Distribution des sauts de rotation, et RECURRENCE de leur valeur. - // Motif : le 28/07 le ROT delta max valait 1,146910 puis 1,146910 puis 1,146947 sur des - // fenetres de 5 s DIFFERENTES -- la meme valeur au 5e chiffre. Un humain au stick ne - // produit jamais ca ; un saut vers un etat FIXE, si. Hypothese a trancher : une SECONDE - // matrice camera (autre passe de rendu franchissant la validation) s'intercale, et le - // vecteur calcule decrit alors l'ECART ENTRE DEUX CAMERAS au lieu d'un mouvement. - // deux cameras -> quelques valeurs distinctes, tres repetees (pic net) - // vrai geste -> valeurs toutes differentes (etalement continu) - // Lecture seule, aucun effet sur le rendu. - private readonly int[] _mvppJumpBins = new int[5]; // <0.01, <0.1, <0.5, <1.0, >=1.0 - private readonly float[] _mvppJumpVals = new float[8]; // valeurs distinctes des GROS sauts + private readonly int[] _mvppJumpBins = new int[5]; + private readonly float[] _mvppJumpVals = new float[8]; private readonly int[] _mvppJumpHits = new int[8]; private int _mvppJumpDistinct; private int _mvppJumpOverflow; - // [CAMSPLIT 28/07] Rejet de la SECONDE camera. Mesure du 28/07 16h55, XC2, camera POSEE - // (Alex : « ca le fait meme sans bouger la camera, a un certain angle ») : la distribution - // des sauts de rotation est BINAIRE -- 121-141 changements sous 0,01 (le jitter du jeu) et - // 10 a 26 sauts valant EXACTEMENT 1,13893, avec RIEN entre 0,1 et 1,0 sur 7 fenetres de - // 5 s. Une valeur unique repetee = deux etats FIXES, pas un geste. Environ une image sur - // sept recevait donc un vecteur decrivant l'ECART ENTRE DEUX CAMERAS au lieu d'un - // mouvement (mesure : 14 px horizontaux declares sur un deplacement purement vertical). - // - // Le seuil vit dans un VIDE mesure : legitime < 0,1, intrus > 1,0, marge x5 des deux cotes. - // - // ⚠️ Pourquoi ce n'est pas CAMGUARD (rejete le 27/07 pour avoir fige la camera a 91-98 % - // de rejets) : celui-ci filtre sur une frontiere MESUREE avec du vide de part et d'autre, - // et surtout il ne peut PAS geler -- au-dela de CamSplitMaxStreak rejets consecutifs, la - // matrice est adoptee de force. Une vraie coupure de scene passe donc toujours, avec au - // pire trois images de retard, au lieu d'un blocage permanent. - // Plafond de rejets CONSECUTIFS avant adoption forcee. Regle a 3 au premier essai - // (28/07 17h04) : le filtre a bien mordu (7-22 rejets / 5 s, verdict d'Alex « ca ne le - // fait plus comme tantot ») mais il restait 1 a 4 adoptions forcees par fenetre, chacune - // = une image polluee. La seconde camera s'installe donc parfois plus de 3 images - // d'affilee. On MESURE la longueur des salves (`streakMax`) avant de choisir le plafond. private static readonly int _mvppCamSplitStreak = Math.Max(1, (int)ParseEnvFloat("RYUJINX_MVPP_CAMSPLIT_STREAK", 3f)); private static readonly float _mvppCamSplit = ParseEnvFloat("RYUJINX_MVPP_CAMSPLIT", 0f); private int _mvppCamRejectStreak; private int _mvppCamRejected; private int _mvppCamForced; - private int _mvppCamStreakMax; // plus longue salve consecutive vue sur la fenetre de log - // [1.2.3 bavard] throttle de la ligne « MV++ en pause » (le silence qui a coûté le - // dossier nuages 4K : mode armé, passe jamais lancée, zéro trace au log). + private int _mvppCamStreakMax; private long _mvppPauseLogMs; + + private static readonly bool _mvppVerbose = + Environment.GetEnvironmentVariable("RYUJINX_MVPP_VERBOSE") == "1"; + private bool _mvppPauseLoggedOnce; + + private bool _mvppHealthRunning; + private bool _mvppHealthKnown; + private int _mvppHealthStreak; private int _mvppPresents; private int _mvppChanged; private int _mvppAlternated; @@ -870,30 +449,22 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private int _mvppDeltaIdx; private long _mvppHeartbeatMs; private readonly ShaderCollection _mvppReprojProgram; - private readonly ShaderCollection _borderFixProgram; // [BORDERFIX] créé si gate armé seulement - private TextureView _borderFixTex; // [BORDERFIX v2] la copie corrigée — c'est ELLE que DLSS reçoit + private readonly ShaderCollection _borderFixProgram; + private TextureView _borderFixTex; private bool _borderFixSkipLogged; private TextureView _mvppMotion; private TextureView _mvppMotionRaw; - // MV++ Phase 2.2 J1 (RYUJINX_MVPP_VEL_DEBUG=1): present the colorized MV field - // (hue = direction, brightness = magnitude) INSTEAD of the game image; the whole - // DLSS/FG path is bypassed while the flag is on, nothing else is touched. private static readonly bool _velDebug = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DEBUG") == "1"; private static readonly bool _velFreshGate = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_FRESH") == "1"; - // (b1) 5b-3 (RYUJINX_MVPP_VEL_DEBUG_MV=1): upgrade the debug overlay from the 3b - // green coverage to a colorization of the OBJECT MVs the ring N-1 bind produces - // (CovTex.rg = NDC delta, camera factored out). Debug-view-only, DLSS path untouched. private static readonly bool _velDebugMv = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_DEBUG_MV") == "1"; private readonly ShaderCollection _velDebugProgram; private TextureView _velDebugTex; - // (b1) 5b-3 triage: per-2s distribution of the debug-view mode actually executed - // (run-2 lesson: no more concluding from eyeballs -- the log says which branch ran). private long _vdLogMs; private int _vdMode0; private int _vdMode1; @@ -904,16 +475,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private readonly VulkanRenderer _gd; private readonly Device _device; private DlssGpuTimer _gpuTimer; - private DlssGpuTimer _reprojTimer; // [18/07] chrono par-passe (reproj seule), label MVPP-REPROJ - private DlssSharpenPass _sharpenPass; // [SHARPEN 02/08] nettete RCAS maison, gate OFF par defaut + private DlssGpuTimer _reprojTimer; + private DlssSharpenPass _sharpenPass; private TextureView _output; - // [DUPSKIP 02/08] Anti-doublons de presentation (journal (348)-(350)) : quand le jeu - // re-presente sans avoir rendu (BOTW ~30 vraies images/s en rotation camera, presentees - // ~60), ne PAS ré-accumuler le temporel sur une image identique a camera figee — re- - // blitter la sortie precedente (_output persiste entre presents). Producteur du signal : - // MvppCameraCapture -> GAL.DlssCameraState.RenderedFrameSeq. Ferme = zero changement. private static readonly bool _dupSkipEnabled = Environment.GetEnvironmentVariable("RYUJINX_DLSS_DUPSKIP") == "1"; @@ -923,11 +489,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private long _dupLogMs; private bool _dupArmedLogged; - // [DUPSKIP v2] Plafond de sauts CONSECUTIFS — trou v1 attrape par Alex a la premiere - // minute (ecran fige en menu : un etat sans dessins 3D immobilise la sequence, et v1 - // re-presentait l'ancienne image pour toujours pendant que le jeu tournait dessous). - // Au-dela du plafond on force une vraie evaluation : au pire on retrouve le comportement - // d'aujourd'hui (accumuler sur un doublon), jamais un gel. 8 presents ~ 130 ms a 60 Hz. private const int DupSkipMaxConsecutive = 8; private int _dupConsecutive; private TextureView _depth; @@ -936,45 +497,22 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private TextureView _prevColor; private bool _hasPrev; - // Two host-mapped uints (unexplained-change + confident-motion pixel counts), read back one - // frame late to drive a DLSS history reset on hard full-screen cuts. _wasChanging and - // _framesSinceReset make that reset edge-triggered + rate-limited, so a fade or a fast pan - // resets once instead of every frame (which would leave the image permanently aliased). private BufferHandle _sceneChangeBuffer; private bool _wasChanging; - // [SKYFLASH] True once the motion meter has measured real motion at least once this - // session; a "low motion" scene-cut verdict means nothing before that (the MVs simply - // have not started flowing), and acting on it resets history on the FIRST camera move. private bool _motionMeterWarm; private int _framesSinceReset; - private long _lastResetMs = Environment.TickCount64; // cooldown counts from DLSS start (huge value = never reset this session) - private string _lastSizeSig; // zoom dossier: last logged (input, mode, output) triple + private long _lastResetMs = Environment.TickCount64; + private string _lastSizeSig; - // RYUJINX_DLSS_RESET_COOLDOWN_MS (zoom-at-startup dossier, 05/07): TOTK's load sequence - // (fade + exposure ramp) swings the changed-fraction above/below the hysteresis for ~7 s - // -> the edge-trigger re-arms repeatedly and fires SIX resets (measured 47.1-54.3 s), - // three of them in-game 1-2 s apart. In quality mode each reset is a visible blur-in - // pulse (DLSS reconverges from the 1.33x input) = the user's "zoom breathing" at startup; - // DLAA hides it (full-res input). A ms cooldown collapses the ramp train to one reset - // (hidden in the black fade). 0/unset = frame-based cooldown only (stock behavior). - - // DLSS Mode B jitter: the offset the just-presented frame was rendered with, and the delta vs - // the previous presented frame, used to de-jitter the motion field so DLSS receives M_real. private float _lastJitterX; private float _lastJitterY; private float _dejitterX; private float _dejitterY; private int _jitterLogCount; - private int _depthState = -1; // B1a depth source, for logging only on change: 2=real, 1=held (last good), 0=dummy - private int _mvppDepthSig = -1; // MV++ depth choice (w<<16|h, 0=none), for logging only on change + private int _depthState = -1; + private int _mvppDepthSig = -1; - // Hold the last VALID real depth so a stray shadow-map capture (wrong dims, e.g. 2048x2048) reuses it - // instead of collapsing to the zeroed dummy -- that flip (real<->zero depth) was a flicker source. - // E9b: one held texture PER SIZE, never disposed during gameplay -- disposing on every - // dyn-res size switch destroyed a texture still referenced by an in-flight command - // buffer (the known lifetime race) and crashed at ~53 s. TOTK uses a handful of - // dyn-res steps; the cache is bounded (~16 MB per size). private readonly System.Collections.Generic.Dictionary<(int, int, Format), TextureView> _heldDepthCache = new(); private TextureView _heldDepth; private bool _hasHeldDepth; @@ -982,9 +520,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private readonly PipelineHelperShader _motionPipeline; private readonly ShaderCollection _motionProgram; - // [MVPP SURVIVE PROBE -- cutout bias mask] Gated conversion of the fragment survivor SSBO - // (scene-pass cutout coverage, see MvppSurviveMaskState) into an R8Unorm mask handed to the - // inject Evaluate as BiasCurrentColorHint. Default OFF => program not even created. private static readonly bool _injectCutoutBias = Environment.GetEnvironmentVariable("RYUJINX_MVPP_INJECT_CUTOUTBIAS") == "1"; private readonly ShaderCollection _cutoutMaskProgram; @@ -998,7 +533,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private readonly ISampler _samplerNearest; private int _inW, _inH, _outW, _outH; - // PINOUT lock: the stable (mode, output) captured on the first good pick; reused every frame after. private StreamlineDlss.DlssMode _pinnedMode; private int _pinnedOutW; private int _pinnedOutH; @@ -1006,16 +540,29 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private bool _modeLogged; private bool _modeWarned; - // Auto-res diagnostic: sample the DLSS internal->output a handful of times over the first seconds so - // we can SEE the one-shot rescale converge (the _modeLogged line only fires once). Auto-res-gated. private int _autoResProbeTick; private int _autoResProbeCount; - // Fires the one-shot auto Resolution Scale exactly once per process (static so a mid-scale-change - // upscaler re-create can't re-fire and loop). See RYUJINX_DLSS_AUTO_RES in DlssIntegration. - private static bool _autoResApplied; + private static long _autoResAppliedSig = -1; + + private static bool _autoResDiagLogged; + + private static long _autoResPendingSig = -1; + private static long _autoResPendingSinceMs; + + private const long AutoResSettleMs = 500; + + private const float AutoResQuantum = 0.1f; + + private static readonly int NoSceneBypassFrames = + int.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DLSS_NOSCENE_BYPASS"), out int nsb) && nsb > 0 + ? nsb + : 0; + + private int _noSceneLastPushes = -1; + private int _noSceneStreak; + private bool _noSceneBypassing; - // DLSS modes from highest quality (smallest upscale) to most aggressive (largest upscale). private static readonly StreamlineDlss.DlssMode[] ModesByQuality = { StreamlineDlss.DlssMode.Dlaa, @@ -1035,7 +582,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss byte[] shader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionVectors.spv"); - // current color (b1), previous color (b3), params UBO (b2), output MV image (b0/set3). ResourceLayout layout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 0) @@ -1045,19 +591,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _sampler = gd.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); - // Depth is NEVER linearly interpolated (standard TAA/DLSS rule): filtering a - // non-linear depth between two texels fabricates phantom depths at every geometry - // edge -- and with in-game dynamic resolution the depth is smaller than the input, - // so UV sampling lands between texels everywhere. Phantom depth x camera drift = - // phantom motion vectors = the whole-image tremble of the mode 2 eye tests. _samplerNearest = gd.CreateSampler(SamplerCreateInfo.Create(MinFilter.Nearest, MagFilter.Nearest)); _motionProgram = gd.CreateProgramWithMinimalLayout([ new ShaderSource(shader, ShaderStage.Compute, TargetLanguage.Spirv) ], layout); - // Second pass: 3x3 median outlier filter. Two storage images (filtered out b0/set3, raw in - // b1/set3), params UBO (b2), stats SSBO (b0/set1). byte[] filterShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionFilter.spv"); ResourceLayout filterLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) @@ -1073,32 +612,23 @@ namespace Ryujinx.Graphics.Vulkan.Dlss if (_mvppReprojMode > 0) { - // MV++ Phase 1b: camera-reprojection MV pass (same binding shape as the motion pass). byte[] mvppShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv"); ResourceLayout mvppLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 0) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 3) - .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 4) // prev depth snapshot (dynamic mask) - .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 5) // J3: VEL coverage (object-MV NDC delta) + .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 4) + .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 5) .Add(ResourceStages.Compute, ResourceType.Image, 0, true) - .Add(ResourceStages.Compute, ResourceType.Image, 1, true) // this frame's depth snapshot - .Add(ResourceStages.Compute, ResourceType.Image, 2, true) // DLSS bias-current-color mask - .Add(ResourceStages.Compute, ResourceType.Image, 3, true).Build(); // pre-deadband MVs (FG) - // NOTE: a binding used by the shader but absent from this layout FAILS SILENTLY - // (reads zero, writes dropped) -- it cost a full debug loop on the dynamic mask. + .Add(ResourceStages.Compute, ResourceType.Image, 1, true) + .Add(ResourceStages.Compute, ResourceType.Image, 2, true) + .Add(ResourceStages.Compute, ResourceType.Image, 3, true).Build(); _mvppReprojProgram = gd.CreateProgramWithMinimalLayout([ new ShaderSource(mvppShader, ShaderStage.Compute, TargetLanguage.Spirv) ], mvppLayout); - // [BORDERFIX v2] passe border-clamp (voir la note au gate). Créée ICI (sous le - // mode reproj) : elle partage le stats-buffer pour son compteur santé, et ne - // sert que le chemin MV++ de toute façon. Gate OFF = zéro création, release - // byte-identique. v2 : LECTURE de la texture du jeu (sampled — la v1 en - // imageStore direct violait STORAGE_BIT, validation 18/07), ÉCRITURE dans - // NOTRE texture, que DLSS reçoit à la place. if (_mvppBorderFix) { byte[] borderShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/BorderClamp.spv"); @@ -1115,16 +645,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppStatsBuffer = gd.BufferManager.CreateWithHandle(gd, MvppStatsCount * sizeof(uint)); - // [SKYGRID] zéro déterministe dès la création : le shader ajoute TOUJOURS les - // résidus de zone (slots 124-187) — si aucun chemin CPU ne les écrit (release - // sans dev/grid), ils doivent être des zéros, pas de la mémoire quelconque. Span statsInitZero = stackalloc uint[MvppStatsCount]; gd.BufferManager.SetData(_mvppStatsBuffer, 0, statsInitZero); } if (_injectCutoutBias) { - // [MVPP cutout bias] survivor SSBO (set1/b0) + params UBO (b2) -> R8 mask image (set3/b0). byte[] cutoutShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppCutoutMask.spv"); ResourceLayout cutoutLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) @@ -1173,25 +699,41 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // [FG lifetime] one tick per present: dispose parked views once dlfg's queue has drained. + if (NoSceneBypassFrames > 0) + { + int pushesNow = GAL.DlssCameraState.StatPushes; + + if (pushesNow != _noSceneLastPushes) + { + _noSceneLastPushes = pushesNow; + _noSceneStreak = 0; + } + else if (_noSceneStreak < int.MaxValue) + { + _noSceneStreak++; + } + + bool bypass = _noSceneStreak >= NoSceneBypassFrames; + + if (bypass != _noSceneBypassing) + { + _noSceneBypassing = bypass; + Logger.Info?.Print(LogClass.Gpu, bypass + ? $"DLSS NOSCENE: aucune camera publiee depuis {_noSceneStreak} images -- DLSS mis de cote, image presentee telle quelle." + : "DLSS NOSCENE: la scene est revenue -- DLSS reprend."); + } + + if (bypass) + { + return false; + } + } + _presentSerial++; DrainFgRetired(all: false); - // MV++: pick up the camera paired with THIS frame (enqueued per present by the GPU - // thread; order does the cross-thread pairing). Consumed HERE and not in the Vulkan - // window's Present: once the DLSS-G swapchain proxy engages, that entry stops running - // every frame and the ring starves -- the frozen-camera regression of 12/07 evening - // (consumer saw 0 changes while the capture published dozens). This method provably - // runs once per evaluated frame: its consumer stats count every present. DlssCameraState.ConsumePresent(); - // MV++ Phase 1 plumbing proof: the presented frame's camera view-projection arrives - // here through the frame queue; keep the previous one (the reprojection pair the MV - // compute pass will consume) and heartbeat so the end-to-end chain is verifiable in - // the log. Inert unless the capture is enabled (RYUJINX_MVPP_CAP=1). - // [CAMSPLIT] Une matrice qui saute en ROTATION au-dela du seuil n'est pas la camera du - // jeu qui a bouge : c'est une AUTRE camera (voir le champ). On ne l'adopte pas -- sauf - // apres CamSplitMaxStreak rejets d'affilee, ou c'est la scene qui a reellement change. bool camSplitReject = false; if (_mvppCamSplit > 0f && _mvppHasCurr && DlssCameraState.PresentVpValid) { @@ -1216,33 +758,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [HOLDCLASS 31/07] INSTRUMENT SEUL, aucune decision ne le lit. Profondeur de la file - // ordonnee APRES la consommation de ce present, deduite des compteurs deja publics de - // DlssCameraState : chaque push ajoute un element, chaque consommation FRAICHE en - // retire un, chaque drop aussi. Aucune modification de la GAL n'est donc necessaire. - // - // CE QU'ON CHERCHE A TRANCHER. Un ecart de presents de 2 ne dit PAS que le vecteur - // couvre deux intervalles : mesure du 31/07 sur 6 evenements, 2 seulement doublaient - // vraiment (rapports 1,86 / 1,97 / 2,19) contre 0,94 / 0,98 / 1,00 pour les autres. - // Hypothese a prouver : si le jeu a produit une camera pendant le present tenu mais en - // RETARD, elle attend encore dans la file au rattrapage (profondeur >= 1 apres - // consommation) ; s'il n'a rien produit, la file est vide (0). - // - // ⚠️ LIMITE CONNUE, a dire avant de lire un chiffre : la consommation a lieu sur le fil - // de presentation (Gpu/Window.cs) et cette lecture a lieu ici, un peu plus tard. Un - // push arrive entre les deux serait compte. C'est une course, non eliminee -- elle - // s'evaluera sur la distribution, pas sur un evenement isole. - // ⚠️ Valide UNIQUEMENT si RYUJINX_MVPP_DEV est ETEINT : le bloc de log de MvppDev remet - // ces compteurs a zero toutes les 5 s, ce qui casserait la difference. _mvppFifoDepth = GAL.DlssCameraState.StatPushes - GAL.DlssCameraState.StatFresh - GAL.DlssCameraState.StatDrops; - // [INTERVALFIX 31/07] Le doublon se lit sur la valeur QUI ARRIVE, avant tout decalage : - // c'est la seule lecture valide dans les deux modes. Correctif ETEINT, incomingDup est - // exactement l'ancienne condition post-decalage (_mvppHasPair && prev.Equals(curr)), - // puisque le decalage fait glisser _mvppVpCurr vers _mvppVpPrev et _mvppHasCurr vers - // _mvppHasPair. Rien ne change donc quand la variable est absente. bool incomingDup = _mvppHasCurr && DlssCameraState.PresentVpValid && DlssCameraState.PresentVp.Equals(_mvppVpCurr); @@ -1253,10 +772,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { if (intervalSkip) { - // Paire ET tampons inchanges : le vecteur de cette image decrit l'intervalle - // PRECEDENT (non nul, plausible) au lieu de zero. L'ecart de tampons passera a - // 2 a l'image de rattrapage, ou le shader divisera par 2. Les deux moities ne - // valent que ensemble : sauter seul = ELECTROT (defaut deplace, pas supprime). _mvppShiftSkipped = true; } else @@ -1268,7 +783,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppVpCurr = DlssCameraState.PresentVp; _mvppHasCurr = true; - // [VPSTAMP] horodatage de l'adoption, en numeros de present (voir le champ). _mvppVpPrev2Stamp = _mvppVpPrevStamp; _mvppVpPrevStamp = _mvppVpCurrStamp; _mvppVpCurrStamp = _presentSerial; @@ -1276,30 +790,13 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppShiftSkipped = false; } - // Change-rate + alternation counters: with the camera untouched a healthy stream - // delivers the SAME VP every present (changed ~0/N). changed~N with a high - // alternation count (A/B/A/B: current equals the one before previous) = TWO - // cameras interleaving (a second scaled pass passing validation, e.g. env-cubemap - // faces); changed~N with ~0 alternations = the game's camera really micro-moves. _mvppPresents++; - // [HOLDCAP 31/07] Comptabilite du figement, INSTRUMENT SEUL. La ligne - // _mvppMoveStreak plus bas est STRICTEMENT equivalente a l'ancienne (meme - // condition, meme resultat) : mvppFreshPair EST l'ancienne condition, extraite - // dans une variable pour pouvoir la lire deux fois sans la recalculer. - // [INTERVALFIX] Reformule sur incomingDup : STRICTEMENT equivalent a l'ancienne - // expression post-decalage quand le correctif est eteint, et seule version juste - // quand il est arme (la paire n'a alors pas bouge, donc la comparer ne dirait plus - // rien du flux camera). Garde l'instrument HOLDCAP vivant dans les deux modes -- - // sans ca, DUMP_ON_HOLD ne se declencherait plus et le test de validation ne - // capturerait rien. bool mvppFreshPair = _mvppHasPair && !incomingDup; _mvppFrozenPair = incomingDup; if (_mvppFrozenPair) { - // Au PREMIER present fige seulement : la serie d'avant n'est plus lisible - // ensuite (elle vaut 0 des le present suivant). if (_mvppHoldRun == 0) { _mvppStreakBeforeFreeze = _mvppMoveStreak; @@ -1314,9 +811,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppMoveStreak = mvppFreshPair ? _mvppMoveStreak + 1 : 0; - // [INTERVALFIX] Meme reformulation, meme raison : identique correctif eteint, et - // n'invente pas un "changement" sur une image ou la paire a deliberement ete - // laissee en place. if (mvppFreshPair) { _mvppChanged++; @@ -1326,10 +820,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppAlternated++; } - // Which element moves, and by how much? Read against the Phase 0 cbuf8 map: - // upper 3x3 = rotation, last column (idx 3/7/11) = translation terms, - // row 3 (idx 12..14) = perspective w-row. Names the physical cause of a - // per-frame camera churn (pitch-clamp spring vs FOV/jitter vs position). Span cur = stackalloc float[16]; Span prv = stackalloc float[16]; WriteMatrix(cur, 0, in _mvppVpCurr); @@ -1345,12 +835,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [ROTDELTA 28/07] Delta sur les seuls termes de ROTATION (bloc 3x3 haut - // gauche). Mesure 28/07 : la matrice change a presque chaque present alors - // que l'image ne bouge PAS d'un pixel -- c'est le jitter sous-pixel du jeu - // (8 phases, +-0,44 px), qui vit dans la projection/translation. La rotation, - // elle, ne bouge que quand le joueur tourne VRAIMENT la camera. On mesure - // l'echelle des deux regimes avant de choisir un seuil : sonde, puis bouton. float rot = 0f; for (int r = 0; r < 3; r++) { @@ -1364,7 +848,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppRotDeltaSum += rot; _mvppRotDeltaN++; - // [JUMPDIST] repartition, puis recurrence des GROS sauts (voir le champ). _mvppJumpBins[rot < 0.01f ? 0 : rot < 0.1f ? 1 : rot < 0.5f ? 2 : rot < 1.0f ? 3 : 4]++; if (rot >= 0.5f) @@ -1372,8 +855,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss bool matched = false; for (int k = 0; k < _mvppJumpDistinct; k++) { - // Tolerance 0,002 : bien au-dessus du bruit float32, bien en dessous - // de l'ecart entre deux sauts physiquement differents. if (Math.Abs(_mvppJumpVals[k] - rot) < 0.002f) { _mvppJumpHits[k]++; @@ -1397,10 +878,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // RYUJINX_MVPP_TRACE=1: raw translation-column series, one line per changed - // present. The SHAPE of the series is the judge the aggregates cannot be: - // a smooth drift = one real camera moving; two interleaved slowly-evolving - // tracks = a second camera (sky/cloud pass?) sneaking through validation. if (_mvppTrace) { Logger.Info?.Print(LogClass.Gpu, @@ -1412,15 +889,14 @@ namespace Ryujinx.Graphics.Vulkan.Dlss if (nowMs - _mvppHeartbeatMs >= 5000) { _mvppHeartbeatMs = nowMs; + if (_mvppVerbose) + { Logger.Info?.Print(LogClass.Gpu, $"MVPP consumer: camera VP changed on {_mvppChanged}/{_mvppPresents} presents, A/B/A/B alternations {_mvppAlternated}, " + $"NO-CAM {_mvppNoCamPresents} presents (paire figee -> vecteur en retard ou cumule), " + $"paire actuelle curr@{_mvppVpCurrStamp} prev@{_mvppVpPrevStamp} (ecart {_mvppVpCurrStamp - _mvppVpPrevStamp}, doit valoir 1), " + $"ROT delta max={_mvppRotDeltaMax:0.######} moy={(_mvppRotDeltaN > 0 ? _mvppRotDeltaSum / _mvppRotDeltaN : 0):0.######} sur {_mvppRotDeltaN} changements, " + $"max element delta {_mvppDeltaMax:0.######} at m[{_mvppDeltaIdx / 4}][{_mvppDeltaIdx % 4}] (row-major)."); - // [JUMPDIST] repartition + les valeurs de GROS saut et leur nombre de repetitions. - // Peu de valeurs tres repetees = etats FIXES = deuxieme camera. Beaucoup de - // valeurs a 1 occurrence = vrai geste continu. var top = new System.Text.StringBuilder(); for (int k = 0; k < _mvppJumpDistinct; k++) { @@ -1436,6 +912,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ? $" || CAMSPLIT seuil={_mvppCamSplit:0.###} plafond={_mvppCamSplitStreak} rejets={_mvppCamRejected} " + $"adoptions-forcees={_mvppCamForced} salve-max={_mvppCamStreakMax}" : "")); + } _mvppCamRejected = 0; _mvppCamForced = 0; @@ -1459,11 +936,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } else { - // [VPSTAMP 28/07] Present arrive SANS camera fraiche : la paire curr/prev ne - // bouge pas, alors que l'image, elle, avance. La passe de reprojection decrira - // donc l'intervalle PRECEDENT (retard), et au present suivant la paire sautera - // par-dessus l'image manquee (vecteur CUMULE sur deux intervalles). C'est le - // mecanisme candidat des vecteurs faux mesures le 28/07 sur le ciel. _mvppNoCamPresents++; } @@ -1472,11 +944,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // [B étape 1, GATED, default A] First native-color feeder. Activates ONLY if the native HDR - // scene color is exactly 2666x1500 R11G11B10Float AND depth is 2666x1500. Any miss (1332, none, - // wrong format/res) => stay on A (presented) this frame. Swapping the local `input` cascades - // 2666 through InputWidth/motion/MvecScale/jitter/depthMatches; a later bail returns false and - // Window.cs falls back to presenting the caller's texture (A). Nothing permanent, nothing else. bool modeB = false; bool bReset = false; string bResetReason = "none"; @@ -1491,8 +958,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss if (liveNatOk && liveDepthOk) { - // Full 2666 native present: feed the LIVE native (proven path) AND snapshot a stable copy - // for a LATER dip. The copy is not read this frame, so there is no same-frame hazard. if (_bNativeHeld == null) { _bNativeHeld = _gd.CreateTexture(MakeInfo(natLive.Info, 2666, 1500, natLive.Info.Format, natLive.Info.BytesPerPixel)) as TextureView; @@ -1501,7 +966,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _bHasNativeHeld = true; _bNativeHeldFrame = DlssJitterState.PresentFrameId; - // A real A->B (re)entry from mode A still clears history; a mere dip does not (below). if (!_bWasActive) { bReset = true; @@ -1513,44 +977,21 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } else if (_bWasActive && _bHasNativeHeld && _blockRes) { - // dyn-res dipped (native now 1332/absent) but a good 2666 frame is locked: keep feeding - // that copy. NO A<->B switch, NO reset -> no black flash. The copy comes from a prior - // frame whose command buffer has completed. Depth-hold below keeps the 2666 depth. input = _bNativeHeld; modeB = true; _bLockedThisFrame = true; } else { - // No locked 2666 frame yet (or lock disabled): clean fallback to A this frame; a later - // B re-entry forces a reset. _bWasActive = false; } } - // Advance the Halton sequence (publishes the NEXT frame's offset to the GPU layer for its scaled - // viewport). THIS frame's actual offset is delivered by the present carrier - // (DlssJitterState.PresentX/Y), which the GPU present set from the value carried alongside this - // exact frame through the queue -- so it always matches the image, independent of frame-queue - // depth. The delta versus the previous presented frame de-jitters the motion field so DLSS - // receives M = M_real. - // Jitter sign calibration (Vulkan Y-down etc. vs the convention Streamline/NGX expects). The SAME - // signs apply to BOTH the offset handed to DLSS (Evaluate, below) AND the de-jitter that removes - // the jitter's apparent motion from the Lucas-Kanade field, so the AI's jitter and our motion - // compensation stay perfectly aligned. - // [JITTER dossier 05/07] Env-tunable: the June calibration (-1,-1) was validated against the - // pre-DLSS4 DLL; the 310.x transformer reads the convention differently (June code + today's - // DLL trembles, proven by the isolated June build). The sign matrix is re-run against the - // current DLL with a numeric stability instrument; defaults keep the June values. float JitterSignX = _jitterSignX; float JitterSignY = _jitterSignY; DlssJitter.Advance(); - // [JITTERVAL-PRES, 10/07] One line per PRESENT: which game frame is on screen (carried id), - // its carried jitter, the freshly-advanced sequence head (fid/ox/oy -- what a mid-frame inject - // evaluate would read as "Offset" until the next present), and the camera-pair state after this - // present's rotation (fresh vs hold, pair delta). Joined offline with JITTERVAL-GEO/-INJ. if (_jitterval) { Logger.Info?.Print(LogClass.Gpu, @@ -1562,18 +1003,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"camD={(_mvppHasPair ? JvFrob(in _mvppVpCurr, in _mvppVpPrev) : -1f):0.000000}"); } - // [FLASHCAP OUT] Read-only dump of the previous present's DLSS output (see FlashOutTick). FlashOutTick(); - // CAMPASS amplitude denominator: the final DLSS-input size (see DlssJitterState). DlssJitterState.InputWidth = input.Width; DlssJitterState.InputHeight = input.Height; - // [Option A] Avoid DOUBLE DLSS: if the mid-frame inject already substituted the scene upscale this - // frame (evaluate OK + SubstituteActive), the composite already carries DLSS quality -> SKIP the - // redundant present DLSS (evaluate + blit). The jitter advance + VP capture above STILL ran (the - // inject reprojection/jitter depend on them). Returning false makes Window present the composite via - // its normal scaling/blit path -- HUD/composite untouched. Fallback: not substituted => present DLSS runs. if (_injectSubstitutedThisFrame) { _injectSubstitutedThisFrame = false; @@ -1590,31 +1024,14 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } float frameJitterX = DlssJitterState.PresentX; float frameJitterY = DlssJitterState.PresentY; - // De-jitter stays at FULL magnitude: it removes a real input-pixel shift from the Lucas-Kanade - // motion field, so it must not be scaled (scaling it would leave jitter motion in the vectors). - // Only the SIGN aligns it with the offset sent to DLSS; the magnitude scale is applied to that - // offset alone (RYUJINX_DLSS_JITTER_SCALE, at the Evaluate call below). - // De-jitter is FLOW-ONLY (rule graved 05/07, validated in game, run H): the flow measures the - // jittered image so its field contains the jitter delta; reprojection MVs (mode >= 2) come - // from matrices and are jitter-free by construction -- subtracting the delta there POLLUTES - // them (+-0.5 px/frame: light ghosting + sky striping under accumulation). Per the DLSS guide, - // clean MVs (MVJittered=0) are used directly. RYUJINX_DLSS_NODEJITTER=1 forces off everywhere. - // [DEPTHGUARD] muted = retour au comportement pré-mode-2 complet, dejitter inclus. bool dejitterApplies = !_noDejitter && (_mvppReprojMode < 2 || _depthGuardMuted); _dejitterX = dejitterApplies ? JitterSignX * (frameJitterX - _lastJitterX) : 0f; _dejitterY = dejitterApplies ? JitterSignY * (frameJitterY - _lastJitterY) : 0f; - // RYUJINX_DLSS_JITTER_PHASE=-1 (diagnostic): declare the PREVIOUS frame's carrier value to - // DLSS instead of the current one -- tests the one-frame phase-slip hypothesis (matrix flat - // across signs + declaration proven consumed + injection proven applied = only a phase error - // is left standing). float declJitterX = _jitterPhaseMinus1 ? _lastJitterX : frameJitterX; float declJitterY = _jitterPhaseMinus1 ? _lastJitterY : frameJitterY; _lastJitterX = frameJitterX; _lastJitterY = frameJitterY; - // Diagnostic probe (first few frames): a non-zero CARRIER offset means the present carrier - // delivered this frame's jitter to DLSS (the old texture-reference lookup returned 0). Remaining - // shimmer with a non-zero offset would then be a sign/convention issue, not a plumbing one. if (DlssJitter.Enabled && _jitterLogCount < 5) { _jitterLogCount++; @@ -1623,11 +1040,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"galNext=({DlssJitterState.OffsetX:0.000},{DlssJitterState.OffsetY:0.000}) dejitter=({_dejitterX:0.000},{_dejitterY:0.000})."); } - // The swapchain (outW x outH) can be letterboxed/pillarboxed relative to the guest's aspect - // ratio. Feeding that raw size to DLSS as the output makes a different-aspect internal exceed - // DLSS's max render rect, so slEvaluateFeature fails every frame -- and NGX leaks on each - // failed evaluate, eventually losing the device. Derive a content size at the input's aspect - // instead; the final blit still positions it into dstRegion (which carries the letterbox). double inAspect = (double)input.Width / input.Height; int contentW = outW; int contentH = (int)Math.Round(outW / inAspect); @@ -1639,29 +1051,94 @@ namespace Ryujinx.Graphics.Vulkan.Dlss contentW = Math.Max(contentW, 1); contentH = Math.Max(contentH, 1); - // DLSS works at the resolution the user chose (their Resolution Scale, optionally preset - // once by the quality mode); we never drive it from here. DLSS-SR only makes sense when - // upscaling (internal smaller than the on-screen content), and DLSS rejects the evaluate - // unless the input is within the chosen preset's dynamic render range, so pick the preset - // that fits this internal resolution and fall back otherwise. - // - // DLAA is the exception: it is DLSS at native (1:1, no upscale), so the internal can equal - // or exceed the on-screen content -- the latter is supersampling, downscaled by the final - // BlitColor below. It must NOT take the upscaling-only bail, and it always uses the DLAA - // preset with the output sized to the input. The internal resolution must be integer-aligned - // (an integer Resolution Scale) to avoid the mip-misalignment device-lost. bool isDlaa = DlssIntegration.Mode == DlssQualityMode.Native; - // [B étape 1] B is a real SR upscale (2666 -> 3200), never DLAA, whatever the mode setting. if (modeB) { isDlaa = false; } + if (!isDlaa && !modeB) + { + long autoResSig = ((long)contentW << 32) | (uint)contentH; + if (DlssIntegration.AutoResScale && _autoResAppliedSig != autoResSig && AutoResSizeSettled(autoResSig)) + { + _autoResAppliedSig = autoResSig; + + if (!_autoResDiagLogged) + { + _autoResDiagLogged = true; + + foreach (StreamlineDlss.DlssMode dm in ModesByQuality) + { + bool okOpt = StreamlineDlss.GetOptimalRenderSize(dm, (uint)contentW, (uint)contentH, out uint oW, out uint oH); + bool okRng = StreamlineDlss.GetRenderRange(dm, (uint)contentW, (uint)contentH, out uint miW, out uint miH, out uint maW, out uint maH); + Logger.Info?.Print(LogClass.Gpu, + $"DLSS range diag [{dm}] @ {contentW}x{contentH}: optimal={(okOpt ? $"{oW}x{oH}" : "n/a")} " + + $"range={(okRng ? $"[{miW}..{maW}]x[{miH}..{maH}]" : "n/a")}."); + } + } + + float s0 = DlssIntegration.ResolutionScaleGetter?.Invoke() ?? 0f; + float factor = DlssIntegration.RenderScaleFactor(DlssIntegration.Mode); + if (s0 > 0f && factor > 0f && input.Height > 0 && DlssIntegration.ResolutionScaleSetter != null) + { + float targetH = contentH * factor; + bool sr_desarme = input.Width >= contentW || input.Height >= contentH; + bool sousOptimal = input.Height < targetH; + + if (!sr_desarme) + { + Logger.Info?.Print(LogClass.Gpu, sousOptimal + ? $"DLSS auto-res: rien a faire -- interne {input.Width}x{input.Height} sous " + + $"l'optimal {(int)targetH}px, mais on ne MONTE plus (l'echelle appartient au joueur ; " + + $"canari Ghostbusters 05/08 : la montee demandait 8x plus de pixels a un jeu en 480p)." + : $"DLSS auto-res: rien a faire -- interne {input.Width}x{input.Height} deja au-dessus " + + $"de l'optimal {(int)targetH}px et sous la sortie {contentW}x{contentH} " + + $"(mieux que {DlssIntegration.Mode}, on ne degrade pas)."); + } + else + { + + float ideal = Math.Clamp(s0 * factor * contentH / input.Height, 0.1f, 8f); + + float newScale = ideal; + + while (newScale > AutoResQuantum && + MathF.Ceiling(input.Height * (newScale / s0)) >= contentH) + { + newScale -= AutoResQuantum; + } + + newScale = MathF.Max(newScale, AutoResQuantum); + + if (!sr_desarme || newScale > s0) + { + float origine = DlssIntegration.UserBaseResScale * factor; + if (origine > newScale && + MathF.Ceiling(input.Height * (origine / s0)) < contentH) + { + newScale = origine; + } + } + + if (MathF.Abs(newScale - s0) > s0 * 0.02f) + { + DlssIntegration.ResolutionScaleSetter(newScale); + _autoResProbeCount = 0; + _autoResProbeTick = 0; + Logger.Info?.Print(LogClass.Gpu, + $"DLSS auto-res: {DlssIntegration.Mode} internal {input.Width}x{input.Height} @ scale {s0:0.000} " + + $"-> scale {newScale:0.000} (target ~{(int)(contentH * factor)}px for display {contentW}x{contentH})."); + return false; + } + } + } + } + } + if (!isDlaa && (input.Width >= contentW || input.Height >= contentH)) { - // DLSS-SR only runs when upscaling (internal render smaller than the on-screen output); - // at native/downscale there is nothing to reconstruct, so fall back to the normal path. return false; } @@ -1671,15 +1148,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss if (isDlaa) { - // 1:1: DLAA anti-aliases at the render resolution; the blit fits it to the screen. mode = StreamlineDlss.DlssMode.Dlaa; dlssOutW = input.Width; dlssOutH = input.Height; } else if (modeB) { - // [B étape 1] force SR native 2666 -> output 3200x1800. Pick a preset whose range accepts - // this input; if none fits, bail => Window.cs falls back to presenting the caller's texture. if (TryPickMode(input.Width, input.Height, 3200, 1800, out mode)) { dlssOutW = 3200; @@ -1698,52 +1172,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } else { - // One-shot auto Resolution Scale (RYUJINX_DLSS_AUTO_RES): the first time we actually upscale, - // nudge the guest resolution scale so THIS internal becomes the chosen mode's optimal preset - // input for THIS display. Then B always takes the ideal path below (DLSS output = display, no - // spatial blit) on every mode. It is a single slider-equivalent write, picked up by the - // existing texture-rescale path -- never per-frame piloting (which historically destabilized - // the renderer). Fires once per process; gated on DLSS-SR so the fractional result is crash-safe. - if (DlssIntegration.AutoResScale && !_autoResApplied) - { - _autoResApplied = true; // once, even on a no-op, so a transient first frame can't loop. - // DIAGNOSTIC (one-shot): DLSS's own optimal + accepted [min,max] render range per mode at - // THIS display, so we can see why a target lands in/out of a preset's range (Ultra angle). - foreach (StreamlineDlss.DlssMode dm in ModesByQuality) - { - bool okOpt = StreamlineDlss.GetOptimalRenderSize(dm, (uint)contentW, (uint)contentH, out uint oW, out uint oH); - bool okRng = StreamlineDlss.GetRenderRange(dm, (uint)contentW, (uint)contentH, out uint miW, out uint miH, out uint maW, out uint maH); - Logger.Info?.Print(LogClass.Gpu, - $"DLSS range diag [{dm}] @ {contentW}x{contentH}: optimal={(okOpt ? $"{oW}x{oH}" : "n/a")} " + - $"range={(okRng ? $"[{miW}..{maW}]x[{miH}..{maH}]" : "n/a")}."); - } - - float s0 = DlssIntegration.ResolutionScaleGetter?.Invoke() ?? 0f; - float factor = DlssIntegration.RenderScaleFactor(DlssIntegration.Mode); - if (s0 > 0f && factor > 0f && input.Height > 0 && DlssIntegration.ResolutionScaleSetter != null) - { - // internal(now) = native * s0 ; want internal = contentH * factor (= display / presetRatio). - // The mode's factor cancels the preset ratio, so this lands the input on the preset's - // optimal internal for the display: newScale = s0 * factor * contentH / internalHeight. - float newScale = Math.Clamp(s0 * factor * contentH / input.Height, 0.25f, 8f); - if (MathF.Abs(newScale - s0) > s0 * 0.02f) // skip if already within 2% of optimal. - { - DlssIntegration.ResolutionScaleSetter(newScale); - Logger.Info?.Print(LogClass.Gpu, - $"DLSS auto-res: {DlssIntegration.Mode} internal {input.Width}x{input.Height} @ scale {s0:0.000} " + - $"-> scale {newScale:0.000} (target ~{(int)(contentH * factor)}px for display {contentW}x{contentH})."); - return false; // render one frame at the old scale; following frames use the corrected one. - } - } - } - - // Ideal path (unchanged behaviour): DLSS output = the display, when the rendered input - // already fits a preset's dynamic range there -- no spatial rescale, best quality. if (_dlssPinOut && _pinnedOutW != 0) { - // PINOUT: reuse the locked (mode, display-output). Never re-derive the output from the - // dyn-res input -> the DLSS output/scale cannot follow the input (the never-coded angle). mode = _pinnedMode; dlssOutW = _pinnedOutW; dlssOutH = _pinnedOutH; @@ -1755,7 +1186,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss if (_dlssPinOut) { - // Lock the GOOD path (output = content/display) once; every later frame reuses it. _pinnedMode = mode; _pinnedOutW = dlssOutW; _pinnedOutH = dlssOutH; @@ -1763,13 +1193,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } else if (_dlssPinOut) { - // PINOUT, no stable lock yet, input out of preset range (spawn ramp): do NOT flap the - // output via input*ratio -- fall back to normal scaling this frame until the input fits. return false; } - // Otherwise the input is just outside every preset's range at the display (Performance at an - // integer scale, or a few-pixel miss): rescale the DLSS output so the input lands safely - // inside a preset, and let the final blit finish to the display. else if (!PickModeAndOutput(input.Width, input.Height, contentW, contentH, out mode, out dlssOutW, out dlssOutH)) { if (!_modeWarned) @@ -1784,12 +1209,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [DUPSKIP 02/08] Presentation-doublon : la sequence des vraies images rendues n'a pas - // bouge depuis le dernier Evaluate -> re-blit de la sortie precedente, telle quelle, - // sans toucher au temporel ni aux ressources. Garde-fous : seq 0 = producteur absent - // (inerte, anti-gel) ; _hasPrev et une sortie a la BONNE taille exiges. En 60 fps - // plein (une vraie image par present), la sequence bouge a chaque present et ce - // chemin ne se declenche JAMAIS — garde-fou n1 du banc, compteur a l'appui. if (_dupSkipEnabled) { if (!_dupArmedLogged) @@ -1858,11 +1277,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss Logger.Info?.Print(LogClass.Gpu, $"DLSS: using mode {mode} for internal {input.Width}x{input.Height} -> {dlssOutW}x{dlssOutH} (content {contentW}x{contentH})."); } - // Size/mode per-change probe (zoom dossier, 05/07): the journal's "output follows the - // input during the spawn ramp" needs the actual timeline. One line per CHANGE of the - // (input, mode, output) triple, silent in steady state -- if this flaps at the moment - // the user sees the zoom, the pinned-output design is the fix; if it stays silent, the - // zoom lives elsewhere (reset reconvergence or game-side). string sizeSig = $"internal {input.Width}x{input.Height} mode={mode} out={dlssOutW}x{dlssOutH}"; if (sizeSig != _lastSizeSig) { @@ -1874,49 +1288,26 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _lastSizeSig = sizeSig; } - // Auto-res convergence probe: unlike _modeLogged, sample a handful of times spaced out (~every - // 60 evaluates) so the log shows the internal growing to the preset-optimal size and the output - // reaching the display (ideal=true = no spatial blit). Only when RYUJINX_DLSS_AUTO_RES is on. if (DlssIntegration.AutoResScale && _autoResProbeCount < 8 && (_autoResProbeTick++ % 60) == 0) { _autoResProbeCount++; double blitPct = 100.0 * (1.0 - Math.Min((double)dlssOutW / contentW, (double)dlssOutH / contentH)); - bool ideal = blitPct < 1.0; // a <=1% final blit is visually lossless (aspect-forced on fixed presets). + bool ideal = blitPct < 1.0; Logger.Info?.Print(LogClass.Gpu, $"DLSS auto-res probe #{_autoResProbeCount}: internal {input.Width}x{input.Height} -> {dlssOutW}x{dlssOutH} (content {contentW}x{contentH}); blit={blitPct:0.00}% ideal={ideal}."); } - // Gold Master: DLSS runs every frame (pure DLSS, no hybrid hand-off to the spatial filter). The - // motion pass below estimates this frame's vectors (previous vs current colour) into _motion, - // and we reset DLSS history on the very first frame and on a detected hard scene cut - // (loading->game, area/menu swap): last frame's motion pass counts the pixels it could not - // explain, and if most of the screen changed in place we clear DLSS history so the stale-history - // ghosting on the cut resolves in ~1 frame. ReadCounters(out uint unexplained, out uint motion); int total = input.Width * input.Height; float changedFraction = total != 0 ? (float)unexplained / total : 0f; float motionFraction = total != 0 ? (float)motion / total : 0f; - // Reset DLSS history only on the rising edge of a *full-screen* change, with hysteresis + a - // cooldown. A localized UI change over a paused scene (an inventory tab, ambient animation while - // standing still) is left alone on purpose: it cannot be told apart from real content, and - // resetting on every such frame stops DLSS ever accumulating, leaving the image aliased. bool changing = _wasChanging ? changedFraction >= SceneChangeLowFraction : changedFraction >= SceneChangeHighFraction; - // A real scene cut (loading->game, area swap) is a near-total change with almost NO coherent - // motion; fast gameplay ALSO produces a high "unexplained change" but with real motion. Requiring - // low motion filters out the spurious mid-gameplay resets that were flickering the image (~one - // every 6s, measured: changed~85% but motion 3-4.5%), while still catching genuine cuts (motion - // near 0). This is the dominant flicker fix. _framesSinceReset++; - // [SKYFLASH 13/07] Trust a low-motion verdict only after the meter has measured real - // motion once. Measured on the user's run: the session's ONLY reset fired at the FIRST - // camera move (changed=91% motion=0.2%) -- the change metric saturates while the MVs - // have not started flowing yet -> false scene cut -> one-frame history reset = the - // startup sky flash. The real session start is already covered by the first-frame reset. if (motionFraction >= _sceneCutMaxMotion) { _motionMeterWarm = true; @@ -1929,34 +1320,24 @@ namespace Ryujinx.Graphics.Vulkan.Dlss && Environment.TickCount64 - _lastResetMs >= (long)_resetCooldownMs; _wasChanging = changing; - // [B étape 2] Swallow the false scene-cut while B feeds native HDR color (the LDR-tuned metric - // saturates -> changed=100%/motion=0% every frame). This is the dominant black-flash fix; the - // resolution lock above only removed the A<->B switch resets. _wasChanging already advanced, so - // the hysteresis state stays coherent; we just do not turn this false edge into a reset. if (modeB && _bNoReset && sceneCut) { _bSuppressedCuts++; sceneCut = false; } - // [CUTONJUMP 27/07] The camera source signals a warp the moment it reads a displacement - // no continuous movement can produce (zone load, teleport, a cutscene taking the view). - // The history describes another place, so it is dropped for this frame. Inert unless - // the source's own gate is armed: without it the sequence never moves. bool camWarp = _hasPrev && GAL.DlssCameraState.TeleportSeq != _lastTeleportSeq; _lastTeleportSeq = GAL.DlssCameraState.TeleportSeq; - bool reset = !_hasPrev || sceneCut || camWarp || bReset; // [B étape 1] bReset forces a clear on B (re)entry / native-res change - // [B étape 2] resolve WHY we reset, for the ALIGN1 log. A dyn-res dip must NOT appear here as a - // reset cause (it stays in B via the native lock) -- that is exactly the flash we removed. + bool reset = !_hasPrev || sceneCut || camWarp || bReset; string resetReason = !reset ? "none" : (!_hasPrev ? "first-frame" : sceneCut ? "scene-cut" : camWarp ? "camera-warp" : bResetReason); if (reset) { _framesSinceReset = 0; _lastResetMs = Environment.TickCount64; - _skyDriftEmaX = 0f; // SKYDRIFT : la dérive appartient au ciel de l'ANCIENNE scène + _skyDriftEmaX = 0f; _skyDriftEmaY = 0f; - Array.Clear(_zoneDriftEmaX); // [SKYGRID] les résidus aussi — même logique + Array.Clear(_zoneDriftEmaX); Array.Clear(_zoneDriftEmaY); } @@ -1965,15 +1346,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss Logger.Info?.Print(LogClass.Gpu, $"DLSS: history reset (changed={100f * unexplained / total:0.#}% motion={100f * motion / total:0.#}%) skyCooldown={_mvppSkyCutCooldown}f."); } - // [POST-MORTEM 18/07, v3 RETIRÉE APRÈS TEST] Tentative "dome quiet par niveau" (famille - // ciel muette tant que changed haut + motion bas, pour couvrir le rideau de chargement - // que le détecteur sur front ne voit pas) : 6 armements pendant du JEU normal en ~1 min - // (pans lents = motion sous le seuil) -> ciel muet en plein pan -> ghost massif. La - // condition n'est PAS pan-proof en lecture par niveau. Ne pas re-tenter tel quel ; le - // vrai discriminant rideau/pan demande la population ciel PAR FRAME (sonde à bâtir). - // Clear the scene-change counters for this frame's pass. The buffer is host-mapped, so this CPU - // write lands before the command buffer is submitted, i.e. before the compute pass runs. Span zero = stackalloc uint[] { 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u }; _gd.BufferManager.SetData(_sceneChangeBuffer, 0, zero); @@ -1984,13 +1357,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _gpuTimer.Begin(cbs.CommandBuffer); } - // [BORDERFIX v3] efface le cadre noir du jeu AVANT TOUT consommateur. v3 : le bloc - // vivait après `Describe(input)` (l.1444) → le descripteur DLSS capturait l'entrée - // ORIGINALE et la texture corrigée n'était consommée par PERSONNE (prouvé au banc - // ECHO A/B : compteur à 1,7 M/s ET bord toujours dans la frame présentée). Il DOIT - // précéder ce Describe — ici, avant les passes LK, qui la voient aussi. La texture - // du jeu est LUE (sampled, légal) et la copie corrigée remplace `input` pour tout - // le reste de la méthode (la v1 en écriture directe violait STORAGE_BIT). if (_mvppBorderFix && _borderFixProgram != null && _mvppReprojMode > 0) { if (input.Info.Format == Format.R8G8B8A8Unorm) @@ -2041,29 +1407,13 @@ namespace Ryujinx.Graphics.Vulkan.Dlss StreamlineDlss.DlssTexture inTex = Describe(input, cbs); StreamlineDlss.DlssTexture outTex = Describe(_output, cbs); - // B1a real depth with a HOLD. The captured guest depth is valid only when it matches the input - // (render) resolution; a size mismatch means it is a secondary target (often a square shadow map - // like 2048x2048). Instead of collapsing to the zeroed dummy on those frames -- which made DLSS - // flip real<->zero depth and flicker -- copy each good real depth into a held texture and reuse - // the LAST good one when the current capture is invalid. The dummy is used only until the first - // real depth ever arrives. bool depthMatches = depth != null && depth.Width == input.Width && depth.Height == input.Height; - // E9: under in-game dynamic resolution the capture NEVER equals the input size, so - // the exact-match condition below meant the held fallback NEVER ARMED in TOTK -- - // every 1-frame capture dropout (measured: 32 in 99 s, one per ~3 s during sweeps) - // sent DLSS a dummy depth + a stale MV field, and the transformer's window grid - // printed nested square outlines = the black-lines exhibit. Hold ANY capture the - // MVPP gate itself accepts (aspect within 2%, at least half the input width). bool depthUsable = depthMatches || (depth != null && depth.Width * 2 >= input.Width && Math.Abs((float)depth.Width / depth.Height - (float)input.Width / input.Height) < 0.02f); - // [B étape 2] While the native lock re-feeds a 2666 frame during a dyn-res dip, do NOT refresh the - // held depth from a low-res live capture -- keep the matching 2666 depth (else a 2666 color would - // pair with a 1332-ish depth). A 1332 capture is already "not usable" vs a 2666 input; this also - // covers the 1333..1499 band where depthUsable would otherwise be true. bool lockKeepsDepth = _bLockedThisFrame && !(depth != null && depth.Width == 2666 && depth.Height == 1500); if (depthUsable && !lockKeepsDepth) { @@ -2079,11 +1429,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _hasHeldDepth = true; } - // Effective depth: live real depth when valid; otherwise the last good held depth; the zeroed - // dummy only until a real depth has ever been seen. - // RYUJINX_DLSS_DEPTH=0 (jitter dossier, 05/07): force the zeroed dummy -- the June jitter - // validation ran depth-dummy; the real/held depth arrives with a 1-frame-old jitter PHASE - // (sub-pixel color/depth disagreement every frame), prime suspect for the jitter tremble. TextureView depthSource = _forceDummyDepth ? _depth : (depthMatches ? depth : (_hasHeldDepth ? _heldDepth : _depth)); int depthState = _forceDummyDepth ? 0 : (depthMatches ? 2 : (_hasHeldDepth ? 1 : 0)); @@ -2097,18 +1442,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss StreamlineDlss.DlssTexture depthTex = Describe(depthSource, cbs); - // MV++ Phase 1b: exact camera-reprojection vectors from real depth + the VP pair. - // Mode 1 computes and compares against the flow (stats in the log, DLSS unaffected); - // mode 2 hands the reprojection vectors to DLSS instead of the flow's. - // Unlike DLSS itself, the reprojection shader samples depth by normalized UV and so - // does not need the capture to match the input size. With in-game dynamic resolution - // (TOTK renders geometry below its output size) the capture NEVER matches the input -- - // accept a live mismatched capture when its aspect ratio agrees with the input (square - // shadow-map strays stay rejected). A live capture beats the held copy: reprojection - // needs THIS frame's depth, not last frame's. - // Size gate on top of the aspect gate: bloom-chain buffers keep the frame's aspect - // (a 256x144 stray was accepted once); real dynamic-res depth never drops below - // half the output size. TextureView mvppDepth = depthMatches ? depth : null; if (mvppDepth == null && depth != null && depth.Width * 2 >= input.Width && @@ -2133,8 +1466,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss bool mvppRan = false; bool mvppPairOk = _mvppVpDelay ? _mvppHasPrev2 : _mvppHasPair; - // [WARMUP] Compte les images consecutives de paire valide ; toute perte remet a zero, - // donc un rechargement ou un changement de scene redemande le delai complet. if (mvppPairOk) { if (_mvppWarmupCount < int.MaxValue) @@ -2157,10 +1488,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss if (_mvppReprojMode > 0 && mvppPairOk && warmedUp && mvppDepth != null && _hasPrev) { - // Double-buffered depth snapshots for the dynamic mask: the pass reads LAST - // frame's and writes THIS frame's on the input grid. (A CmdCopyImage of the - // depth aspect failed silently -- 100% masked, prev read as garbage; the - // imageStore path is the same proven mechanics as the MV texture itself.) if (_mvppDepthSnapA == null || _mvppDepthSnapA.Width != input.Width || _mvppDepthSnapA.Height != input.Height) { RetireFgVisible(_mvppDepthSnapA); @@ -2181,15 +1508,34 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [1.2.3 bavard] Mode reproj armé mais passe jamais lancée = la pile MV/ciel - // entière muette SANS une ligne au log (le dossier nuages 4K a coûté une soirée - // de sonde pour un silence). Nommer la cause manquante, cadence 5 s. + if (_mvppReprojMode > 0) + { + if (_mvppHealthKnown && mvppRan == _mvppHealthRunning) + { + _mvppHealthStreak = 0; + } + else if (++_mvppHealthStreak >= 60) + { + _mvppHealthStreak = 0; + _mvppHealthKnown = true; + _mvppHealthRunning = mvppRan; + + Logger.Info?.Print(LogClass.Gpu, mvppRan + ? "MV++ ACTIF : reprojection en cours (camera appariee, vecteurs livres a DLSS)." + : "MV++ DECROCHE : " + (!mvppPairOk ? "camera non appariee (aucune passe scene detectee)" + : mvppDepth == null ? "profondeur non appariee" + : !_hasPrev ? "pas d'image precedente" + : "passe refusee en interne") + " -- DLSS tourne sans nos vecteurs."); + } + } + if (_mvppReprojMode > 0 && !mvppRan) { long pauseNow = Environment.TickCount64; - if (pauseNow - _mvppPauseLogMs >= 5000) + if (pauseNow - _mvppPauseLogMs >= 5000 && (_mvppVerbose || !_mvppPauseLoggedOnce)) { _mvppPauseLogMs = pauseNow; + _mvppPauseLoggedOnce = true; string pauseCause = !mvppPairOk ? "caméra non appariée (aucune passe scène détectée)" : mvppDepth == null ? "depth non appariée" : !_hasPrev ? "pas d'image précédente" @@ -2201,31 +1547,15 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } TextureView mvSource = DlssIntegration.MvFilterEnabled ? _motionFiltered : _motion; - // [DEPTHGUARD] muted -> l'injection reproj n'est pas consommée : DLSS reçoit les - // MV flow/LK (pré-mode-2). Toute la famille aval (SKYROT/DRIFT/GRID/FARROT) vit - // dans _mvppMotion -> muette par non-consommation, sans toucher à la passe. if (_mvppReprojMode >= 2 && mvppRan && !_depthGuardMuted) { mvSource = _mvppMotion; } - // REPROJ dump instrument: raw MV field + scene depth to disk, 2 s apart. v2: ALSO - // dump the PREVIOUS frame's DLSS OUTPUT (the artifact lives in what DLSS makes; - // dumping inputs alone proved insufficient -- the frozen-zone lead died on E10). - // Correlation method: find the squares in out_XX at exact pixel coords, then read - // every input at those same coords. if (_mvppDump && mvppRan && _mvppDumpCount < 10 && Environment.TickCount64 >= _mvppDumpNotBeforeMs && Environment.TickCount64 - _mvppDumpLastMs >= (_mvppDumpCount % _mvppDumpBurst != 0 ? 0L : (long)_mvppDumpIntervalMs) && - // [MOVESTREAK] garde de mouvement : ne s'applique qu'au DEBUT d'une rafale. - // [HOLDCAP 31/07] Si DUMP_ON_HOLD est arme (> 0), il REMPLACE la garde de - // mouvement au debut de rafale : on tire sur le figement lui-meme, au PREMIER - // present fige (_mvppHoldRun == 1), et seulement s'il etait precede d'au moins - // presents de mouvement continu. La suite de la rafale (N+1, N+2) reste - // libre, exactement comme avec DUMP_ON_MOVE. - // A 0 (defaut) l'expression se reduit a (_mvppDumpOnMove == 0 || - // _mvppMoveStreak >= _mvppDumpOnMove) : comportement d'origine inchange. (_mvppDumpCount % _mvppDumpBurst != 0 || (_mvppDumpOnHold > 0 ? (_mvppFrozenPair && _mvppHoldRun == 1 && @@ -2234,10 +1564,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { _mvppDumpLastMs = Environment.TickCount64; - // [VPSTAMP 28/07] A cote de chaque image dumpee, l'identite EXACTE de la paire de - // matrices qui a servi a la calculer. L'ecart curr-prev doit valoir 1 ; 2 ou plus - // = le vecteur couvre plusieurs intervalles, ce qui se lit directement dans le MV. - // Ecrit AVANT les champs pour que le numero de slot corresponde. try { string mdir = System.IO.Path.Combine(AppContext.BaseDirectory, "mvpp-dump"); @@ -2247,11 +1573,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"present={_presentSerial}\ncurrStamp={_mvppVpCurrStamp}\nprevStamp={_mvppVpPrevStamp}\n" + $"prev2Stamp={_mvppVpPrev2Stamp}\necart={_mvppVpCurrStamp - _mvppVpPrevStamp}\n" + $"vpDelay={(_mvppVpDelay ? 1 : 0)}\nnoCamSinceLastLog={_mvppNoCamPresents}\n" + - // [HOLDCAP 31/07] L'etat de figement de CETTE image, pour que l'analyse - // hors ligne n'ait pas a le redeviner en comparant les matrices. $"frozenPair={(_mvppFrozenPair ? 1 : 0)}\nholdRun={_mvppHoldRun}\n" + $"streakBeforeFreeze={_mvppStreakBeforeFreeze}\nmoveStreak={_mvppMoveStreak}\n" + - // [HOLDCLASS 31/07] le classifieur candidat, voir le champ. $"fifoDepth={_mvppFifoDepth}\nfifoPushes={GAL.DlssCameraState.StatPushes}\n" + $"fifoFresh={GAL.DlssCameraState.StatFresh}\nfifoHolds={GAL.DlssCameraState.StatHolds}\n" + $"fifoDrops={GAL.DlssCameraState.StatDrops}\n" + @@ -2269,21 +1592,15 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } if (_output != null) { - DumpField(_output, "out"); // previous frame's DLSS result (1-frame skew, fine) + DumpField(_output, "out"); } - DumpField(input, "in"); // v3: the COLOR INPUT -- the seams arrive in it (scale-1x proof) + DumpField(input, "in"); - // [HOLDCAP 31/07] L'image PRECEDENTE. Sans elle on ne mesure que la MOITIE du - // mecanisme : le depassement a l'image N+1 se lit avec in_N/in_N+1, mais la - // RETENUE a l'image du figement exige l'image d'avant, qui n'est dans aucun slot. if (_prevColor != null) { DumpField(_prevColor, "prev"); } - // [DYNSONDE étape 2] la carte par-pixel du masque dynamique (R8, 1=dynPixel) : - // c'est ELLE qui donne tailles et formes des objets auto-mobiles (les zones - // 8x4 du log ne donnent que la répartition grossière). if ((_mvppDynMask || _mvppDynSonde) && _mvppBiasMask != null) { DumpField(_mvppBiasMask, "dynmask"); @@ -2292,7 +1609,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppDumpCount++; } - // J1 debug view: colorized MV field instead of the game image; DLSS/FG bypassed. if (_velDebug && _velDebugProgram != null) { RunVelDebugView(mvSource, cbs); @@ -2323,15 +1639,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss StreamlineDlss.DlssTexture mvTex = Describe(mvSource, cbs); - // DLSS jitter offset in PIXELS (Streamline/NGX convention): the value the frame was actually - // rendered with, carried through the present queue (frameJitterX/Y), with the validated signs. - // The clip-space injection in the vertex shader shifts the image by exactly this many pixels, so - // DLSS is told the matching pixel offset. The de-jitter stays at full pixel magnitude too. float evalJitterX = declJitterX * JitterSignX; float evalJitterY = declJitterY * JitterSignY; - // [JITTERVAL-DLSS, 07/07] What we DECLARE to DLSS this frame (carried frame id, resolutions, - // the exact pixel jitter sent to slSetConstants). Joined offline with JITTERVAL-GEO. if (_jitterval) { Logger.Info?.Print(LogClass.Gpu, @@ -2341,8 +1651,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"signX={JitterSignX:0.0} signY={JitterSignY:0.0} declPxX={evalJitterX:0.0000} declPxY={evalJitterY:0.0000}"); } - // [ALIGN0, 07/07] Resource-alignment audit right before Evaluate. Logs on signature change - // (dyn-res / alignment rupture) plus a 2s heartbeat. Reads only; no resource/mode/eval change. if (_align0) { long sig = ((long)input.Width << 40) ^ ((long)depthSource.Width << 26) @@ -2357,11 +1665,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss bool mvDepthObj = object.ReferenceEquals(mvppDepth, depthSource); bool mvDepthDim = mvppDepth != null && mvppDepth.Width == depthSource.Width && mvppDepth.Height == depthSource.Height; bool colorDepthSameFrame = DlssJitterState.PresentFrameId == DlssJitterState.SceneDepthFrameId; - // NATIVE CANDIDATE = the color B WOULD use, logged for alignment proof only -- NOT fed to Evaluate. TextureView natCand = MvppColorSnapshot.SceneColorBackend as TextureView; string natStr = natCand == null ? "none" : $"{natCand.Width}x{natCand.Height} {natCand.Info.Format} colorFrame={DlssJitterState.SceneColorFrameId} id={Hc(natCand)}"; - // Valid same-counter comparison: both stamped with DlssJitterState.FrameId in StashSceneDepth. bool natVsDepthSameFrame = natCand != null && DlssJitterState.SceneColorFrameId == DlssJitterState.SceneDepthFrameId; Logger.Info?.Print(LogClass.Gpu, $"DLSS-ALIGN0[{_abMode}]: COLOR src={(modeB ? "native" : "presented")} {input.Width}x{input.Height} {input.Info.Format} id={Hc(input)} frame={DlssJitterState.PresentFrameId} | " + @@ -2374,9 +1680,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // Bias-current-color hint: only meaningful when DLSS consumes OUR reprojection MVs - // and the dynamic mask ran this frame -- it marks exactly the pixels whose motion - // those MVs cannot describe (animated objects, disocclusions). bool useBiasMask = _mvppReprojMode >= 2 && mvppRan && !_depthGuardMuted && _mvppDynMask && _mvppBias && _mvppBiasMask != null; StreamlineDlss.DlssTexture biasTex = useBiasMask ? Describe(_mvppBiasMask, cbs) : default; @@ -2394,11 +1697,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss in biasTex, useBiasMask); - // [ALIGN1 étape 1] per-frame result of the feeder (A or B). Logs on (modeB, ok, res, reset) - // change + a 2s heartbeat, so A<->B switches and OK/FAIL are all captured. Reads only. if (_align0) { - // Bit 3 = locked-this-frame so a lock enter/exit is always logged even if nothing else moved. long sig1 = (modeB ? 1L : 0L) | (ok ? 2L : 0L) | (reset ? 4L : 0L) | (_bLockedThisFrame ? 8L : 0L) | ((long)input.Width << 8); long now1 = System.Environment.TickCount64; if (sig1 != _align1Sig || now1 - _align1Ms >= 2000) @@ -2421,33 +1721,14 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // DLSS-FG Porte B.2: feed frame generation. Only when the manual-hooking proxies are - // armed (RYUJINX_DLSS_FG=1 + probe OK); reuses THIS evaluate's frame token, command - // buffer and textures - the constants (MvecScale=1/renderRes etc.) set above apply to - // the same token, so FG and SR see one coherent frame. Flag off => FgHooks null => no-op. if (_gd.FgHooks != null) { - // Interpolation needs the REAL inter-frame motion: when the reprojection pass ran, - // optionally hand FG the pre-deadband vectors instead of the SR-stabilized ones - // (the deadband's "nothing moves" is what made FG hold a still image through the - // camera settle = the slide). SR keeps consuming mvTex untouched. StreamlineDlss.DlssTexture fgMvTex = mvTex; if (_mvppFgRawMv && _mvppReprojMode >= 2 && mvppRan && !_depthGuardMuted) { fgMvTex = Describe(_mvppMotionRaw, cbs); } - // [HUDLESSFEED 28/07] L'image SANS INTERFACE, publiee par la couche Gpu via le pont - // MvppColorSnapshot. C'est ce qui manque a dlfg depuis le debut : sans elle il - // derive UI = backbuffer - hudless = 0, conclut qu'il n'y a pas d'interface, et - // deplace le HUD comme du decor -- le HUD dedouble mesure sur les captures d'Alex - // (losange et « 129 » en double, decales de ~110 px). - // - // Publiee a la resolution NATIVE du jeu (1920x1080) alors que le backbuffer est - // upscale : Streamline a deja rejete un HUD-less de taille differente une fois - // (« HUD-less buffer extent does not match color buffer size »). On tague quand - // meme et on LIT LE VERDICT au lieu de le supposer -- si le rejet revient, il - // faudra redimensionner avant de taguer, et on le saura par la mesure. StreamlineDlss.DlssTexture hudlessTex = default; if (GAL.MvppColorSnapshot.SceneColorHost is TextureView hudlessView) @@ -2471,8 +1752,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss BarrierOutput(cbs); } - // [SHARPEN 02/08] Nettete RCAS maison en pre-passe (gate OFF par defaut) : le blit - // final consomme la version nette, tout le reste (tonemap, flip, barres) inchange. TextureView presentSrc = _output; if (DlssSharpenPass.Enabled) @@ -2481,8 +1760,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss presentSrc = _sharpenPass.Run(_output, cbs) ?? _output; } - // DLSS output is already at the final resolution; blit it to the swapchain applying the - // same scRGB/HDR tone-map as the normal present path. _gd.HelperShader.BlitColor( _gd, cbs, @@ -2501,20 +1778,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss whiten, pqOutput); - // Keep this frame's color as the "previous" frame for next time's motion estimate. CopyToPrev(input, cbs); _hasPrev = true; return true; } - /// - /// MV++ Phase 1b: computes camera-reprojection motion vectors into _mvppMotion from the - /// depth buffer and the current/previous view-projection pair. In diagnostic mode the - /// shader also accumulates agreement stats against the optical-flow vectors (read back - /// one frame stale, logged ~1/s): on static world pixels the two must agree, so the - /// percentages are the objective judge of the convention chain (sign, y, depth range). - /// private bool RunMvppReprojectPass(TextureView depth, TextureView input, CommandBufferScoped cbs) { if (_mvppMotion == null || _mvppMotion.Width != input.Width || _mvppMotion.Height != input.Height) @@ -2525,9 +1794,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _mvppMotionRaw = _gd.CreateTexture(MakeInfo(input.Info, input.Width, input.Height, Format.R16G16Float, 4)) as TextureView; } - // Matrices are stored row-major, column-vector convention (clip = M * world). GLSL - // mat4 is column-major, so upload the TRANSPOSE: the shader's standard M * v then - // computes our M * v. System.Numerics.Matrix4x4 vpCurr = _mvppVpDelay ? _mvppVpPrev : _mvppVpCurr; System.Numerics.Matrix4x4 vpPrev = _mvppVpDelay ? _mvppVpPrev2 : _mvppVpPrev; @@ -2539,28 +1805,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss System.Numerics.Matrix4x4 invT = System.Numerics.Matrix4x4.Transpose(invVpCurr); System.Numerics.Matrix4x4 prevT = System.Numerics.Matrix4x4.Transpose(vpPrev); - // [VPJUMP sonde 18/07, lecture seule] accumulé chaque frame dispatché, loggé à 1 Hz - // dans le bloc dev ci-dessous. Ne change RIEN au rendu. - // [PROJQUIET v4] calculé CHAQUE frame (pas seulement en dev) : c'est le déclencheur du - // silence du dôme, il doit vivre aussi chez les users release (SKYROT/EDGE en dur). float vpProj = VpProjJump(in vpCurr, in vpPrev); - // [PROJSERIE 19/07] fenêtre glissante des 8 derniers projJump. Vit en RELEASE : - // la garde anti-zoom ci-dessous lit les 4 frames precedentes. (Sonde du jour, - // run scripte 11:32 : la visee a l'arc = rampe 0,009→0,030→0,060→0,258 qui - // RE-armait le rideau en continu (rearms 8-12/s) = ciel mue pendant toute la - // visee = les nuages qui "reviennent" au soleil. Carte/lunette/paravoile = 0.) _projJumpRing[_projJumpRingIdx] = vpProj; _projJumpRingIdx = (_projJumpRingIdx + 1) & 7; - // [PROJCUT v5 19/07] Le rideau ne s'arme que sur une MARCHE SECHE DEPUIS LE CALME : - // - amplitude >= 0,5 (rideau mesure 0,888-1,53 ; zooms de visee 0,108-0,676 au - // croisement mais TOUJOURS precedes d'une rampe) ; - // - les 4 frames d'avant < 0,02 (un zoom qui croise le seuil traine 0,03-0,095 - // derriere lui ; un vrai cut sort du silence : serie 0,0,0,0→0,888 ; un pan - // ne bouge pas la projection du tout, invariance verifiee 18/07). - // Un zoom progressif est PARFAITEMENT gere par la reprojection (le FOV est dans - // la paire de matrices) -- le silencer etait inutile et visible plein soleil. bool preCalm = true; for (int i = 1; i <= ProjCutCalmWindow; i++) { @@ -2575,8 +1824,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { if (MvppDev.Enabled && _projQuietFrames == 0) { - // [1.2.3 propreté] gated dev : le silence du dôme s'arme en release à chaque - // transition ; la ligne reste un instrument de banc. System.Text.StringBuilder serie = new(); for (int i = 0; i < 8; i++) { @@ -2595,8 +1842,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } else if (MvppDev.Enabled && _mvppSkyCutCooldown > 0 && vpProj > ProjJumpThreshold) { - // [PROJCUT v5, dev] aurait arme en v4 (seuil 0,1) mais la garde l'a retenu : - // le compteur zoomskip du 1 Hz = la preuve banc que les zooms passent. _projZoomSkips++; } else if (_projQuietFrames > 0) @@ -2621,9 +1866,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // Log the PREVIOUS frame's agreement stats ~1/s (host-mapped buffer, no GPU stall), - // then clear the counters for this frame's dispatch. Dev gauge: with MvppDev off the - // stats are never read, so the whole read/zero cycle is skipped (release default). long nowMs = Environment.TickCount64; if ((MvppDev.Enabled || _mvppSkyDrift || _mvppSkyGrid || _mvppDynSonde || _mvppDepthSonde || _mvppDepthGuard) && nowMs - _mvppStatsLogMs >= 1000) { @@ -2632,13 +1874,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss using PinnedSpan data = _gd.BufferManager.GetData(_mvppStatsBuffer, 0, MvppStatsCount * sizeof(uint)); ReadOnlySpan s = System.Runtime.InteropServices.MemoryMarshal.Cast(data.Get()); - // [1.2.3 propreté] gated dev : en release SKYDRIFT arme la grille de stats, s[0] - // accumule — sans cette gate la ligne partirait à 1 Hz dans les logs des users. if (MvppDev.Enabled && s[0] > 0) { - // The flow's confidence gating kills its magnitude in motion but never flips its - // direction: per-axis sign votes are the convention judge (~100% same = as-is, - // ~0% = that axis is flipped, ~50% = no signal on that axis this second). uint vx = s[2] + s[3]; uint vy = s[4] + s[5]; Logger.Info?.Print(LogClass.Gpu, @@ -2646,47 +1883,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"mean |flow|=({s[6] / 16f / s[0]:0.00},{s[7] / 16f / s[0]:0.00}) px, |reproj|=({s[8] / 16f / s[0]:0.00},{s[9] / 16f / s[0]:0.00}) px, sky {s[1]}."); } - // SKYDRIFT sonde: son propre bloc, gated sur skyCount SEULEMENT -- les samples s[0] - // du diag n'accumulent qu'en mode 1, alors que la sonde ciel accumule dans TOUS les - // modes (leçon du 17/07 : la ligne ne sortait jamais en mode 2, config release). if (s[1] > 0) { float meanFX = unchecked((int)s[22]) / 16f / s[1]; float meanFY = unchecked((int)s[23]) / 16f / s[1]; - // SKYDRIFT EMA (armé seulement) : alpha 0.3 par tick ~= convergé en 3-5 s. - // La moyenne spatiale est déjà propre (sonde : signe stable chaque seconde) ; - // l'EMA absorbe les changements de cadrage et les frames aberrantes. - // Pendant le cooldown post-reset, on n'ingère RIEN : la somme lue peut contenir - // une frame à cheval sur le cut (flow inter-scènes) et α=0.3 en ferait une - // dérive fantôme injectée au dôme dès la reprise. - // [PROJQUIET v4] pas d'ingestion pendant la fenêtre non plus : la somme lue peut - // contenir des frames du rideau (mesuré 18/07 : EMA 0.46 = 30x le vrai signal, - // injecté au dôme plusieurs secondes après l'entrée en jeu). - // [SKYCALM 27/07] Ingest only when the measurement is trustworthy, i.e. when the - // camera is calm. RYUJINX_MVPP_SKYCALM=1, OFF by default. - // - // WHY. Measured on Alex's XC2 run: driftEMA = 0.161 DURING pans against 0.040 at - // rest, and after he stops the camera it takes ~6 seconds to come down - // (117 -> 80 -> 55 -> 44 -> 32). An EMA charged by the pans keeps injecting - // movement into an image that has become still - the "elastic sky" of 18/07. - // Note the mechanism is the OPPOSITE of what was assumed back then (it was - // believed to melt during pans and swell at rest). - // - // The value we want is the sky's OWN drift - the cloud sea moving 4 to 10 px per - // MINUTE, i.e. a hair per frame. During a pan the same measurement also carries - // whatever the camera compensation left over, which is orders of magnitude - // larger. So a pan cannot inform this estimate; it can only poison it. - // - // Threshold in PIXELS PER FRAME, deliberately: it is a screen-space quantity, so - // unlike a world-unit constant it means the same thing in every game. 0.1 px per - // frame is 6 px per second - already far above the real drift, and far below the - // 0.161 measured mid-pan. - // - // WORST CASE IS THE CURRENT BEHAVIOUR. If the camera never goes calm the EMA - // simply stops updating and holds its last calm value; it never runs away. That - // is why this is safe where a motion threshold was not: refusing to ingest costs - // nothing, whereas refusing to reproject cost two regressions this month. bool calm = !_mvppSkyCalm || (MathF.Abs(meanFX) <= SkyCalmMaxPxPerFrame && MathF.Abs(meanFY) <= SkyCalmMaxPxPerFrame); @@ -2696,32 +1897,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _skyDriftEmaX += 0.3f * (meanFX - _skyDriftEmaX); _skyDriftEmaY += 0.3f * (meanFY - _skyDriftEmaY); - // [SKYGRID v2] résidus par zone = (moyenne de zone - moyenne globale) de CE - // tick, EMA α=0.25, clampés ±0.15 px (mesure : résidus réels <= ~0.05 — - // borne 3x, un oiseau/effet traversant une zone ne peut pas l'empoisonner). - // Zone sous-peuplée (<200 échantillons : peu de dôme dans cette zone) => - // son EMA décroît vers 0 => elle retombe sur la dérive globale seule. if (_mvppSkyGrid) { - // [SKYBAND 27/07] Fallback by BAND before falling back to the whole sky. - // - // WHAT THE LOG SAID. Every single tick of Alex's run reads - // "y0=(...) y1=(...) y2=(...) y3=(-)": the fourth band NEVER has an - // estimate. That band is the horizon - where his distant clouds are, and - // the only place he still sees ghosting. Its individual zones each hold - // too little sky to clear the quota (the horizon is largely hidden by - // terrain), so every one of them falls back on the average of the WHOLE - // sky, which is dominated by the wide-open zones overhead. A distant - // cloud is then described by the motion of a nearby one. - // - // WHY NOT JUST LOWER THE QUOTA - we tried, and Alex's eye rejected it: - // at 40 samples the starved zones produce noise, and the noise costs - // more than the ghosting it fixes. The quota is right; the FALLBACK was - // wrong. Eight zones that are individually too thin are, together, a - // perfectly populated band - same measurement, eight times the samples, - // and still far closer to those clouds than a global average. - // - // Costs one pass over 32 entries per tick (1 Hz) and nothing per frame. if (_skyBand) { for (int b = 0; b < 4; b++) @@ -2747,35 +1924,13 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { uint zc = s[92 + i]; float tx = 0f, ty = 0f; - // [SKYGRID pop 27/07] 200 samples was the bar for a zone to earn its - // own estimate. Measured on Alex's XC2: "zones=16/32" in most ticks, - // so HALF the sky gets no fine correction at all and falls back on - // the single global drift - and the zones that starve are exactly - // the ones holding small, distant clouds, which is where he still - // sees ghosting. The bar protects against a noisy estimate built on - // a handful of pixels; it is the VALUE that was never measured here. - // Exposed so it can be, default unchanged. if (zc >= _skyGridMinSamples) { - // [SKYGRID clamp 27/07] The 0.15 bound was set on Zelda, where the - // per-zone residuals measured <= 0.05 - a 3x margin there. On - // Alex's XC2 the dispersion between zones measures 0.40 in X and - // 0.60 in Y (37 samples, median 0.17/0.29), so the correction was - // being clipped to a third of what the probe had just measured: - // the residual is seen, then thrown away, and what remains of it - // is exactly the ghosting he still sees on the far clouds. - // - // The bound exists so a bird or an effect crossing one zone - // cannot poison that zone's estimate, so it must stay - it is - // only its VALUE that was borrowed from another game. Exposed as - // a variable to be measured here before it is made automatic. tx = Math.Clamp(unchecked((int)s[28 + i]) / 16f / zc - meanFX, -_skyGridClamp, _skyGridClamp); ty = Math.Clamp(unchecked((int)s[60 + i]) / 16f / zc - meanFY, -_skyGridClamp, _skyGridClamp); } else if (_skyBand && _bandCount[i / 8] >= _skyGridMinSamples) { - // Too thin alone, but its band has plenty. Same clamp: a band - // estimate is an estimate like any other and gets no privilege. tx = Math.Clamp(_bandX[i / 8], -_skyGridClamp, _skyGridClamp); ty = Math.Clamp(_bandY[i / 8], -_skyGridClamp, _skyGridClamp); } @@ -2795,21 +1950,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [SKYCOH sonde 18/07] cohérence spatiale du flow sur le dôme (dossier pointillés - // SKYFLOW) : moyenne |Δflow| entre voisins à 2 px et 16 px. Grille de lecture : - // d2≈d16 = bruit blanc (vote/lissage) ; d2< 0) { Logger.Info?.Print(LogClass.Gpu, $"MVPP SKYCOH sonde: sky={s[1]} d2={s[26] / 16f / s[1]:0.0000} d16={s[27] / 16f / s[1]:0.0000} px."); } - // [SKYGRID sonde 18/07] la lecture qui décide SKYFLOW v2 : les zones du dôme - // (grille 8x4) ont-elles des dérives DIFFÉRENTES (grille d'EMA justifiée) ou la - // même (SKYDRIFT global suffit) ? spread = étendue des moyennes des zones valides - // (>=200 échantillons) ; rangées y = bandes verticales (couches de nuages, le - // plus attendu physiquement). Verdict : spread >> bruit de moyenne (~0.005 px) - // ET motif stable de seconde en seconde = structure réelle. if (MvppDev.Enabled) { int zValid = 0; @@ -2852,11 +1998,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [DYNSONDE étape 2 18/07] LA ligne de la cartographie : population du masque - // (bandes denses, mêmes frames que les compteurs santé s[16]/s[17]), vitalité - // du flow sur ces pixels (le piège connu : confidence gating qui rétrécit sur - // les objets rapides) et désaccord flow-vs-caméra (= le signal qu'une fusion - // injecterait). Rangées y0..y3 = répartition verticale ; max = la zone foyer. if (_mvppDynSonde && s[16] > 0 && s[17] > 0) { Span dRow = stackalloc uint[4]; @@ -2883,16 +2024,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"|flow|=({s[188] / 16f / s[17]:0.00},{s[189] / 16f / s[17]:0.00}) |flow-cam|=({s[190] / 16f / s[17]:0.00},{s[191] / 16f / s[17]:0.00}) px | {dRows}."); } - // [DEPTHSONDE 19/07] la ligne qui départage les dialectes de depth. Grille de - // lecture : farRes GRAND (~10-30 px) + far% gros = le jeu ment sur sa depth - // (attendu BOTW) ; farRes ~0 + midRes vivant en mouvement = dialecte sain, - // la sonde mesure bien (attendu TOTK). Base s[0] = samples (exige DEV=1). if (_mvppDepthSonde && s[230] > 0) { - // [v2] partition EXACTE de la grille : grid = sky + samples + degen + behind. - // degen/behind écrivent MV=0 quand farRot=0 -> décor gelé en pan = ghost. - // Le % dégénéré persistant = LE candidat garde-fou (v1 : les survivants - // étaient sains sur BOTW, la pathologie vit dans les sorties E7). uint gridN = s[230]; uint farN = s[226]; uint midN = s[228]; @@ -2903,13 +2036,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"mid={(s[0] > 0 ? 100f * midN / s[0] : 0f):0.0}% midRes={(midN > 0 ? s[229] / 4f / midN : 0f):0.00} px."); } - // [CHECKER 28/07] La ligne qui tranche l'hypothèse du damier. Lecture : - // diag ÉLEVÉ (quelques % et plus) -> le contenu lointain EST en damier et - // nos deux familles de vecteurs s'y alternent = la cause du clignotement. - // diag ~0 avec split vivant -> discontinuités normales (silhouettes), - // l'hypothèse MEURT et on n'a rien cassé pour le savoir. - // chkN à 0 -> la bande lointaine n'est pas peuplée - // ici : refaire la mesure en visant le ciel, pas un décor proche. if (_mvppChecker && s[233] > 0) { uint cN = s[233]; @@ -2918,10 +2044,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"sky={100f * s[236] / cN:0.0}% (diag = signature damier stricte ; split seul = non concluant)."); } - // [DEPTHGUARD 19/07] évaluation du critère à 1 Hz. On n'évalue que quand les - // deux populations existent (menus/écrans titre = bins vides -> streaks gelés, - // ni bons ni mauvais). Tripped, la passe reproj tourne toujours -> la sonde - // reste vivante et la récupération est possible (dialecte sain retrouvé). if (_mvppDepthGuard && s[230] > 0 && s[0] > 0) { uint gFarN = s[226]; @@ -2958,18 +2080,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [BORDERFIX santé] pixels du cadre remplacés cette seconde. Attendu au banc : - // ~2 rangées + 2 colonnes × fps ≈ 0,5-1,5 M/s quand le cadre est là ; 0 = passe - // morte OU cadre absent (menus). Ligne émise seulement si la passe est armée. if (MvppDev.Enabled && _mvppBorderFix) { - // [1.2.3 propreté] gated dev : BORDERFIX est armé en release, la ligne santé - // reste un instrument de banc. Logger.Info?.Print(LogClass.Gpu, $"MVPP BORDERFIX: filled={s[MvppBorderFilledIndex]}/s."); } - // [VPJUMP sonde 18/07] la ligne qui départage rideau vs pan (voir VpPairJump/VpProjJump). - // [PROJSERIE 19/07] += rearms (armements pendant un silence déjà actif = zoom soutenu). if (MvppDev.Enabled && _vpJumpN > 0) { Logger.Info?.Print(LogClass.Gpu, @@ -2984,9 +2099,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _vpProjMax = 0f; } - // [PERF 18/07] Zéro des compteurs + upload des résidus SKYGRID, UNE fois par - // seconde (ici, juste après la lecture) — jamais par frame (voir la note au - // site de l'ancien bloc : la réécriture par frame sérialisait CPU/GPU, ÷3 fps). Span statsReset = stackalloc uint[MvppStatsCount]; if (_mvppSkyGrid) { @@ -2999,12 +2111,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _gd.BufferManager.SetData(_mvppStatsBuffer, 0, statsReset); - // Center-pixel reconstruction check (any mode): a sane hit reads mid-range d, - // worldW ~ -1/viewZ (meters-scale distance), worldY ~ plausible terrain height. - // Near-collapse or absurd heights = the depth interpretation is wrong, which - // amplifies translation MVs (tiny real camera drift -> visible tremble). - // [1.2.3 propreté] gated dev : le bloc 1 Hz tourne en release (EMA SKYDRIFT), - // la ligne centre reste un instrument de banc. if (MvppDev.Enabled) { Logger.Info?.Print(LogClass.Gpu, @@ -3014,8 +2120,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"dyn masked {(s[16] > 0 ? 100f * s[17] / s[16] : 0f):0.0}%, prevD={BitConverter.UInt32BitsToSingle(s[18]):0.#######}."); } - // J3 freshness-gate health: frames composed vs skipped-stale since the last - // line (stale = no graft reached the coverage between two presents). if (_mvppVelCompose) { Logger.Info?.Print(LogClass.Gpu, @@ -3025,17 +2129,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [PERF 18/07] La remise à zéro des stats + l'upload des résidus SKYGRID vivent - // désormais dans le bloc 1 Hz ci-dessus (juste après la lecture), PLUS jamais par - // frame : un SetData par frame sur un buffer que les frames en vol lisent encore - // sérialisait CPU et GPU (mesuré : app 60 -> 18-20 fps dès SKYDRIFT/SKYGRID armés, - // et le coût caché du mode DEV du banc depuis le début). Les compteurs accumulent - // maintenant sur ~1 s : toutes les lectures sont des ratios/moyennes, rien ne change - // sauf la base statistique (meilleure). - - // Window-depth -> NDC-z convention: from the guest's viewport transform unless the - // env override forces it. Rotation MVs are immune to a wrong mapping, translation - // MVs are ruined by it -- the "pans clean, any camera translation wobbles" signature. bool zMinusOne = _mvppZglEnv == "1" || (_mvppZglEnv != "0" && DlssCameraState.GuestDepthMinusOneToOne); if (!_mvppZLogged || zMinusOne != _mvppZMinusOne) { @@ -3045,18 +2138,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"MVPP z convention: {(zMinusOne ? "MinusOneToOne (GL-style)" : "ZeroToOne")} " + $"({(_mvppZglEnv != null ? "forced" : "auto from guest viewport")})."); - // Run-identity marker (identity ritual: an E1-vs-E2 run was once unreadable from - // the log): the reproj pass's tunables, printed once at first dispatch. Logger.Info?.Print(LogClass.Gpu, $"MVPP reproj tunables: deadband {_mvppDeadLo}/{_mvppDeadHi} px, depthTap={(_mvppDepthLinear ? "LINEAR" : "nearest")}, " + $"farRot={_mvppFarRot}m, edge={_mvppEdgeMode}, compose={_mvppVelCompose}, vpDelay={_mvppVpDelay}, dynMask={_mvppDynMask}."); } - // J3 composition source: the VEL coverage (object-MV NDC delta). Freshness is - // MANDATORY here: no graft since the last present -> the texels still describe - // the PREVIOUS actor positions -> compose off, pure camera path = exact - // baseline. This read consumes the flag (the reproj pass runs BEFORE the debug - // view; the display-only _velFreshGate stays off in J3 runs). TextureView mvppCov = _mvppVelCompose ? MvppVelState.CoverageBackend as TextureView : null; bool mvppCompose = false; if (mvppCov != null) @@ -3067,81 +2153,36 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } _mvppComposedThisFrame = mvppCompose; - // std140: two mat4 (16 floats each) + a run of scalars (padded to 16 bytes). - // [31/07] COMPTE REEL, le commentaire precedent etait perime (il annoncait 52 flottants - // et "17 scalaires (32-48)") : 32 flottants de matrices + 22 scalaires p[32..53] = 54, - // et le bloc GLSL declare exactement 2 mat4 + 22 scalaires. TOUT AJOUT SE FAIT EN FIN - // DE BLOC et se compte DES DEUX COTES avant deploiement -- un bloc plus court cote CPU - // que cote shader = lecture hors limites a chaque image, artefacts qu'aucun reglage ne - // desarme (demi-journee perdue le 28/07, toutes les comparaisons de ce jour faussees). - // En std140 53 et 54 flottants padent tous deux a 224 octets : la taille du bloc ne - // change pas, seul le nombre de flottants ecrits. Span p = stackalloc float[54]; WriteMatrix(p, 0, in invT); WriteMatrix(p, 16, in prevT); p[32] = input.Width; p[33] = input.Height; p[34] = MvppMaxMotion; - p[35] = _mvppReprojMode == 1 ? 1f : 0f; // stats atomics only in diagnostic mode + p[35] = _mvppReprojMode == 1 ? 1f : 0f; p[36] = _mvppFlipY ? 1f : 0f; p[37] = zMinusOne ? 1f : 0f; p[38] = _mvppZeroMv ? 1f : 0f; p[39] = _mvppDeadLo; p[40] = _mvppDeadHi; - // [DYNSONDE] tri-état : 1 = masque armé (zérote), 2 = sonde (compte seulement). p[41] = !_mvppHasPrevDepth ? 0f : (_mvppDynMask ? 1f : (_mvppDynSonde ? 2f : 0f)); p[42] = mvppCompose ? 1f : 0f; - // Cooldown famille ciel/lointain post-reset (voir _mvppSkyCutCooldown) : pendant la - // fenêtre, chaque feature retombe sur sa valeur pré-famille (0) -> dôme muet, edge - // par défaut = le comportement du profil de base, prouvé propre à la transition. bool skyCut = _mvppSkyCutCooldown > 0 && (_framesSinceReset < _mvppSkyCutCooldown || _projQuietFrames > 0); p[43] = skyCut ? 0f : _mvppFarRot; - p[44] = skyCut ? 0f : _mvppEdgeMode; + p[44] = (skyCut && !_mvppEdgeAlways) ? 0f : _mvppEdgeMode; - // E8: the scene depth can be an allocation whose ACTIVE sub-rect (top-left - // anchored) is what the game rendered THIS frame -- the margins hold STALE depth - // from previous dynamic-resolution sizes, and sampling them painted rectangle - // outlines (the user's "nested squares" = the black lines; survived E1-E7 because - // those fixed VALUES, not the sampling DOMAIN). Map the frustum UV onto the - // active area only; 1.0 when there is no sub-rect = exact previous behavior. p[45] = depth.ActiveWidth > 0 ? (float)depth.ActiveWidth / depth.Width : 1f; p[46] = depth.ActiveHeight > 0 ? (float)depth.ActiveHeight / depth.Height : 1f; - p[47] = _mvppSkyRot && !skyCut ? 1f : 0f; // sky-only rotation MV (far plane d>=1.0), no FARROT distance blend - p[48] = _mvppSkyFlow && !skyCut ? 1f : 0f; // sky pixels take the flow sample when credible (cloud drift) - // [CADENCE 18/07] RYUJINX_MVPP_STATS_STRIDE=N : bandes de stats armées 1 frame sur N - // (défaut 1 = chaque frame). Le drift lit à 1 Hz -> insensible à la cadence ; les - // sommes accumulent sur la seconde -> moyennes valides avec 1/N échantillons. - // Test discriminant du 4,4 ms de la passe : chrono qui tombe avec N=4 -> le coût est - // le TRAVAIL des stats (cadence = fix shippable) ; inchangé -> pression REGISTRES - // (le bloc stats gonfle l'allocation du shader entier) -> passe séparée requise. + p[47] = _mvppSkyRot && !skyCut ? 1f : 0f; + p[48] = _mvppSkyFlow && !skyCut ? 1f : 0f; bool statsThisFrame = _mvppStatsStride <= 1 || (_framesSinceReset % _mvppStatsStride) == 0; - p[49] = (MvppDev.Enabled || _mvppSkyDrift || _mvppDynSonde || _mvppDepthSonde || _mvppDepthGuard || _mvppChecker) && statsThisFrame ? 1f : 0f; // grille de stats : sondes dev OU boucle SKYDRIFT OU DYNSONDE OU DEPTHSONDE OU DEPTHGUARD OU CHECKER - p[50] = _mvppSkyDrift && !skyCut ? _skyDriftEmaX : 0f; // SKYDRIFT : dérive contenu ciel (px/frame, EMA 1 Hz) + p[49] = (MvppDev.Enabled || _mvppSkyDrift || _mvppDynSonde || _mvppDepthSonde || _mvppDepthGuard || _mvppChecker) && statsThisFrame ? 1f : 0f; + p[50] = _mvppSkyDrift && !skyCut ? _skyDriftEmaX : 0f; p[51] = _mvppSkyDrift && !skyCut ? _skyDriftEmaY : 0f; - // [SKYDEPTH 27/07] Window depth at or above which a pixel counts as sky. 1.0 is the - // historic rule - the far plane and nothing else - and stays the default, so without - // the variable every game keeps byte-identical behaviour. - // - // Lowering it slightly brings in what is drawn JUST in front of the dome: distant - // clouds. They were counted by nobody - excluded from the sky drift estimate (measured: - // sky = 1.5 M texels while only 2 zones of 32 were populated) and left to a reprojection - // that can only describe camera motion, never an object's own drift. Alex had already - // narrowed it by eye: "it is the far clouds, not the ones flying close". - // - // The bound must stay TIGHT: go too low and distant mountains join the sky and receive - // a drift that is not theirs. This is a value to measure, not to guess, which is why it - // is a variable before it is ever a default. p[52] = _skyDepth; - // [INTERVALFIX 31/07] Facteur temporel : combien d'intervalles la paire (curr, prev) - // couvre-t-elle REELLEMENT. Les tampons sont en numeros de present (_presentSerial, - // un tick par present, jamais remis a zero), donc leur ecart EST ce nombre. - // Normalement 1. Il ne vaut 2 qu'a l'image de rattrapage d'un figement, quand le - // decalage a ete saute au present precedent. Plafonne a 2 : au-dela, l'hypothese de - // vitesse constante ne tient plus, et le saut de decalage est deja limite a un seul - // de suite. Correctif eteint => aucun saut => ecart toujours 1 => facteur 1.0. long vpSpan = _mvppVpCurrStamp - _mvppVpPrevStamp; p[53] = _mvppIntervalFix && vpSpan == 2 ? 0.5f : 1.0f; @@ -3156,8 +2197,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 1, depth, _mvppDepthLinear ? _sampler : _samplerNearest); _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _motion, _sampler); _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 4, _mvppHasPrevDepth ? snapRead : depth, _samplerNearest); - // Binding 5 is in the static layout, so it must always hold a valid view; when - // composition is off this frame the shader never samples it (covCompose = 0). _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 5, mvppCov ?? _motion, _samplerNearest); _motionPipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]); _motionPipeline.SetStorageBuffers([new BufferAssignment(0, new BufferRange(_mvppStatsBuffer, 0, MvppStatsCount * sizeof(uint), true))]); @@ -3166,10 +2205,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _motionPipeline.SetImage(2, _mvppBiasMask.GetImageView()); _motionPipeline.SetImage(3, _mvppMotionRaw.GetImageView()); - // [CHRONO PAR PASSE 18/07] deuxième chrono, autour de la passe reproj SEULE — pour - // nommer les ~4,5 ms résiduelles du chemin devStats (chrono global : 8 vs socle 3,5). - // Si "MVPP-REPROJ avg" ≈ 4-5 ms -> le coût vit DANS cette passe (bandes/fetches/ - // réductions) ; s'il reste ≈ 1 ms -> le coût vit AILLEURS (autre passe/sync du chemin). bool timeReproj = DlssIntegration.GpuTimingEnabled && MvppDev.Enabled; if (timeReproj) { @@ -3191,15 +2226,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// - /// [MVPP cutout bias] Converts this frame's fragment survivor SSBO (scene-pass cutout - /// coverage, captured via the mirror binding -- see MvppSurviveMaskState) into the R8Unorm - /// bias mask at the DLSS input resolution. Runs in the inject command buffer, after the - /// scene draws and before Evaluate; the caller passes the mask as BiasCurrentColorHint. - /// - /// DLSS input color (defines the mask dimensions) - /// Command buffer scope of the inject - /// True if the mask was generated and can be fed to Evaluate private bool RunInjectCutoutMask(TextureView scene, CommandBufferScoped cbs) { if (!MvppSurviveMaskState.HasMask || _cutoutMaskProgram == null) @@ -3213,8 +2239,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _injectCutoutMask = _gd.CreateTexture(MakeInfo(scene.Info, scene.Width, scene.Height, Format.R8Unorm, 1)) as TextureView; } - // The survivor stores were written by fragment shaders during the scene draws earlier - // in this same command buffer; make them visible to the conversion compute. MemoryBarrier preBarrier = new() { SType = StructureType.MemoryBarrier, @@ -3250,12 +2274,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// - /// [MVPP cutout bias] Resets the survivor SSBO to zero for the next frame (the mask is - /// per-frame) and orders the fill against the compute read before it and the next frame's - /// fragment stores after it. Called only after a successful Evaluate consumed the mask. - /// - /// Command buffer scope of the inject private void ResetCutoutMaskBuffer(CommandBufferScoped cbs) { BufferRange range = MvppSurviveMaskState.MaskRange; @@ -3263,7 +2281,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss unsafe { - // WAR: the conversion compute read the buffer earlier in this command buffer. MemoryBarrier warBarrier = new() { SType = StructureType.MemoryBarrier, @@ -3284,7 +2301,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss (ulong)range.Size, 0); - // Fill -> next frame's fragment survivor stores. MemoryBarrier postBarrier = new() { SType = StructureType.MemoryBarrier, @@ -3300,11 +2316,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// J1 debug view: colorize the MV field (hue = direction, brightness = magnitude, - /// black = still) into an output-sized RGBA8, presented by the caller instead of the - /// game image. - /// private void RunVelDebugView(TextureView mv, CommandBufferScoped cbs) { if (_velDebugTex == null || _velDebugTex.Width != _output.Width || _velDebugTex.Height != _output.Height) @@ -3313,13 +2324,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _velDebugTex = _gd.CreateTexture(MakeInfo(_output.Info, _output.Width, _output.Height, Format.R8G8B8A8Unorm, 4)) as TextureView; } - // 3b: actor coverage overlay. The bridge holds a ThreadedTexture; unwrap to the - // backend view (null until the queued create ran, or before the first graft). TextureView coverage = MvppVelState.CoverageBackend as TextureView; - // Freshness gate (RYUJINX_MVPP_VEL_FRESH=1, display-only): skip compositing a - // coverage that received no graft since the last present -- a stale overlay - // frozen on screen sabotaged two run readings (parking dossier). bool covFresh = true; if (_velFreshGate) { @@ -3330,11 +2336,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss Span p = stackalloc float[4]; p[0] = _output.Width; p[1] = _output.Height; - p[2] = 16f; // px of motion mapped to full brightness - // Mode: 0 = no overlay, 1 = 3b green coverage, 2 = 5b-3 object-MV colorize. - // J3: under composition the MAIN field (MvTex = _mvppMotion) already carries - // the composed camera+object vectors -- an overlay would repaint the actor - // tints the calibration run exists to read, so it is forced off. + p[2] = 16f; p[3] = _mvppVelCompose ? 0f : ((coverage != null && covFresh) ? (_velDebugMv ? 2f : 1f) : 0f); if (p[3] > 1.5f) { _vdMode2++; } else if (p[3] > 0.5f) { _vdMode1++; } else { _vdMode0++; } @@ -3366,10 +2368,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _motionPipeline.Finish(); } - /// - /// REPROJ dump instrument: one texture's full contents to mvpp-dump/ as raw bytes, - /// dimensions and format in the filename so the offline analysis knows how to parse. - /// private void DumpField(TextureView tex, string tag) { try @@ -3388,17 +2386,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [VPJUMP sonde 18/07] Distance relative élément-par-élément entre les deux matrices VP que - // la reprojection APPARIE cette frame. Normalisée par élément (les termes translation sont - // à l'échelle monde, ~milliers ; un L1 brut serait dominé par leur jitter) -> sans dimension, - // borné ~[0..16]. Hypothèse à MESURER avant tout bouton : pan = évolution continue (delta - // petit), rideau/swap d'UI/téléport = saut de projection (delta O(1)) — le discriminant que - // ni le flow ni les métriques d'écran n'ont su donner (post-mortem v2/v3 plus haut). - // [CAMSPLIT 28/07] Plus grand ecart element-a-element sur le bloc 3x3 de ROTATION. La - // rotation est choisie exprès : le jitter sous-pixel du jeu vit dans la projection et la - // translation (il fait changer la matrice a presque chaque image sans que l'image bouge - // d'un pixel), alors que la rotation ne bouge que si la camera tourne VRAIMENT. Mesure - // 28/07 : legitime < 0,1 ; intrusion de la seconde camera = 1,13893, constant. private static float RotJump(in System.Numerics.Matrix4x4 a, in System.Numerics.Matrix4x4 b) { Span fa = stackalloc float[16]; @@ -3434,12 +2421,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return sum; } - // [VPJUMP v2 18/07] Delta de la PROJECTION seule entre les deux matrices appariées. En pan - // (même violent) la projection est INVARIANTE : les normes des lignes de VP l'extraient - // (clip = P*V, V orthonormale -> ||row0.xyz|| = sx, ||row1.xyz|| = sy, ||row3.xyz|| = 1 en - // perspective / 0 en ortho, quelle que soit la rotation). Un swap UI/chargement la change - // structurellement. Sonde v1 mesurée : rideau max 6,5 vs pan rapide max 2,2 = marge x3, - // insuffisante pour un seuil -- v2 vise la séparation en ordres de grandeur. private static float VpProjJump(in System.Numerics.Matrix4x4 a, in System.Numerics.Matrix4x4 b) { static float RowNorm(float x, float y, float z) => MathF.Sqrt(x * x + y * y + z * z); @@ -3458,26 +2439,13 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private float _vpProjSum; private float _vpProjMax; - // [PROJQUIET 18/07, v4 — branché sur la sonde v2 APRÈS mesure] Frames restantes de silence - // de la famille ciel, armées par un saut de PROJECTION entre les deux matrices appariées. - // Mesuré (runs 07-49/07-55) : pan le plus violent projJump<=0.008, rideau=0.888 -> seuil 0.1 - // = 12x au-dessus du bruit pan, 9x sous le signal rideau. Durée 2x le cooldown (~2 s) car le - // rideau fait DEUX sauts : swap de projection PUIS téléport de vue pur (~1 s plus tard, - // projection intacte) -- la fenêtre doit couvrir les deux. Un pan ne peut PAS l'armer - // (la projection est invariante en rotation, vérifié à la sonde) — c'est ce qui distingue - // ce v4 du v3 retiré (métriques d'écran = pan-ambiguës ; la projection ne l'est pas). private const float ProjJumpThreshold = 0.1f; private int _projQuietFrames; - // [PROJCUT v5 19/07] garde marche-seche-depuis-le-calme (mesures du jour, run 11:32) : - // amplitude minimale d'un vrai cut + fenetre de calme requise avant lui. Le seuil 0,1 - // (ProjJumpThreshold) ne sert plus qu'au compteur dev zoomskip (ce que v4 aurait arme). private const float ProjCutMagnitude = 0.5f; private const float ProjCutCalmEps = 0.02f; private const int ProjCutCalmWindow = 4; - // [PROJSERIE 19/07] fenêtre glissante des 8 derniers projJump (release : la garde - // calme la lit ; dev : forme zoom-vs-cut a l'armement) + compteurs de banc. private readonly float[] _projJumpRing = new float[8]; private int _projJumpRingIdx; private int _projRearms; @@ -3510,8 +2478,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 1, input, _sampler); _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _prevColor, _sampler); - // 8 floats = a full std140 2x vec4 block (width/height/maxMotion/metrics + dejitterX/Y + 2 pad). - // metrics is hardcoded off: the heavy stat atomics in the shader are dead in the Gold Master. ReadOnlySpan p = [input.Width, input.Height, MaxMotion, 0f, _dejitterX, _dejitterY, 0f, 0f]; using ScopedTemporaryBuffer buffer = _gd.BufferManager.ReserveOrCreate(_gd, cbs, p.Length * sizeof(float)); buffer.Holder.SetDataUnchecked(buffer.Offset, p); @@ -3527,14 +2493,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private void RunMotionFilterPass(TextureView input, CommandBufferScoped cbs) { - // Second compute pass: 3x3 median outlier filter on the raw motion field. Reads _motion - // (made visible by the previous pass's ComputeBarrier) and writes _motionFiltered -- this is - // what makes the Lucas-Kanade field clean enough to hand to DLSS. _motionPipeline.SetCommandBuffer(cbs); _motionPipeline.SetProgram(_motionFilterProgram); - // 8 floats = a full std140 2x vec4 block (width/height/maxMotion/metrics + dejitterX/Y + 2 pad). - // metrics is hardcoded off: the heavy stat atomics in the shader are dead in the Gold Master. ReadOnlySpan p = [input.Width, input.Height, MaxMotion, 0f, _dejitterX, _dejitterY, 0f, 0f]; using ScopedTemporaryBuffer buffer = _gd.BufferManager.ReserveOrCreate(_gd, cbs, p.Length * sizeof(float)); buffer.Holder.SetDataUnchecked(buffer.Offset, p); @@ -3549,12 +2510,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _motionPipeline.Finish(); } - /// - /// Reads back the previous frame's scene-change counters (unexplained-change and confident-motion - /// pixel counts) from the host-mapped buffer. One frame of latency is fine: scene-cut ghosting - /// spans several frames, so resetting one frame late still clears it, and reading last frame's - /// value avoids a GPU stall. - /// private void ReadCounters(out uint changed, out uint motion) { using PinnedSpan data = _gd.BufferManager.GetData(_sceneChangeBuffer, 0, CounterCount * sizeof(uint)); @@ -3591,8 +2546,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss in region); } - // Copies the current valid real depth into the held texture (same dims/format), so a later frame - // whose capture is a stray shadow map can reuse this last good depth instead of the zeroed dummy. private void CopyDepth(TextureView src, TextureView dst, CommandBufferScoped cbs) { ImageSubresourceLayers layers = new() @@ -3627,17 +2580,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss Image = (IntPtr)tex.GetImage().Get(cbs).Value.Handle, View = (IntPtr)tex.GetImageView().Get(cbs).Value.Handle, NativeFormat = (uint)tex.VkFormat, - Layout = (uint)ImageLayout.General, // Ryujinx keeps its textures in General. + Layout = (uint)ImageLayout.General, Width = (uint)tex.Width, Height = (uint)tex.Height, }; } - /// - /// Barriers the DLSS output between the NGX evaluate (which wrote it via raw Vulkan handles, - /// invisible to Ryujinx's resource tracking and the validation layer) and the blit that reads it. - /// Closes the otherwise-missing write -> read dependency: a candidate for the rare device-lost hang. - /// private unsafe void BarrierOutput(CommandBufferScoped cbs) { Image image = _output.GetImage().Get(cbs).Value; @@ -3659,7 +2607,25 @@ namespace Ryujinx.Graphics.Vulkan.Dlss 0, 0, null, 0, null, 1, &b); } - // Ideal fit: the smallest-upscale preset whose dynamic range contains the input at the display. + private static bool AutoResSizeSettled(long sig) + { + if (_autoResAppliedSig < 0) + { + return true; + } + + long nowMs = Environment.TickCount64; + if (_autoResPendingSig != sig) + { + _autoResPendingSig = sig; + _autoResPendingSinceMs = nowMs; + + return false; + } + + return nowMs - _autoResPendingSinceMs >= AutoResSettleMs; + } + private static bool TryPickMode(int inW, int inH, int outW, int outH, out StreamlineDlss.DlssMode mode) { foreach (StreamlineDlss.DlssMode candidate in ModesByQuality) @@ -3678,12 +2644,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // Make DLSS-SR engage at ANY resolution scale / any display. Forcing the DLSS output to the - // display and hoping the rendered input lands exactly inside a preset's dynamic range is razor - // thin -- a 4px miss (input 1840 vs a 1844 minimum) drops to "no preset fits". Instead: pick the - // preset whose natural upscale ratio best matches this input, set the DLSS output to that preset's - // natural output for our input (so the input is always safely inside the range), and let the - // existing final blit scale that output to the real display (a fraction of a percent, invisible). private static bool PickModeAndOutput(int inW, int inH, int displayW, int displayH, out StreamlineDlss.DlssMode mode, out int outW, out int outH) { @@ -3698,33 +2658,24 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { if (candidate == StreamlineDlss.DlssMode.Dlaa) { - continue; // DLAA is native 1:1, handled on its own path above. + continue; } - // The preset's optimal internal for the display gives its upscale ratio. if (!StreamlineDlss.GetOptimalRenderSize(candidate, (uint)displayW, (uint)displayH, out uint optW, out uint optH) || optW == 0 || optH == 0) { continue; } - // Output = our input scaled up by this preset's ratio, so our input becomes the preset's - // optimal internal. double ratioW = (double)displayW / optW; double ratioH = (double)displayH / optH; - // A fixed-ratio preset (UltraPerformance reports a DEGENERATE range [opt..opt] with no dynamic - // window) accepts only its exact optimal input. Aim the output at input*ratio with NO bias so - // the preset's optimal at that output rounds back to our input. Ranged presets keep the ~0.5% - // down-bias so rounding never leaves the input just below the range minimum (the 1840/1844 case); - // that same down-bias would push a fixed preset's required optimal UNDER our input and reject it. bool degenerate = StreamlineDlss.GetRenderRange(candidate, (uint)displayW, (uint)displayH, out uint dMinW, out _, out uint dMaxW, out _) && dMinW == dMaxW; int candOutW = degenerate ? (int)Math.Round(inW * ratioW) : (int)(inW * ratioW * 0.995); int candOutH = degenerate ? (int)Math.Round(inH * ratioH) : (int)(inH * ratioH * 0.995); - // Confirm the input really is inside this preset's dynamic range at that output. if (!StreamlineDlss.GetRenderRange(candidate, (uint)candOutW, (uint)candOutH, out uint minW, out uint minH, out uint maxW, out uint maxH) || inW < minW || inW > maxW || inH < minH || inH > maxH) @@ -3732,7 +2683,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss continue; } - // Prefer the preset whose output sits closest to the real display (smallest final blit). long delta = Math.Abs((long)candOutW - displayW) + Math.Abs((long)candOutH - displayH); if (delta < bestDelta) { @@ -3747,13 +2697,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return found; } - /// - /// [INJECT étape C3] Run DLSS from the validated mid-frame hook at the REAL injection point and write - /// directly into the game's real DESTINATION render target (carried as a handle by the GAL command, - /// like the source). Returns TRUE when the guest upscale draw should be SKIPPED (C3b): DLSS evaluate - /// succeeded (real dst written) AND substitution is active. Returns FALSE on any failure/miss, so the - /// caller keeps the guest draw = automatic fallback. Host dimensions read from the dst TextureView. - /// public bool RunInjectMidFrame(CommandBufferScoped cbs, ITexture src, ITexture dst, ITexture depth) { if (!_injectPass || !DlssIntegration.IsEnabled || !Streamline.IsInitialized) @@ -3761,10 +2704,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // Deferred dump (DumpField model): read the real dst captured last frame, before it changes again. - // With the guest draw kept, this shows the guest-overwritten dst (a normal game frame) -- a sanity - // check that we target a real scene RT and that reading the real dst is safe. DLSS's own pixels are - // validated in C3b (draw skipped). if (_injectPendingDump && !_injectDumped && _injectLastDst != null) { _injectPendingDump = false; @@ -3772,9 +2711,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss DumpInjectPng(_injectLastDst); } - // [FLASHCAP / Mesure A] Moved to Window.cs (measures the PRESENTED frame, which is readable; the - // NGX-written DLSS output reads back black, so measuring it here was useless). - TextureView scene = src as TextureView; TextureView dstView = dst as TextureView; if (scene == null || dstView == null || @@ -3785,12 +2721,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // [DUMPCOLOR, GATED default OFF] Dump the RAW scene src and the snapshot (what POSTCOLOR would - // feed) to PNGs, to SEE if they really differ. `scene` is the raw src here (before any swap). - // ROBUST vs loading screens: only commits on a BRIGHT frame (a black/loading frame is rejected - // and it retries). Throttled to once/second so the retry readbacks don't stall every frame. On a - // real capture it arms MvppColorSnapshot.DumpArmed so the present-side COLORDUMP fires the SAME - // gameplay moment. Read-only. if (_injectDumpColor && !_injectColorDumped && _injectFrame >= 120 && Environment.TickCount64 - _injectDumpLastTryMs > 1000) { @@ -3806,11 +2736,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [SUBRECT, GATED default OFF] Hypothesis C measurement (read-only). Classify this inject - // frame by the game's current dynres step (camera viewport published by StateUpdater), - // count them, and dump the FULL inject input once per class: at a low step the PNG shows - // what the RT holds outside the active corner (stale previous frame? garbage?), which the - // Evaluate nevertheless tags as valid full-extent input. if (_dumpSubrect) { float camW = DlssJitterState.CamVpW; @@ -3859,11 +2784,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [POSTCOLOR, GATED default OFF] Swap the raw pre-tonemap mid-frame scene for the native - // POST-TONEMAP snapshot (same resource mode B feeds), so DLSS reconstructs on the tamed image - // (no highlight bleed). Only when the snapshot exists AND matches the raw scene's host dims + - // format; any miss keeps the raw scene => automatic fallback, no hazard. Dims are preserved so - // rw/rh, the aux textures and the MV pass below are unaffected. _injectPostColorUsed = false; if (_injectPostColor) { @@ -3876,9 +2796,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [HELDCOLOR, GATED default OFF] Feed the PRESENT-captured TONEMAPPED held copy instead of the raw - // live scene. Proven (dumps 09/07) a genuinely different, balanced image (vs the raw crushed - // mid-frame). 1-frame stale => judge static. Miss => keep the current scene (automatic fallback). _injectHeldColorUsed = false; if (_injectHeldColor) { @@ -3891,8 +2808,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - int rw = scene.Width, rh = scene.Height; // host source dims - int ow = dstView.Width, oh = dstView.Height; // host destination dims (the REAL RT) + int rw = scene.Width, rh = scene.Height; + int ow = dstView.Width, oh = dstView.Height; if (!TryPickMode(rw, rh, ow, oh, out StreamlineDlss.DlssMode mode)) { @@ -3906,14 +2823,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // Placeholders (zeroed) for anything not yet wired. Motion still zero (C4 next). Depth: use the - // REAL scene depth when it is present AND at the same host resolution as the colour input; else - // fall back to the zeroed placeholder (never feed DLSS a mismatched depth). EnsureInjectAux(rw, rh, scene, cbs); - // [TINTPROBE, GATED default OFF] Replace the DLSS colour input with a magenta texture we own. - // Same dims/format as the scene, so depth/MV/aux below stay valid. Decisive: magenta on screen => - // inject colour reaches the display; normal screen => the game overwrites it downstream. _injectTintUsed = false; if (_injectTintProbe) { @@ -3931,38 +2842,25 @@ namespace Ryujinx.Graphics.Vulkan.Dlss string depthReason = depthOk ? "real" : (realDepth == null ? "zero(no-depth)" : $"zero(dim-mismatch {realDepth.Width}x{realDepth.Height} vs {rw}x{rh})"); - // [C4] GENERATE the scene-res camera-reprojection MV (validated). Runs only with a valid real depth. TextureView genMv = depthOk ? RunInjectReprojectPass(realDepth, cbs) : null; - // [C4 wire] Feed the REAL MV when it was generated at the scene render res; else fall back to the - // zero placeholder (automatic fallback, never a mismatched MV). bool mvReal = genMv != null && genMv.Width == rw && genMv.Height == rh; TextureView mvForDlss = mvReal ? genMv : _injectMotionZero; - // [RAW-MV-SR A/B] pre-deadband field instead (same res, written by the same dispatch). if (_rawMvSr && mvReal && _injectMotionRaw != null && _injectMotionRaw.Width == rw && _injectMotionRaw.Height == rh) { mvForDlss = _injectMotionRaw; } - // Jitter DECLARED to DLSS = the offset the scene was rendered with (scene is jittered, RYUJINX_DLSS_JITTER=1). - // MV stay CLEAN (MotionVectorsJittered=false). Jitter system untouched -- only its published offset is read. - // Source + signs are knob-overridable (default = present-path behaviour) to bisect the inject convention. float jitRawX = _injectJitUseOffset ? DlssJitterState.OffsetX : DlssJitterState.PresentX; float jitRawY = _injectJitUseOffset ? DlssJitterState.OffsetY : DlssJitterState.PresentY; float declJitterX = jitRawX * _injectJitSignX * _injectJitScale; float declJitterY = jitRawY * _injectJitSignY * _injectJitScale; - // [C4 wire] Reset ONLY on a real discontinuity so DLSS ACCUMULATES temporally (the whole point of - // wiring MV): first frame, MV not real this frame (cannot accumulate with zero MV), a resolution - // change, or a gap since the last inject (scene-colour flap / menu / pause left the viewport-1 - // history stale). No more per-frame reset. long nowC4 = Environment.TickCount64; bool inputDimsChanged = rw != _injectPrevRw || rh != _injectPrevRh; bool outputDimsChanged = ow != _injectPrevOw || oh != _injectPrevOh; - // DYNRES: a dyn-res INPUT-size change alone must NOT wipe history (DLSS handles it natively); - // only an OUTPUT-size change forces a reset. OFF = any dims change resets (current behaviour). bool dimsChanged = _injectDynRes ? outputDimsChanged : (inputDimsChanged || outputDimsChanged); bool gap = _injectHasHistory && (nowC4 - _injectLastRunMs) > _injectGapMs; bool reset = !_injectHasHistory || !mvReal || dimsChanged || gap; @@ -3970,11 +2868,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _injectPrevRw = rw; _injectPrevRh = rh; _injectPrevOw = ow; _injectPrevOh = oh; _injectLastRunMs = nowC4; - // [JITTERVAL-INJ, 10/07] One line per inject EVALUATE (game clock): the jitter DECLARED to DLSS - // (raw source value + final), the sequence head it was read from (fid -- compare with the - // JITTERVAL-GEO fid of the frame being upscaled to catch a present advancing the sequence - // mid-frame), the camera pair the reproj MV was computed from (fresh/hold/delta -- a hold pair - // on a really-moving frame means MV=0 was declared for real motion), and reset. Log-only. if (_jitterval) { Logger.Info?.Print(LogClass.Gpu, @@ -3988,8 +2881,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"in={rw}x{rh} out={ow}x{oh}"); } - // [RESET-COUNT] Tally resets by reason + periodic summary so the flash cause is visible in NUMBERS - // (no per-frame spam, no user eye-judgment). Gated; default OFF = zero overhead. if (_resetCount) { _rcFrames++; @@ -4010,7 +2901,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [DIAG C4, read-only] one-shot: MV real/size/scale, reset reason, declared jitter vs real offset. if (_injectDiagCount < 6) { _injectDiagCount++; @@ -4025,7 +2915,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"held={(_injectHeldColorUsed ? "ON" : "off")}."); } - // hdr = true: the source is an HDR colour format (generic; DLSS ColorBuffersHDR). if (!StreamlineDlss.SetOptions(InjectViewportId, mode, (uint)ow, (uint)oh, true)) { InjectStatus("midframe skip: SetOptions failed", false); @@ -4033,37 +2922,33 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // [MVPP cutout bias] Convert this frame's survivor SSBO into the R8 bias mask (gated; - // requires the survive probe to have mirrored the mask buffer, see MvppSurviveMaskState). bool cutoutBiasOk = _injectCutoutBias && RunInjectCutoutMask(scene, cbs); StreamlineDlss.DlssTexture inTex = Describe(scene, cbs); - StreamlineDlss.DlssTexture outTex = Describe(dstView, cbs); // <-- the REAL destination render target - StreamlineDlss.DlssTexture depthTex = Describe(depthForDlss, cbs); // <-- real scene depth, else zeroed fallback - StreamlineDlss.DlssTexture mvTex = Describe(mvForDlss, cbs); // <-- REAL reproj MV, else zeroed fallback + StreamlineDlss.DlssTexture outTex = Describe(dstView, cbs); + StreamlineDlss.DlssTexture depthTex = Describe(depthForDlss, cbs); + StreamlineDlss.DlssTexture mvTex = Describe(mvForDlss, cbs); StreamlineDlss.DlssTexture cutoutBiasTex = cutoutBiasOk ? Describe(_injectCutoutMask, cbs) : default; bool ok = StreamlineDlss.Evaluate( (IntPtr)cbs.CommandBuffer.Handle, InjectViewportId, _injectFrame++, - reset, // [C4] scene-cut/invalidity only -> DLSS accumulates temporally - declJitterX, // [C4] real declared jitter (scene is rendered jittered), not zero + reset, + declJitterX, declJitterY, in inTex, in outTex, in depthTex, in mvTex, in cutoutBiasTex, - cutoutBiasOk); // BiasCurrentColorHint: 1.0 where scene cutout fragments survived + cutoutBiasOk); if (ok) { - _injectHasHistory = true; // [C4] DLSS now has accumulated history for viewport 1 - MvppColorSnapshot.LastInjectMs = nowC4; // [FLASHCAP gate] mark a real 3D gameplay frame + _injectHasHistory = true; + MvppColorSnapshot.LastInjectMs = nowC4; - // [MVPP cutout bias] The mask was consumed by this Evaluate: reset the survivor - // SSBO so the next frame's mask is per-frame fresh, and report. if (cutoutBiasOk) { ResetCutoutMaskBuffer(cbs); @@ -4094,12 +2979,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss "MVPP-INJECT-MASK: bias flag ON but no mask available (survive probe mirror not seen yet) -- evaluating WITHOUT bias."); } - // Order the NGX write to the real dst before the guest draw's attachment write (WAW) and any - // read. The guest draw runs next (or is skipped in C3b). BarrierInjectTarget(dstView, cbs); _injectLastDst = dstView; - // Deferred dump timing (skip the load transient), now of the real dst. if (!_injectDumped && !_injectPendingDump) { if (_injectOkCount == 0) @@ -4121,12 +3003,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [C3b] Skip the guest draw only if DLSS actually wrote the real dst (evaluate OK) AND substitution - // is active. Otherwise the caller keeps the guest draw (automatic fallback). bool substitute = ok && SubstituteActive; string reason = substitute ? "-" : (!ok ? "eval-FAIL" : "SubstituteActive=false"); - // [Option A] Tell the present TryRun (later this frame) to skip its redundant DLSS. Only on success. if (substitute) { _injectSubstitutedThisFrame = true; @@ -4144,9 +3023,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return substitute; } - /// [INJECT C4] Allocate the scene-res resources for the inject reprojection MV pass. The 3 - /// extra images are required by the shared MvppReproject layout (written every pixel) even though only - /// _injectMotion is consumed. Recreated on res change. private void EnsureInjectReprojResources(int w, int h, TextureView template) { if (_injectMotion != null && _injectReprojW == w && _injectReprojH == h) @@ -4171,14 +3047,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// [INJECT C4, approach A] Generate scene-resolution (2666x1500) camera-reprojection motion vectors by - /// reusing the proven MvppReproject shader with every extra feature OFF (no optical flow, no dynamic - /// mask, no compose, no sky/far-rot). Reads the REAL scene depth + the VP matrix pair (from the present - /// stream, ~1 frame skew -- fine for validation). Writes clean, jitter-free MV into _injectMotion. - /// NOT fed to DLSS at this step -- only generated and logged for validation (static -> ~0, motion -> - /// non-zero, clamped by maxMotion so no explosive values). Returns the MV texture, or null if unavailable. - /// private TextureView RunInjectReprojectPass(TextureView depth, CommandBufferScoped cbs) { if (_mvppReprojProgram == null || !_mvppHasPair || depth == null) @@ -4211,10 +3079,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss System.Numerics.Matrix4x4 prevT = System.Numerics.Matrix4x4.Transpose(vpPrev); bool zMinusOne = _mvppZglEnv == "1" || (_mvppZglEnv != "0" && DlssCameraState.GuestDepthMinusOneToOne); - // Throttled validation log (~1/s): read the PREVIOUS dispatch's stats first (already completed, no - // stall), then clear + dispatch this frame. Center-pixel dbg is written every frame (not gated by - // diagMode); diagMode=1 adds the mean |reproj| magnitude. Non-zero in motion + ~0 static + bounded - // = MV good. (The flow comparison is meaningless here -- dummy flow -- and ignored.) long now = Environment.TickCount64; if (!_injectQuiet && now - _injectReprojLogMs >= 1000) { @@ -4223,8 +3087,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ReadOnlySpan s = System.Runtime.InteropServices.MemoryMarshal.Cast(data.Get()); float meanRX = s[0] > 0 ? s[8] / 16f / s[0] : 0f; float meanRY = s[0] > 0 ? s[9] / 16f / s[0] : 0f; - // SKYROT sonde: mean |sky MV| SENT to DLSS (post-deadband), over the same sparse grid as skyCount (s[1]). - // x1024 cap 4 px depuis le 17/07 (précision drift) — les grosses valeurs saturent à 4. float skyMX = s[1] > 0 ? s[20] / 1024f / s[1] : 0f; float skyMY = s[1] > 0 ? s[21] / 1024f / s[1] : 0f; Logger.Info?.Print(LogClass.Gpu, @@ -4244,19 +3106,19 @@ namespace Ryujinx.Graphics.Vulkan.Dlss p[32] = w; p[33] = h; p[34] = MvppMaxMotion; - p[35] = _injectQuiet ? 0f : 1f; // diagMode: quiet -> 0 (no stat accumulation), else 1 (|reproj| magnitude) - p[36] = _injectMvFlipY >= 0 ? _injectMvFlipY : (_mvppFlipY ? 1f : 0f); // knob-overridable MV Y convention + p[35] = _injectQuiet ? 0f : 1f; + p[36] = _injectMvFlipY >= 0 ? _injectMvFlipY : (_mvppFlipY ? 1f : 0f); p[37] = zMinusOne ? 1f : 0f; - p[38] = 0f; // zeroMv off + p[38] = 0f; p[39] = _mvppDeadLo; p[40] = _mvppDeadHi; - p[41] = 0f; // dynMask off - p[42] = 0f; // compose off - p[43] = 0f; // farRot off - p[44] = 0f; // edge off + p[41] = 0f; + p[42] = 0f; + p[43] = 0f; + p[44] = 0f; p[45] = depth.ActiveWidth > 0 ? (float)depth.ActiveWidth / depth.Width : 1f; p[46] = depth.ActiveHeight > 0 ? (float)depth.ActiveHeight / depth.Height : 1f; - p[47] = _injectSkyRot ? 1f : 0f; // SKYROT knob (inject): default OFF = byte-identical + p[47] = _injectSkyRot ? 1f : 0f; using ScopedTemporaryBuffer buffer = _gd.BufferManager.ReserveOrCreate(_gd, cbs, p.Length * sizeof(float)); buffer.Holder.SetDataUnchecked(buffer.Offset, p); @@ -4264,9 +3126,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _motionPipeline.SetCommandBuffer(cbs); _motionPipeline.SetProgram(_mvppReprojProgram); _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 1, depth, _mvppDepthLinear ? _sampler : _samplerNearest); - _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 3, depth, _samplerNearest); // dummy flow (unused) - _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 4, depth, _samplerNearest); // dummy prev-depth (unused) - _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 5, depth, _samplerNearest); // dummy coverage (unused) + _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 3, depth, _samplerNearest); + _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 4, depth, _samplerNearest); + _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 5, depth, _samplerNearest); _motionPipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]); _motionPipeline.SetStorageBuffers([new BufferAssignment(0, new BufferRange(_injectReprojStats, 0, MvppStatsCount * sizeof(uint), true))]); _motionPipeline.SetImage(0, _injectMotion.GetImageView()); @@ -4281,8 +3143,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return _injectMotion; } - /// [INJECT étape B] Create + zero the scene-res-coherent depth/motion placeholders for the - /// inject evaluate. Recreated only when the scene render res changes (dyn-res). Zeroed once. private unsafe void EnsureInjectAux(int rw, int rh, TextureView template, CommandBufferScoped cbs) { if (_injectMotionZero != null && _injectAuxW == rw && _injectAuxH == rh) @@ -4311,13 +3171,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ClearDepthStencilValue zeroDepth = new() { Depth = 0f, Stencil = 0 }; _gd.Api.CmdClearDepthStencilImage(cbs.CommandBuffer, _injectDepthZero.GetImage().Get(cbs).Value, ImageLayout.General, in zeroDepth, 1, in depthRange); - // Execution barrier so the clears complete before this same command buffer's evaluate reads them. _gd.Api.CmdPipelineBarrier(cbs.CommandBuffer, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.AllCommandsBit, 0, 0, null, 0, null, 0, null); } - /// [TINTPROBE] Create + magenta-clear a scene-res colour texture we own, fed to DLSS as the - /// decisive downstream-overwrite probe. Recreated only on a res change. Same shape as EnsureInjectAux. private unsafe void EnsureInjectTint(int rw, int rh, TextureView template, CommandBufferScoped cbs) { if (_injectTintTex != null && _injectTintW == rw && _injectTintH == rh) @@ -4335,19 +3192,13 @@ namespace Ryujinx.Graphics.Vulkan.Dlss AspectMask = ImageAspectFlags.ColorBit, BaseMipLevel = 0, LevelCount = 1, BaseArrayLayer = 0, LayerCount = 1, }; - // Vivid HDR magenta (R,B high, G zero) -- never a natural TotK colour, bright enough to survive tonemap. ClearColorValue magenta = new(4f, 0f, 4f, 1f); _gd.Api.CmdClearColorImage(cbs.CommandBuffer, _injectTintTex.GetImage().Get(cbs).Value, ImageLayout.General, in magenta, 1, in colorRange); - // Execution barrier so the clear completes before this same command buffer's evaluate reads it. _gd.Api.CmdPipelineBarrier(cbs.CommandBuffer, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.AllCommandsBit, 0, 0, null, 0, null, 0, null); } - /// [INJECT étape C] Barrier the given target after the NGX evaluate wrote it via raw handles - /// (invisible to Ryujinx tracking). Orders the write before subsequent reads AND before the guest - /// draw's colour-attachment write (WAW) that follows on the real dst. Same pattern as - /// , on an arbitrary texture. private unsafe void BarrierInjectTarget(TextureView target, CommandBufferScoped cbs) { Image image = target.GetImage().Get(cbs).Value; @@ -4370,8 +3221,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss 0, 0, null, 0, null, 1, &b); } - // [INJECT étape B] Throttled status line (on message change or every 3 s), so a run shows OK/FAIL, - // the sizes used, and the mode/depth/motion sourcing without spamming. private void InjectStatus(string msg, bool ok) { long sig = ((long)(uint)msg.GetHashCode()) | (ok ? 1L << 40 : 0L); @@ -4384,9 +3233,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// [FLASHCAP / Mesure A] Downscale the last DLSS output to a tiny texture, read it back, and - /// measure the frame-to-frame change. A sudden spike above the local baseline = a flash -> dump the - /// full frames around it (so we can SEE where the artifact is). Gated; measures the DLSS output only. private void FlashCapTick(CommandBufferScoped cbs, TextureView lastDst) { try @@ -4397,7 +3243,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _flashTiny = _gd.CreateTexture(MakeInfo(lastDst.Info, TW, TH, Format.R11G11B10Float, 4)) as TextureView; } - // GPU downscale (cheap) then a tiny readback (small) -- avoids a full-frame per-frame stall. _gd.HelperShader.BlitColor(_gd, cbs, lastDst, _flashTiny, new Extents2D(0, 0, lastDst.Width, lastDst.Height), new Extents2D(0, 0, TW, TH), true); @@ -4427,19 +3272,17 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { diff += Math.Abs(_flashCur[i] - _flashPrev[i]); } - float metric = (float)diff / (n * 3); // avg per-channel sRGB8 change (0..255) + float metric = (float)diff / (n * 3); _flashAvg = _flashAvg <= 0f ? metric : 0.9f * _flashAvg + 0.1f * metric; if (metric > _flashMax) _flashMax = metric; - // Spike = a big change AND well above the local baseline (so steady camera motion, which - // raises the baseline, does not false-trigger; an immobile-camera flash trips it easily). bool spike = metric > 10f && metric > 3f * _flashAvg; long now = Environment.TickCount64; if (spike && _flashDumpNext == 0 && _flashDumpCount < 15) { Logger.Info?.Print(LogClass.Gpu, $"MVPP-FLASHCAP: SPIKE metric={metric:0.0} baseline={_flashAvg:0.0} -> dumping full frames from flash{_flashDumpCount:D3}"); - _flashDumpNext = 3; // this frame + next 2, to see the flash unfold + _flashDumpNext = 3; } if (_flashDumpNext > 0 && _flashDumpCount < 15) { @@ -4465,12 +3308,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// [INJECT étape B sous-étape 4] Read back the DLSS intermediate and write ONE viewable PNG. The HDR - /// R11G11B10Float floats are decoded at full precision and only tone-mapped (Reinhard) + sRGB-encoded - /// at the very last step, so no HDR range is lost before the conversion. Purely diagnostic: it reads - /// _injectOutput, touches nothing on the displayed frame. On any invalid data it dumps nothing and logs why. - /// private bool DumpInjectPng(TextureView tex, string tag = "output", float minMeanSrgb = 0f) { try @@ -4492,7 +3329,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss for (int i = 0; i < w * h; i++) { uint p = BitConverter.ToUInt32(packed, i * 4); - // VK_FORMAT_B10G11R11_UFLOAT_PACK32: R = bits 0..10, G = 11..21, B = 22..31. byte r = ToSrgb8(DecodeFloat11(p & 0x7FF)); byte g = ToSrgb8(DecodeFloat11((p >> 11) & 0x7FF)); byte b = ToSrgb8(DecodeFloat10((p >> 22) & 0x3FF)); @@ -4502,7 +3338,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss sum += r + g + b; } - // Reject black/loading frames: wait for a real (bright) gameplay frame before committing. float meanSrgb = rgb.Length > 0 ? (float)sum / rgb.Length : 0f; if (meanSrgb < minMeanSrgb) { @@ -4530,16 +3365,14 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // 11-bit UFLOAT (5-bit exponent, 6-bit mantissa, bias 15). No sign. private static float DecodeFloat11(uint v) { uint e = (v >> 6) & 0x1F, m = v & 0x3F; - if (e == 0) return m * (1f / 64f) * 6.10351562e-5f; // subnormal: mant * 2^-14 + if (e == 0) return m * (1f / 64f) * 6.10351562e-5f; if (e == 0x1F) return m == 0 ? float.PositiveInfinity : float.NaN; return (1f + m * (1f / 64f)) * MathF.Pow(2f, (int)e - 15); } - // 10-bit UFLOAT (5-bit exponent, 5-bit mantissa, bias 15). No sign. private static float DecodeFloat10(uint v) { uint e = (v >> 5) & 0x1F, m = v & 0x1F; @@ -4552,29 +3385,27 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { if (float.IsNaN(c) || c < 0f) c = 0f; if (float.IsPositiveInfinity(c)) c = 1e4f; - c = c / (1f + c); // Reinhard tone-map HDR -> [0,1] (last-moment conversion) + c = c / (1f + c); float s = c <= 0.0031308f ? c * 12.92f : 1.055f * MathF.Pow(c, 1f / 2.4f) - 0.055f; return (byte)Math.Clamp((int)(s * 255f + 0.5f), 0, 255); } - // Minimal PNG writer (8-bit RGB, no dependency): IDAT via ZLibStream, chunk CRC32 hand-rolled. private static void WritePng(string path, byte[] rgb, int w, int h) { using System.IO.FileStream fs = System.IO.File.Create(path); - fs.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }); // signature + fs.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }); byte[] ihdr = new byte[13]; WriteBE(ihdr, 0, (uint)w); WriteBE(ihdr, 4, (uint)h); - ihdr[8] = 8; // bit depth - ihdr[9] = 2; // colour type 2 = truecolour RGB - // 10..12 = compression/filter/interlace = 0 + ihdr[8] = 8; + ihdr[9] = 2; WriteChunk(fs, "IHDR", ihdr); using System.IO.MemoryStream raw = new(); for (int y = 0; y < h; y++) { - raw.WriteByte(0); // filter type 0 (None) per scanline + raw.WriteByte(0); raw.Write(rgb, y * w * 3, w * 3); } @@ -4629,13 +3460,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return crc; } - // [FG lifetime] DLSS's consumers read the tagged views on command streams invisible to our - // fence tracking: dlfg's present workers, but ALSO NGX's own references from the SR evaluate - // (history/tag state held between frames). Destroying a view the instant we recreate it - // (dyn-res ramp, fullscreen toggle / resize) can pull it from under in-flight work: - // intermittent DEVICE_LOST. Park old views and dispose them a few presents later, once any - // consumer's queue depth has certainly drained. (Was FG-only; user logs showed the same - // DEVICE_LOST on swapchain recreation with FG off — the SR path needs the deferral too.) private readonly System.Collections.Generic.List<(TextureView View, long RetireAfter)> _fgRetired = new(); private long _presentSerial; @@ -4668,9 +3492,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return; } - // DYNRES: input changed but the DLSS output is unchanged (TOTK dyn-res ramp). Keep _output - // (and the DLSS history tied to it) ALIVE -- rebuild only the input-sized helper buffers. - // Recreating _output every ramp frame is what pulses DLSS reconvergence = the startup zoom. if (_dlssDynRes && _output != null && _outW == outW && _outH == outH && (_inW != input.Width || _inH != input.Height)) { @@ -4699,11 +3520,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss RetireFgVisible(_depth); RetireFgVisible(_motion); RetireFgVisible(_motionFiltered); - RetireFgVisible(_prevColor); // was leaked on every reallocation (dynamic-resolution games churn this) - // E9b: _heldDepth points INTO the per-size cache -- never dispose entries during - // gameplay (in-flight CB race); just drop the reference, the cache reuses/keeps them. + RetireFgVisible(_prevColor); _heldDepth = null; - _hasHeldDepth = false; // the held depth is stale at a new resolution; recapture before reuse + _hasHeldDepth = false; _inW = input.Width; _inH = input.Height; @@ -4742,8 +3561,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private void ClearResources(CommandBufferScoped cbs) { - // Zero the output, the previous-frame color and the depth/motion buffers so the first - // frame reads defined data (depth stays zeroed - no real depth in this integration). ImageSubresourceRange colorRange = new() { AspectMask = ImageAspectFlags.ColorBit, @@ -4775,16 +3592,16 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public void Dispose() { DrainFgRetired(all: true); - _sharpenPass?.Dispose(); // [SHARPEN] + _sharpenPass?.Dispose(); _output?.Dispose(); _depth?.Dispose(); _motion?.Dispose(); _motionFiltered?.Dispose(); _prevColor?.Dispose(); - _injectTintTex?.Dispose(); // [TINTPROBE] was the one view Dispose() forgot + _injectTintTex?.Dispose(); _injectDepthZero?.Dispose(); _injectMotionZero?.Dispose(); - _injectMotion?.Dispose(); // [C4] scene-res reproj MV + its shader-required aux images + _injectMotion?.Dispose(); _injectMotionRaw?.Dispose(); _injectReprojDepthSnap?.Dispose(); _injectReprojBiasMask?.Dispose(); @@ -4792,8 +3609,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss { _gd.BufferManager.Delete(_injectReprojStats); } - // NOTE: _injectLastDst is the guest's real render target (not owned here) -- never disposed. - _bNativeHeld?.Dispose(); // [B étape 2] fixed 2666 native lock copy, created once, freed here only + _bNativeHeld?.Dispose(); foreach (TextureView held in _heldDepthCache.Values) { held?.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/MvppSurviveMaskState.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/MvppSurviveMaskState.cs index 06840d454..3814b1f0d 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/MvppSurviveMaskState.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/MvppSurviveMaskState.cs @@ -1,19 +1,8 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Graphics.GAL; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// [MVPP SURVIVE PROBE -- cutout bias mask] Backend-side handoff of the survivor-mask buffer. - /// StateUpdater binds the REAL mask once to the mirror binding - /// (MvppSurviveProbe.BackendMirrorBinding = 81, never in any pipeline layout); - /// DescriptorSetUpdater captures the backend-mapped range as it passes through, and - /// DlssUpscaler reads it to run the SSBO -> R8 mask conversion. Written once, in-order on the - /// backend thread (normal command replay) -- not a cross-thread side channel. - /// static class MvppSurviveMaskState { public static BufferRange MaskRange; diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/NvOpticalFlow.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/NvOpticalFlow.cs index 4ca4f1b4e..16389b623 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/NvOpticalFlow.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/NvOpticalFlow.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using Silk.NET.Core; @@ -10,17 +7,6 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// Manual binding + driving of VK_NV_optical_flow (NVOFA). Silk.NET 2.23.0 ships the structs/enums/ - /// handles in its core assembly but not the function wrappers (those live in the unreferenced - /// Silk.NET.Vulkan.Extensions.NV package), so we resolve the four entry points ourselves via - /// vkGetDeviceProcAddr -- no extra NuGet dependency. - /// - /// Session images MUST be created with a VkOpticalFlowImageFormatInfoNV chained into - /// VkImageCreateInfo.pNext (declaring INPUT/OUTPUT usage); Ryujinx's texture factory cannot inject - /// that pNext, so we create the three images as raw VkImages here. Everything is gated on - /// /; on any failure the caller keeps Lucas-Kanade. - /// internal sealed unsafe class NvOpticalFlow { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -35,16 +21,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void ExecuteDelegate(CommandBuffer commandBuffer, OpticalFlowSessionNV session, OpticalFlowExecuteInfoNV* executeInfo); - // VK_OPTICAL_FLOW_USAGE_*_BIT_NV private const uint UsageInput = 1; private const uint UsageOutput = 2; - // Output flow grid bit (1/2/4/8 = 1x1/2x2/4x4/8x8). Chosen at runtime from the hardware's - // supportedOutputGridSizes (see QueryHardware). The flow image is 1/_outputGrid the input size, - // and the bit value conveniently == the divisor. Default 4x4 until the query runs. private uint _outputGrid = 4; - // VkOpticalFlowSessionBindingPointNV private const uint BindInput = 1; private const uint BindReference = 2; private const uint BindFlowVector = 4; @@ -58,8 +39,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private Device _device; private PhysicalDevice _physicalDevice; - // B2 multi-queue: dedicated optical-flow queue (fam5) + its own command pool/buffer + a fence for a - // self-contained synchronous submit. vkCmdOpticalFlowExecuteNV MUST run here, not on graphics. private Queue _ofaQueue; private uint _ofaFamily = uint.MaxValue; private uint _graphicsFamily; @@ -70,7 +49,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private OpticalFlowSessionNV _session; - // Raw OFA session images: input (frame N), reference (frame N-1), flow-vector output. private Image _imgInput, _imgRef, _imgFlow; private DeviceMemory _memInput, _memRef, _memFlow; private ImageView _viewInput, _viewRef, _viewFlow; @@ -78,25 +56,16 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private bool _bound; private int _execLogCount; - // 3c-i measurement: flow dims + a host-visible readback of the flow field to compute RMS/outliers - // on the CPU and compare against the Lucas-Kanade metrics. private uint _flowW, _flowH; private Silk.NET.Vulkan.Buffer _readbackBuffer; private DeviceMemory _readbackMemory; private void* _readbackPtr; private int _measureLogCount; - /// True only when all four entry points resolved, i.e. the device really exposes NVOFA. public bool Available { get; private set; } - /// True when a session + its images are created and bound, ready to execute. public bool SessionReady { get; private set; } - /// - /// Resolves the four VK_NV_optical_flow functions on the active device and logs a probe line. Safe - /// to call once after device creation; if anything is missing, stays false - /// and the caller falls back to the Lucas-Kanade motion estimator. - /// public void TryBind(Vk api, Device device, PhysicalDevice physicalDevice, Queue opticalFlowQueue, uint opticalFlowQueueFamily, uint graphicsQueueFamily) { _api = api; @@ -129,11 +98,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// Creates the dedicated command pool + buffer + fence on the optical-flow queue family so the OFA - /// execute can be submitted there (the only legal family). Without it, _queueReady stays false and - /// the caller keeps Lucas-Kanade. - /// private void SetupOfaQueue(Device device) { if (_ofaFamily == uint.MaxValue) @@ -187,14 +151,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss Logger.Info?.Print(LogClass.Gpu, $"NVOFA queue setup: command pool + buffer + fence ready on optical-flow family {_ofaFamily}."); } - /// - /// Interrogates the real NVOFA hardware limits (logged at startup, before any execute, so the data - /// survives even if a later command TDRs). Reads supportedOutputGridSizes via - /// vkGetPhysicalDeviceProperties2 + VkPhysicalDeviceOpticalFlowPropertiesNV and picks the coarsest - /// supported grid (safest vs TDR). Also logs each queue family's optical-flow capability - /// (VK_QUEUE_OPTICAL_FLOW_BIT_NV = 0x100): vkCmdOpticalFlowExecuteNV is only valid on a queue family - /// that advertises it, so this tells us whether the graphics queue we record into is even allowed. - /// private void QueryHardware() { PhysicalDeviceOpticalFlowPropertiesNV ofaProps = new() { SType = StructureType.PhysicalDeviceOpticalFlowPropertiesNV }; @@ -203,7 +159,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss uint grids = (uint)ofaProps.SupportedOutputGridSizes; - // Coarsest supported = least hardware load = best chance against a TDR. if ((grids & 8) != 0) _outputGrid = 8; else if ((grids & 4) != 0) _outputGrid = 4; else if ((grids & 2) != 0) _outputGrid = 2; @@ -225,7 +180,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _api.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, ref count, p); } - const uint OfaQueueBit = 0x100; // VK_QUEUE_OPTICAL_FLOW_BIT_NV + const uint OfaQueueBit = 0x100; string s = ""; for (int i = 0; i < count; i++) { @@ -237,12 +192,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// Creates the session, its three raw OFA images (with the mandatory format pNext), transitions - /// them to GENERAL, clears them and binds them. Records the transition/clear into . - /// Any failure is reported and leaves false (caller keeps Lucas-Kanade); - /// a wrong format/parameter surfaces as a VkResult error rather than a device loss. - /// public bool CreateSession(Device device, CommandBuffer cmd, uint width, uint height) { if (!Available) @@ -260,9 +209,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ImageFormat = Format.R8Unorm, FlowVectorFormat = Format.R16G16Sfloat, CostFormat = Format.Undefined, - OutputGridSize = (OpticalFlowGridSizeFlagsNV)_outputGrid, // chosen from supportedOutputGridSizes (QueryHardware) + OutputGridSize = (OpticalFlowGridSizeFlagsNV)_outputGrid, HintGridSize = (OpticalFlowGridSizeFlagsNV)0, - PerformanceLevel = (OpticalFlowPerformanceLevelNV)2, // MEDIUM + PerformanceLevel = (OpticalFlowPerformanceLevelNV)2, }; OpticalFlowSessionNV session; @@ -278,8 +227,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _session = session; - // The three session images MUST carry VkOpticalFlowImageFormatInfoNV in their pNext. Inputs are - // full render resolution; the flow output is 1/_outputGrid (one motion vector per grid cell). uint flowW = (width + _outputGrid - 1) / _outputGrid; uint flowH = (height + _outputGrid - 1) / _outputGrid; @@ -293,7 +240,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // UNDEFINED -> GENERAL + clear (defined contents) for all three. TransitionToGeneral(cmd, _imgInput); TransitionToGeneral(cmd, _imgRef); TransitionToGeneral(cmd, _imgFlow); @@ -301,7 +247,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ClearImage(cmd, _imgRef); ClearImage(cmd, _imgFlow); - // Bind (host call); VK_NV_optical_flow has no dedicated layouts, the images stay GENERAL. Result rIn = _bindImage(device, _session, (OpticalFlowSessionBindingPointNV)BindInput, _viewInput, ImageLayout.General); Result rRef = _bindImage(device, _session, (OpticalFlowSessionBindingPointNV)BindReference, _viewRef, ImageLayout.General); Result rFlow = _bindImage(device, _session, (OpticalFlowSessionBindingPointNV)BindFlowVector, _viewFlow, ImageLayout.General); @@ -327,7 +272,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// Destroys the current session and its images, if any. public void DestroySession(Device device) { DestroyImage(device, ref _imgInput, ref _memInput, ref _viewInput); @@ -362,10 +306,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _bound = false; } - /// - /// Records vkCmdOpticalFlowExecuteNV, wrapped in a coarse memory barrier so prior writes to the - /// input images are visible and the flow write is visible afterwards. No-op unless ready. - /// public void Execute(CommandBuffer cmd) { if (!Available || !SessionReady || !_bound) @@ -378,7 +318,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss OpticalFlowExecuteInfoNV info = new() { SType = StructureType.OpticalFlowExecuteInfoNV, - // Flags = 0 (forward flow), RegionCount = 0 (whole image). }; _execute(cmd, _session, &info); @@ -392,12 +331,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// Self-contained synchronous run of the hardware optical flow on its OWN queue (fam5): records the - /// execute into the dedicated command buffer, submits it to the optical-flow queue and waits the - /// fence. This is the only legal place for vkCmdOpticalFlowExecuteNV. Output is not yet consumed by - /// graphics, so no cross-queue semaphores are needed for the dry run (they come with 3c). - /// public void ExecuteOnOfaQueue() { if (!Available || !SessionReady || !_bound || !_queueReady) @@ -418,10 +351,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return; } - Execute(_cmdBuffer); // records the barrier + vkCmdOpticalFlowExecuteNV into the OFA command buffer + Execute(_cmdBuffer); - // 3c-i: copy the flow field into the host-visible buffer (fam5 supports transfer) so we can - // measure it on the CPU after the fence. Same command buffer -> covered by the same fence. if (_readbackBuffer.Handle != 0) { BufferImageCopy copy = new() @@ -453,13 +384,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss MeasureFlow(); } - /// - /// Uploads the real current (frame N) and previous (frame N-1) colour images into the OFA R8 inputs - /// via a transfer blit (RGBA -> R, recorded on the GRAPHICS command buffer since it owns the - /// colour images; the OFA images are concurrent so graphics may write them). Replaces the dry-run - /// zeros so the hardware optical flow runs on real content. One frame of latency vs the synchronous - /// OFA submit is fine for measurement. - /// public void UploadFrames(CommandBuffer graphicsCmd, Image current, Image previous, uint width, uint height) { if (!Available || !SessionReady) @@ -487,8 +411,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _api.CmdBlitImage(cmd, src, ImageLayout.General, dst, ImageLayout.General, 1, ®ion, Filter.Nearest); } - // 3c-i: parse the readback (R16G16_SFLOAT) and log the RMS / outlier rate of the hardware flow so - // it can be compared to the Lucas-Kanade metrics line. Throttled to ~every 120 frames. private void MeasureFlow() { if (_readbackPtr == null) @@ -496,8 +418,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return; } - // NV OFA flow vectors are S10.5 fixed-point: signed int16 where pixels = value / 32. They are - // NOT fp16 -- reading them as half gives garbage (huge values / fake NaNs) for any non-zero flow. short* data = (short*)_readbackPtr; int count = (int)(_flowW * _flowH); double sumSq = 0.0; @@ -516,7 +436,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss maxSq = magSq; } - if (magSq > 4.0f) // |mv| > 2px + if (magSq > 4.0f) { outliers++; } @@ -533,7 +453,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private void CreateReadbackBuffer(Device device, uint flowW, uint flowH) { - ulong size = (ulong)flowW * flowH * 4; // R16G16_SFLOAT = 4 bytes/texel + ulong size = (ulong)flowW * flowH * 4; BufferCreateInfo bci = new() { @@ -583,7 +503,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _readbackPtr = ptr; } - /// Full teardown: session + images, then the queue command pool/buffer and fence. public void Dispose(Device device) { DestroySession(device); @@ -615,8 +534,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss Usage = (OpticalFlowUsageFlagsNV)ofaUsage, }; - // Shared between the graphics family (clears/fills inputs, reads the flow) and the optical-flow - // family (the OFA execute) -- concurrent sharing avoids explicit queue-family ownership transfers. uint* families = stackalloc uint[2]; families[0] = _graphicsFamily; families[1] = _ofaFamily; @@ -760,9 +677,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // Coarse global memory barrier across ALL_COMMANDS -- conservatively covers the optical-flow stage - // for the dry run; replaced by a precise sync2 (PIPELINE_STAGE_2_OPTICAL_FLOW_BIT_NV) barrier once - // the flow output is actually consumed (3b-ii/3c). private void Barrier(CommandBuffer cmd) { MemoryBarrier mb = new() diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs index e55e3f1a5..7f31f6054 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -9,60 +6,34 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// Minimal managed interop for NVIDIA Streamline (MIT) - just enough to initialize the - /// SDK and ask "is DLSS supported on this machine?". - /// - /// Clean-room note: every type and signature in this file is OUR code. The struct layouts - /// and enum values are re-declared from the public MIT-licensed Streamline headers - /// (sl_struct.h, sl_core_types.h, sl_result.h, sl_appidentity.h, sl_device_wrappers.h, - /// sl_version.h). No proprietary NVIDIA SDK is copied. The proprietary nvngx_dlss.dll is - /// never shipped - it is located on the user's machine by . - /// - /// ABI: the structs below mirror the x64 C++ layout exactly (MSVC default packing). C# blittable - /// fields use the same natural alignment, so offsets match field-for-field: - /// - C++ `bool` -> (1 byte, NOT marshalled bool which is 4) - /// - C++ enums -> (underlying type uint32_t) - /// - C++ `size_t` -> (8 bytes on x64) - /// - C++ pointers -> - /// public static class Streamline { private const string InterposerName = "sl.interposer"; private const string InterposerDll = "sl.interposer.dll"; - // sl::kSDKVersion for the v2.12.0 SDK: (major<<48)|(minor<<32)|(patch<<16)|0xfedc. - // Expressed as the formula (not a transcribed literal) so it stays self-evidently correct. private const ulong KSdkVersion = ((ulong)2 << 48) | ((ulong)12 << 32) | ((ulong)0 << 16) | 0xfedc; - // sl::Feature (uint32_t) - sl_consts.h public const uint FeatureDLSS = 0; public const uint FeatureNIS = 2; - public const uint FeatureReflex = 3; // FG dependency; must be requested explicitly (PCL auto-loads) - public const uint FeaturePCL = 4; // PC-Latency; confirmed id 4 in sl.log (loads as a common dep) + public const uint FeatureReflex = 3; + public const uint FeaturePCL = 4; public const uint FeatureDLSS_G = 1000; public const uint FeatureDLSS_RR = 1001; - // sl::RenderAPI (uint32_t) - sl_device_wrappers.h private const uint RenderApiVulkan = 2; - // sl::EngineType (uint32_t) - sl_appidentity.h private const uint EngineTypeCustom = 0; - // sl::LogLevel (uint32_t) - sl_core_types.h private const uint LogLevelDefault = 1; private const uint LogLevelVerbose = 2; - // sl::kStructVersion* - sl_struct.h private const uint StructVersion1 = 1; private const uint StructVersion3 = 3; - // sl::PreferenceFlags (uint64_t) - sl_core_types.h v2.12.0 private const ulong PrefFlagDisableCLStateTracking = 1UL << 0; private const ulong PrefFlagUseManualHooking = 1UL << 2; - private const ulong PrefFlagUseFrameBasedResourceTagging = 1UL << 7; // verified vs sl_core_types.h + private const ulong PrefFlagUseFrameBasedResourceTagging = 1UL << 7; - /// sl::Result - sl_result.h. eOk == 0; everything else is an error/warning. public enum Result { Ok = 0, @@ -107,7 +78,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss WarnOutOfVRAM, } - // sl::StructType - a 16-byte GUID. Alignment 4. [StructLayout(LayoutKind.Sequential)] private struct SlStructType { @@ -117,89 +87,74 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public byte B0, B1, B2, B3, B4, B5, B6, B7; } - // sl::Preferences (derives sl::BaseStructure) - sl_core_types.h. sizeof == 144 on x64. [StructLayout(LayoutKind.Sequential)] private struct Preferences { - // --- BaseStructure header (32 bytes) --- - public IntPtr Next; // 0 - public SlStructType StructType; // 8 - public nuint StructVersion; // 24 - // --- Preferences body --- - public byte ShowConsole; // 32 (C++ bool) - public uint LogLevel; // 36 - public IntPtr PathsToPlugins; // 40 (const wchar_t**) - public uint NumPathsToPlugins; // 48 - public IntPtr PathToLogsAndData; // 56 (const wchar_t*) - public IntPtr AllocateCallback; // 64 - public IntPtr ReleaseCallback; // 72 - public IntPtr LogMessageCallback; // 80 - public ulong Flags; // 88 (PreferenceFlags : uint64_t) - public IntPtr FeaturesToLoad; // 96 (const Feature*) - public uint NumFeaturesToLoad; // 104 - public uint ApplicationId; // 108 - public uint Engine; // 112 (EngineType) - public IntPtr EngineVersion; // 120 (const char*) - public IntPtr ProjectId; // 128 (const char*) - public uint RenderApi; // 136 (RenderAPI) + public IntPtr Next; + public SlStructType StructType; + public nuint StructVersion; + public byte ShowConsole; + public uint LogLevel; + public IntPtr PathsToPlugins; + public uint NumPathsToPlugins; + public IntPtr PathToLogsAndData; + public IntPtr AllocateCallback; + public IntPtr ReleaseCallback; + public IntPtr LogMessageCallback; + public ulong Flags; + public IntPtr FeaturesToLoad; + public uint NumFeaturesToLoad; + public uint ApplicationId; + public uint Engine; + public IntPtr EngineVersion; + public IntPtr ProjectId; + public uint RenderApi; } - // sl::AdapterInfo (derives sl::BaseStructure) - sl_core_types.h. sizeof == 56 on x64. - // Kept for the per-device check (Phase 0 uses the null/general check first). [StructLayout(LayoutKind.Sequential)] private struct AdapterInfo { - public IntPtr Next; // 0 - public SlStructType StructType; // 8 - public nuint StructVersion; // 24 - public IntPtr DeviceLuid; // 32 (uint8_t*) - public uint DeviceLuidSizeInBytes; // 40 - public IntPtr VkPhysicalDevice; // 48 (void*) + public IntPtr Next; + public SlStructType StructType; + public nuint StructVersion; + public IntPtr DeviceLuid; + public uint DeviceLuidSizeInBytes; + public IntPtr VkPhysicalDevice; } - // sl::VulkanInfo (derives sl::BaseStructure, kStructVersion3) - sl_helpers_vk.h. sizeof == 96 on x64. - // Tells SL about a device/instance created WITHOUT SL's vkCreate* proxies (our case). [StructLayout(LayoutKind.Sequential)] private struct VulkanInfo { - public IntPtr Next; // 0 - public SlStructType StructType; // 8 - public nuint StructVersion; // 24 - public IntPtr Device; // 32 (VkDevice) - public IntPtr Instance; // 40 (VkInstance) - public IntPtr PhysicalDevice; // 48 (VkPhysicalDevice) - public uint ComputeQueueIndex; // 56 - public uint ComputeQueueFamily; // 60 - public uint GraphicsQueueIndex; // 64 - public uint GraphicsQueueFamily; // 68 - public uint OpticalFlowQueueIndex; // 72 - public uint OpticalFlowQueueFamily; // 76 - public byte UseNativeOpticalFlowMode; // 80 (C++ bool) - public uint ComputeQueueCreateFlags; // 84 - public uint GraphicsQueueCreateFlags; // 88 - public uint OpticalFlowQueueCreateFlags;// 92 + public IntPtr Next; + public SlStructType StructType; + public nuint StructVersion; + public IntPtr Device; + public IntPtr Instance; + public IntPtr PhysicalDevice; + public uint ComputeQueueIndex; + public uint ComputeQueueFamily; + public uint GraphicsQueueIndex; + public uint GraphicsQueueFamily; + public uint OpticalFlowQueueIndex; + public uint OpticalFlowQueueFamily; + public byte UseNativeOpticalFlowMode; + public uint ComputeQueueCreateFlags; + public uint GraphicsQueueCreateFlags; + public uint OpticalFlowQueueCreateFlags; } - // Streamline exports are `extern "C"` (no mangling); x64 has a single calling convention. [DllImport(InterposerName, EntryPoint = "slInit", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern Result slInit(in Preferences pref, ulong sdkVersion); [DllImport(InterposerName, EntryPoint = "slShutdown", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern Result slShutdown(); - // adapterInfo is a `const AdapterInfo&` at source level, i.e. a pointer at the ABI. - // On Vulkan the adapter MUST be provided (via VkPhysicalDevice): the DLSS plugin throws an - // internal exception on a null adapter when renderAPI is Vulkan, so we never pass null here. [DllImport(InterposerName, EntryPoint = "slIsFeatureSupported", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern Result slIsFeatureSupported(uint feature, in AdapterInfo adapterInfo); [DllImport(InterposerName, EntryPoint = "slSetVulkanInfo", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern Result slSetVulkanInfo(in VulkanInfo info); - // vkGetDeviceProcAddr EXPORTED BY sl.interposer.dll (ManualHooking guide §1.1-Vulkan/§4.2): - // returns vulkan-1.dll addresses for the entire API EXCEPT the entry points SL intercepts - // (sl_hooks.h FunctionHookID) - for those it returns SL's proxies. This is the documented - // way to obtain the swapchain/present proxies when device+swapchain are host-created. [DllImport(InterposerName, EntryPoint = "vkGetDeviceProcAddr", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr slVkGetDeviceProcAddr(IntPtr device, [MarshalAs(UnmanagedType.LPStr)] string name); @@ -209,27 +164,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static bool _initialized; private static bool _everInitialized; - /// True once slInit has succeeded and the device has been registered. public static bool IsInitialized => _initialized; - /// - /// True once slInit has succeeded at least once in this process. Unlike - /// this is NEVER reset by . NGX only supports a single slInit per process, so - /// the UI uses this to force a cold process restart instead of re-initializing in-process. - /// public static bool WasEverInitialized => _everInitialized; - /// - /// Loads sl.interposer.dll from and calls slInit, - /// requesting the DLSS plugin. Returns true only if slInit succeeds. Logs the outcome. - /// - /// Folder containing sl.interposer.dll and the SL plugins. - /// - /// Porte A (RYUJINX_DLSS_FG=1): also request the DLSS_G (frame generation) plugin in - /// featuresToLoad. Diagnostic only at this stage - no frames are generated (the present - /// path is untouched). When false, featuresToLoad is {DLSS} exactly as before, so the - /// default and SR paths are byte-identical. - /// public static unsafe bool Initialize(string streamlineFolder, bool loadFrameGeneration = false) { if (_initialized) @@ -252,8 +190,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // Preload by full path. Subsequent [DllImport("sl.interposer")] calls resolve to this - // already-loaded module (Windows matches the base name), so no DllImportResolver needed. if (!NativeLibrary.TryLoad(interposerPath, out _interposerHandle)) { Logger.Warning?.Print(LogClass.Gpu, $"DLSS: failed to load \"{interposerPath}\"."); @@ -265,44 +201,20 @@ namespace Ryujinx.Graphics.Vulkan.Dlss pref.StructType = MakeGuid(0x1ca10965, 0xbf8e, 0x432b, 0x8d, 0xa1, 0x67, 0x16, 0xd8, 0x79, 0xfb, 0x14); pref.StructVersion = StructVersion1; pref.ShowConsole = 0; - // Normal logging to sl.log next to the interposer. Verbose (2) is the bring-up - // setting: it writes tens of MB per session - re-enable it (or drop an - // sl.interposer.json with "logLevel": 2) only to re-diagnose. pref.LogLevel = LogLevelDefault; _logPathPtr = Marshal.StringToHGlobalUni(streamlineFolder); pref.PathToLogsAndData = _logPathPtr; pref.Engine = EngineTypeCustom; pref.RenderApi = RenderApiVulkan; - // Only eDisableCLStateTracking. eAllowOTA (1<<3) and eLoadDownloadedPlugins (1<<6) make - // slInit do background plugin download/load, which crashed intermittently in slInit on the - // next mode-switch cycle (native 0xc0000005, not catchable from managed). Dropped on purpose. pref.Flags = PrefFlagDisableCLStateTracking; if (loadFrameGeneration) { - // Porte B.1: declare the "manual hooking" contract (ManualHooking guide §3.0). - // We fetch SL's Vulkan proxies for the sl_hooks.h entry points (swapchain/present/ - // wait-idle) OURSELVES via the interposer's exported vkGetDeviceProcAddr and route - // Window.cs through them; this flag tells sl.common not to expect global API - // interception (the source of the "Hook ... is NOT supported, plugin will not - // function properly" warnings in sl.log). Only ever set behind RYUJINX_DLSS_FG. pref.Flags |= PrefFlagUseManualHooking; - // Porte B.2: FG feeds its inputs (color/depth/mvec/HUD-less) per frame via - // slSetTagForFrame; without this flag that call returns error 19 ("FG has no inputs") - // and no frame is generated. eUseFrameBasedResourceTagging = 1<<7 (sl_core_types.h, - // verified). Only ever set behind RYUJINX_DLSS_FG. pref.Flags |= PrefFlagUseFrameBasedResourceTagging; } - // featuresToLoad is required, otherwise no plugins are loaded. Use a STABLE unmanaged buffer - // rather than the address of a stack local: if Streamline stores this pointer to load the DLSS - // plugin lazily (instead of copying it during slInit), a stack address would dangle after this - // method returns and crash intermittently. The buffer is freed in Shutdown. - // Porte A: DLSS_G is only ever added behind the explicit opt-in flag. sl.log proved DLSS_G - // hard-requires sl.reflex ("will be unloaded since it requires plugin 'sl.reflex'"), and unlike - // PCL (which auto-loads) Reflex is ignored unless the host requests it -- so request both. PCL - // is listed too for robustness (harmless if it would auto-load anyway). uint[] features = loadFrameGeneration ? new[] { FeatureDLSS, FeatureDLSS_G, FeatureReflex, FeaturePCL } : new[] { FeatureDLSS }; @@ -346,11 +258,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// - /// Asks Streamline whether DLSS is supported on the given Vulkan physical device. - /// Logs "DLSS: available" / the failure reason. Requires first. - /// - /// Native VkPhysicalDevice handle of the device in use. public static bool IsDlssSupported(IntPtr vkPhysicalDevice) { if (!_initialized) @@ -377,14 +284,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - /// - /// Porte A probe: asks Streamline whether DLSS Frame Generation (DLSS_G) is supported on - /// the given Vulkan physical device. MUST run after , so the - /// answer reflects our real device/queues rather than a default adapter. Purely a query - - /// nothing is allocated and the present path is untouched. Each failure mode is logged - /// with a distinct hint so one run tells us whether the wall is the plugin, the NGX - /// binary, a missing dependency (Reflex/PCL), the device, or the queue/hook mapping. - /// public static bool IsDlssGSupported(IntPtr vkPhysicalDevice) { if (!_initialized) @@ -404,8 +303,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } catch (Exception ex) { - // The DLSS-G plugin is new territory; never let a probe failure take down the - // working SR path. (Native AVs are not catchable here, but sl.log will have them.) Logger.Warning?.Print(LogClass.Gpu, $"DLSS-FG: slIsFeatureSupported(DLSS_G) threw: {ex.Message}"); return false; @@ -442,11 +339,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - /// - /// Registers our natively-created Vulkan device/instance with Streamline. Mandatory when not - /// using SL's vkCreate* proxies. Must be called after and after the - /// device is created, before any feature evaluation. - /// public static bool SetVulkanInfo(IntPtr instance, IntPtr physicalDevice, IntPtr device, uint queueFamily, uint slQueueStartIndex) { if (!_initialized) @@ -460,12 +352,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss info.Device = device; info.Instance = instance; info.PhysicalDevice = physicalDevice; - // sl::VulkanInfo queue indices = where STREAMLINE's OWN queues START, AFTER the host's queues - // (ProgrammingGuideManualHooking: "where first SL queue starts after host queues") -- NOT the - // app's present-queue index. With 0, SL believed the host owned no queues in this family and so - // rejected our present queue ("Invalid VK app queue - not created by the application" in sl.log). - // The host uses queues [0, slQueueStartIndex) here (index 0 = present, index 1 = background), so - // SL starts after them. Same family for all (Ryujinx has one graphics/compute/present family). info.GraphicsQueueFamily = queueFamily; info.GraphicsQueueIndex = slQueueStartIndex; info.ComputeQueueFamily = queueFamily; @@ -487,13 +373,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - /// - /// Porte B.1: resolves a Vulkan entry point through sl.interposer's exported - /// vkGetDeviceProcAddr. For functions SL intercepts (sl_hooks.h) this is SL's proxy; - /// for everything else it is the plain vulkan-1.dll address. IntPtr.Zero when SL is - /// not initialized, the export is missing (old/renamed interposer), or the name is - /// unknown - callers must fall back to the native loader. - /// public static IntPtr GetVulkanDeviceProxy(IntPtr device, string name) { if (!_initialized) @@ -511,7 +390,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// Shuts Streamline down if it was initialized. public static void Shutdown() { if (_initialized) @@ -520,7 +398,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _initialized = false; } - // Free the unmanaged buffers slInit was given, now that Streamline has been shut down. if (_logPathPtr != IntPtr.Zero) { Marshal.FreeHGlobal(_logPathPtr); diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs index a0c96a243..8fc4cf13a 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineDlss.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,44 +5,31 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// DLSS-SR evaluation interop: the structs, exported functions and feature functions needed to - /// actually run DLSS via slEvaluateFeature. Clean-room re-declaration of the public MIT - /// Streamline headers (sl_core_types.h, sl_consts.h, sl_dlss.h). All blittable, exact x64 layout. - /// public static unsafe class StreamlineDlss { private const string Interposer = "sl.interposer"; private const uint FeatureDLSS = 0; - // sl::BufferType (uint32) - the four tags DLSS-SR requires. private const uint BufferTypeDepth = 0; private const uint BufferTypeMotionVectors = 1; private const uint BufferTypeScalingInputColor = 3; private const uint BufferTypeScalingOutputColor = 4; - // Optional input: non-zero pixels tell DLSS to bias toward the CURRENT frame there -- - // the anti-ghosting hint for pixels whose motion we cannot describe (animated objects, - // disocclusions), fed from the MV++ dynamic mask. private const uint BufferTypeBiasCurrentColorHint = 29; - // sl::ResourceLifecycle - tag stays valid until the evaluate returns. private const uint LifecycleValidUntilEvaluate = 2; - // sl::ResourceType::eTex2d private const byte ResourceTypeTex2d = 0; - // sl::Boolean (char) private const byte BoolFalse = 0; private const byte BoolTrue = 1; - /// One image to hand to DLSS (input/output/depth/motion). public struct DlssTexture { - public IntPtr Image; // VkImage - public IntPtr View; // VkImageView - public uint NativeFormat; // VkFormat (Silk.NET value) - public uint Layout; // VkImageLayout the image is in when DLSS runs + public IntPtr Image; + public IntPtr View; + public uint NativeFormat; + public uint Layout; public uint Width; public uint Height; } @@ -79,27 +63,26 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public uint Value; } - // sl::Resource - sl_core_types.h. sizeof == 112 on x64. [StructLayout(LayoutKind.Sequential)] private struct Resource { public IntPtr Next; public StructType Type; public nuint Version; - public byte ResType; // ResourceType : char (32, padded to 40) - public IntPtr Native; // 40 VkImage - public IntPtr Memory; // 48 - public IntPtr View; // 56 VkImageView - public uint State; // 64 VkImageLayout - public uint Width; // 68 - public uint Height; // 72 - public uint NativeFormat; // 76 VkFormat - public uint MipLevels; // 80 - public uint ArrayLayers; // 84 - public ulong GpuVirtualAddress; // 88 - public uint Flags; // 96 - public uint Usage; // 100 - public uint Reserved; // 104 + public byte ResType; + public IntPtr Native; + public IntPtr Memory; + public IntPtr View; + public uint State; + public uint Width; + public uint Height; + public uint NativeFormat; + public uint MipLevels; + public uint ArrayLayers; + public ulong GpuVirtualAddress; + public uint Flags; + public uint Usage; + public uint Reserved; } [StructLayout(LayoutKind.Sequential)] @@ -108,14 +91,13 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public uint Top, Left, Width, Height; } - // sl::ResourceTag - sizeof == 64. [StructLayout(LayoutKind.Sequential)] private struct ResourceTag { public IntPtr Next; public StructType Type; public nuint Version; - public IntPtr ResourcePtr; // Resource* + public IntPtr ResourcePtr; public uint BufferType; public uint Lifecycle; public Extent Extent; @@ -138,7 +120,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // sl::Constants (kStructVersion2) - sl_consts.h. sizeof == 456 on x64. [StructLayout(LayoutKind.Sequential)] private struct Constants { @@ -172,7 +153,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public float MinRelativeLinearDepthObjectSeparation; } - // sl::DLSSOptions (kStructVersion3) - sl_dlss.h. sizeof == 88. [StructLayout(LayoutKind.Sequential)] private struct DlssOptions { @@ -198,7 +178,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public byte AlphaUpscalingEnabled; } - // sl::DLSSOptimalSettings (kStructVersion1) - sl_dlss.h. sizeof == 64. [StructLayout(LayoutKind.Sequential)] private struct DlssOptimalSettings { @@ -214,7 +193,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public uint RenderHeightMax; } - // --- exported functions --- [DllImport(Interposer, EntryPoint = "slGetNewFrameToken", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern int slGetNewFrameToken(out IntPtr token, in uint frameIndex); @@ -227,17 +205,14 @@ namespace Ryujinx.Graphics.Vulkan.Dlss [DllImport(Interposer, EntryPoint = "slGetFeatureFunction", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern int slGetFeatureFunction(uint feature, [MarshalAs(UnmanagedType.LPStr)] string functionName, out IntPtr function); - // DLSS feature functions, bound lazily via slGetFeatureFunction (they are not direct exports). private static delegate* unmanaged[Cdecl] _slDLSSSetOptions; private static delegate* unmanaged[Cdecl] _slDLSSGetOptimalSettings; private static bool _functionsBound; private static bool _loggedSizes; private static bool _loggedUndefinedFormat; - /// DLSS-FG Porte B.2: the sl::FrameToken* of the most recent successful Evaluate. public static IntPtr LastFrameToken { get; private set; } - /// DLSS-FG Porte B.2: the frame index paired with . public static uint LastFrameIndex { get; private set; } private static StructType Guid(uint d1, ushort d2, ushort d3, byte b0, byte b1, byte b2, byte b3, byte b4, byte b5, byte b6, byte b7) @@ -288,20 +263,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return vp; } - // DLSS 4: preset for the UPSCALING modes. Default 11 = Preset K (transformer, DLSS 4) -- the - // modern model, chosen by the user. Set RYUJINX_DLSS_PRESET=6 to fall back to Preset F (the - // older CNN, ultra-stability) for A/B. Other useful values: 10=J (transformer), 5=E (last CNN), - // 13=M (newer transformer -- preset-M chantier). When the env is set EXPLICITLY it now also - // overrides the DLAA slot (preset selectable end-to-end, per external user request); without - // it, defaults are untouched: DLAA pinned to K, upscaling modes K. private static readonly uint? _presetEnv = ParsePresetEnv(); private static readonly bool _presetEnvSet = _presetEnv.HasValue; private static readonly uint _upscalePreset = _presetEnv ?? 11u; - // Validated: the shipped Streamline's DLSSPreset enum stops at 13 (M). An undefined value - // (99, garbage) would go straight to slDLSSSetOptions with driver-undefined behavior. private static uint? ParsePresetEnv() { string raw = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_PRESET"); @@ -322,17 +289,11 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return null; } - // Optional DLSS sharpening (RYUJINX_DLSS_SHARPNESS, 0..1; default 0 = off, unchanged). private static readonly float _sharpness = float.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_SHARPNESS"), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float _s) ? System.Math.Clamp(_s, 0f, 1f) : 0f; - // [M1 exposure measurement] RYUJINX_DLSS_FIXED_EXPOSURE= disables auto-exposure and - // uses the given fixed exposure scale instead. Hypothesis under test: auto-exposure estimated - // on the RAW pre-tonemap HDR input (huge dynamic range, large binary-alternating foliage - // areas) oscillates and pumps the WHOLE image ("everything flickers at times"). Absent or - // <= 0 = auto-exposure, byte-identical to stock. private static readonly float _fixedExposure = float.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_FIXED_EXPOSURE"), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float _fe) @@ -340,7 +301,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static bool _optionsLogged; - /// Sets DLSS options for the viewport (mode + final output size). Call when size/mode changes. public static bool SetOptions(uint viewportId, DlssMode mode, uint outputWidth, uint outputHeight, bool hdr) { if (!BindFunctions()) @@ -358,13 +318,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss opt.ExposureScale = _fixedExposure > 0f ? _fixedExposure : 1.0f; opt.Sharpness = _sharpness; opt.ColorBuffersHDR = hdr ? BoolTrue : BoolFalse; - // [M1] Fixed exposure (measurement) vs stock auto-exposure; absent flag = stock. opt.UseAutoExposure = _fixedExposure > 0f ? BoolFalse : BoolTrue; - // Locked preset matrix (validated presets, no manual override): DLAA (1:1 ratio) gets Preset K - // (transformer, maximum sharpness); the upscaling modes get Preset F (CNN, ultra-stability / - // anti-flicker). Every per-mode slot is set, so whichever mode is active uses its mapped preset. - const uint PresetK = 11; // transformer sharpness, for DLAA + const uint PresetK = 11; opt.DlaaPreset = _presetEnvSet ? _upscalePreset : PresetK; opt.QualityPreset = _upscalePreset; opt.BalancedPreset = _upscalePreset; @@ -391,7 +347,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// Queries DLSS's optimal render resolution for a given output size + mode. public static bool GetOptimalRenderSize(DlssMode mode, uint outputWidth, uint outputHeight, out uint renderWidth, out uint renderHeight) { renderWidth = outputWidth; @@ -424,10 +379,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// - /// Returns the dynamic render-resolution range DLSS accepts for a given mode + output size. - /// The actual input must fall within [min, max] or slEvaluateFeature rejects it. - /// public static bool GetRenderRange(DlssMode mode, uint outputWidth, uint outputHeight, out uint minWidth, out uint minHeight, out uint maxWidth, out uint maxHeight) { @@ -503,13 +454,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss c.ClipToLensClip = Mat4.Identity(); c.ClipToPrevClip = Mat4.Identity(); c.PrevClipToClip = Mat4.Identity(); - c.JitterOffsetX = jitterX; // sub-pixel jitter applied to the image this frame (0 unless Mode B) + c.JitterOffsetX = jitterX; c.JitterOffsetY = jitterY; - // The motion buffer stores vectors in render-resolution pixels, but Streamline expects - // them normalized (sl_consts.h: "scale factors used to normalize motion vectors ... in - // [-1,1] range"). Leaving this at 1.0 feeds DLSS vectors ~renderW times too large, so - // temporal reprojection samples history from far off-screen on any camera motion and the - // image collapses to a desaturated smear (only correct when still). 1/render normalizes it. c.MvecScaleX = renderW != 0 ? 1.0f / renderW : 1f; c.MvecScaleY = renderH != 0 ? 1.0f / renderH : 1f; c.CameraUpY = 1f; @@ -523,7 +469,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss c.DepthInverted = BoolFalse; c.CameraMotionIncluded = BoolTrue; c.MotionVectors3D = BoolFalse; - c.Reset = reset ? BoolTrue : BoolFalse; // history off only on the first frame / scene cut + c.Reset = reset ? BoolTrue : BoolFalse; c.OrthographicProjection = BoolFalse; c.MotionVectorsDilated = BoolFalse; c.MotionVectorsJittered = BoolFalse; @@ -532,10 +478,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return c; } - /// - /// Runs DLSS for one frame: sets constants, tags the four buffers and evaluates into the - /// output image, recording into . The caller submits that buffer. - /// public static bool Evaluate( IntPtr cmdBuffer, uint viewportId, @@ -555,14 +497,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // The bias hint is optional: drop it (never the whole evaluate) if unusable. bool useBias = hasBiasMask && biasMask.NativeFormat != 0; - // Never hand DLSS a resource with an undefined Vulkan format (NativeFormat == 0). Streamline - // rejects it ("Cannot have undefined format") and feeding it on can drive an illegal GPU op -> - // ErrorDeviceLost. Skip this frame's evaluate (clean fallback to normal scaling) and log which - // of the four buffers is the culprit, so the root cause (why that texture has no format) can be - // fixed. One-shot to avoid spamming. if (input.NativeFormat == 0 || output.NativeFormat == 0 || depth.NativeFormat == 0 || motion.NativeFormat == 0) { if (!_loggedUndefinedFormat) @@ -586,10 +522,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return false; } - // DLSS-FG Porte B.2: expose THIS frame's token so the frame-generation path (tags with - // eValidUntilPresent + PCL/Reflex markers around the present) references the exact same - // sl::FrameToken as the constants set below - never a second token for the same frame. - // The pointer is SL's own ring-buffered token, valid through this frame's present. LastFrameToken = token; LastFrameIndex = frameIndex; @@ -616,7 +548,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ResourceTag tMotion = MakeTag(&rMotion, BufferTypeMotionVectors, motion.Width, motion.Height); ResourceTag tBias = useBias ? MakeTag(&rBias, BufferTypeBiasCurrentColorHint, biasMask.Width, biasMask.Height) : default; - // Inputs to slEvaluateFeature: the viewport plus the local resource tags. IntPtr* inputs = stackalloc IntPtr[6]; inputs[0] = (IntPtr)(&vp); inputs[1] = (IntPtr)(&tDepth); @@ -645,14 +576,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// - /// True while DLSS super resolution is actually evaluating frames -- drives the green - /// status-bar DLSS chip (user request 05/07: same treatment as the VRR/FG indicators). - /// Freshness-based: refreshed by every successful evaluate, considered off when none - /// happened for 2 seconds. A latched bool only went false on evaluate FAILURE, so the - /// chip kept lying whenever DLSS simply stopped being called (path switched away, game - /// stopped, emulation paused). - /// public static bool UiActive { get => Environment.TickCount64 - System.Threading.Interlocked.Read(ref _uiActiveAtMs) < 2000; diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs index 265b8a90c..02055625a 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/StreamlineFrameGen.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common.Logging; using System; @@ -8,84 +5,41 @@ using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Vulkan.Dlss { - /// - /// DLSS-FG Porte B.2 - frame-generation interop: DLSS-G options, Reflex options/sleep and - /// PCL pacing markers. Clean-room re-declaration of the public MIT Streamline 2.12 headers - /// (sl_dlss_g.h, sl_reflex.h, sl_pcl.h, sl_core_types.h); every GUID/enum value below is - /// cited from those headers. Deliberately SELF-CONTAINED: nothing here touches Streamline.cs - /// (slInit/slSetVulkanInfo are being reworked in parallel) - shared struct shapes - /// (ViewportHandle/Resource/ResourceTag) are re-declared, not imported. - /// - /// Per-frame flow (all gated on VulkanRenderer.FgHooks != null, i.e. RYUJINX_DLSS_FG=1 + - /// probe OK + proxies armed; flag off => none of this code runs): - /// DlssUpscaler.TryRun, after a successful SR evaluate: - /// OnFrameEvaluated() -> lazy bind + ReflexSetOptions(eLowLatency) + DLSSGSetOptions(eOn) - /// -> slSetTagForFrame(HUDLESS+DEPTH+MVEC, eValidUntilPresent) - /// -> PCL eSimulationStart/End (degenerate: the emulator has no host - /// sim we can pace - stated, not hidden) + eRenderSubmitStart - /// Window.Present, just before vkQueuePresentKHR: eRenderSubmitEnd + ePresentStart - /// Window.Present, just after: ePresentEnd + slReflexSleep + state poll - /// - /// HONEST LIMITS: the color tagged as HUDLESS is the full SR output - the game HUD is baked - /// in (no HUD-less source exists in an emulator), so HUD elements will warp in generated - /// frames. MVs are reconstructed (noisy), jitter is the guest tick's. FG output WILL be - /// artefacted; the goal of B.2 is to make it RUN and be MEASURABLE (slDLSSGGetState: - /// status + numFramesActuallyPresented), not to make it pretty. - /// public static unsafe class StreamlineFrameGen { private const string Interposer = "sl.interposer"; - // sl::Feature (sl_core_types.h): kFeatureReflex = 3, kFeaturePCL = 4, kFeatureDLSS_G = 1000. private const uint FeatureReflex = 3; private const uint FeaturePCL = 4; private const uint FeatureDLSSG = 1000; - // sl::PCLMarker (sl_pcl.h, uint32): the six pacing markers FG needs. - private const uint MarkerSimulationStart = 0; // eSimulationStart - private const uint MarkerSimulationEnd = 1; // eSimulationEnd - private const uint MarkerRenderSubmitStart = 2; // eRenderSubmitStart - private const uint MarkerRenderSubmitEnd = 3; // eRenderSubmitEnd - private const uint MarkerPresentStart = 4; // ePresentStart - private const uint MarkerPresentEnd = 5; // ePresentEnd + private const uint MarkerSimulationStart = 0; + private const uint MarkerSimulationEnd = 1; + private const uint MarkerRenderSubmitStart = 2; + private const uint MarkerRenderSubmitEnd = 3; + private const uint MarkerPresentStart = 4; + private const uint MarkerPresentEnd = 5; - // sl::ReflexMode (sl_reflex.h, C enum): eOff=0, eLowLatency=1, eLowLatencyWithBoost=2. private const int ReflexModeLowLatency = 1; - // sl::DLSSGMode (sl_dlss_g.h, uint32): eOff=0, eOn=1, eAuto=2, eDynamic=3. private const uint DlssgModeOn = 1; - // sl::ResourceLifecycle (sl_core_types.h): eOnlyValidNow=0, eValidUntilPresent=1, - // eValidUntilEvaluate=2. FG consumes the tags AT PRESENT, hence eValidUntilPresent. private const uint LifecycleValidUntilPresent = 1; - // sl::BufferType (sl_core_types.h): kBufferTypeDepth=0, kBufferTypeMotionVectors=1, - // kBufferTypeHUDLessColor=2. private const uint BufferTypeDepth = 0; private const uint BufferTypeMotionVectors = 1; private const uint BufferTypeHudLessColor = 2; - private const byte ResourceTypeTex2d = 0; // sl::ResourceType::eTex2d + private const byte ResourceTypeTex2d = 0; - // Kill-switch for bisecting (default ON when FG is armed): RYUJINX_DLSS_FG_ACTIVATE=0 - // keeps Porte B.1 behavior (hooks armed, present routed) but never turns DLSS-G on nor - // tags/marks anything - isolates "hooks crash" from "activation crashes". private static readonly bool _activationEnabled = Environment.GetEnvironmentVariable("RYUJINX_DLSS_FG_ACTIVATE") is not ("0" or "false" or "off"); - // RYUJINX_DLSS_FG_FRAMES: frames DLSS-G generates per real frame (display multiplier = - // value + 1). 1 = 2x (default); 3 = 4x, which lands a 60 fps cap exactly on a 240 Hz - // panel. Hardware ceiling on this adapter per DLSSGState::numFramesToGenerateMax = 5. private static readonly uint _framesToGenerate = uint.TryParse(Environment.GetEnvironmentVariable("RYUJINX_DLSS_FG_FRAMES"), out uint n) ? Math.Clamp(n, 1u, 5u) : 1u; - // --------------------------------------------------------------------------------------- - // Structs - exact x64 MSVC layout, same conventions as StreamlineDlss.cs - // (C++ bool/Boolean -> byte, enums -> uint/int, size_t -> nuint, pointers -> IntPtr). - // --------------------------------------------------------------------------------------- - [StructLayout(LayoutKind.Sequential)] private struct StructType { @@ -95,7 +49,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public byte B0, B1, B2, B3, B4, B5, B6, B7; } - // sl::ViewportHandle - sl_core_types.h {171B6435-9B3C-4FC8-9994-FBE52569AAA4}. [StructLayout(LayoutKind.Sequential)] private struct ViewportHandle { @@ -105,7 +58,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public uint Value; } - // sl::Resource - sl_core_types.h {3A9D70CF-2418-4B72-8391-13F8721C7261}. sizeof == 112. [StructLayout(LayoutKind.Sequential)] private struct Resource { @@ -134,7 +86,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public uint Top, Left, Width, Height; } - // sl::ResourceTag - sl_core_types.h {4C6A5AAD-B445-496C-87FF-1AF3845BE653}. sizeof == 64. [StructLayout(LayoutKind.Sequential)] private struct ResourceTag { @@ -147,9 +98,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public Extent Extent; } - // sl::ReflexOptions - sl_reflex.h {F03AF81A-6D0B-4902-A651-C4965E215434}, kStructVersion1. - // Body: mode(int)@32, frameLimitUs(u32)@36, useMarkersToOptimize(bool)@40, virtualKey(u16)@42, - // idThread(u32)@44 -> sizeof 48. [StructLayout(LayoutKind.Sequential)] private struct ReflexOptions { @@ -163,45 +111,34 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public uint IdThread; } - // sl::DLSSGOptions - sl_dlss_g.h {FAC5F1CB-2DFD-4F36-A1E6-3A9E865256C5}, kStructVersion5. - // Body offsets (after the 32-byte BaseStructure header): mode@32 ... uiBufferFormat@88, - // onErrorCallback(ptr)@96, bReserved15(Boolean:char)@104, queueParallelismMode(u32)@108, - // enableUserInterfaceRecomposition(Boolean:char)@112, dynamicTargetFrameRate(float)@116 - // -> sizeof 120. [StructLayout(LayoutKind.Sequential)] private struct DlssgOptions { public IntPtr Next; public StructType Type; public nuint Version; - public uint Mode; // 32 DLSSGMode - public uint NumFramesToGenerate; // 36 - public uint Flags; // 40 DLSSGFlags - public uint DynamicResWidth; // 44 - public uint DynamicResHeight; // 48 - public uint NumBackBuffers; // 52 - public uint MvecDepthWidth; // 56 - public uint MvecDepthHeight; // 60 - public uint ColorWidth; // 64 - public uint ColorHeight; // 68 - public uint ColorBufferFormat; // 72 - public uint MvecBufferFormat; // 76 - public uint DepthBufferFormat; // 80 - public uint HudLessBufferFormat; // 84 - public uint UiBufferFormat; // 88 - public IntPtr OnErrorCallback; // 96 (after 4-byte pad) - public byte BReserved15; // 104 (sl::Boolean : char; default eInvalid=2) - public uint QueueParallelismMode; // 108 (after 3-byte pad) - public byte EnableUserInterfaceRecomposition; // 112 - public float DynamicTargetFrameRate; // 116 (after 3-byte pad) + public uint Mode; + public uint NumFramesToGenerate; + public uint Flags; + public uint DynamicResWidth; + public uint DynamicResHeight; + public uint NumBackBuffers; + public uint MvecDepthWidth; + public uint MvecDepthHeight; + public uint ColorWidth; + public uint ColorHeight; + public uint ColorBufferFormat; + public uint MvecBufferFormat; + public uint DepthBufferFormat; + public uint HudLessBufferFormat; + public uint UiBufferFormat; + public IntPtr OnErrorCallback; + public byte BReserved15; + public uint QueueParallelismMode; + public byte EnableUserInterfaceRecomposition; + public float DynamicTargetFrameRate; } - // sl::DLSSGState - sl_dlss_g.h {CC8AC8E1-A179-44F5-97FA-E74112F9BC61}, kStructVersion4. - // Body: estimatedVRAMUsageInBytes(u64)@32, status(u32)@40, minWidthOrHeight@44, - // numFramesActuallyPresented@48, numFramesToGenerateMax@52, bReserved4(char)@56, - // bIsVsyncSupportAvailable(char)@57, inputsProcessingCompletionFence(ptr)@64, - // lastPresentInputsProcessingCompletionFenceValue(u64)@72, bIsDynamicMFGSupported(char)@80 - // -> sizeof 88. [StructLayout(LayoutKind.Sequential)] private struct DlssgState { @@ -209,7 +146,7 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public StructType Type; public nuint Version; public ulong EstimatedVRAMUsageInBytes; - public uint Status; // DLSSGStatus bitmask + public uint Status; public uint MinWidthOrHeight; public uint NumFramesActuallyPresented; public uint NumFramesToGenerateMax; @@ -220,32 +157,18 @@ namespace Ryujinx.Graphics.Vulkan.Dlss public byte BIsDynamicMFGSupported; } - // --------------------------------------------------------------------------------------- - // Exports + feature functions - // --------------------------------------------------------------------------------------- - [DllImport(Interposer, EntryPoint = "slGetFeatureFunction", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern int slGetFeatureFunction(uint feature, [MarshalAs(UnmanagedType.LPStr)] string functionName, out IntPtr function); - // sl::Result slSetTagForFrame(const FrameToken&, const ViewportHandle&, const ResourceTag*, uint32_t, CommandBuffer*) - // Core export (sl.h; usage per ManualHooking guide §6.2). SL copies the tag/resource - // DESCRIPTORS during the call (the guide itself passes stack locals); only the GPU - // resources must stay valid until present - which eValidUntilPresent declares. [DllImport(Interposer, EntryPoint = "slSetTagForFrame", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] private static extern int slSetTagForFrame(IntPtr frameToken, in ViewportHandle viewport, ResourceTag* tags, uint numTags, IntPtr cmdBuffer); - // Feature functions, bound lazily via slGetFeatureFunction (per the inline helpers in - // sl_dlss_g.h / sl_reflex.h / sl_pcl.h; they are not direct exports). private static delegate* unmanaged[Cdecl] _slDLSSGSetOptions; private static delegate* unmanaged[Cdecl] _slDLSSGGetState; private static delegate* unmanaged[Cdecl] _slReflexSetOptions; - private static delegate* unmanaged[Cdecl] _slReflexSleep; // (const FrameToken&) - private static delegate* unmanaged[Cdecl] _slPCLSetMarker; // (PCLMarker, const FrameToken&) + private static delegate* unmanaged[Cdecl] _slReflexSleep; + private static delegate* unmanaged[Cdecl] _slPCLSetMarker; - // Frames the input sizes must hold before (re)enabling DLSS-G. slDLSSGSetOptions with new - // sizes makes dlfg tear down and rebuild its present pipeline while frames are in flight - - // proven device-lost 38 ms after the second SetOptions (auto-res one-shot retargeting, - // run 19-56-23). ~0.5 s at 60 fps outlives that startup transient. private const int SizeStabilityFrames = 30; private static bool _bindTried; @@ -253,32 +176,18 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static bool _reflexOn; private static bool _optionsSet; private static uint _optColorW, _optColorH, _optMvecW, _optMvecH; - private static DlssgOptions _lastEnabledOptions; // the exact options dlfg accepted at eOn (reused for eOff) + private static DlssgOptions _lastEnabledOptions; private static uint _seenColorW, _seenColorH, _seenMvecW, _seenMvecH; private static int _sizeStableFrames; - private static IntPtr _frameToken; // token of the frame currently in flight (set at evaluate, cleared after present) + private static IntPtr _frameToken; private static bool _tagFailedLogged; private static bool _markerFailedLogged; private static bool _hudlessLogged; private static uint _presentsSinceStateLog; private static uint _lastLoggedGenerated; - /// - /// Live FG multiplier for the UI status bar: 1 = FG off or passthrough, 2+ = frame - /// generation is interpolating at that factor. Updated once per second by the - /// present-side counter; read from the UI thread (volatile, no locking needed). - /// public static volatile int UiMultiplier = 1; - /// - /// True while frame generation is ARMED for the running game (counter alive), whatever - /// the current multiplier. Drives the status-bar FG chip (user request 05/07: green like - /// the VRR indicator when interpolating, visible-but-neutral when armed at x1 -- the old - /// suffix silently vanished at x1, which read as "it disappeared"). - /// Freshness-based like the DLSS chip: refreshed by the once-per-second counter poll, - /// considered off when no poll happened for 3 seconds -- a latched bool survived the - /// game's shutdown and showed a stale FG chip on the next game until FG re-armed. - /// public static bool UiArmed { get => Environment.TickCount64 - System.Threading.Interlocked.Read(ref _uiArmedAtMs) < 3000; @@ -287,15 +196,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static long _uiArmedAtMs; - // FG counter (NVIDIA-overlay style): count OUR app presents over a wall-clock 1s window, and read - // DLSSGState.numFramesActuallyPresented (= frames SL actually pushed to the display since the last - // slDLSSGGetState call) ONCE per window. Ratio display/app = the true FG multiplier. Poll exactly - // once per window so the "since last call" counter accumulates a full second (the old every-600 - // poll never let it build). This is the definitive "is FG really generating?" readout. private static int _appPresentsInWindow; private static long _windowStartMs; - /// Swapchain info pushed by Window.CreateSwapchain when FG is armed (DLSSGOptions wants them). public static uint SwapchainImageCount; public static uint SwapchainFormat; public static uint SwapchainWidth; @@ -329,8 +232,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _bindTried = true; - // Each bind is logged individually so ONE run attributes a failure to the exact - // plugin (dlss_g vs reflex vs pcl) and function. bool ok = true; ok &= BindOne(FeatureDLSSG, "slDLSSGSetOptions", out IntPtr pSetOpt); ok &= BindOne(FeatureDLSSG, "slDLSSGGetState", out IntPtr pGetState); @@ -383,11 +284,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// - /// Reflex must be ACTIVE while DLSS-G runs (sl_dlss_g.h: DLSSGStatus:: - /// eFailReflexNotDetectedAtRuntime), so eLowLatency is set before FG is enabled. - /// No FPS cap (frameLimitUs=0). - /// private static bool EnableReflex() { if (_reflexOn) @@ -423,9 +319,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - // Never reconfigure dlfg mid-flight: on any size change, drop to eOff first (the - // documented resize protocol - dlfg flushes its worker queues on the transition), - // then require the sizes to hold for SizeStabilityFrames before enabling. if (SwapchainWidth != _seenColorW || SwapchainHeight != _seenColorH || motion.Width != _seenMvecW || motion.Height != _seenMvecH) { @@ -456,16 +349,12 @@ namespace Ryujinx.Graphics.Vulkan.Dlss opt.NumBackBuffers = SwapchainImageCount; opt.MvecDepthWidth = motion.Width; opt.MvecDepthHeight = motion.Height; - // The color FG interpolates is the backbuffer itself. No HUD-less is tagged: with the - // HUD baked into the scene (emulator), a HUD-less identical to the backbuffer teaches - // dlfg nothing (it derives UI = backbuffer - hudless = 0) and a content-size one is - // rejected ("HUD-less buffer extent does not match color buffer size", run 19-46-32). opt.ColorWidth = SwapchainWidth; opt.ColorHeight = SwapchainHeight; opt.ColorBufferFormat = SwapchainFormat; opt.MvecBufferFormat = motion.NativeFormat; opt.DepthBufferFormat = depth.NativeFormat; - opt.BReserved15 = 2; // sl::Boolean::eInvalid (header default) + opt.BReserved15 = 2; ViewportHandle vp = MakeViewport(viewportId); int r = _slDLSSGSetOptions(in vp, in opt); @@ -493,19 +382,10 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return true; } - /// - /// slDLSSGSetOptions(eOff): the supported way to stop interpolation before anything about - /// the inputs/swapchain changes. dlfg flushes its worker queues on this transition. - /// private static void DisableFrameGeneration(uint viewportId, string reason) { - // eOff goes through the SAME options validation as eOn: a zeroed struct (sizes, - // formats, backbuffer count) is rejected with eErrorInvalidParameter (25) and dlfg - // KEEPS RUNNING - the swapchain teardown then hangs the device (fullscreen crash, - // runs 06-02-55 / 06-10-28). Reuse the exact options dlfg accepted at eOn and - // change only the mode. DlssgOptions opt = _lastEnabledOptions; - opt.Mode = 0; // DLSSGMode::eOff + opt.Mode = 0; ViewportHandle vp = MakeViewport(viewportId); int r = _slDLSSGSetOptions(in vp, in opt); @@ -516,10 +396,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss $"DLSS-FG: {reason} - DLSS-G set to eOff (result {SlResult(r)}); re-enabling once sizes hold for {SizeStabilityFrames} frames."); } - /// - /// Decodes an sl::Result to "code/name" for logs (sl_result.h, SDK 2.12). An undecoded - /// "result 25" cost a session: it reads like success noise but meant eErrorInvalidParameter. - /// private static string SlResult(int r) { string[] names = @@ -542,14 +418,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return r >= 0 && r < names.Length ? $"{r}/{names[r]}" : r.ToString(); } - /// - /// Called by the swapchain owner right BEFORE the swapchain is destroyed for a recreate - /// (window resize, fullscreen toggle). The documented resize protocol: DLSS-G must drop - /// to eOff before the old swapchain goes away - dlfg flushes its present workers on the - /// transition; tearing the chain down under an active dlfg hangs the device (VK sync - /// object timeout on the fullscreen toggle, run 2026-07-02 06-02-55). Re-enabling goes - /// through the normal size stability gate once frames flow at the new size. - /// public static void OnSwapchainTeardown(uint viewportId) { if (!_optionsSet || _slDLSSGSetOptions == null) @@ -607,13 +475,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// Per-frame entry, called by DlssUpscaler right after a successful SR evaluate, on the - /// SAME command buffer and with the SAME frame token as the evaluate/constants. Binds and - /// activates lazily; tags DEPTH/MVEC for present-time consumption; opens the PCL marker - /// sequence. The Constants (incl. MvecScale=1/renderRes) were already set for this token - /// by StreamlineDlss.Evaluate - FG reads the same ones; nothing is re-declared. - /// public static void OnFrameEvaluated( IntPtr cmdBuffer, uint viewportId, @@ -634,15 +495,9 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _frameToken = frameToken; - // FG inputs, consumed by sl.dlss_g inside the present hook: render-res depth + - // reconstructed MVs - the exact same images the SR evaluate consumed, same formats, - // same MvecScale convention. The color comes from the backbuffer, which SL already - // intercepts; no HUD-less tag (see EnableFrameGeneration). Resource rDepth = MakeResource(in depth); Resource rMotion = MakeResource(in motion); - // [HUDLESSFEED 28/07] Troisieme tag quand l'image sans interface est disponible. - // Sans lui, dlfg derive UI = backbuffer - hudless = 0 et deforme le HUD. bool hasHudless = hudless.Image != IntPtr.Zero && hudless.Width > 0 && hudless.Height > 0; Resource rHudless = hasHudless ? MakeResource(in hudless) : default; @@ -659,7 +514,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss ViewportHandle vp = MakeViewport(viewportId); int r = slSetTagForFrame(frameToken, in vp, tags, nTags, cmdBuffer); - // [HUDLESSFEED] Verdict LU, pas suppose : une seule ligne, au premier tag avec HUD-less. if (hasHudless && !_hudlessLogged) { _hudlessLogged = true; @@ -682,26 +536,17 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return; } - // PCL sequence for this frame. "Simulation" is degenerate on purpose: the emulator's - // guest loop cannot be paced by Reflex, so sim start/end bracket nothing - stated - // honestly rather than pretending there is a host simulation. SetMarker(MarkerSimulationStart); SetMarker(MarkerSimulationEnd); SetMarker(MarkerRenderSubmitStart); } - /// Window.Present, right before vkQueuePresentKHR (on the SL proxy). public static void BeforePresent() { SetMarker(MarkerRenderSubmitEnd); SetMarker(MarkerPresentStart); } - /// - /// Window.Present, right after vkQueuePresentKHR returns: closes the marker sequence, - /// lets Reflex pace us (sleep), and periodically polls slDLSSGGetState - THE measurement: - /// numFramesActuallyPresented > presents means FG is really injecting frames. - /// public static void AfterPresent(uint viewportId) { if (_frameToken == IntPtr.Zero) @@ -711,9 +556,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss SetMarker(MarkerPresentEnd); - // Reflex sleep with the just-presented frame's token ~= pacing the start of the next - // frame. Approximation (proper placement is at frame start, before input sampling - - // which the emulator does not surface); good enough for bring-up, revisit for pacing QA. int r = _slReflexSleep(_frameToken); if (r != 0 && !_markerFailedLogged) { @@ -723,9 +565,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _frameToken = IntPtr.Zero; - // FG counter: one app present here. Once per wall-clock second, read how many frames SL actually - // presented to the display (numFramesActuallyPresented, since our last GetState = since the window - // start) and compare to our app presents -> the real FG multiplier. _appPresentsInWindow++; long now = Environment.TickCount64; if (_windowStartMs == 0) @@ -740,11 +579,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - /// - /// FG counter report: app presents vs SL's actual display presents over the elapsed window -> - /// the multiplier. ~2x on a 2x setup means FG is really injecting a generated frame per real one; - /// ~1x means FG is enabled but NOT generating. This is the ground-truth "does FG work" number. - /// private static void ReportCounter(uint viewportId, int elapsedMs) { DlssgState state = default; @@ -757,11 +591,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return; } - // numFramesActuallyPresented semantics: the sl_dlss_g.h comment says "frames presented - // since the last GetState call" but the programming guide computes actualFPS = - // appFPS * numFramesActuallyPresented. Observed values settle it: always exactly 1 - // (scRGB run, passthrough) or exactly 2 (SDR run, interpolation enabled) regardless of - // elapsed time - it is a PER-PRESENT MULTIPLIER, not a cumulative count. double secs = elapsedMs / 1000.0; double appFps = _appPresentsInWindow / secs; uint mult = state.NumFramesActuallyPresented; @@ -770,8 +599,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss UiMultiplier = mult >= 2 ? (int)mult : 1; UiArmed = true; - // The poll itself feeds the status-bar chips and must keep running; only the log - // line is a dev gauge (MvppDev, release default quiet). if (GAL.MvppDev.Enabled) { Logger.Info?.Print(LogClass.Gpu, @@ -780,14 +607,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss (mult >= 2 ? "FG IS INTERPOLATING." : "FG enabled but NOT generating extra frames.")); } - // [FG WATCHDOG 19/07] La FG peut s'endormir EN SILENCE : enabled, status=0x0, mais - // x1 en continu (mesuré : 1 min complète en plein écran, session 11-12-50, 06:29 -> - // 07:33, aucun evenement d'etat — seul un F11 la ressuscitait via son cycle - // eOff/eOn). Le chien de garde applique le MEME remede automatiquement : si la FG a - // deja interpole cette session (ecarte les faux positifs de menu/boot) et reste x1 - // pendant 8 rapports (~8 s), cycle eOff + remise a zero de la stabilite -> la - // machinerie existante re-enable apres 30 frames stables. Anti-boucle : 1 cycle par - // 30 s max. Kill-switch banc : RYUJINX_DLSS_FG_WATCHDOG=0. if (mult >= 2) { _fgEverInterpolated = true; @@ -811,8 +630,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss } } - // [FG WATCHDOG] etat : la FG a-t-elle deja interpole cette session, rapports x1 - // consecutifs, dernier cycle (anti-boucle), et kill-switch d'environnement. private static bool _fgEverInterpolated; private static int _fgX1Reports; private static long _fgWatchdogLastCycleMs; @@ -834,7 +651,6 @@ namespace Ryujinx.Graphics.Vulkan.Dlss return; } - // status: DLSSGStatus bitmask (sl_dlss_g.h). 0 == eOk; 1<<1 == eFailReflexNotDetectedAtRuntime etc. if (force || state.Status != 0 || state.NumFramesActuallyPresented != _lastLoggedGenerated) { _lastLoggedGenerated = state.NumFramesActuallyPresented; diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs index 719937218..c89afe2b1 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs @@ -88,8 +88,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects new ShaderSource(sharpeningShaderHdr, ShaderStage.Compute, TargetLanguage.Spirv) ], sharpeningResourceLayout); - // PQ variant (same layout): rgb10_a2 output with an ST.2084 encode, for HDR10 - // swapchains (the DLSS-FG-compatible HDR container). byte[] sharpeningShaderPq = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpeningPq.spv"); _sharpeningProgramPq = _renderer.CreateProgramWithMinimalLayout([ @@ -171,8 +169,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); buffer.Holder.SetDataUnchecked(buffer.Offset, dimensionsBuffer); - // Non-HDR: [sharpening]. HDR: [sharpening, paperWhite, peak, curve, gamma, blend] (the HDR sharpening - // shader reads the extra floats from the same uniform block to tone-map to scRGB). int sharpeningCount = hdr ? 7 : 1; Span sharpeningBufferData = stackalloc float[7]; sharpeningBufferData[0] = 1.5f - (Level * 0.01f * 1.5f); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/NisScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/NisScalingFilter.cs index af1b66f91..edfe9b74c 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/NisScalingFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/NisScalingFilter.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common; using Ryujinx.Graphics.GAL; @@ -14,20 +11,12 @@ using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; namespace Ryujinx.Graphics.Vulkan.Effects { - /// - /// NVIDIA Image Scaling (NIS) upscaler. Wraps the MIT-licensed NIS NVScaler compute - /// shader (see Effects/Shaders/NisScaler.h, nis_coef.glsl, NisScaling.glsl). - /// NVScaler is only defined for upscale ratios in [1x, 2x]; outside that the input - /// is simply stretched without the NIS quality benefit. SDR (rgba8) path for now; - /// an scRGB/HDR variant is a follow-up. - /// internal class NisScalingFilter : IScalingFilter { - // NIS shader block dimensions (must match NisScaling.glsl: NIS_BLOCK_WIDTH/HEIGHT). private const int BlockWidth = 32; private const int BlockHeight = 24; - private const int ConfigFloats = 30; // NISConfig: 28 named values + 2 reserved. + private const int ConfigFloats = 30; private readonly VulkanRenderer _renderer; private PipelineHelperShader _pipeline; @@ -37,7 +26,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects private ShaderCollection _scalingProgramPq; private Device _device; - // Slider value (0..100). Mapped to NIS sharpness (0..1) in Run. 50 = neutral. public float Level { get; set; } = 50f; public NisScalingFilter(VulkanRenderer renderer, Device device) @@ -64,7 +52,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects byte[] scalingShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaling.spv"); - // Same layout as the other scaling filters: config UBO (2), input+sampler (1), output image (0/set 3). ResourceLayout scalingResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) @@ -78,7 +65,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects byte[] scalingShaderHdr = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScalingHdr.spv"); - // HDR variant adds the tone-map params UBO at binding 3 and outputs to rgba16f (scRGB). ResourceLayout scalingResourceLayoutHdr = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 3) @@ -89,8 +75,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects new ShaderSource(scalingShaderHdr, ShaderStage.Compute, TargetLanguage.Spirv) ], scalingResourceLayoutHdr); - // PQ variant (same layout as HDR): outputs to rgb10_a2 with an ST.2084 encode, for - // HDR10 swapchains (the DLSS-FG-compatible HDR container). byte[] scalingShaderPq = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScalingPq.spv"); _scalingProgramPq = _renderer.CreateProgramWithMinimalLayout([ @@ -98,10 +82,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects ], scalingResourceLayoutHdr); } - // NIS NVScaler is only defined for upscale ratios in [1x, 2x] (kScale in [0.5, 1.0]). - // Outside that range - notably when the internal resolution is at/above the output, as - // with high-res mods or texture packs - its block tile cache reads out of bounds and - // corrupts the image. Report unsupported so Window.Present falls back to a normal blit. public bool IsResolutionSupported(int srcWidth, int srcHeight, int dstWidth, int dstHeight) { float scaleX = (float)srcWidth / dstWidth; @@ -131,8 +111,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects _pipeline.SetProgram(hdr ? (pqOutput ? _scalingProgramPq : _scalingProgramHdr) : _scalingProgram); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _sampler); - // v1: stretch the full source frame to the full output (swapchain) surface. - // Letterboxing/flip via source/destination extents is a follow-up. uint inW = (uint)view.Width; uint inH = (uint)view.Height; uint outW = (uint)width; @@ -147,7 +125,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); buffer.Holder.SetDataUnchecked(buffer.Offset, cb); - // HDR tone-map params (paperWhite, peak, curve, gamma, blend, whiten), matching NisScalingHdr.glsl. Span hdrData = stackalloc float[] { paperWhite, peak, curve, gamma, blend, whiten }; using ScopedTemporaryBuffer hdrBuffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, hdrData.Length * sizeof(float)); hdrBuffer.Holder.SetDataUnchecked(hdrBuffer.Offset, hdrData); @@ -172,12 +149,10 @@ namespace Ryujinx.Graphics.Vulkan.Effects _pipeline.Finish(); } - // Port of NVScalerUpdateConfig (NIS_Config.h, MIT) for the SDR path. Fills the - // NISConfig constant buffer in the exact field order expected by NisScaling.glsl. private static void BuildConfig(Span cb, float sharpness, uint inW, uint inH, uint outW, uint outH) { sharpness = Math.Clamp(sharpness, 0f, 1f); - float sharpenSlider = sharpness - 0.5f; // map 0..1 to -0.5..+0.5 + float sharpenSlider = sharpness - 0.5f; float maxScale = sharpenSlider >= 0.0f ? 1.25f : 1.75f; float minScale = sharpenSlider >= 0.0f ? 1.25f : 1.0f; @@ -185,7 +160,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects float kDetectRatio = 2f * 1127f / 1024f; - // SDR params (HDR variant handled separately later). float kDetectThres = 64f / 1024f; float kMinContrastRatio = 2.0f; float kMaxContrastRatio = 10.0f; @@ -214,8 +188,8 @@ namespace Ryujinx.Graphics.Vulkan.Effects cb[i++] = kDetectThres; cb[i++] = kMinContrastRatio; cb[i++] = kRatioNorm; - cb[i++] = 1.0f; // kContrastBoost - cb[i++] = 1.0f / 255.0f; // kEps + cb[i++] = 1.0f; + cb[i++] = 1.0f / 255.0f; cb[i++] = kSharpStartY; cb[i++] = kSharpScaleY; cb[i++] = kSharpStrengthMin; @@ -228,19 +202,18 @@ namespace Ryujinx.Graphics.Vulkan.Effects cb[i++] = kDstNormY; cb[i++] = kSrcNormX; cb[i++] = kSrcNormY; - cb[i++] = AsFloat(0); // kInputViewportOriginX - cb[i++] = AsFloat(0); // kInputViewportOriginY - cb[i++] = AsFloat(inW); // kInputViewportWidth - cb[i++] = AsFloat(inH); // kInputViewportHeight - cb[i++] = AsFloat(0); // kOutputViewportOriginX - cb[i++] = AsFloat(0); // kOutputViewportOriginY - cb[i++] = AsFloat(outW); // kOutputViewportWidth - cb[i++] = AsFloat(outH); // kOutputViewportHeight - cb[i++] = 0f; // reserved0 - cb[i++] = 0f; // reserved1 + cb[i++] = AsFloat(0); + cb[i++] = AsFloat(0); + cb[i++] = AsFloat(inW); + cb[i++] = AsFloat(inH); + cb[i++] = AsFloat(0); + cb[i++] = AsFloat(0); + cb[i++] = AsFloat(outW); + cb[i++] = AsFloat(outH); + cb[i++] = 0f; + cb[i++] = 0f; } - // Reinterpret a uint's bits as a float so the UBO's uint members get the right bytes. private static float AsFloat(uint value) => BitConverter.Int32BitsToSingle((int)value); } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/BorderClamp.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/BorderClamp.glsl index 1d7348b27..eb32e30cd 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/BorderClamp.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/BorderClamp.glsl @@ -1,15 +1,5 @@ #version 450 -// [BORDERFIX v2 18/07] Le jeu rendu a resolution scalee laisse un cadre de texels -// PUR NOIR au bord de sa compose finale (mesure aux dumps : 2 px haut+gauche a -// l'echelle x2 ; largeur variable, d'ou la DETECTION). v1 ecrivait EN PLACE dans -// la texture du jeu -> VIOLATION nommee par la validation (image sans -// VK_IMAGE_USAGE_STORAGE_BIT) : ecritures jetees + descripteur invalide par frame -// (la regression visuelle du 18/07). v2 : on LIT la texture du jeu (sampled, -// legal) et on ECRIT une copie corrigee dans NOTRE texture -- c'est elle que -// DLSS recoit. Pleine image : interieur recopie tel quel, bande de bord noire -// remplacee par le premier pixel non-noir vers l'interieur (cap `scan`). Un bord -// noir EPAIS (cinematique) ne trouve rien dans le cap -> recopie intact. layout (local_size_x = 16, local_size_y = 16) in; layout (rgba8, binding = 0, set = 3) uniform writeonly image2D imgDst; @@ -18,12 +8,12 @@ layout (binding = 1, set = 2) uniform sampler2D TexColor; layout (binding = 2) uniform Params { float width; float height; - float scan; // profondeur max de balayage depuis le bord (defaut 8) - float thresh; // seuil "noir" par canal, echelle 0-255 (mesure : 0 exact ; defaut 2) + float scan; + float thresh; }; layout (std430, set = 1, binding = 0) buffer BorderStats { - uint filled; // pixels remplaces depuis le dernier zero 1 Hz (sante au banc) + uint filled; uint pad; }; @@ -44,9 +34,6 @@ void main() bool inBand = p.x < S || p.y < S || p.x >= w - S || p.y >= h - S; if (inBand && max(c.r, max(c.g, c.b)) <= t) { - // Bord le plus proche -> direction de balayage vers l'interieur, profondeur - // de CE pixel depuis ce bord. Coins : l'axe le plus proche gagne (l'autre - // axe sera couvert par le voisin, meme mecanique). int dl = p.x; int dt = p.y; int dr = w - 1 - p.x; @@ -67,7 +54,6 @@ void main() break; } } - // Rien dans le cap : bord noir epais = contenu legitime, recopie intact. } imageStore(imgDst, p, c); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl index 810436b36..65a5f737d 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl @@ -6,8 +6,6 @@ layout( rgb10_a2, binding = 0, set = 3) uniform image2D imgOutput; #elif defined(HDR_OUTPUT) layout( rgba16f, binding = 0, set = 3) uniform image2D imgOutput; #elif defined(LINEAR_OUTPUT) -// [SHARPEN 02/08] Variante nettete post-DLSS : RCAS brut sur float lineaire, AUCUN encodage -// (le blit final garde le tonemap/PQ). Compilee seule vers DlssSharpenLinear.spv. layout( rgba16f, binding = 0, set = 3) uniform image2D imgOutput; #else layout( rgba8, binding = 0, set = 3) uniform image2D imgOutput; @@ -19,7 +17,7 @@ layout( binding = 2 ) uniform invResolution layout( binding = 3 ) uniform outvResolution { vec2 outvResolution_data; -}; +}; layout( binding = 1, set = 2) uniform sampler2D source; layout( binding = 4 ) uniform sharpening { @@ -287,7 +285,7 @@ layout( binding = 4 ) uniform sharpening //------------------------------------------------------------------------------------------------------------------------------ // TODO // ==== -// - Replace transcendentals with manual versions. +// - Replace transcendentals with manual versions. //============================================================================================================================== #ifdef A_GCC A_STATIC AD1 AAbsD1(AD1 a){return __builtin_fabs(a);} @@ -348,7 +346,7 @@ layout( binding = 4 ) uniform sharpening A_STATIC AL1 AMaxL1(AL1 a,AL1 b){return a>b?a:b;} A_STATIC AU1 AMaxU1(AU1 a,AU1 b){return a>b?a:b;} //------------------------------------------------------------------------------------------------------------------------------ - // These follow the convention that A integer types don't have signage, until they are operated on. + // These follow the convention that A integer types don't have signage, until they are operated on. A_STATIC AL1 AMaxSL1(AL1 a,AL1 b){return (ASL1_(a)>ASL1_(b))?a:b;} A_STATIC AU1 AMaxSU1(AU1 a,AU1 b){return (ASU1_(a)>ASU1_(b))?a:b;} //------------------------------------------------------------------------------------------------------------------------------ @@ -607,7 +605,7 @@ layout( binding = 4 ) uniform sharpening #ifndef A_SKIP_EXT #ifdef A_HALF #extension GL_EXT_shader_16bit_storage:require - #extension GL_EXT_shader_explicit_arithmetic_types:require + #extension GL_EXT_shader_explicit_arithmetic_types:require #endif //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_LONG @@ -1128,7 +1126,7 @@ layout( binding = 4 ) uniform sharpening #define AU1_AH1_AF1(a) AU1_AH1_AF1_x(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ AU1 AU1_AH2_AF2_x(AF2 a){return f32tof16(a.x)|(f32tof16(a.y)<<16);} - #define AU1_AH2_AF2(a) AU1_AH2_AF2_x(AF2(a)) + #define AU1_AH2_AF2(a) AU1_AH2_AF2_x(AF2(a)) #define AU1_AB4Unorm_AF4(x) D3DCOLORtoUBYTE4(AF4(x)) //------------------------------------------------------------------------------------------------------------------------------ AF2 AF2_AH2_AU1_x(AU1 x){return AF2(f16tof32(x&0xFFFF),f16tof32(x>>16));} @@ -1645,7 +1643,7 @@ layout( binding = 4 ) uniform sharpening // - V_CVT_F32_UBYTE{0,1,2,3} - Unsigned byte to float. // - V_CVT_PKACC_U8_F32 - Float to unsigned byte (does bit-field insert into 32-bit integer). // V_PERM_B32 does byte packing with ability to zero fill bytes as well. -// - Can pull out byte values from two sources, and zero fill upper 8-bits of packed hi and lo. +// - Can pull out byte values from two sources, and zero fill upper 8-bits of packed hi and lo. //------------------------------------------------------------------------------------------------------------------------------ // BYTE : FLOAT - ABuc{0,1,2,3}{To,From}U1() - Designed for V_CVT_F32_UBYTE* and V_CVT_PKACCUM_U8_F32 ops. // ==== ===== @@ -1740,8 +1738,8 @@ layout( binding = 4 ) uniform sharpening // ENCODING (without zero-based encoding) // ======== // 0 = unused (can be used to mean something else) -// 1 = lowest value -// 128 = exact zero center (zero based encoding +// 1 = lowest value +// 128 = exact zero center (zero based encoding // 255 = highest value //------------------------------------------------------------------------------------------------------------------------------ // Zero-based [Zb] flips the MSB bit of the byte (making 128 "exact zero" actually zero). @@ -1753,8 +1751,8 @@ layout( binding = 4 ) uniform sharpening // 1 : -126/512 // 2 : -125/512 // ... -// 128 : 0 -// ... +// 128 : 0 +// ... // 255 : 127/512 // : 1/4 (just outside the encoding range) //============================================================================================================================== @@ -1975,7 +1973,7 @@ layout( binding = 4 ) uniform sharpening // - Parabolic takes 8 clocks and 8 issue slots (only fract is non-packed). AH1 APSinH1(AH1 x){return x*abs(x)-x;} AH2 APSinH2(AH2 x){return x*abs(x)-x;} // AND,FMA - AH1 APCosH1(AH1 x){x=AFractH1(x*AH1_(0.5)+AH1_(0.75));x=x*AH1_(2.0)-AH1_(1.0);return APSinH1(x);} + AH1 APCosH1(AH1 x){x=AFractH1(x*AH1_(0.5)+AH1_(0.75));x=x*AH1_(2.0)-AH1_(1.0);return APSinH1(x);} AH2 APCosH2(AH2 x){x=AFractH2(x*AH2_(0.5)+AH2_(0.75));x=x*AH2_(2.0)-AH2_(1.0);return APSinH2(x);} // 3x FMA, 2xFRACT, AND AH2 APSinCosH1(AH1 x){AH1 y=AFractH1(x*AH1_(0.5)+AH1_(0.75));y=y*AH1_(2.0)-AH1_(1.0);return APSinH2(AH2(x,y));} #endif @@ -2209,9 +2207,9 @@ layout( binding = 4 ) uniform sharpening return clamp(j.xxx,c*j.yyy,pow(c,j.zzz)*k.xxx+k.yyy);} //------------------------------------------------------------------------------------------------------------------------------ // Note 'rcpX' is '1/x', where the 'x' is what would be used in AFromGamma(). - AF1 AToGammaF1(AF1 c,AF1 rcpX){return pow(c,AF1_(rcpX));} - AF2 AToGammaF2(AF2 c,AF1 rcpX){return pow(c,AF2_(rcpX));} - AF3 AToGammaF3(AF3 c,AF1 rcpX){return pow(c,AF3_(rcpX));} + AF1 AToGammaF1(AF1 c,AF1 rcpX){return pow(c,AF1_(rcpX));} + AF2 AToGammaF2(AF2 c,AF1 rcpX){return pow(c,AF2_(rcpX));} + AF3 AToGammaF3(AF3 c,AF1 rcpX){return pow(c,AF3_(rcpX));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AToPqF1(AF1 x){AF1 p=pow(x,AF1_(0.159302)); return pow((AF1_(0.835938)+AF1_(18.8516)*p)/(AF1_(1.0)+AF1_(18.6875)*p),AF1_(78.8438));} @@ -2245,9 +2243,9 @@ layout( binding = 4 ) uniform sharpening AF3 AFrom709F3(AF3 c){AF3 j=AF3(0.081/4.5,1.0/4.5,1.0/0.45);AF2 k=AF2(1.0/1.099,0.099/1.099); return AZolSelF3(AZolSignedF3(c-j.xxx),c*j.yyy,pow(c*k.xxx+k.yyy,j.zzz));} //------------------------------------------------------------------------------------------------------------------------------ - AF1 AFromGammaF1(AF1 c,AF1 x){return pow(c,AF1_(x));} - AF2 AFromGammaF2(AF2 c,AF1 x){return pow(c,AF2_(x));} - AF3 AFromGammaF3(AF3 c,AF1 x){return pow(c,AF3_(x));} + AF1 AFromGammaF1(AF1 c,AF1 x){return pow(c,AF1_(x));} + AF2 AFromGammaF2(AF2 c,AF1 x){return pow(c,AF2_(x));} + AF3 AFromGammaF3(AF3 c,AF1 x){return pow(c,AF3_(x));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AFromPqF1(AF1 x){AF1 p=pow(x,AF1_(0.0126833)); return pow(ASatF1(p-AF1_(0.835938))/(AF1_(18.8516)-AF1_(18.6875)*p),AF1_(6.27739));} @@ -2349,14 +2347,14 @@ layout( binding = 4 ) uniform sharpening // Details, // LANE TO 8x8 MAPPING // =================== - // 00 01 08 09 10 11 18 19 + // 00 01 08 09 10 11 18 19 // 02 03 0a 0b 12 13 1a 1b // 04 05 0c 0d 14 15 1c 1d - // 06 07 0e 0f 16 17 1e 1f - // 20 21 28 29 30 31 38 39 + // 06 07 0e 0f 16 17 1e 1f + // 20 21 28 29 30 31 38 39 // 22 23 2a 2b 32 33 3a 3b // 24 25 2c 2d 34 35 3c 3d - // 26 27 2e 2f 36 37 3e 3f + // 26 27 2e 2f 36 37 3e 3f AU2 ARmpRed8x8(AU1 a){return AU2(ABfiM(ABfe(a,2u,3u),a,1u),ABfiM(ABfe(a,3u,3u),ABfe(a,1u,2u),2u));} //============================================================================================================================== #ifdef A_HALF @@ -2413,7 +2411,7 @@ layout( binding = 4 ) uniform sharpening // ... // 1023 = 2^(-14)*(1-2^(-10)) = 2^(-14)*(1-1/1024) ... last denormal value // 1024 = 2^(-14) = 1/16384 .......................... first normal value that still maps to integers -// 2047 .............................................. last normal value that still maps to integers +// 2047 .............................................. last normal value that still maps to integers // Scaling limits, // 2^15 = 32768 ...................................... largest power of 2 scaling // Largest pow2 conversion mapping is at *32768, @@ -2693,7 +2691,6 @@ layout( binding = 4 ) uniform sharpening AF4 opARcpF4(outAF4 d,inAF4 a){d=ARcpF4(a);return d;} #endif - #define FSR_RCAS_F 1 AU4 con0; @@ -2737,12 +2734,12 @@ void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {} // ===== // FSR is a collection of algorithms relating to generating a higher resolution image. // This specific header focuses on single-image non-temporal image scaling, and related tools. -// +// // The core functions are EASU and RCAS: // [EASU] Edge Adaptive Spatial Upsampling ....... 1x to 4x area range spatial scaling, clamped adaptive elliptical filter. // [RCAS] Robust Contrast Adaptive Sharpening .... A non-scaling variation on CAS. // RCAS needs to be applied after EASU as a separate pass. -// +// // Optional utility functions are: // [LFGA] Linear Film Grain Applicator ........... Tool to apply film grain after scaling. // [SRTM] Simple Reversible Tone-Mapper .......... Linear HDR {0 to FP16_MAX} to {0 to 1} and back. @@ -2782,7 +2779,7 @@ void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {} // and limits output to the minimum and maximum of that neighborhood. //------------------------------------------------------------------------------------------------------------------------------ // Input image requirements: -// +// // Color needs to be encoded as 3 channel[red, green, blue](e.g.XYZ not supported) // Each channel needs to be in the range[0, 1] // Any color primaries are supported @@ -2792,11 +2789,11 @@ void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {} // There should be no noise in the input that is not at input pixel granularity // For performance purposes, use 32bpp formats //------------------------------------------------------------------------------------------------------------------------------ -// Best to apply EASU at the end of the frame after tonemapping +// Best to apply EASU at the end of the frame after tonemapping // but before film grain or composite of the UI. //------------------------------------------------------------------------------------------------------------------------------ // Example of including this header for D3D HLSL : -// +// // #define A_GPU 1 // #define A_HLSL 1 // #define A_HALF 1 @@ -2805,9 +2802,9 @@ void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {} // #define FSR_RCAS_H 1 // //declare input callbacks // #include "ffx_fsr1.h" -// +// // Example of including this header for Vulkan GLSL : -// +// // #define A_GPU 1 // #define A_GLSL 1 // #define A_HALF 1 @@ -2816,9 +2813,9 @@ void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {} // #define FSR_RCAS_H 1 // //declare input callbacks // #include "ffx_fsr1.h" -// +// // Example of including this header for Vulkan HLSL : -// +// // #define A_GPU 1 // #define A_HLSL 1 // #define A_HLSL_6_2 1 @@ -2829,11 +2826,11 @@ void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {} // #define FSR_RCAS_H 1 // //declare input callbacks // #include "ffx_fsr1.h" -// +// // Example of declaring the required input callbacks for GLSL : // The callbacks need to gather4 for each color channel using the specified texture coordinate 'p'. // EASU uses gather4 to reduce position computation logic and for free Arrays of Structures to Structures of Arrays conversion. -// +// // AH4 FsrEasuRH(AF2 p){return AH4(textureGather(sampler2D(tex,sam),p,0));} // AH4 FsrEasuGH(AF2 p){return AH4(textureGather(sampler2D(tex,sam),p,1));} // AH4 FsrEasuBH(AF2 p){return AH4(textureGather(sampler2D(tex,sam),p,2));} @@ -3313,7 +3310,7 @@ A_STATIC void FsrEasuConOffset( // RCAS is based on the following logic. // RCAS uses a 5 tap filter in a cross pattern (same as CAS), // w n -// w 1 w for taps w m e +// w 1 w for taps w m e // w s // Where 'w' is the negative lobe weight. // output = (w*(n+e+w+s)+m)/(4*w+1) @@ -3332,16 +3329,16 @@ A_STATIC void FsrEasuConOffset( // This is used as a noise detection filter, to reduce the effect of RCAS on grain, and focus on real edges. // // GLSL example for the required callbacks : -// +// // AH4 FsrRcasLoadH(ASW2 p){return AH4(imageLoad(imgSrc,ASU2(p)));} // void FsrRcasInputH(inout AH1 r,inout AH1 g,inout AH1 b) // { // //do any simple input color conversions here or leave empty if none needed // } -// +// // FsrRcasCon need to be called from the CPU or GPU to set up constants. // Including a GPU example here, the 'con' value would be stored out to a constant buffer. -// +// // AU4 con; // FsrRcasCon(con, // 0.0); // The scale is {0.0 := maximum sharpness, to N>0, where N is the number of stops (halving) of the reduction of sharpness}. @@ -3393,7 +3390,7 @@ AF1 sharpness){ AU2 ip, // Integer pixel position in output. AU4 con){ // Constant generated by RcasSetup(). // Algorithm uses minimal 3x3 pixel neighborhood. - // b + // b // d e f // h ASU2 sp=ASU2(ip); @@ -3468,7 +3465,7 @@ AF1 sharpness){ pixR=(lobe*bR+lobe*dR+lobe*hR+lobe*fR+eR)*rcpL; pixG=(lobe*bG+lobe*dG+lobe*hG+lobe*fG+eG)*rcpL; pixB=(lobe*bB+lobe*dB+lobe*hB+lobe*fB+eB)*rcpL; - return;} + return;} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3491,7 +3488,7 @@ AF1 sharpness){ AU2 ip, // Integer pixel position in output. AU4 con){ // Constant generated by RcasSetup(). // Sharpening algorithm uses minimal 3x3 pixel neighborhood. - // b + // b // d e f // h ASW2 sp=ASW2(ip); @@ -3900,42 +3897,29 @@ AF1 sharpness){ cB=ASatH2(nB+AGtZeroH2(dit-rB)*AH2_(1.0/1023.0));} #endif - #ifdef HDR_OUTPUT -// Decode the SDR image to linear light using gamma 2.2 (not the exact sRGB curve). SDR content is -// authored for a ~2.2 display gamma; decoding with the sRGB curve lifts the shadows and raises blacks -// once shown in HDR. Gamma 2.2 keeps blacks properly black on OLED. (Same fix as the plain blit path.) vec3 sdrToLinear(vec3 c, float gamma) { return pow(max(c, vec3(0.0)), vec3(gamma)); } -// First-pass SDR->HDR: scale to paper white + expand ONLY the highlights (blacks stay black). -// scRGB: 1.0 == 80 nits, paperWhite 2.0 ~= 160 nits, peak 8.0 ~= 640 nits. vec3 sdrToHdr(vec3 srgb) { vec3 lin = sdrToLinear(srgb, gamma_data); float paperWhite = paperWhite_data; float peak = peak_data; float curve = curve_data; - // Detect highlights by a blend of luma and max RGB (same change as the plain blit path). Pure max - // over-drove warm/saturated highlights (flashy orange sun, hue drift); the 0.5 blend keeps coloured - // lights rising while pulling warm highlights toward white. Greys unaffected (max == luma). float luma = dot(lin, vec3(0.2126, 0.7152, 0.0722)); float maxc = max(max(lin.r, lin.g), lin.b); float hl = mix(luma, maxc, blend_data); float hlAmount = pow(clamp(hl, 0.0, 1.0), curve); float boost = mix(1.0, peak / paperWhite, hlAmount); vec3 outc = lin * paperWhite * boost; - // Desaturate the most extreme highlights toward white (same change as the plain blit path): - // a blinding light reads as white, not a saturated colour. Luminance-preserving. float t = whiten_data * hlAmount; float lumaOut = dot(outc, vec3(0.2126, 0.7152, 0.0722)); return mix(outc, vec3(lumaOut), t); } #ifdef PQ_OUTPUT -// scRGB linear (Rec.709, 1.0 == 80 nits) -> HDR10 signal (Rec.2020 + ST.2084 PQ). Same math as -// the plain HDR blit (ColorBlitHdrFragmentShaderSource.frag). vec3 scRgbToPq(vec3 scrgb) { const mat3 rec709To2020 = mat3( @@ -3969,8 +3953,8 @@ void CurrFilter(AU2 pos) } void main() { - FsrRcasCon(con0, sharpening_data); - + FsrRcasCon(con0, sharpening_data); + AU2 gxy = ARmp8x8(gl_LocalInvocationID.x) + AU2(gl_WorkGroupID.x << 4u, gl_WorkGroupID.y << 4u); CurrFilter(gxy); gxy.x += 8u; diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionFilter.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionFilter.glsl index 7f1cb5d41..20d3a1c8d 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionFilter.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionFilter.glsl @@ -1,21 +1,16 @@ -// Motion-vector outlier filter: a 3x3 component-wise median over the raw optical-flow field. -// An isolated wrong vector is never the median of its 8 neighbours, so it is rejected while coherent -// motion is preserved. Parameter-free (no tunable threshold). It also re-measures the field -// statistics on the filtered output so the log can show the raw-vs-filtered outlier rate side by -// side -- the A/B proof that the filter actually removes the motion-induced outliers. #version 430 core layout (local_size_x = 16, local_size_y = 16) in; -layout (rg16f, binding = 0, set = 3) uniform writeonly image2D imgOut; // filtered motion -layout (rg16f, binding = 1, set = 3) uniform readonly image2D imgIn; // raw motion +layout (rg16f, binding = 0, set = 3) uniform writeonly image2D imgOut; +layout (rg16f, binding = 1, set = 3) uniform readonly image2D imgIn; layout (binding = 2) uniform params { float width; float height; float maxMotion; - float metrics; // >0.5 = accumulate the filtered-field instrumentation stats (dev only) - float dejitterX; // unused here; kept so the UBO layout matches the motion pass + float metrics; + float dejitterX; float dejitterY; }; @@ -26,14 +21,13 @@ layout (std430, set = 1, binding = 0) buffer SceneChange { uint sumMagFx; uint sumMagSqFx; uint outlierCount; - uint sumMagFxF; // filtered: sum of clamped |mv|, x256 - uint sumMagSqFxF; // filtered: sum of clamped |mv|^2, x1024 - uint outlierCountF; // filtered: sampled pixels with |mv| > 2px + uint sumMagFxF; + uint sumMagSqFxF; + uint outlierCountF; }; float median9(float a[9]) { - // Partial selection sort: only the first five passes are needed to expose the median at [4]. for (int i = 0; i < 5; ++i) { int m = i; @@ -80,8 +74,6 @@ void main() vec2 mv = vec2(median9(xs), median9(ys)); imageStore(imgOut, p, vec4(mv, 0.0, 0.0)); - // Re-measure on the filtered field, same 1/16 grid as the raw pass, so the log shows the outlier - // rate before and after filtering. if (metrics > 0.5 && (p.x & 3) == 0 && (p.y & 3) == 0) { float mag = length(mv); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionVectors.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionVectors.glsl index 3d58453fb..12b31fc8b 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionVectors.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionVectors.glsl @@ -1,7 +1,3 @@ -// Motion-vector estimation (Lucas-Kanade optical flow) between the previous and current frame. -// Produces a per-pixel screen-space motion vector (in render-resolution pixels) for DLSS. -// This is a lightweight stand-in for hardware optical flow: it runs on the graphics/compute -// queue inside the present command buffer, so it needs no dedicated optical-flow queue. #version 430 core layout (local_size_x = 16, local_size_y = 16) in; @@ -13,26 +9,19 @@ layout (binding = 3, set = 2) uniform sampler2D PrevColor; layout (binding = 2) uniform params { float width; float height; - float maxMotion; // clamp range in pixels - float metrics; // >0.5 = accumulate the instrumentation stats (dev only; heavy atomics) - float dejitterX; // (J_n - J_(n-1)): jitter's apparent shift, removed so DLSS gets M_real (0 unless Mode B) + float maxMotion; + float metrics; + float dejitterX; float dejitterY; }; -// Scene-change accumulators, read back by the CPU one frame later to drive a DLSS history reset: -// - changedCount: pixels whose temporal change the optical flow cannot explain (new content). -// - motionCount : pixels in confident, real motion. Lets the CPU tell a menu/UI change over a -// paused scene (mostly static + a changed chunk) from active gameplay (motion everywhere). layout (std430, set = 1, binding = 0) buffer SceneChange { uint changedCount; uint motionCount; - // Phase 1 motion-field statistics (sparse 1/16 grid), read back by the CPU to compute the - // RMS/variance/outlier-rate of the vectors handed to DLSS. Fixed-point so the atomic sums stay - // integer and overflow-safe. - uint statSamples; // sampled pixels - uint sumMagFx; // sum of clamped |mv|, x256 - uint sumMagSqFx; // sum of clamped |mv|^2, x1024 - uint outlierCount; // sampled pixels with |mv| > 2px + uint statSamples; + uint sumMagFx; + uint sumMagSqFx; + uint outlierCount; }; float luma(vec3 c) @@ -58,11 +47,8 @@ void main() ivec2 maxc = ivec2(w - 1, h - 1); - // Lucas-Kanade: accumulate the 2x2 normal-equation system over a small window. - // Stt (sum of squared temporal differences) is also accumulated for the goodness-of-fit - // confidence below. float Sxx = 0.0, Sxy = 0.0, Syy = 0.0, Sxt = 0.0, Syt = 0.0, Stt = 0.0; - const int R = 3; // 7x7 window + const int R = 3; for (int dy = -R; dy <= R; ++dy) { @@ -70,7 +56,6 @@ void main() { ivec2 q = clamp(p + ivec2(dx, dy), ivec2(0), maxc); - // Spatial gradients (central difference) and temporal difference. float ix = 0.5 * (lumaAt(q + ivec2(1, 0), maxc) - lumaAt(q - ivec2(1, 0), maxc)); float iy = 0.5 * (lumaAt(q + ivec2(0, 1), maxc) - lumaAt(q - ivec2(0, 1), maxc)); float it = luma(texelFetch(CurrentColor, q, 0).rgb) - luma(texelFetch(PrevColor, q, 0).rgb); @@ -84,64 +69,37 @@ void main() } } - // Regularized Lucas-Kanade solve. The structure tensor M = [[Sxx,Sxy],[Sxy,Syy]] is PSD - // (det >= 0) but rank-deficient on flat/edge regions; add a tiny trace-relative ridge so the - // solve never divides by ~0 (no branch, no NaN). float trace = Sxx + Syy; float reg = 1e-4 * trace + 1e-8; float Sxxr = Sxx + reg; float Syyr = Syy + reg; float detr = Sxxr * Syyr - Sxy * Sxy; - // Solve (M + reg*I) [u v]^T = -[Sxt Syt]^T (flow from previous to current). float u = (-Sxt * Syyr + Syt * Sxy) / detr; float v = (-Syt * Sxxr + Sxt * Sxy) / detr; - // ---- Optical-flow confidence (CUT 1), in [0,1] ---- - // (a) Conditioning: the smaller eigenvalue of M relative to its trace (Shi-Tomasi). Near 0 - // on flat areas and on aperture-dominated edges (motion unobservable along the edge), - // saturating to 1 on well-textured corners. 0.15 = eigenvalue-ratio knee. float det = Sxx * Syy - Sxy * Sxy; float disc = max(trace * trace - 4.0 * det, 0.0); float lambdaMin = 0.5 * (trace - sqrt(disc)); float wStruct = clamp(lambdaMin / (0.15 * trace + 1e-6), 0.0, 1.0); - // (b) Goodness-of-fit: fraction of the local temporal change explained by this single flow - // vector ( = (Stt - E_min) / Stt ). Collapses at occlusion / transparency / multi-motion. float explained = -(u * Sxt + v * Syt); float wFit = clamp(explained / (Stt + 1e-6), 0.0, 1.0); float conf = wStruct * wFit; - // Scene-change detector: a pixel counts as "cut" when it has a sizeable temporal change - // (Stt above the local spatial energy, i.e. >~1px of apparent change) that the single-flow - // model fails to explain (wFit low). New content appearing in place - a page/menu swap or a - // hard camera cut - lights up most of the screen here; coherent camera motion does not - // (the flow explains it -> wFit high). The CPU turns a large fraction into a DLSS reset. if (wFit < 0.4 && Stt > (Sxx + Syy) + 1e-5) { atomicAdd(changedCount, 1u); } - // DLSS wants the vector pointing to the pixel's previous-frame position (negate the flow). - // Continuous gating: scale by confidence (shrink unreliable motion toward zero), never a - // hard binary cut, so the motion field stays temporally stable. - // Subtract the jitter's apparent shift (J_n - J_(n-1)) so the field handed to DLSS is M_real, not - // M_real + dJ (the spec's fundamental rule). Zero unless Mode B; the sign is calibrated at test time. vec2 mv = (clamp(-vec2(u, v), vec2(-maxMotion), vec2(maxMotion)) - vec2(dejitterX, dejitterY)) * conf; - // Count confident, real motion (> ~1px) so the CPU can recognise a paused scene. if (conf > 0.5 && dot(mv, mv) > 1.0) { atomicAdd(motionCount, 1u); } - // ---- Phase 1 instrumentation: motion-field statistics ---- - // Characterise the optical-flow noise floor. On a static (held-still) scene the true motion is 0, - // so |mv| here is pure noise; its RMS is the number that decides whether temporal upscaling - // (Mode B) is viable (target < 0.5px). Sampled on a 1/16 grid, accumulated in fixed point to keep - // the atomics cheap and overflow-safe; the inlier magnitude is clamped so the squared sum cannot - // overflow uint32, and outliers (>2px) are counted apart. This measures the exact mv fed to DLSS. if (metrics > 0.5 && (p.x & 3) == 0 && (p.y & 3) == 0) { float mag = length(mv); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppCutoutMask.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppCutoutMask.glsl index b4bf9cf52..1568ac5dc 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppCutoutMask.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppCutoutMask.glsl @@ -1,7 +1,3 @@ -// [MVPP SURVIVE PROBE -- cutout bias mask] Converts the fragment survivor SSBO (one u32 per host -// pixel, fixed row stride 4096, nonzero = a cutout/KIL-surviving fragment covered this pixel in -// the scene pass) into an R8Unorm mask (1.0 = bias DLSS toward the current frame here). -// Dispatched once per injected frame, over the DLSS input (scene) dimensions. #version 430 core layout (local_size_x = 16, local_size_y = 16) in; @@ -26,7 +22,6 @@ void main() return; } - // Row stride is the fixed survivor-map stride (4096), NOT the image width. uint v = survivors[p.y * 4096 + p.x]; imageStore(imgMask, p, vec4(v != 0u ? 1.0 : 0.0, 0.0, 0.0, 0.0)); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.glsl index f9fb71799..f6d87207f 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.glsl @@ -1,224 +1,102 @@ -// MV++ Phase 1b: camera-reprojection motion vectors. -// -// For each pixel: reconstruct its world position from the depth buffer and the inverse of the -// CURRENT frame's view-projection, reproject it with the PREVIOUS frame's view-projection, and -// output the screen-space delta in render-resolution pixels - an EXACT motion vector for -// everything static in the world (the optical flow's guesswork remains only for objects that -// actually move). Matrices are uploaded pre-transposed on the CPU so the standard M * v works. -// -// DIAGNOSTIC (diagMode > 0.5): also read the optical-flow motion texture and accumulate -// statistics into the SSBO. The flow's magnitude is NOT a usable baseline in motion (its -// confidence gating shrinks it toward zero exactly when the camera moves - measured mean -// 0.2-0.7 px during pans vs tens of px real), but the gating is a positive multiply that -// never flips direction: the flow's SIGN is the invariant. So we vote, per axis, on sign -// agreement wherever both fields are non-negligible; a clear majority names the convention. #version 430 core -// [SUBGROUP 18/07] Réduction par sous-groupes pour les stats (chrono B : la grêle d'atomiques -// par pixel coûtait 33 ms/frame — ~360k statPixels × ~10 atomicAdd sur les mêmes adresses). -// Les ops subgroup sont LÉGALES dans le flow divergent (pas de barrier — le shader a des -// early-returns partout, une réduction shared-memory serait illégale ici). #extension GL_KHR_shader_subgroup_basic : require #extension GL_KHR_shader_subgroup_arithmetic : require #extension GL_KHR_shader_subgroup_vote : require layout (local_size_x = 16, local_size_y = 16) in; layout (rg16f, binding = 0, set = 3) uniform image2D imgReprojMv; -layout (r32f, binding = 1, set = 3) uniform image2D imgDepthSnap; // this frame's depth, resampled - // to the input grid, for the NEXT - // frame's dynamic mask (the copy- - // image path silently failed; the - // pass snapshots it itself, double- - // buffered against read/write races) -layout (r8, binding = 2, set = 3) uniform image2D imgBiasMask; // DLSS bias-current-color hint: - // 1 where our MVs cannot describe - // the pixel's motion (animated - // object, disocclusion) -> DLSS - // favors the current frame there - // instead of ghosting its history -layout (rg16f, binding = 3, set = 3) uniform image2D imgRawMv; // PRE-deadband MVs for frame - // generation: the deadband answers - // a DLSS-SR need (the game's TAA - // already stabilized the image); - // interpolation must see the REAL - // inter-frame motion or it holds a - // sliding image still (settle slide) +layout (r32f, binding = 1, set = 3) uniform image2D imgDepthSnap; +layout (r8, binding = 2, set = 3) uniform image2D imgBiasMask; +layout (rg16f, binding = 3, set = 3) uniform image2D imgRawMv; layout (binding = 1, set = 2) uniform sampler2D DepthTex; -layout (binding = 3, set = 2) uniform sampler2D FlowMvTex; // optical-flow MV (diag comparison) -layout (binding = 4, set = 2) uniform sampler2D PrevDepthTex; // previous frame's scene depth -layout (binding = 5, set = 2) uniform sampler2D CovTex; // J3: VEL coverage -- per-object MV - // as an NDC delta (clip_N - clip_(N-1), - // camera factored out) written by the - // variant FS with the ring N-1 palette +layout (binding = 3, set = 2) uniform sampler2D FlowMvTex; +layout (binding = 4, set = 2) uniform sampler2D PrevDepthTex; +layout (binding = 5, set = 2) uniform sampler2D CovTex; layout (binding = 2) uniform params { - mat4 invVpCurr; // inverse of current frame's view-projection (clip -> world) - mat4 vpPrev; // previous frame's view-projection (world -> clip) - float width; // render resolution + mat4 invVpCurr; + mat4 vpPrev; + float width; float height; - float maxMotion; // clamp, in pixels (same as the flow pass) - float diagMode; // > 0.5: accumulate agreement stats vs the flow MV - float flipY; // > 0.5: assume the flipped NDC y orientation (see below) - float zMinusOne; // > 0.5: window depth stores (ndcZ + 1) / 2 (GL-style viewport) - float zeroMv; // > 0.5: compute everything but WRITE zero MVs (isolation test: - // still trembles = the pass itself disturbs the game, not the values) - float deadLo; // |mv| below this reads as zero (the game's TAA flattened it) - float deadHi; // full MV passthrough above this; smooth ramp in between - float dynMask; // > 0.5: zero the MV where the depth disagrees with the previous - // frame's reprojected depth = the pixel moved BY ITSELF (dynamic - // object) or was just disoccluded; camera MVs are wrong for both. - float covCompose; // > 0.5: add the per-object delta (CovTex) to the camera MV (J3). - // Set ONLY on frames whose coverage received grafts (freshness - // gate on the CPU side): a stale coverage under a moving camera - // would put object MVs on the wrong pixels. - float farRotStart; // > 0: distance (meters) where the reprojection starts blending to - // ROTATION-ONLY (full at 2x this). Far depth-buffer quanta span huge - // world slabs at grazing angles -> banded reconstructed positions -> - // banded MVs (the black-lines exhibit); rotation is depth-immune, and - // translation is negligible at distance -- reproject the RAY (point at - // infinity, homogeneous w=0) instead of the quantized point. 0 = off. - float edgeMode; // Leading-edge disocclusion policy (E5, the black-lines exhibit -- - // user nailed the geometry: lines on UPWARD sweeps = the strip of - // newly revealed content whose history reprojects OFF-SCREEN). - // 0 = zero MV (historic behavior: traded the old "dark band" for - // the current fine lines), 1 = CLAMP prevUv to the border - // (standard TAA: nearest valid history), 2 = raw off-screen MV - // (DLSS validates history itself; the pre-M runs saw a dark - // band -- retest under preset M). - float depthActiveX; // E8: fraction of the depth texture that is the ACTIVE sub-rect the - float depthActiveY; // game rendered THIS frame (top-left anchored; dyn-res). The margins - // hold STALE depth of previous sizes -- sampling them painted - // rectangle outlines (the "nested squares" black lines; survived - // E1-E7 which all fixed VALUES, not the sampling DOMAIN). 1.0 = full. - float skyRot; // SKYROT (> 0.5): far-plane/sky pixels (d >= 1.0) get rotation-only MV - // instead of zero, WITHOUT FARROT's distance blend on real geometry. - float skyFlow; // SKYFLOW (> 0.5): far-plane/sky pixels take the optical-flow sample - // where it is sane -- the sky's CONTENT drifts on its own (cloud sea - // 4-10 px/min, night skybox rotation; wander tiles 09/07) and rotation - // only describes the camera. Fallback stays the skyRot/zero path. - float devStats; // (> 0.5): arm the sparse stat grid OUTSIDE diagnostic mode (dev gauge, - // RYUJINX_MVPP_DEV) -- 17/07: the SKYDRIFT probe needs stats in mode 2, - // where statPixel was silently dead. OFF in release = byte-identical. - float driftX; // SKYDRIFT (v2): accumulated sky CONTENT drift (px/frame, signed) -- the - float driftY; // CPU's 1 Hz read of the sonde sums, EMA-smoothed, fed back as params. - // 0 when unarmed: the addition below is a no-op, path byte-identical. - float skyDepth; // [SKYDEPTH 27/07] Window depth at or above which a pixel counts as sky. - // 1.0 = the historic behaviour (the far plane and nothing else). - // - // WHY IT IS A PARAMETER NOW. The sky estimate only ever looked at d >= 1.0, - // i.e. the dome itself. Distant clouds are GEOMETRY drawn in front of it - - // their depth is 0.999-something - so they were counted by nobody: excluded - // from the sky drift estimate, and handed to a reprojection that only knows - // how to describe CAMERA motion, never an object's own drift. Measured on - // XC2: sky = 1.5 M texels while zones = 2/32, the contradiction that gave - // this away. Alex had already narrowed it by eye - "it is the far clouds, - // not the ones flying close". + float maxMotion; + float diagMode; + float flipY; + float zMinusOne; + float zeroMv; + float deadLo; + float deadHi; + float dynMask; + float covCompose; + float farRotStart; + float edgeMode; + float depthActiveX; + float depthActiveY; + float skyRot; + float skyFlow; + float devStats; + float driftX; + float driftY; + float skyDepth; - // [INTERVALFIX 31/07] AJOUTE EN FIN DE BLOC (index 53) -- aucun index existant ne bouge. - // Combien d'intervalles la paire (curr, prev) couvre REELLEMENT, sous forme de facteur : - // 1.0 en regime normal, 0.5 a l'image de rattrapage d'un figement de camera (la paire y - // couvre DEUX presents ; mesure du 31/07 sur dumps : x1,94 a x3,50 le mouvement reel). - // Applique a la part CAMERA seulement -- jamais a la derive du ciel, deja exprimee en px - // par image. Correctif eteint cote CPU => vaut 1.0 en permanence, sortie inchangee. float intervalScale; }; layout (std430, set = 1, binding = 0) buffer ReprojStats { - uint samples; // sampled pixels (sparse 1/16 grid, skips sky/invalid depth) - uint skyCount; // pixels skipped for far-plane/invalid depth - uint voteXSame; // sign(reproj.x) == sign(flow.x), where both are non-negligible + uint samples; + uint skyCount; + uint voteXSame; uint voteXOpp; uint voteYSame; uint voteYOpp; - uint sumFlowXFx; // sum |flow.x|, x16 fixed point (motion-magnitude witness) - uint sumFlowYFx; // sum |flow.y|, x16 - uint sumReprojXFx; // sum |reproj.x|, x16 (scale-error witness vs flow) - uint sumReprojYFx; // sum |reproj.y|, x16 - uint dbgCenterD; // center pixel, raw window depth (floatBitsToUint) - uint dbgCenterZ; // center pixel, NDC z fed to invVP - uint dbgCenterW; // center pixel, homogeneous w of invVP*ndc (~ +-1/viewZ) - uint dbgCenterY; // center pixel, reconstructed world-space height - uint dbgCenterMvX; // center pixel, COMPUTED mv.x in pixels (before any zeroMv override) - uint dbgCenterMvY; // center pixel, COMPUTED mv.y in pixels - uint maskSampled; // dynamic-mask health: pixels tested (sparse 1/16 grid) - uint maskMasked; // of which zeroed by the depth-disagreement mask - uint dbgCenterPrevD; // center pixel, PREV depth as sampled by the mask (proof of plumbing) + uint sumFlowXFx; + uint sumFlowYFx; + uint sumReprojXFx; + uint sumReprojYFx; + uint dbgCenterD; + uint dbgCenterZ; + uint dbgCenterW; + uint dbgCenterY; + uint dbgCenterMvX; + uint dbgCenterMvY; + uint maskSampled; + uint maskMasked; + uint dbgCenterPrevD; uint dbgPad; - uint skySumXFx; // SKYROT sonde (read-only): sum |sky mv.x| WRITTEN to imgReprojMv (fed to DLSS), x16, sparse - uint skySumYFx; // SKYROT sonde (read-only): sum |sky mv.y| WRITTEN to imgReprojMv (fed to DLSS), x16, sparse - int skyFlowSumXFx; // SKYDRIFT sonde (read-only): sum of the SIGNED raw flow.x on sky statPixels, x16. - int skyFlowSumYFx; // Signed on purpose: the cloud drift is coherent so it survives the mean, - // while the estimator's noise is symmetric and cancels out. - uint skyD1Count; // debug 17/07: sky statPixels with d >= 1.0 (the drift/SKYROT gate side). - // skyCount - skyD1Count = the d <= 0.0 population. Locates the dome. - uint skyDbgDriftY; // debug 17/07: floatBitsToUint(driftY) as SEEN by the shader (param echo, - // written by the (0,0) thread) -- proves the UBO plumbing end-to-end. - uint skyNbDiff2Fx; // SKYCOH sonde 18/07 (read-only): sum |flow(p) - flow(p + 2px)| on sky - // statPixels, x16. Neighbour-to-neighbour flow noise = the source of the - // SKYFLOW salt-and-pepper (per-pixel accept with a single tap). - uint skyNbDiff16Fx; // Same at 16 px. d2 vs d16 = the noise's coherence length: d2~d16 = white - // noise (vote/smooth wins), d2<=1.0) only. The read - // that decides SKYFLOW v2: zones with genuinely DIFFERENT drifts -> - // per-zone EMA grid justified; all alike -> global SKYDRIFT suffices. + uint skySumXFx; + uint skySumYFx; + int skyFlowSumXFx; + int skyFlowSumYFx; + uint skyD1Count; + uint skyDbgDriftY; + uint skyNbDiff2Fx; + uint skyNbDiff16Fx; + int zoneFlowXFx[32]; + int zoneFlowYFx[32]; uint zoneCount[32]; - uint zoneDriftX[32]; // SKYGRID v2 (CPU -> shader): per-zone drift RESIDUALS (vs the global - uint zoneDriftY[32]; // driftX/Y), EMA px/frame as floatBitsToUint, written by the CPU before - // each dispatch. All-zero when the grid is unarmed (deterministic zero - // at buffer creation) -> the bilinear addition below is a no-op. - uint dynFlowXFx; // DYNSONDE etape 2 (read-only): sum |flow.x| on dynamic band pixels, - uint dynFlowYFx; // x16 -- does the flow respond AT ALL on self-moving objects (the - // known trap: confidence gating shrinks it on fast movers). - uint dynDiffXFx; // Sum |flow - camera mv| on the same pixels, x16 -- the size of the - uint dynDiffYFx; // signal a flow fusion would inject on these pixels. - uint dynFlowAlive; // Dynamic band pixels whose |flow| clears deadLo (flow "alive" count). + uint zoneDriftX[32]; + uint zoneDriftY[32]; + uint dynFlowXFx; + uint dynFlowYFx; + uint dynDiffXFx; + uint dynDiffYFx; + uint dynFlowAlive; uint dynPad; - uint dynZoneCount[32]; // WHERE the dynamic pixels live: counts on the same 8x4 grid as the - // sky zones (band pixels only; population shape, not exact blobs -- - // the per-pixel map is the dynmask dump). - uint depthFarN; // [DEPTHSONDE 19/07] statPixels (normal path) claiming quasi-far - // depth (0.98 <= d < 1.0). Slots 226-229 = the dead padding between - // this struct (ends 225) and the BorderClamp slot (256): no resize. - uint depthFarResFx; // sum min(|point-mv - rot-only-mv|, 64 px) x4 on those pixels. A - // pixel claiming far CANNOT carry screen translation: big residual - // here = the depth dialect lies (the BOTW whole-decor ghost). - uint depthMidN; // Control population, mid-range depth (0.50 <= d < 0.90): - uint depthMidResFx; // translation legitimately lives here -- proves the probe measures. - uint depthGridN; // [DEPTHSONDE v2] ALL statPixels, counted at the grid definition - // BEFORE any early return: the true denominator. Accounting closes - // exactly: gridN = skyCount + samples + degenN + behindN. - uint depthDegenN; // statPixels leaving via the E7 "point at infinity" path: with - // farRot=0 they write MV=0 -> frozen decor in a pan = ghost. The - // population the v1 probe structurally missed (BOTW suspect #1). - uint depthBehindN; // statPixels leaving via the E7 "behind previous camera" path. + uint dynZoneCount[32]; + uint depthFarN; + uint depthFarResFx; + uint depthMidN; + uint depthMidResFx; + uint depthGridN; + uint depthDegenN; + uint depthBehindN; - // [CHECKER 28/07] Sonde de DISCONTINUITÉ DE CHEMIN entre pixels VOISINS. Lecture seule : - // n'écrit aucun MV, ne modifie aucune branche. Slots 233-236 (réserve libre 233..255). - // - // POURQUOI. Le choix du chemin de reprojection est fait PAR PIXEL sur un seuil de depth - // (d <= 0 || d >= skyDepth -> rotation seule ; sinon reconstruction géométrique). Si le - // ciel/la brume lointaine sont rendus en DEMI-RÉSOLUTION ALTERNÉE (damier), un pixel sur - // deux du MÊME nuage tombe de l'autre côté du seuil et reçoit un vecteur d'une autre - // famille -- et comme le damier alterne d'une image à l'autre, un même point bascille - // entre les deux chemins. Symptôme d'Alex 28/07 : « d'autres nuages apparaissent et - // disparaissent », au loin, seulement en bougeant la caméra. - // - // COMMENT LIRE. chkSplit/chkN = discordance quelconque (inclut les silhouettes normales, - // donc NON concluant seul). chkDiag/chkN = signature DAMIER STRICTE : les deux voisins - // orthogonaux prennent l'autre chemin ET le voisin DIAGONAL reprend le même. Un bord - // d'objet ordinaire ne peut PAS produire ça (une silhouette est une courbe, pas un - // échiquier). C'est chkDiag qui tranche, pas chkSplit. - uint chkN; // dénominateur : statPixels de la bande lointaine (d >= 0.98 ou ciel) - uint chkSplit; // dont au moins un voisin orthogonal a pris l'AUTRE chemin - uint chkDiag; // dont les DEUX voisins orthogonaux diffèrent ET le diagonal concorde - uint chkSky; // dont le pixel courant est parti par le chemin ciel (répartition) + uint chkN; + uint chkSplit; + uint chkDiag; + uint chkSky; }; -// [SKYGRID v2 18/07] Bilinear interpolation of the per-zone drift residuals over the 8x4 -// grid (zone centers). Smooth BY CONSTRUCTION: no per-pixel accept/reject anywhere -- the -// measured fix for SKYFLOW v1's salt-and-pepper (SKYCOH probe: block-correlated flow noise -// at 3-6x the drift signal). All-zero residuals (grid unarmed) = exact no-op. vec2 ZoneDrift(int zx, int zy) { int zi = zy * 8 + zx; @@ -240,11 +118,6 @@ vec2 ZoneDriftBilinear(ivec2 pp, int w, int h) return mix(a, b, ty); } -// Rotation-only reprojection of the pixel's RAY (point at infinity, homogeneous w = 0): -// exact for anything at/near the far plane, immune to depth quantization and to the -// degenerate reconstructions that plague the far regime. Returns a clamped MV in pixels, -// or zero when even the ray is unusable. Used by every far/degenerate path (E6/E7): the -// historic zero answers painted frozen-MV stripes on moving distant ground = black lines. vec2 RotOnlyMv(vec2 uvIn, int w, int h) { float ndcYr = flipY > 0.5 ? (1.0 - 2.0 * uvIn.y) : (uvIn.y * 2.0 - 1.0); @@ -274,10 +147,6 @@ vec2 RotOnlyMv(vec2 uvIn, int w, int h) vec2 pn = pc.xy / pc.w; float puy = flipY > 0.5 ? (0.5 - 0.5 * pn.y) : (pn.y * 0.5 + 0.5); - // [INTERVALFIX 31/07] Normalisation temporelle : ce vecteur decrit l'ecart entre vpPrev et - // vpCurr, qui vaut DEUX presents a l'image de rattrapage d'un figement. intervalScale vaut - // 1.0 partout ailleurs. Applique ici pour couvrir d'un seul endroit les deux consommateurs - // de RotOnlyMv : le chemin ciel (SKYROT) et le melange de distance (FARROT). return intervalScale * clamp((vec2(pn.x * 0.5 + 0.5, puy) - uvIn) * vec2(w, h), vec2(-maxMotion), vec2(maxMotion)); } @@ -293,22 +162,10 @@ void main() } vec2 uv = (vec2(p) + 0.5) / vec2(w, h); - // Depth is read by normalized UV, not texelFetch: with in-game dynamic resolution (TOTK) - // the depth buffer is SMALLER than the presented color, but covers the same frustum. - // E8: the frustum maps onto the ACTIVE sub-rect only -- the allocation's margins hold - // stale depth of previous dyn-res sizes (the rectangle-outline black lines). float d = textureLod(DepthTex, uv * vec2(depthActiveX, depthActiveY), 0.0).r; - // [SUBGROUP 18/07] Échantillonnage en BANDES DENSES (2 rangées pleines toutes les 32) au - // lieu de la grille éparse %4/%4 : MÊME densité (1/16), mais 32 voisins contigus tombent - // dans le même subgroup -> leurs contributions fusionnent en une écriture au lieu de 32. - // La grille éparse ne donnait que ~4 lanes actives par subgroup = gain x4 ; les bandes - // donnent le plein x32. Les moyennes/ratios consommateurs sont insensibles au motif. bool statPixel = (diagMode > 0.5 || devStats > 0.5) && (p.y % 32) < 2; - // [DEPTHSONDE v2 19/07] le vrai dénominateur, compté AVANT toute sortie anticipée -- - // la v1 ne voyait que les survivants du chemin normal (BOTW : farRes sain à 0,25 px - // pendant que le décor ghoste => la pathologie vit dans les sorties E7, jamais comptées). if (statPixel) { uint rG = subgroupAdd(1u); @@ -318,37 +175,24 @@ void main() } } - // Écho du paramètre drift (debug 17/07) : un seul thread écrit la valeur telle que le - // shader la reçoit -- tranche « le param n'arrive pas » vs « la branche ne s'applique pas ». if (p == ivec2(0)) { skyDbgDriftY = floatBitsToUint(driftY); } - // Center-pixel debug: the depth -> world reconstruction has never been validated by any - // of the previous wins (rotation MVs and the static case are depth-immune). Raw window - // depth is stored before the sky test so it is visible even when invalid. bool centerPixel = p.x == w / 2 && p.y == h / 2; if (centerPixel) { dbgCenterD = floatBitsToUint(d); } - // Snapshot the raw window depth on the input grid for next frame's dynamic mask -- - // written for EVERY pixel (sky included) before any early-out below. imageStore(imgDepthSnap, p, vec4(d, 0.0, 0.0, 0.0)); - // [CHECKER 28/07] Sonde de discontinuité de chemin (lecture seule). Placée AVANT l'early-out - // ci-dessous : les pixels ciel sortent par un return, il faut les compter tant qu'ils sont là. - // Restreinte à la BANDE LOINTAINE (le défaut est au loin, et compter tout l'écran noierait - // le signal sous les silhouettes des objets proches, qui sont des discontinuités légitimes). if (statPixel) { bool skyHere = d <= 0.0 || d >= skyDepth; if (skyHere || d >= 0.98) { - // Voisins échantillonnés dans le MÊME dialecte que le pixel courant (UV normalisées - // × sous-rect actif) : un texelFetch ici lirait un autre domaine et mentirait. ivec2 lim = ivec2(w - 1, h - 1); vec2 uvR = (vec2(min(p + ivec2(1, 0), lim)) + 0.5) / vec2(w, h); vec2 uvD = (vec2(min(p + ivec2(0, 1), lim)) + 0.5) / vec2(w, h); @@ -363,7 +207,6 @@ void main() bool sX = dX <= 0.0 || dX >= skyDepth; bool split = (skyHere != sR) || (skyHere != sD); - // Signature damier stricte : orthogonaux opposés, diagonal concordant. bool diag = (skyHere != sR) && (skyHere != sD) && (skyHere == sX); uint rN = subgroupAdd(1u); @@ -380,29 +223,11 @@ void main() } } - // Far plane / cleared depth carries no geometry to reconstruct -- but PROOF (E4 log, - // center d=0 while aimed at the WORLD) shows distant TOTK ground lives at the reversed-Z - // far sentinel: rows quantize across the exact-0 boundary, and the historic "emit zero" - // answer painted stripes of frozen MV on moving ground = THE BLACK LINES (survived five - // other fixes because none touched this early-out). Rotation-only reprojection is EXACT - // at infinity (sky and infinite ground alike), so when the E4 machinery is armed - // (farRotStart > 0) route sentinel pixels through it instead of zero. if (d <= 0.0 || d >= skyDepth) { - // SKYROT: the far-plane/SKY case (d >= 1.0) gets rotation-only MV so the camera's - // rotation of the sky is described instead of a frozen zero (ghost sky/clouds). This - // applies to d >= 1.0 ONLY (not the d <= 0.0 near-degenerate case), and does NOT touch - // the distance-blend path further down (that one is FARROT's, gated on farRotStart and - // acting on real geometry). Distant foliage at d~0.98 never enters here -> its reproj - // MV is untouched, so the sky-ghost fix and the foliage-shimmer verdict stay separable. bool useRot = farRotStart > 0.0 || (skyRot > 0.5 && d >= skyDepth); vec2 mvFar = useRot ? RotOnlyMv(uv, w, h) : vec2(0.0); - // SKYFLOW: on the sky sentinel only (d >= 1.0, same guard as SKYROT), replace the - // rotation MV with the flow sample when it is credible: above the dead-zone floor - // (below it the flow is estimator noise on soft gradients -- keep the rotation) and - // under the same maxMotion clamp as everywhere else. The flow sees rotation AND - // content drift composited, which is exactly what the clouds need. bool flowTaken = false; if (skyFlow > 0.5 && d >= skyDepth) { @@ -418,29 +243,16 @@ void main() imageStore(imgRawMv, p, vec4(mvFar, 0.0, 0.0)); mvFar *= smoothstep(deadLo, deadHi, length(mvFar)); - // SKYDRIFT (v2): the accumulated content drift rides ON TOP of the (deadbanded) - // camera MV, deliberately EXEMPT from the deadband -- its whole purpose is feeding - // DLSS the tiny coherent motion (~0.01-0.03 px/frame, sonde 17/07) that the deadband - // suppresses as noise. Not added when the flow sample was taken directly: the flow - // already contains the drift (rotation + content composited). if (!flowTaken && d >= skyDepth) { - // Global drift (SKYDRIFT) + per-zone residual (SKYGRID v2, bilinear = smooth). mvFar += vec2(driftX, driftY) + ZoneDriftBilinear(p, w, h); } imageStore(imgReprojMv, p, vec4(mvFar, 0.0, 0.0)); - imageStore(imgBiasMask, p, vec4(0.0)); // far plane: history is fine, no bias + imageStore(imgBiasMask, p, vec4(0.0)); if (statPixel) { - // [SUBGROUP 18/07] Toutes les sondes du ciel réduites par sous-groupe : une seule - // rafale d'atomiques par subgroup actif au lieu d'une par pixel (mesuré chrono B : - // 33 ms -> cible <2). Les compteurs conditionnels (d>=1.0) sont hissés en ternaires - // pour tout réduire en un passage. Sémantique des sommes inchangée à l'unité près. bool d1 = d >= skyDepth; - // SKYROT sonde (read-only, does NOT change MV): sky MV WRITTEN to imgReprojMv - // (post-deadband = fed to DLSS), x1024 cap 4 px. SKYDRIFT sonde: signed raw flow. - // SKYCOH sonde: neighbour taps (clamped) for the flow field's coherence length. vec2 flowProbe = texelFetch(FlowMvTex, p, 0).rg; ivec2 sz = textureSize(FlowMvTex, 0); vec2 flowN2 = texelFetch(FlowMvTex, min(p + ivec2(2, 0), sz - 1), 0).rg; @@ -467,9 +279,6 @@ void main() atomicAdd(skyNbDiff16Fx, rNb16); } - // SKYGRID sonde: per-zone signed sums, dome only. Les 32 lanes d'un subgroup (pixels - // contigus) visent presque toujours la MÊME zone (~400 px de large) -> fusion quand - // la zone est uniforme, atomiques directes sinon (frontières de zones, rares). if (d1) { int zx = clamp(int(8.0 * float(p.x) / w), 0, 7); @@ -500,27 +309,13 @@ void main() return; } - // Reconstruct: NDC -> world (homogeneous). NVN/Vulkan clip z is [0,1]; x/y in [-1,1]. - // Whatever the game's y orientation is, it cancels: we go THROUGH world space and back - // with the same convention on both legs. - // The NDC y orientation versus texture space is the one genuinely ambiguous convention in - // this chain (x has no flip in any convention). flipY selects the alternative mapping; it - // must apply to BOTH legs (reconstruction and reprojection) since it is one convention - // choice, not an output sign. Settled empirically with DLSS as the judge (mode 2). float ndcY = flipY > 0.5 ? (1.0 - 2.0 * uv.y) : (uv.y * 2.0 - 1.0); - // Window depth -> the NDC z the game's matrices actually produce. GL-style viewports - // (MinusOneToOne) store (z+1)/2 in the buffer and must be undone here. Rotation-induced - // motion vectors are immune to a wrong mapping (rays map to rays); translation-induced - // ones are ruined by it - the "pans clean, camera translation wobbles" signature. float zNdc = zMinusOne > 0.5 ? (d * 2.0 - 1.0) : d; vec4 ndc = vec4(uv.x * 2.0 - 1.0, ndcY, zNdc, 1.0); vec4 worldH = invVpCurr * ndc; if (abs(worldH.w) < 1e-9) { - // Degenerate reconstruction = the point sits at infinity: exactly the case the - // rotation-only ray handles (E7 -- near-sentinel rows land here after passing the - // far-plane test by one depth quantum; the historic zero striped them). vec2 mvInf = farRotStart > 0.0 ? RotOnlyMv(uv, w, h) : vec2(0.0); imageStore(imgRawMv, p, vec4(mvInf, 0.0, 0.0)); @@ -528,7 +323,6 @@ void main() imageStore(imgReprojMv, p, vec4(mvInf, 0.0, 0.0)); imageStore(imgBiasMask, p, vec4(0.0)); - // [DEPTHSONDE v2] population fantôme n°1 : reconstruction au point à l'infini. if (statPixel) { uint rDg = subgroupAdd(1u); @@ -548,13 +342,9 @@ void main() dbgCenterY = floatBitsToUint(world.y); } - // Reproject into the previous frame's clip space. vec4 prevClip = vpPrev * world; if (prevClip.w <= 1e-6) { - // "Behind the previous camera" is a numerical mirage in the far regime (a genuine - // behind-the-camera surface cannot be on screen two frames apart at 60 fps): the - // huge reconstructed w flips prevClip.w's sign by precision. Same E7 treatment. vec2 mvBk = farRotStart > 0.0 ? RotOnlyMv(uv, w, h) : vec2(0.0); imageStore(imgRawMv, p, vec4(mvBk, 0.0, 0.0)); @@ -562,7 +352,6 @@ void main() imageStore(imgReprojMv, p, vec4(mvBk, 0.0, 0.0)); imageStore(imgBiasMask, p, vec4(1.0)); - // [DEPTHSONDE v2] population fantôme n°2 : « derrière la caméra précédente ». if (statPixel) { uint rBk = subgroupAdd(1u); @@ -577,68 +366,52 @@ void main() float prevUvY = flipY > 0.5 ? (0.5 - 0.5 * prevNdc.y) : (prevNdc.y * 0.5 + 0.5); vec2 prevUv = vec2(prevNdc.x * 0.5 + 0.5, prevUvY); - // Reprojected off-screen = the previous frame holds no history for this pixel - // [EDGE3] Drapeau porte jusqu'a l'ecriture du masque, tout en bas : l'ecriture finale - // (dynPixel) ecraserait un imageStore fait ici. bool edgeBias = false; - // (disocclusion at the screen edge). Policy is selectable (E5): the historic zero-MV - // answer paints its own artifact -- a strip of "nothing moves" on newly revealed - // textured content each frame of an upward sweep = the fine black lines (user-nailed - // geometry). Clamp = standard TAA nearest-valid-history; raw = let DLSS validate. + float edgeOutAmt = 0.0; + if (prevUv.x < 0.0 || prevUv.x > 1.0 || prevUv.y < 0.0 || prevUv.y > 1.0) { + edgeOutAmt = max(max(max(-prevUv.x, prevUv.x - 1.0), max(-prevUv.y, prevUv.y - 1.0)), 0.0); + if (edgeMode < 0.5) { imageStore(imgReprojMv, p, vec4(0.0)); imageStore(imgRawMv, p, vec4(0.0)); - imageStore(imgBiasMask, p, vec4(1.0)); // screen-edge disocclusion: no history there + imageStore(imgBiasMask, p, vec4(1.0)); return; } else if (edgeMode < 1.5) { prevUv = clamp(prevUv, vec2(0.0), vec2(1.0)); } - // edgeMode >= 1.5: keep the raw off-screen target (DLSS handles OOB history itself). - // [EDGE3 28/07] Mode 3 = le vecteur BRUT du mode 2, PLUS le drapeau « pas d'historique ici ». - // - // Mesure du jour, verdict d'Alex sur les trois modes existants : - // 0 -> lignes noires autour des arbres, disparition LENTE - // 1 -> idem (son reglage courant) - // 2 -> lignes qui partent D'UN COUP... mais image plus SOMBRE - // - // Le mode 2 est donc le seul qui reconstruit vite : donner le vrai deplacement aide. - // Son prix vient de ce qu'il ne PREVIENT PAS DLSS : le vecteur pointe hors de l'image, - // DLSS y cherche un historique qui n'existe pas et ramene du vide -- la « bande sombre » - // que le dossier avait deja notee sans en identifier la cause. - // - // Le masque de biais dit exactement « pour ce pixel, fie-toi a la couleur ACTUELLE ». En - // le posant sur ces pixels-la, on garde la reconstruction rapide du mode 2 et on supprime - // la raison de l'assombrissement. Aucun parametre ajoute au bloc d'uniformes (edgeMode - // existe deja) -- la regle du 28/07 matin reste respectee. if (edgeMode >= 2.5) { edgeBias = true; } } - // Dynamic-object / disocclusion mask: reproject the CURRENT surface into the previous - // frame and compare against what the previous depth buffer actually held there. A static, - // still-visible surface matches; a self-moving object or a freshly revealed background - // does not -- and for both, camera-reprojection MVs are wrong, so they read zero (the - // proven-stable baseline behaviour). Compared on 1-ndcZ (~1/viewZ), tolerant 15%. + float edgeBiasStrength = 1.0; + if (edgeMode >= 5.5) + { + edgeBiasStrength = clamp(edgeOutAmt / 0.02, 0.0, 1.0); + } + else if (edgeMode >= 4.5) + { + edgeBiasStrength = 0.3; + } + else if (edgeMode >= 3.5) + { + edgeBiasStrength = 0.5; + } + bool dynPixel = false; if (dynMask > 0.5) { float predNdcZ = prevClip.z / prevClip.w; float invA = max(1.0 - predNdcZ, 1e-5); - // Edge-tolerant compare: at geometry outlines the game's TAA jitter flips which - // surface the previous-depth tap lands on every frame -- a single-tap test masks - // (and flickers) every STATIC object outline (seen on rocks in game). The predicted - // depth is accepted if ANY tap of a small cross agrees; only pixels where even the - // best neighbor disagrees (truly moved or disoccluded) are masked. vec2 ts = 1.0 / vec2(w, h); float bestRel = 1e9; for (int i = 0; i < 5; i++) @@ -662,9 +435,6 @@ void main() dynPixel = bestRel > 0.15; - // [SUBGROUP 18/07] même remède que le bloc ciel : bandes denses (même densité 1/16) + - // réduction par sous-groupe — l'ancienne grille %4/%4 payait une atomique par pixel. - // Le ratio maskMasked/maskSampled (santé du masque) est insensible au motif. if ((p.y % 32) < 2) { uint rS = subgroupAdd(1u); @@ -677,27 +447,13 @@ void main() } } - // DLSS convention (matches the flow pass): motion in render-resolution pixels, pointing - // from the current pixel towards where it was in the previous frame. - // [INTERVALFIX 31/07] Meme normalisation que dans RotOnlyMv, pour le chemin geometrique : - // prevUv vient de vpPrev, qui peut etre a DEUX presents de vpCurr. Place AVANT mvPointPx et - // avant le melange FARROT pour que la sonde de profondeur et le melange voient la meme - // echelle que la sortie. Vaut 1.0 tant que le correctif est eteint. vec2 mv = intervalScale * ((prevUv - uv) * vec2(w, h)); - // [DEPTHSONDE 19/07] the raw point-based MV, BEFORE the FARROT blend below: the probe - // measures the reconstruction itself, not its mitigation. vec2 mvPointPx = mv; - // E4 / R-D fix: beyond farRotStart the point-based reprojection is dominated by depth- - // buffer quantization (far quanta span huge world slabs at grazing angles -> banded - // positions -> banded MVs = the black lines). Blend to ROTATION-ONLY: reproject the - // pixel's RAY as a point at infinity (homogeneous w = 0 -- translation cancels exactly, - // rotation is preserved and depth-immune). The ray direction comes from two fixed-z - // reconstructions of the SAME pixel, so it does not depend on the pixel's depth at all. if (farRotStart > 0.0) { - float viewDist = abs(1.0 / worldH.w); // ~ meters (same measure as the center debug) + float viewDist = abs(1.0 / worldH.w); float t = smoothstep(farRotStart, farRotStart * 2.0, viewDist); if (t > 0.0) @@ -708,21 +464,13 @@ void main() mv = clamp(mv, vec2(-maxMotion), vec2(maxMotion)); - // [DYNSONDE 18/07] dynMask est TRI-ETAT : 1.0 = masque arme (zero les MV, l'historique), - // 2.0 = SONDE (etape 2, cartographie) -- le compare-depth tourne et compte, mais ne - // touche a RIEN : la sortie MV est celle du bat royal a l'octet. Sonde d'abord. if (dynPixel && dynMask < 1.5) { mv = vec2(0.0); } - imageStore(imgBiasMask, p, vec4((dynPixel || edgeBias) ? 1.0 : 0.0)); + imageStore(imgBiasMask, p, vec4(dynPixel ? 1.0 : (edgeBias ? edgeBiasStrength : 0.0))); - // [DYNSONDE 18/07] mesures sur les pixels dynamiques des bandes denses, mode sonde - // seulement : vitalite du flow (le piege connu = confidence gating qui retrecit sur - // les objets rapides) et desaccord flow-vs-camera (= le signal qu'une fusion - // injecterait ici). mv a ce point = mouvement-camera final, non zerote (mode 2). - // subgroupAdd en flow divergent = semantique lanes-actives, le pattern du bloc ciel. if (dynMask > 1.5 && dynPixel && (p.y % 32) < 2) { vec2 dynFlow = texelFetch(FlowMvTex, p, 0).rg; @@ -774,18 +522,6 @@ void main() mv = vec2(0.0); } - // J3 composition: the per-object delta rides ON TOP of the camera MV, on actor pixels - // only. The coverage clears to ZERO at each frame's first graft, so the never-written - // background composes to pure camera = exact baseline (additive identity, no sentinel - // needed in production). NDC delta -> "towards previous" pixels: mv = -0.5*dndc*dims, - // the y axis through the SAME flipY convention as both camera legs. NEAREST sampling - // (normalized UV, scene-sized coverage vs input grid maps 1:1 like DepthTex): never - // filter against the DEBUG_MV sentinel. Guards: sentinel, non-finite (the F1 IR guard - // makes these impossible upstream; defense in depth), |dndc| > 1 (a real per-frame - // pose delta is far below one full screen). - // dynMask interplay (both opt-in, never armed together in J3 runs): a masked actor - // pixel composes to object-only motion, missing the camera part -- revisit if DYNMASK - // ever ships armed. vec2 covPx = vec2(0.0); if (covCompose > 0.5 && zeroMv < 0.5) { @@ -798,30 +534,14 @@ void main() } } - // The raw (pre-deadband) vector is the truth about inter-frame motion; frame generation - // interpolates BETWEEN two presented frames and needs exactly that, deadband included - // would tell it "nothing moves" while the image still settles (the measured settle slide). imageStore(imgRawMv, p, vec4(clamp(mv + covPx, vec2(-maxMotion), vec2(maxMotion)), 0.0, 0.0)); - // The presented color is the game's own TAA output: small camera motion is partially - // suppressed/lagged IN THE IMAGE, so raw-camera MVs at that scale describe motion the - // pixels do not perform -- DLSS then warps a stable image by the mismatch every frame - // (the measured tremble; MVs forced to zero were proven stable). What the game's TAA - // flattened must read as zero here too; zero-MV behaviour below the band equals the - // proven-stable baseline, so nothing visible is lost. Thresholds are env-tunable - // (RYUJINX_MVPP_DEAD_LO/HI) to settle the decay transient by eye without rebuilds. - // J3: the deadband applies to the CAMERA component only -- it answers a TAA need on - // static geometry; object motion (~1-2 px/frame walking) is real in the image and is - // exactly the range this whole chain exists to serve. The object delta is added AFTER. mv *= smoothstep(deadLo, deadHi, length(mv)); imageStore(imgReprojMv, p, vec4(clamp(mv + covPx, vec2(-maxMotion), vec2(maxMotion)), 0.0, 0.0)); if (statPixel) { - // [SUBGROUP 18/07] dernier bloc converti (il tournait AUSSI en mode 2 via devStats — - // 113 lignes de votes au log du chrono B) : votes et sommes hissés en ternaires, - // une rafale d'atomiques par subgroup au lieu de 9 par pixel. vec2 flow = texelFetch(FlowMvTex, p, 0).rg; bool vx = abs(flow.x) > 0.1 && abs(mv.x) > 0.5; @@ -836,13 +556,6 @@ void main() uint rReprojX = subgroupAdd(uint(min(abs(mv.x), maxMotion) * 16.0)); uint rReprojY = subgroupAdd(uint(min(abs(mv.y), maxMotion) * 16.0)); - // [DEPTHSONDE 19/07] garde-fou depth-sanity, phase sonde (lecture seule). La loi - // mesurée : un pixel qui se DIT quasi-far ne peut pas porter une translation écran - // forte -- son MV point-based doit ~= la rotation pure. Résidu grand sur ces pixels - // = le dialecte depth ment (BOTW : décor lu 0.98-0.999, worldY absurdes -> ghost). - // Le chemin normal garantit 0 < d < 1 ici (sentinelles parties ligne ~276). x4 cap - // 64 px : pire cas 256/pixel x ~142k statPixels x 60 Hz = 2.2G < uint32 (x16 aurait - // débordé). Conditionnels hissés en ternaires, même rafale elect que les votes. bool farBin = d >= 0.98; bool midBin = d >= 0.50 && d < 0.90; vec2 rotRef = (farBin || midBin) ? RotOnlyMv(uv, w, h) : vec2(0.0); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv index f97e60f0b0e4df1abd822dc94e53f8ae887e2654..1dae27f12a9a7aadaa0916b512188efbe16565a4 100644 GIT binary patch literal 51000 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 DM-&T? diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_depthsonde b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_depthsonde deleted file mode 100644 index 45d663a050a5e248b8e3554fc4adf8ab3c2f4ae3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41920 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 diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_flowgate b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_flowgate deleted file mode 100644 index 3e211c3fc37dfd472bfe004bdeb8f7778699f2c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29412 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 diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_skydepth b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_skydepth deleted file mode 100644 index 3b8baad4e983cf086bc613844d2ddde18c2d8dfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44832 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 DCcw8C diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_skyflow b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_skyflow deleted file mode 100644 index c36bdd214ea037c21aeafc9edd9195a3c1407687..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26776 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 OGxnp1_nkK1|NjTihD0#{ diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_subgroup b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/MvppReproject.spv.bak_avant_subgroup deleted file mode 100644 index 1ba10091f4669f68ca9294fadbeb45b53b446685..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35104 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 1.5) { - // 5b-3: OBJECT-MV colorize. CovTex.rg = NDC delta (clip_N - clip_(N-1)) written by - // the variant FS with the REAL ring N-1 palette; camera matrices are current-frame - // on both sides -> camera motion is factored out (still actor = black even in pans; - // composition camera+object = J3). NDC -> ~px with the output dims (debug scale). - // SENTINEL diag: the RT is cleared to (-1000,-1000) under DEBUG_MV -- a pixel still - // at the sentinel = the variant FS never wrote it (background); written but ~zero - // = dark BLUE (bind/source issue); written and moving = colorized. One-run triage. vec2 mvRaw = textureLod(CovTex, uv, 0.0).rg; - outc *= 0.35; // camera field dimmed to a background reference + outc *= 0.35; - // 5B3 NaN probe: NaN fails every comparison -> it was INVISIBLE to this decoder AND - // to the readback thresholds (the "written=662, max=0" signature). Paint it PURE RED. if (isnan(mvRaw.r) || isnan(mvRaw.g) || isinf(mvRaw.r) || isinf(mvRaw.g)) { outc = vec3(1.0, 0.0, 0.0); @@ -64,22 +50,18 @@ void main() if (magA > 0.02) { - // Object MVs are small (walking ~1-2 px/frame): full brightness at maxMag/4 - // (= 4 px) instead of the 16 px camera scale -- run 1 lesson: only Link's - // JUMP was fast enough to light up at the camera scale. float hueA = atan(mvA.y, mvA.x) / 6.2831853 + 0.5; float vA = clamp(magA / (maxMag * 0.25), 0.0, 1.0); vec3 cA = clamp(abs(fract(hueA + vec3(0.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0) - 1.0, 0.0, 1.0); - outc = max(vA, 0.35) * cA; // brightness floor: slow motion stays visible + outc = max(vA, 0.35) * cA; } else { - outc = vec3(0.0, 0.15, 0.5); // WRITTEN but ~still: the sentinel diag marker + outc = vec3(0.0, 0.15, 0.5); } } } - // 3b: actor coverage overlay -- anything the re-drawn FS wrote lights up green. else if (hasCov > 0.5 && textureLod(CovTex, uv, 0.0).r > 0.003) { outc = mix(outc, vec3(0.0, 1.0, 0.2), 0.6); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaler.h b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaler.h index 1ca7d4a04..adf4ebcc7 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaler.h +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaler.h @@ -1,121 +1,8 @@ -// The MIT License(MIT) -// -// Copyright(c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files(the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -//--------------------------------------------------------------------------------- -// NVIDIA Image Scaling SDK - v1.0.3 -//--------------------------------------------------------------------------------- -// The NVIDIA Image Scaling SDK provides a single spatial scaling and sharpening algorithm -// for cross-platform support. The scaling algorithm uses a 6-tap scaling filter combined -// with 4 directional scaling and adaptive sharpening filters, which creates nice smooth images -// and sharp edges. In addition, the SDK provides a state-of-the-art adaptive directional sharpening algorithm -// for use in applications where no scaling is required. -// -// The directional scaling and sharpening algorithm is named NVScaler while the adaptive-directional-sharpening-only -// algorithm is named NVSharpen. Both algorithms are provided as compute shaders and -// developers are free to integrate them in their applications. Note that if you integrate NVScaler, you -// should NOT integrate NVSharpen, as NVScaler already includes a sharpening pass -// -// Pipeline Placement -// ------------------ -// The call into the NVIDIA Image Scaling shaders must occur during the post-processing phase after tone-mapping. -// Applying the scaling in linear HDR in-game color-space may result in a sharpening effect that is -// either not visible or too strong. Since sharpening algorithms can enhance noisy or grainy regions, it is recommended -// that certain effects such as film grain should occur after NVScaler or NVSharpen. Low-pass filters such as motion blur or -// light bloom are recommended to be applied before NVScaler or NVSharpen to avoid sharpening attenuation. -// -// Color Space and Ranges -// ---------------------- -// NVIDIA Image Scaling shaders can process color textures stored as either LDR or HDR with the following -// restrictions: -// 1) LDR -// - The range of color values must be in the [0, 1] range -// - The input color texture must be in display-referred color-space after tone mapping and OETF (gamma-correction) -// has been applied -// 2) HDR PQ -// - The range of color values must be in the [0, 1] range -// - The input color texture must be in display-referred color-space after tone mapping with Rec.2020 PQ OETF applied -// 3) HDR Linear -// - The recommended range of color values is [0, 12.5], where luminance value (as per BT. 709) of -// 1.0 maps to brightness value of 80nits (sRGB peak) and 12.5 maps to 1000nits -// - The input color texture may have luminance values that are either linear and scene-referred or -// linear and display-referred (after tone mapping) -// -// If the input color texture sent to NVScaler/NVSharpen is in HDR format set NIS_HDR_MODE define to either -// NIS_HDR_MODE_LINEAR (1) or NIS_HDR_MODE_PQ (2). -// -// Supported Texture Formats -// ------------------------- -// Input and output formats: -// Input and output formats are expected to be in the rages defined in previous section and should be -// specified using non-integer data types such as DXGI_FORMAT_R8G8B8A8_UNORM. -// -// Coefficients formats: -// The scaler coefficients and USM coefficients format should be specified using float4 type such as -// DXGI_FORMAT_R32G32B32A32_FLOAT or DXGI_FORMAT_R16G16B16A16_FLOAT. -// -// Resource States, Buffers, and Sampler: -// The game or application calling NVIDIA Image Scaling SDK shaders must ensure that the textures are in -// the correct state. -// - Input color textures must be in pixel shader read state. Shader Resource View (SRV) in DirectX -// - The output texture must be in read/write state. Unordered Access View (UAV) in DirectX -// - The coefficients texture for NVScaler must be in read state. Shader Resource View (SRV) in DirectX -// - The configuration variables must be passed as constant buffer. Constant Buffer View (CBV) in DirectX -// - The sampler for texture pixel sampling. Linear clamp SamplerState in Direct -// -// Adding NVIDIA Image Scaling SDK to a Project -// -------------------------------------------- -// Include NIS_Scaler.h directly in your application or alternative use the provided NIS_Main.hlsl shader file. -// Use NIS_Config.h to get the ideal shader dispatch values for your platform, to configure the algorithm constant -// values (NVScalerUpdateConfig, and NVSharpenUpdateConfig), and to access the algorithm coefficients (coef_scale and coef_USM). -// -// Defines: -// NIS_SCALER: default (1) NVScaler, (0) fast NVSharpen only, no upscaling -// NIS_HDR_MODE: default (0) disabled, (1) Linear, (2) PQ -// NIS_BLOCK_WIDTH: pixels per block width. Use GetOptimalBlockWidth query for your platform -// NIS_BLOCK_HEIGHT: pixels per block height. Use GetOptimalBlockHeight query for your platform -// NIS_THREAD_GROUP_SIZE: number of threads per group. Use GetOptimalThreadGroupSize query for your platform -// NIS_USE_HALF_PRECISION: default (0) disabled, (1) enable half pression computation -// NIS_HLSL: (1) enabled, (0) disabled -// NIS_HLSL_6_2: default (0) HLSL v5, (1) HLSL v6.2 forces NIS_HLSL=1 -// NIS_GLSL: (1) enabled, (0) disabled -// NIS_VIEWPORT_SUPPORT: default(0) disabled, (1) enable input/output viewport support -// NIS_NV12_SUPPORT: default(0) disabled, (1) enable NV12 input -// NIS_CLAMP_OUTPUT: default(0) disabled, (1) enable output clamp -// -// Default NVScaler shader constants: -// [NIS_BLOCK_WIDTH, NIS_BLOCK_HEIGHT, NIS_THREAD_GROUP_SIZE] = [32, 24, 256] -// -// Default NVSharpen shader constants: -// [NIS_BLOCK_WIDTH, NIS_BLOCK_HEIGHT, NIS_THREAD_GROUP_SIZE] = [32, 32, 256] -// -// NIS_UNROLL: default [unroll] -// NIS_UNROLL_INNER: default NIS_UNROLL, define in case of a compiler error for inner nested loops -//--------------------------------------------------------------------------------- - -// NVScaler enable by default. Set to 0 for NVSharpen only #ifndef NIS_SCALER #define NIS_SCALER 1 #endif -// HDR Modes #define NIS_HDR_MODE_NONE 0 #define NIS_HDR_MODE_LINEAR 1 #define NIS_HDR_MODE_PQ 2 @@ -124,12 +11,10 @@ #endif #define kHDRCompressionFactor 0.282842712f -// Viewport support #ifndef NIS_VIEWPORT_SUPPORT #define NIS_VIEWPORT_SUPPORT 0 #endif -// HLSL, GLSL #if NIS_HLSL==0 && !defined(NIS_GLSL) #define NIS_GLSL 1 #endif @@ -144,13 +29,11 @@ #define NIS_GLSL 0 #endif -// Half precision #ifndef NIS_USE_HALF_PRECISION #define NIS_USE_HALF_PRECISION 0 #endif #if NIS_HLSL -// Generic type and function aliases for HLSL #define NVF float #define NVF2 float2 #define NVF3 float3 @@ -171,13 +54,13 @@ #define NVH2 min16float2 #define NVH3 min16float3 #define NVH4 min16float4 -#endif // NIS_HLSL_6_2 -#else // FP32 types +#endif +#else #define NVH NVF #define NVH2 NVF2 #define NVH3 NVF3 #define NVH4 NVF4 -#endif // NIS_USE_HALF_PRECISION +#endif #define NVSHARED groupshared #define NVTEX_LOAD(x, pos) x[pos] #define NVTEX_SAMPLE(x, sampler, pos) x.SampleLevel(sampler, pos, 0) @@ -188,9 +71,8 @@ #ifndef NIS_UNROLL #define NIS_UNROLL [unroll] #endif -#endif // NIS_HLSL +#endif -// Generic type and function aliases for GLSL #if NIS_GLSL #define NVF float #define NVF2 vec2 @@ -206,20 +88,20 @@ #define NVH2 f16vec2 #define NVH3 f16vec3 #define NVH4 f16vec4 -#else // FP32 types +#else #define NVH NVF #define NVH2 NVF2 #define NVH3 NVF3 #define NVH4 NVF4 -#endif // NIS_USE_HALF_PRECISION +#endif #define NVSHARED shared -#define NVTEX_LOAD(arr, pos) arr[(pos).y][(pos).x] // Ryujinx: coef from inline const arrays (param renamed to avoid .x swizzle collision) -#define NVTEX_SAMPLE(x, sampler, pos) textureLod(x, pos, 0.0) // Ryujinx: combined sampler2D +#define NVTEX_LOAD(arr, pos) arr[(pos).y][(pos).x] +#define NVTEX_SAMPLE(x, sampler, pos) textureLod(x, pos, 0.0) #define NVTEX_SAMPLE_RED(x, sampler, pos) textureGather(x, pos, 0) #define NVTEX_SAMPLE_GREEN(x, sampler, pos) textureGather(x, pos, 1) #define NVTEX_SAMPLE_BLUE(x, sampler, pos) textureGather(x, pos, 2) #ifndef NIS_OUTPUT_TRANSFORM -#define NIS_OUTPUT_TRANSFORM(c) (c) // Ryujinx: HDR wrapper overrides this to tonemap to scRGB +#define NIS_OUTPUT_TRANSFORM(c) (c) #endif #define NVTEX_STORE(x, pos, v) imageStore(x, NVI2(pos), NIS_OUTPUT_TRANSFORM(v)) #define saturate(x) clamp(x, 0, 1) @@ -228,22 +110,19 @@ #ifndef NIS_UNROLL #define NIS_UNROLL #endif -#endif // NIS_GLSL +#endif #ifndef NIS_UNROLL_INNER #define NIS_UNROLL_INNER NIS_UNROLL #endif -// Texture gather #ifndef NIS_TEXTURE_GATHER #define NIS_TEXTURE_GATHER 0 #endif -// NIS Scaling #define NIS_SCALE_INT 1 #define NIS_SCALE_FLOAT NVF(1.f) -// NIS output clamp #if NIS_CLAMP_OUTPUT #if NIS_HDR_MODE == NIS_HDR_MODE_LINEAR #define NVCLAMP(x) ( clamp(x, 0.0f, 12.5f) ) @@ -340,14 +219,8 @@ NVF4 GetEdgeMap(NVF p[5][5], NVI i, NVI j) #define kFilterSize 6 #define kSupportSize 6 #define kPadSize kSupportSize -// 'Tile' is the region of source luminance values that we load into shPixelsY. -// It is the area of source pixels covered by the destination 'Block' plus a -// 3 pixel border of support pixels. #define kTilePitch (NIS_BLOCK_WIDTH + kPadSize) #define kTileSize (kTilePitch * (NIS_BLOCK_HEIGHT + kPadSize)) -// 'EdgeMap' is the region of source pixels for which edge map vectors are derived. -// It is the area of source pixels covered by the destination 'Block' plus a -// 1 pixel border. #define kEdgeMapPitch (NIS_BLOCK_WIDTH + 2) #define kEdgeMapSize (kEdgeMapPitch * (NIS_BLOCK_HEIGHT + 2)) @@ -357,8 +230,6 @@ NVSHARED NVH shCoefUSM[kPhaseCount][kFilterSize]; NVSHARED NVH4 shEdgeMap[kEdgeMapSize]; void LoadFilterBanksSh(NVI i0) { - // Load up filter banks to shared memory - // The work is spread over (kPhaseCount * 2) threads NVI i = i0; #if( kPhaseCount * 2 > NIS_THREAD_GROUP_SIZE ) for (; i < kPhaseCount * 2; i += NIS_THREAD_GROUP_SIZE) @@ -390,7 +261,6 @@ void LoadFilterBanksSh(NVI i0) { } } - NVF CalcLTI(NVF p0, NVF p1, NVF p2, NVF p3, NVF p4, NVF p5, NVI phase_index) { const NVB selector = (phase_index <= kPhaseCount / 2); @@ -434,19 +304,15 @@ NVF EvalPoly6(const NVF pxl[6], NVI phase_int) } } - // let's compute a piece-wise ramp based on luma const NVF y_scale = 1.0f - saturate((y * (1.0f / NIS_SCALE_FLOAT) - kSharpStartY) * kSharpScaleY); - // scale the ramp to sharpen as a function of luma const NVF y_sharpness = y_scale * kSharpStrengthScale + kSharpStrengthMin; y_usm *= y_sharpness; - // scale the ramp to limit USM as a function of luma const NVF y_sharpness_limit = (y_scale * kSharpLimitScale + kSharpLimitMin) * y; y_usm = min(y_sharpness_limit, max(-y_sharpness_limit, y_usm)); - // reduce ringing y_usm *= CalcLTI(pxl[0], pxl[1], pxl[2], pxl[3], pxl[4], pxl[5], phase_int); return y + y_usm; @@ -467,7 +333,6 @@ NVF FilterNormal(const NVF p[6][6], NVI phase_x_frac_int, NVI phase_y_frac_int) h_acc += v_acc * shCoefScaler[phase_x_frac_int][j]; } - // let's return the sum unpacked -> we can accumulate it later return h_acc; } @@ -476,7 +341,6 @@ NVF AddDirFilters(NVF p[6][6], NVF phase_x_frac, NVF phase_y_frac, NVI phase_x_f NVF f = 0; if (w.x > 0.0f) { - // 0 deg filter NVF interp0Deg[6]; { NIS_UNROLL @@ -489,7 +353,6 @@ NVF AddDirFilters(NVF p[6][6], NVF phase_x_frac, NVF phase_y_frac, NVI phase_x_f } if (w.y > 0.0f) { - // 90 deg filter NVF interp90Deg[6]; { NIS_UNROLL @@ -503,7 +366,6 @@ NVF AddDirFilters(NVF p[6][6], NVF phase_x_frac, NVF phase_y_frac, NVI phase_x_f } if (w.z > 0.0f) { - //45 deg filter NVF pphase_b45 = 0.5f + 0.5f * (phase_x_frac - phase_y_frac); NVF temp_interp45Deg[7]; @@ -546,7 +408,6 @@ NVF AddDirFilters(NVF p[6][6], NVF phase_x_frac, NVF phase_y_frac, NVI phase_x_f } if (w.w > 0.0f) { - //135 deg filter NVF pphase_b135 = 0.5f * (phase_x_frac + phase_y_frac); NVF temp_interp135Deg[7]; @@ -590,13 +451,8 @@ NVF AddDirFilters(NVF p[6][6], NVF phase_x_frac, NVF phase_y_frac, NVI phase_x_f return f; } - -//----------------------------------------------------------------------------------------------- -// NVScaler -//----------------------------------------------------------------------------------------------- void NVScaler(NVU2 blockIdx, NVU threadIdx) { - // Figure out the range of pixels from input image that would be needed to be loaded for this thread-block NVI dstBlockX = NVI(NIS_BLOCK_WIDTH * blockIdx.x); NVI dstBlockY = NVI(NIS_BLOCK_HEIGHT * blockIdx.y); @@ -608,27 +464,20 @@ void NVScaler(NVU2 blockIdx, NVU threadIdx) NVI numTilePixelsX = srcBlockEndX - srcBlockStartX + kSupportSize - 1; NVI numTilePixelsY = srcBlockEndY - srcBlockStartY + kSupportSize - 1; - // round-up load region to even size since we're loading in 2x2 batches numTilePixelsX += numTilePixelsX & 0x1; numTilePixelsY += numTilePixelsY & 0x1; const NVI numTilePixels = numTilePixelsX * numTilePixelsY; - // calculate the equivalent values for the edge map const NVI numEdgeMapPixelsX = numTilePixelsX - kSupportSize + 2; const NVI numEdgeMapPixelsY = numTilePixelsY - kSupportSize + 2; const NVI numEdgeMapPixels = numEdgeMapPixelsX * numEdgeMapPixelsY; - // fill in input luma tile (shPixelsY) in batches of 2x2 pixels - // we use texture gather to get extra support necessary - // to compute 2x2 edge map outputs too { for (NVU i = threadIdx * 2; i < NVU(numTilePixels) >> 1; i += NIS_THREAD_GROUP_SIZE * 2) { NVU py = (i / numTilePixelsX) * 2; NVU px = i % numTilePixelsX; - // 0.5 to be in the center of texel - // - (kSupportSize - 1) / 2 to shift by the kernel support size NVF kShift = 0.5f - (kSupportSize - 1) / 2; #if NIS_VIEWPORT_SUPPORT const NVF tx = (srcBlockStartX + px + kInputViewportOriginX + kShift) * kSrcNormX; @@ -674,7 +523,6 @@ void NVScaler(NVU2 blockIdx, NVU threadIdx) } GroupMemoryBarrierWithGroupSync(); { - // fill in the edge map of 2x2 pixels for (NVU i = threadIdx * 2; i < NVU(numEdgeMapPixels) >> 1; i += NIS_THREAD_GROUP_SIZE * 2) { NVU py = (i / numEdgeMapPixelsX) * 2; @@ -703,17 +551,11 @@ void NVScaler(NVU2 blockIdx, NVU threadIdx) LoadFilterBanksSh(NVI(threadIdx)); GroupMemoryBarrierWithGroupSync(); - // output coord within a tile const NVI2 pos = NVI2(NVU(threadIdx) % NVU(NIS_BLOCK_WIDTH), NVU(threadIdx) / NVU(NIS_BLOCK_WIDTH)); - // x coord inside the output image const NVI dstX = dstBlockX + pos.x; - // x coord inside the input image const NVF srcX = (0.5f + dstX) * kScaleX - 0.5f; - // nearest integer part const NVI px = NVI(floor(srcX) - srcBlockStartX); - // fractional part const NVF fx = srcX - floor(srcX); - // discretized phase const NVI fx_int = NVI(fx * kPhaseCount); #if NIS_VIEWPORT_SUPPORT if (NVU(srcX) > kInputViewportWidth || NVU(dstX) > kOutputViewportWidth) @@ -723,22 +565,16 @@ void NVScaler(NVU2 blockIdx, NVU threadIdx) #endif for (NVI k = 0; k < NIS_BLOCK_WIDTH * NIS_BLOCK_HEIGHT / NIS_THREAD_GROUP_SIZE; ++k) { - // y coord inside the output image const NVI dstY = dstBlockY + pos.y + k * (NIS_THREAD_GROUP_SIZE / NIS_BLOCK_WIDTH); - // y coord inside the input image const NVF srcY = (0.5f + dstY) * kScaleY - 0.5f; #if NIS_VIEWPORT_SUPPORT if (!(NVU(srcY) > kInputViewportHeight || NVU(dstY) > kOutputViewportHeight)) #endif { - // nearest integer part const NVI py = NVI(floor(srcY) - srcBlockStartY); - // fractional part const NVF fy = srcY - floor(srcY); - // discretized phase const NVI fy_int = NVI(fy * kPhaseCount); - // generate weights for directional filters const NVI startEdgeMapIdx = py * kEdgeMapPitch + px; NVF4 edge[2][2]; NIS_UNROLL @@ -747,13 +583,11 @@ void NVScaler(NVU2 blockIdx, NVU threadIdx) NIS_UNROLL for (NVI j = 0; j < 2; j++) { - // need to shift edge map sampling since it's a 2x2 centered inside 6x6 grid edge[i][j] = shEdgeMap[startEdgeMapIdx + (i * kEdgeMapPitch) + j]; } } const NVF4 w = GetInterpEdgeMap(edge, fx, fy) * NIS_SCALE_INT; - // load 6x6 support to regs const NVI startTileIdx = py * kTilePitch + px; NVF p[6][6]; { @@ -768,16 +602,12 @@ void NVScaler(NVU2 blockIdx, NVU threadIdx) } } - // weigth for luma const NVF baseWeight = NIS_SCALE_FLOAT - w.x - w.y - w.z - w.w; - // final luma is a weighted product of directional & normal filters NVF opY = 0; - // get traditional scaler filter output opY += FilterNormal(p, fx_int, fy_int) * baseWeight; - // get directional filter bank output opY += AddDirFilters(p, fx, fy, fx_int, fy_int, w); #if NIS_VIEWPORT_SUPPORT @@ -787,7 +617,6 @@ void NVScaler(NVU2 blockIdx, NVU threadIdx) NVF2 coord = NVF2((srcX + 0.5f) * kSrcNormX, (srcY + 0.5f) * kSrcNormY); NVF2 dstCoord = NVF2(dstX, dstY); #endif - // do bilinear tap for chroma upscaling #if NIS_NV12_SUPPORT NVF y = NVTEX_SAMPLE(in_texture_y, samplerLinearClamp, coord); NVF2 uv = NVTEX_SAMPLE(in_texture_uv, samplerLinearClamp, coord); @@ -850,13 +679,9 @@ NVF CalcLTIFast(const NVF y[5]) NVF EvalUSM(const NVF pxl[5], const NVF sharpnessStrength, const NVF sharpnessLimit) { - // USM profile NVF y_usm = -0.6001f * pxl[1] + 1.2002f * pxl[2] - 0.6001f * pxl[3]; - // boost USM profile y_usm *= sharpnessStrength; - // clamp to the limit y_usm = min(sharpnessLimit, max(-sharpnessLimit, y_usm)); - // reduce ringing y_usm *= CalcLTIFast(pxl); return y_usm; @@ -864,15 +689,11 @@ NVF EvalUSM(const NVF pxl[5], const NVF sharpnessStrength, const NVF sharpnessLi NVF4 GetDirUSM(const NVF p[5][5]) { - // sharpness boost & limit are the same for all directions const NVF scaleY = 1.0f - saturate((p[2][2] - kSharpStartY) * kSharpScaleY); - // scale the ramp to sharpen as a function of luma const NVF sharpnessStrength = scaleY * kSharpStrengthScale + kSharpStrengthMin; - // scale the ramp to limit USM as a function of luma const NVF sharpnessLimit = (scaleY * kSharpLimitScale + kSharpLimitMin) * p[2][2]; NVF4 rval; - // 0 deg filter NVF interp0Deg[5]; { for (NVI i = 0; i < 5; ++i) @@ -883,7 +704,6 @@ NVF4 GetDirUSM(const NVF p[5][5]) rval.x = EvalUSM(interp0Deg, sharpnessStrength, sharpnessLimit); - // 90 deg filter NVF interp90Deg[5]; { for (NVI i = 0; i < 5; ++i) @@ -894,7 +714,6 @@ NVF4 GetDirUSM(const NVF p[5][5]) rval.y = EvalUSM(interp90Deg, sharpnessStrength, sharpnessLimit); - //45 deg filter NVF interp45Deg[5]; interp45Deg[0] = p[1][1]; interp45Deg[1] = lerp(p[2][1], p[1][2], 0.5f); @@ -904,7 +723,6 @@ NVF4 GetDirUSM(const NVF p[5][5]) rval.z = EvalUSM(interp45Deg, sharpnessStrength, sharpnessLimit); - //135 deg filter NVF interp135Deg[5]; interp135Deg[0] = p[3][1]; interp135Deg[1] = lerp(p[3][2], p[2][1], 0.5f); @@ -916,17 +734,11 @@ NVF4 GetDirUSM(const NVF p[5][5]) return rval; } -//----------------------------------------------------------------------------------------------- -// NVSharpen -//----------------------------------------------------------------------------------------------- void NVSharpen(NVU2 blockIdx, NVU threadIdx) { const NVI dstBlockX = NVI(NIS_BLOCK_WIDTH * blockIdx.x); const NVI dstBlockY = NVI(NIS_BLOCK_HEIGHT * blockIdx.y); - // fill in input luma tile in batches of 2x2 pixels - // we use texture gather to get extra support necessary - // to compute 2x2 edge map outputs too const NVF kShift = 0.5f - kSupportSize / 2; for (NVI i = NVI(threadIdx) * 2; i < kNumPixelsX * kNumPixelsY / 2; i += NIS_THREAD_GROUP_SIZE * 2) @@ -961,7 +773,6 @@ void NVSharpen(NVU2 blockIdx, NVU threadIdx) { const NVI2 pos = NVI2(NVU(k) % NVU(NIS_BLOCK_WIDTH), NVU(k) / NVU(NIS_BLOCK_WIDTH)); - // load 5x5 support to regs NVF p[5][5]; NIS_UNROLL for (NVI i = 0; i < 5; ++i) @@ -973,16 +784,12 @@ void NVSharpen(NVU2 blockIdx, NVU threadIdx) } } - // get directional filter bank output NVF4 dirUSM = GetDirUSM(p); - // generate weights for directional filters NVF4 w = GetEdgeMap(p, kSupportSize / 2 - 1, kSupportSize / 2 - 1); - // final USM is a weighted sum filter outputs const NVF usmY = (dirUSM.x * w.x + dirUSM.y * w.y + dirUSM.z * w.z + dirUSM.w * w.w); - // do bilinear tap and correct rgb texel so it produces new sharpened luma const NVI dstX = dstBlockX + pos.x; const NVI dstY = dstBlockY + pos.y; diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaling.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaling.glsl index 1aae48625..da4b83e42 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaling.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScaling.glsl @@ -1,27 +1,3 @@ -// The MIT License (MIT) -// -// Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// NVIDIA Image Scaling (NIS) v1.0.3 - MIT License, (c) 2022 NVIDIA CORPORATION. -// Vulkan compute wrapper adapted for Ryujinx: combined samplers + inline coefficient -// tables (see NisScaler.h macro edits and nis_coef.glsl). Output path: SDR (rgba8). #version 450 core #extension GL_GOOGLE_include_directive : require @@ -34,7 +10,6 @@ layout(local_size_x = NIS_THREAD_GROUP_SIZE, local_size_y = 1, local_size_z = 1) in; -// NISConfig constant buffer (exact field order; filled host-side by NisScalingFilter.cs). layout(binding = 2) uniform cb { float kDetectRatio; @@ -77,7 +52,6 @@ layout(binding = 2) uniform cb layout(binding = 1, set = 2) uniform sampler2D in_texture; layout(rgba8, binding = 0, set = 3) uniform image2D out_texture; -// coef_scaler[64][2] and coef_usm[64][2] (vec4 = taps0-3 / taps4-7), MIT, verbatim values. #include "nis_coef.glsl" #include "NisScaler.h" diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScalingHdr.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScalingHdr.glsl index 55ed37261..2a2ef7185 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScalingHdr.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/NisScalingHdr.glsl @@ -1,28 +1,3 @@ -// The MIT License (MIT) -// -// Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// NVIDIA Image Scaling (NIS) v1.0.3 - HDR variant for Ryujinx. Same as NisScaling.glsl -// but outputs to an scRGB (rgba16f) swapchain: NIS runs in SDR space, then the result is -// inverse-tone-mapped to scRGB via NIS_OUTPUT_TRANSFORM. The tonemap below matches the -// fork's existing HDR path (FsrSharpeningHdr) exactly. #version 450 core #extension GL_GOOGLE_include_directive : require @@ -76,13 +51,11 @@ layout(binding = 2) uniform cb layout(binding = 1, set = 2) uniform sampler2D in_texture; #ifdef PQ_OUTPUT -// HDR10 swapchain (RGB10A2 + ST.2084): the PQ encode happens in NIS_OUTPUT_TRANSFORM below. layout(rgb10_a2, binding = 0, set = 3) uniform image2D out_texture; #else layout(rgba16f, binding = 0, set = 3) uniform image2D out_texture; #endif -// HDR tone-map parameters (filled host-side; same values the other filters receive). layout(binding = 3, std140) uniform hdrParams { float hdrPaperWhite; @@ -95,7 +68,6 @@ layout(binding = 3, std140) uniform hdrParams #include "nis_coef.glsl" -// SDR -> scRGB inverse tone map. Ported verbatim from the fork's FsrSharpeningHdr path. vec3 sdrToLinear(vec3 c, float g) { return pow(max(c, vec3(0.0)), vec3(g)); @@ -116,8 +88,6 @@ vec3 sdrToHdr(vec3 srgb) } #ifdef PQ_OUTPUT -// scRGB linear (Rec.709, 1.0 == 80 nits) -> HDR10 signal (Rec.2020 + ST.2084 PQ). Same math as -// the plain HDR blit (ColorBlitHdrFragmentShaderSource.frag). vec3 scRgbToPq(vec3 scrgb) { const mat3 rec709To2020 = mat3( diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl index 4713174c5..b1bc9896f 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl @@ -1,14 +1,10 @@ -// TAA optical-flow pre-pass: bilinear downsample of the colour to the motion-estimation working -// resolution. Running Lucas-Kanade on a half (or quarter) resolution image cuts the per-pixel optical -// flow cost ~4x (or ~16x) with little quality loss -- the reconstructed field is upscaled and scaled by -// the same factor when applied in the TAA blend. #version 430 core layout (local_size_x = 16, local_size_y = 16) in; -layout (rgba16f, binding = 0, set = 3) uniform image2D imgOut; // working-resolution colour -layout (binding = 1, set = 2) uniform sampler2D Source; // full-resolution colour +layout (rgba16f, binding = 0, set = 3) uniform image2D imgOut; +layout (binding = 1, set = 2) uniform sampler2D Source; layout (binding = 2) uniform params { float outWidth; @@ -24,8 +20,6 @@ void main() return; } - // Normalized coord maps the small output onto the full-res source; the linear sampler averages the - // covered source texels (2x2 at half res), which is the cheap box-ish downsample we want for flow. vec2 uv = (vec2(loc) + 0.5) / vec2(outWidth, outHeight); imageStore(imgOut, loc, texture(Source, uv)); } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl index 6c1d43864..5245b40ca 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl @@ -1,18 +1,3 @@ -// Temporal Anti-Aliasing (clean-room). -// -// Phase 4 + velocity-drop hysteresis. On top of motion reprojection (reduced-res field) and the YCoCg -// neighbourhood variance clamp, a per-pixel SMOOTHED velocity (carried in the history's alpha channel) -// removes the single-frame flash seen when the camera stops dead: the smoothed velocity rises instantly -// with motion but decays over ~3-4 frames, and BOTH the clamp width and the blend weight are continuous -// functions of it. So when you stop, the clamp loosens and the history eases back to its "marble" weight -// gradually instead of snapping in one frame. -// -// vSmooth = max(|mv|, vSmooth_prev * VEL_DECAY) // instant rise, slow fall -// motion = smoothstep(VEL_LOW, VEL_HIGH, vSmooth) // 0 static .. 1 moving -// gamma = mix(clampGamma*STATIC_CLAMP, clampGamma, motion) -// wBlend = mix(blend, blend*MOVING_BLEND, motion) -// -// prevPos = loc + motionSign * mv // motionSign = +1 (calibrated) #version 430 core @@ -20,31 +5,27 @@ layout (local_size_x = 16, local_size_y = 16) in; layout (rgba8, binding = 0, set = 3) uniform image2D imgOutput; layout (rgba16f, binding = 1, set = 3) uniform image2D imgHistoryWrite; -layout (binding = 1, set = 2) uniform sampler2D Source; // current color -layout (binding = 3, set = 2) uniform sampler2D HistoryRead; // previous result (rgb) + smoothed velocity (a) -layout (binding = 5, set = 2) uniform sampler2D Motion; // rg16f motion field, motion-res pixels +layout (binding = 1, set = 2) uniform sampler2D Source; +layout (binding = 3, set = 2) uniform sampler2D HistoryRead; +layout (binding = 5, set = 2) uniform sampler2D Motion; layout (binding = 2) uniform params { float width; float height; - float blend; // history weight in [0,1]; current weight is (1 - blend) - float hasHistory; // >0.5 once the read-side history holds a valid previous frame - float motionSign; // +1 (calibrated) maps a pixel back to its previous position (prevPos = loc + mv) - float hasMotion; // >0.5 once a previous frame exists to estimate motion from - float clampGamma; // variance-box half-width in std-devs (smaller = tighter = less ghost, more flicker) - float mvScale; // full-res / motion-res ratio: upscales the low-res motion field to full-res pixels + float blend; + float hasHistory; + float motionSign; + float hasMotion; + float clampGamma; + float mvScale; }; -// Velocity-drop hysteresis constants (full-res pixel units). Hardcoded tuning -- continuity, not the exact -// values, is what removes the flash. -const float VEL_DECAY = 0.85; // per-frame decay of the smoothed velocity when motion stops (gentle: longer - // ramp, ~5-6 frames of easing -- lets a huge distant surface like a waterfall - // re-align with no visible break) -const float VEL_LOW = 0.10; // below this smoothed velocity the pixel is treated as fully static -const float VEL_HIGH = 1.00; // above this it is treated as fully moving -const float STATIC_CLAMP = 2.00; // clamp box is this much looser when fully static (history trusted = marble) -const float MOVING_BLEND = 0.92; // history weight is scaled by this when fully moving (slightly more reactive) -const float VEL_MAX = 64.0; // clamp the stored velocity so it can never run away +const float VEL_DECAY = 0.85; +const float VEL_LOW = 0.10; +const float VEL_HIGH = 1.00; +const float STATIC_CLAMP = 2.00; +const float MOVING_BLEND = 0.92; +const float VEL_MAX = 64.0; vec3 RGBToYCoCg(vec3 c) { @@ -78,7 +59,6 @@ void main() vec4 current = texelFetch(Source, loc, 0); vec3 currentY = RGBToYCoCg(current.rgb); - // Build the current 3x3 neighbourhood's YCoCg mean and variance for the clamp box. vec3 m1 = vec3(0.0); vec3 m2 = vec3(0.0); @@ -96,8 +76,6 @@ void main() vec3 mean = m1 / 9.0; vec3 sigma = sqrt(max(vec3(0.0), m2 / 9.0 - mean * mean)); - // Reproject and read the history (rgb) plus the previous smoothed velocity (alpha). Motion is estimated - // at reduced resolution: bilinear-sample it and scale to full-res pixels. vec2 locUv = (vec2(loc) + 0.5) / vec2(width, height); vec2 mv = hasMotion > 0.5 ? texture(Motion, locUv).xy * mvScale : vec2(0.0); vec2 prevPos = vec2(loc) + 0.5 + motionSign * mv; @@ -109,13 +87,10 @@ void main() vec4 histSample = texture(HistoryRead, uv); float prevVSmooth = valid ? histSample.a : 0.0; - // Smoothed velocity: jumps up the instant the pixel moves, eases down over several frames when it stops. float vRaw = length(mv); float vSmooth = min(max(vRaw, prevVSmooth * VEL_DECAY), VEL_MAX); float motion = smoothstep(VEL_LOW, VEL_HIGH, vSmooth); - // Continuous transition guards: clamp loosens and blend rises back to the static "marble" values as the - // smoothed velocity decays, so the stop is amortised instead of snapping in a single frame. float gamma = mix(clampGamma * STATIC_CLAMP, clampGamma, motion); float wBlend = mix(blend, blend * MOVING_BLEND, motion); @@ -127,7 +102,6 @@ void main() vec3 resultY = mix(currentY, historyY, w); vec3 resultRGB = YCoCgToRGB(resultY); - // Output carries the real alpha; the history alpha carries the smoothed velocity for next frame. imageStore(imgOutput, loc, vec4(resultRGB, current.a)); imageStore(imgHistoryWrite, loc, vec4(resultRGB, vSmooth)); } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/nis_coef.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/nis_coef.glsl index f33ed3b9d..45f8c4596 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/nis_coef.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/nis_coef.glsl @@ -1,26 +1,3 @@ -// The MIT License (MIT) -// -// Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// NIS coefficient tables, extracted verbatim from NVIDIA NIS_Config.h (v1.0.3, MIT). -// Layout: [phase 0..63][col 0=taps0-3, 1=taps4-7] matching texelFetch(coef, ivec2(vIdx,phase)). const vec4 coef_scaler[64][2] = vec4[64][2]( vec4[2](vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0)), diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs index ad0fdb2f2..2fb9fc24a 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Common; using Ryujinx.Common.Logging; @@ -15,28 +12,13 @@ using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; namespace Ryujinx.Graphics.Vulkan.Effects { - /// - /// Clean-room native Temporal Anti-Aliasing. - /// - /// Phase 4: the Phase 3 pipeline (motion-vector reprojection + YCoCg neighbourhood variance clamp) with - /// the optical flow moved to REDUCED resolution for performance. Each frame the colour is bilinear- - /// downsampled to 1/N (default half) and the Lucas-Kanade + 3x3 median passes run there (~N^2 less work, - /// the dominant cost), then the field is bilinear-upscaled and scaled by N when the blend reprojects the - /// history. Motion estimation is self-contained (reuses the DLSS shaders) so TAA works with DLSS off. - /// - /// Selected from the UI as an Anti-Aliasing option (AntiAliasing.Taa); RYUJINX_TAA=1 also force-enables it - /// (dev override). Tunables (no rebuild): RYUJINX_TAA_BLEND (history weight, default 0.935 = soft/cinematic, - /// low flicker), RYUJINX_TAA_MV_SIGN (reprojection direction, default +1), RYUJINX_TAA_CLAMP (variance-box - /// half-width in std-devs, default 1.25), RYUJINX_TAA_MV_DOWNSCALE (flow resolution divisor 1/2/4, default 2). - /// internal class TemporalFilter : IPostProcessingEffect { - /// True when the user opted into the experimental TAA via RYUJINX_TAA=1. public static readonly bool IsEnabled = Environment.GetEnvironmentVariable("RYUJINX_TAA") is "1" or "true" or "TRUE" or "True"; - private const float MaxMotion = 32f; // motion-vector clamp, in motion-resolution pixels - private const int CounterCount = 9; // uints in the motion-pass SSBO (unused by TAA, required by the shader) + private const float MaxMotion = 32f; + private const int CounterCount = 9; private static readonly float HistoryBlend = ParseBlend(); private static readonly float MotionSign = ParseSign(); @@ -44,8 +26,8 @@ namespace Ryujinx.Graphics.Vulkan.Effects private static readonly int MotionDownscale = ParseDownscale(); private readonly VulkanRenderer _renderer; - private readonly PipelineHelperShader _pipeline; // accumulation/blend pass - private readonly PipelineHelperShader _motionPipeline; // downsample + motion estimate + median filter + private readonly PipelineHelperShader _pipeline; + private readonly PipelineHelperShader _motionPipeline; private ISampler _sampler; private ShaderCollection _program; private ShaderCollection _downsampleProgram; @@ -53,17 +35,13 @@ namespace Ryujinx.Graphics.Vulkan.Effects private ShaderCollection _motionFilterProgram; private BufferHandle _sceneChangeBuffer; - // Output keeps the input format so the downstream blit/scaling filter sees an identical texture. private TextureView _output; - // Ping-pong history, rgba16f. Each frame one is read (previous result) and one is written (new result). private TextureView _history0; private TextureView _history1; private bool _readFromHistory0; private bool _hasHistory; - // Reduced-resolution optical flow: ping-pong working-res colour (current/previous), raw and median- - // filtered motion (rg16f, in motion-resolution pixels). _mvScale lifts the field back to full-res pixels. private TextureView _colorHalf0; private TextureView _colorHalf1; private TextureView _motion; @@ -95,20 +73,16 @@ namespace Ryujinx.Graphics.Vulkan.Effects return blend; } - return 0.935f; // validated in-game default (soft, cinematic, very low flicker); RYUJINX_TAA_BLEND overrides. + return 0.935f; } private static float ParseSign() { - // Default +1 (prevPos = loc + mv): calibrated in game. The Lucas-Kanade field is oriented so that - // following +mv lands the read on the history; -1 doubled the image. RYUJINX_TAA_MV_SIGN=-1 flips back. return Environment.GetEnvironmentVariable("RYUJINX_TAA_MV_SIGN") is "-1" ? -1f : 1f; } private static float ParseClamp() { - // Variance-box half-width in std-devs: smaller crushes more ghosting (but flickers more), larger - // smooths more (but lets more ghost through). 1.0 is the standard starting point. string value = Environment.GetEnvironmentVariable("RYUJINX_TAA_CLAMP"); if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float gamma) && @@ -117,13 +91,11 @@ namespace Ryujinx.Graphics.Vulkan.Effects return gamma; } - return 1.25f; // validated in-game default (lets native texture detail through); RYUJINX_TAA_CLAMP overrides. + return 1.25f; } private static int ParseDownscale() { - // Optical-flow resolution divisor. 2 (half res) is the sweet spot: ~4x less flow work, little - // quality loss. 4 is faster/rougher; 1 disables the optimization (full-res flow, old behavior). return Environment.GetEnvironmentVariable("RYUJINX_TAA_MV_DOWNSCALE") switch { "1" => 1, @@ -139,8 +111,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects _sampler = _renderer.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); - // Accumulation/blend pass: current color (b1), history read (b3), motion (b5), params (b2), - // present output image (b0/set3), history write image (b1/set3). byte[] blendShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.spv"); ResourceLayout blendLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) @@ -154,7 +124,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects new ShaderSource(blendShader, ShaderStage.Compute, TargetLanguage.Spirv) ], blendLayout); - // Colour downsample to the flow working resolution: source (b1), params (b2), output image (b0/set3). byte[] downsampleShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.spv"); ResourceLayout downsampleLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) @@ -165,8 +134,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects new ShaderSource(downsampleShader, ShaderStage.Compute, TargetLanguage.Spirv) ], downsampleLayout); - // Motion estimate (Lucas-Kanade): current (b1), previous (b3), params (b2), stats SSBO (b0/set1), - // motion image (b0/set3). Reused verbatim from the DLSS path. byte[] motionShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionVectors.spv"); ResourceLayout motionLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) @@ -179,7 +146,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects new ShaderSource(motionShader, ShaderStage.Compute, TargetLanguage.Spirv) ], motionLayout); - // 3x3 median outlier filter: params (b2), stats SSBO (b0/set1), filtered out (b0/set3), raw in (b1/set3). byte[] filterShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/MotionFilter.spv"); ResourceLayout filterLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) @@ -241,7 +207,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects _motion = _renderer.CreateTexture(MakeInfo(view.Info, _motionWidth, _motionHeight, Format.R16G16Float, 4)) as TextureView; _motionFiltered = _renderer.CreateTexture(MakeInfo(view.Info, _motionWidth, _motionHeight, Format.R16G16Float, 4)) as TextureView; - // Fresh resources: nothing to blend or estimate motion against yet. _readFromHistory0 = true; _hasHistory = false; _readColorHalf0 = false; @@ -260,19 +225,16 @@ namespace Ryujinx.Graphics.Vulkan.Effects $"flow 1/{MotionDownscale} = {_motionWidth}x{_motionHeight})."); } - // Downsample this frame's colour to the flow working resolution (ping-pong with last frame's). TextureView curHalf = _readColorHalf0 ? _colorHalf1 : _colorHalf0; TextureView prevHalf = _readColorHalf0 ? _colorHalf0 : _colorHalf1; RunDownsamplePass(view, curHalf, cbs); - // Reconstruct the motion field at reduced resolution, then median-filter it. if (_hasPrev) { RunMotionPass(curHalf, prevHalf, cbs); RunMotionFilterPass(cbs); } - // Ping-pong: read last frame's result, write this frame's result to the other target. TextureView historyRead = _readFromHistory0 ? _history0 : _history1; TextureView historyWrite = _readFromHistory0 ? _history1 : _history0; @@ -308,7 +270,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects _pipeline.Finish(); - // Next frame reads what we just wrote and has both a history and a previous downsampled frame. _readFromHistory0 = !_readFromHistory0; _readColorHalf0 = !_readColorHalf0; _hasHistory = true; @@ -342,8 +303,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 1, current, _sampler); _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 3, previous, _sampler); - // width/height/maxMotion/metrics + dejitterX/Y + 2 pad. No clip-space jitter on the TAA input - // (that is a DLSS-only feature), so de-jitter is zero; metrics off. ReadOnlySpan p = [_motionWidth, _motionHeight, MaxMotion, 0f, 0f, 0f, 0f, 0f]; using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, p.Length * sizeof(float)); buffer.Holder.SetDataUnchecked(buffer.Offset, p); diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs index 113f9dbc2..231265ea3 100644 --- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs +++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs @@ -178,22 +178,7 @@ namespace Ryujinx.Graphics.Vulkan AttachmentsCount = count; _totalCount = colors.Length; } - - // E3 fractional-scale guard: a view's metadata can claim one pixel more than the - // physical mip it sits on (host mips halve by floor while scaled guest mip sizes - // round up; the Gpu-side ClampViewToStorage covers the view-creation paths, but at - // least one path slips through -- proven on TOTK quality mode: framebuffer 481 over - // a 480 mip-1 attachment, VUID-04533 then device-lost). The framebuffer is the last - // gate, so bound it by each attachment's PHYSICAL mip extent: a strict no-op when - // metadata is consistent (every integer-scale regime), a 1px underscan on a low mip - // otherwise -- invisible, and the read side sees the physical size anyway. - // - // [FBCLAMP 02/08, journal (379)] Suspect n°1 du dossier ECRAN VERT (4 reproducteurs, - // 3 UE) : le « no-op strict » suppose des metadonnees coherentes — les vues ALIASEES - // d'Unreal (pools de cibles recycles entre formats/topologies) peuvent donner - // PhysicalMip < taille de vue ⇒ framebuffer SOUS-dimensionne = rendu tronque/jamais - // ecrit. RYUJINX_FBCLAMP_STOCK=1 rend la garde transparente (comportement stock - // exact) pour l'A/B ; OFF par defaut = comportement v1.2.3 inchange. + private static readonly bool _fbClampStock = Environment.GetEnvironmentVariable("RYUJINX_FBCLAMP_STOCK") == "1"; @@ -210,7 +195,7 @@ namespace Ryujinx.Graphics.Vulkan public FramebufferParams Update(ReadOnlySpan colors, ITexture depthStencil) { int colorsCount = 0; - + foreach (ITexture color in colors) { if (IsValidTextureView(color)) @@ -220,7 +205,7 @@ namespace Ryujinx.Graphics.Vulkan } int count = colorsCount + (IsValidTextureView(depthStencil) ? 1 : 0); - + Array.Clear(_attachments); Array.Clear(_colors); @@ -253,7 +238,7 @@ namespace Ryujinx.Graphics.Vulkan Array.Clear(AttachmentSamples); Array.Clear(AttachmentFormats); Array.Clear(AttachmentIndices); - + if (AttachmentSamples.Length < count) { uint[] attachmentSamples = AttachmentSamples; @@ -272,7 +257,7 @@ namespace Ryujinx.Graphics.Vulkan Array.Resize(ref attachmentIndices, colorsCount); AttachmentIndices = attachmentIndices; } - + uint width = uint.MaxValue; uint height = uint.MaxValue; uint layers = uint.MaxValue; @@ -448,8 +433,6 @@ namespace Ryujinx.Graphics.Vulkan return new Auto(new DisposableFramebuffer(api, _device, framebuffer), null, _attachments[..AttachmentsCount]); } - // [OMAPVENT] (journal 185) zero-alloc colour view accessor for the draw-level - // ventilation probe: index is the ATTACHMENT position (rt slot = AttachmentIndices[index]). public TextureView MvppGetColorView(int index) { return _colors != null && (uint)index < (uint)_colors.Length ? _colors[index] : null; @@ -478,7 +461,7 @@ namespace Ryujinx.Graphics.Vulkan if (_colors != null) { int count = ColorAttachmentsCount; - + for (int i = 0; i < count; i++) { TextureView color = _colors[i]; @@ -497,7 +480,7 @@ namespace Ryujinx.Graphics.Vulkan if (_colors != null) { int count = ColorAttachmentsCount; - + for (int i = 0; i < count; i++) { TextureView color = _colors[i]; diff --git a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs index 893cd71f9..842cd9188 100644 --- a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs +++ b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs @@ -402,8 +402,6 @@ namespace Ryujinx.Graphics.Vulkan buffer.Holder.SetDataUnchecked(buffer.Offset, region); - // Reserved at method scope (must stay alive until the draw). Only filled/bound for HDR; - // the HDR blit program's layout has a fragment uniform buffer at binding 2. using ScopedTemporaryBuffer hdrBuffer = gd.BufferManager.ReserveOrCreate(gd, cbs, 32); if (hdr) @@ -415,7 +413,7 @@ namespace Ryujinx.Graphics.Vulkan hdrParams[3] = gamma; hdrParams[4] = blend; hdrParams[5] = whiten; - hdrParams[6] = pqOutput ? 1f : 0f; // shader re-encodes scRGB -> HDR10 PQ for RGB10A2 swapchains + hdrParams[6] = pqOutput ? 1f : 0f; hdrBuffer.Holder.SetDataUnchecked(hdrBuffer.Offset, hdrParams); _pipeline.SetUniformBuffers([new BufferAssignment(1, buffer.Range), new BufferAssignment(2, hdrBuffer.Range)]); @@ -486,12 +484,6 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.Finish(gd, cbs); } - /// - /// Blits over with a constant-alpha blend - /// (out = src*alpha + dst*(1-alpha)), used to cross-fade the spatial (NIS) result over the DLSS - /// frame already in the swapchain during a hybrid-mode transition. Plain copy in display space: - /// both inputs are already tone-mapped, so there is no HDR transform here. - /// public void BlitColorWithAlpha( VulkanRenderer gd, CommandBufferScoped cbs, @@ -530,7 +522,6 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetRenderTargetColorMasks([0xf]); _pipeline.SetScissors([new Rectangle(0, 0, dst.Width, dst.Height)]); - // Constant-alpha blend: out = src*alpha + dst*(1-alpha). alpha lives in the blend constant. _pipeline.SetBlendState(0, new BlendDescriptor( true, new ColorF(0f, 0f, 0f, alpha), @@ -541,7 +532,6 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); _pipeline.Draw(4, 1, 0, 0); - // Restore no-blend so the shared helper pipeline does not leak this state into later blits. _pipeline.SetBlendState(0, new BlendDescriptor( false, new ColorF(0f, 0f, 0f, 0f), @@ -990,7 +980,7 @@ namespace Ryujinx.Graphics.Vulkan int primitiveCount = pattern.GetPrimitiveCount(indexCount); int convertedCount = pattern.GetConvertedCount(indexCount); int outputIndexSize = 4; - + Buffer dstBuffer = dstIndexBuffer.GetBuffer().Get(cbs, 0, convertedCount * outputIndexSize).Value; const int ParamsBufferSize = 16 * sizeof(int); diff --git a/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs b/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs index c66276b0f..c7137a4b0 100644 --- a/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs +++ b/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs @@ -29,7 +29,7 @@ namespace Ryujinx.Graphics.Vulkan Offset = offset; Size = size; - MvppMemAliasProbe.OnAlloc(memory.Handle, offset, size, host: false); // [MEMALIAS] read-only, self-gated + MvppMemAliasProbe.OnAlloc(memory.Handle, offset, size, host: false); } public MemoryAllocation( @@ -45,12 +45,12 @@ namespace Ryujinx.Graphics.Vulkan Offset = offset; Size = size; - MvppMemAliasProbe.OnAlloc(memory.Handle, offset, size, host: true); // [MEMALIAS] read-only, self-gated + MvppMemAliasProbe.OnAlloc(memory.Handle, offset, size, host: true); } public void Dispose() { - MvppMemAliasProbe.OnFree(Memory.Handle, Offset, Size); // [MEMALIAS] read-only, self-gated + MvppMemAliasProbe.OnFree(Memory.Handle, Offset, Size); if (_hostMemory != null) { diff --git a/src/Ryujinx.Graphics.Vulkan/MvppClearRectProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppClearRectProbe.cs index 8e71dcc87..b1ab86b05 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppClearRectProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppClearRectProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Silk.NET.Vulkan; @@ -9,23 +6,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Vulkan { - /// - /// [CLEARRECT] Read-only probe (RYUJINX_CLEARRECT_PROBE=1), inert unless set. Logs the rectangle - /// ACTUALLY passed to vkCmdClearAttachments for clears on 1280x720 framebuffers. - /// - /// Why. The XC2 object-MV buffer (R10G10B10A2 720p) is the proven carrier of the artifact - /// (journal 126), and the guest issues a correct FULL-SCREEN clear of it every frame (MVBUF - /// probe: 2 clears/frame, mask 0xF, full scissor, neutral value 0.5/0.5). Yet stale rectangles - /// survive. At this level the cleared area is ClearScissor -- a pipeline field updated by every - /// SetScissors call -- so a stale or clamped scissor at clear time would silently shrink the - /// clear to a SUB-RECTANGLE: exactly the artifact's shape family. This probe records the final - /// ClearRect (post GetClearRect clamping) as deduplicated signatures. - /// - /// Reading grid, written before coding: any signature with rect smaller than the framebuffer on - /// the MV clear (identified by its 0.5/0.5 colour) => ROOT FOUND at this line (stale scissor); - /// all rects full => the clear executes on the right area, move to identity/barrier angles - /// (the Gpu-side MVBUF identity extension runs in the same session). - /// static class MvppClearRectProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppDecompProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppDecompProbe.cs index b5659c123..24cc74f09 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppDecompProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppDecompProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -11,22 +8,6 @@ using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Vulkan { - /// - /// [MVDECOMP] EXP 20 (RYUJINX_MVDECOMP=1), inert unless set. Journal 171. - /// - /// Weaponizes the oldest unexplained clue of the case -- "a readback CHANGES the artifact" - /// (journal 112): a readback forces the driver to decompress/resolve the image's metadata. - /// If the poison is desynced compression metadata (the H1 of journal 170, still standing - /// after NOFBL), then forcing a decompression round-trip EVERY FRAME on the two MV images - /// should kill or visibly mutate the artifact: - /// flat-block counter ~0, or artifact character clearly changed -> H1 CONFIRMED, and this - /// round-trip is a per-format WORKAROUND candidate while a real fix is designed; - /// strictly unchanged -> H1 seriously wounded -> pivot to twin roles (journal 169 reserve). - /// - /// Mechanism: at present, outside any render pass, emit for each tracked MV-shaped storage a - /// layout round-trip General -> TransferSrcOptimal -> General (full subresource). On NVIDIA - /// this is a metadata resolve point. Engine-visible layout is unchanged (General everywhere). - /// static class MvppDecompProbe { public static readonly bool Enabled = @@ -48,7 +29,6 @@ namespace Ryujinx.Graphics.Vulkan view.Info.Format == Ryujinx.Graphics.GAL.Format.R10G10B10A2Unorm; } - /// Colour-target bind (SetRenderTargetsInternal): track live MV-shaped storages. public static void OnBind(Span colors) { if (!Enabled) @@ -74,7 +54,6 @@ namespace Ryujinx.Graphics.Vulkan } } - /// Present-time tick (outside render pass): emit the decompression round-trip. public static unsafe void Tick(VulkanRenderer gd, CommandBufferScoped cbs) { if (!Enabled) diff --git a/src/Ryujinx.Graphics.Vulkan/MvppDescTruthProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppDescTruthProbe.cs index 922427463..18f1fb330 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppDescTruthProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppDescTruthProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,19 +5,6 @@ using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Vulkan { - /// - /// [DESCTRUTH] (RYUJINX_DESCTRUTH=1, inert unless set). Journal 193, read-only. - /// - /// After 192: the copy engine reads the MV images CLEAN (848 dumps + 104k forced copies - /// with zero visual effect) while the sampler reads garbage. Every identity check so far - /// compared C# objects; the one thing never verified is the actual Vulkan HANDLE the - /// sampler uses: descriptor writes resolve the Auto<DisposableImageView> CACHED in - /// TextureRef at bind time -- if the view recreated its image view since, the descriptor - /// carries a stale VkImageView over a dead VkImage (recycled memory = amorphous garbage; - /// copies unaffected -- no descriptors; OpenGL immune -- no descriptor sets; barriers - /// useless). This probe compares, at every descriptor write of an MV-shaped view, the - /// cached Auto against view.GetImageView() live. - /// static class MvppDescTruthProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppDofStateProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppDofStateProbe.cs index f6f49164a..83e3f7ddf 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppDofStateProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppDofStateProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,17 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Vulkan { - /// - /// [DOFSTATE] (RYUJINX_DOFSTATE=1, inert unless set). Journal 214, read-only. - /// - /// 213: the whole chain's shader math is read/innocented end to end -- what was never - /// audited is the FIXED-FUNCTION state of the bokeh accumulation pass. Top suspect: the - /// winding of the GS-emitted triangle strips vs Vulkan's negative-viewport Y flip (a - /// mis-compensated FrontFace would draw quads the console culls: double-painted sprites - /// = blocks, exactly where sprites exist = where tiles vary). Probe first: log, once per - /// distinct state, the cull/front-face/topology/blend of every draw whose framebuffer - /// holds a DoF-chain-sized colour target (512x288 / 320x180 / 64x36). - /// static class MvppDofStateProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppDrawClearProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppDrawClearProbe.cs index d9f61e1f7..18d198c0f 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppDrawClearProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppDrawClearProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Silk.NET.Vulkan; @@ -9,28 +6,6 @@ using System.Threading; namespace Ryujinx.Graphics.Vulkan { - /// - /// [DRAWCLEAR] Temporary VALIDATION experiment (RYUJINX_DRAWCLEAR=1), inert unless set. - /// Tests the LAST standing mechanism for the XC2 stale-motion corruption. - /// - /// State of the elimination (journal 127): the guest orders a correct full-screen neutral clear - /// of the object-MV buffer every frame; the final vkCmdClearAttachments rect is FULL every time - /// (CLEARRECT probe); and the cleared host instances are exactly the sampled ones (MVBUF/ID). - /// Command, area and identity are all correct -- yet stale rectangles survive, only in motion, - /// on RTX 50. What remains is the EXECUTION ORDERING of CmdClearAttachments itself against the - /// surrounding work on this driver family: the exact hazard family of upstream PR #4596 (RTX - /// 3000+, Xenoblade named explicitly, heuristic fix that can miss cases). - /// - /// While this flag is set, full-mask clears on 1280x720 A2B10G10R10 attachments (the MV buffer; - /// nothing else in XC2 has that shape+format) are routed through the HelperShader draw-based - /// clear -- the same code path Ryujinx already uses when componentMask is partial -- instead of - /// CmdClearAttachments. A draw participates in normal pipeline ordering and barriers. - /// artifact GONE (flat-block counter ~0) -> root confirmed: CmdClearAttachments ordering on - /// this driver; the fix is this reroute, generalized behind a compat option (fully GENERIC: - /// format+size trigger, no game address -- shippable, unlike the shader-side overrides); - /// artifact STAYS -> the clear path is innocent end-to-end; rethink from the writes/decode. - /// The [DRAWCLEAR] reroute counter is the witness: zero reroutes logged = void, not "innocent". - /// static class MvppDrawClearProbe { public static readonly bool Enabled = @@ -40,7 +15,6 @@ namespace Ryujinx.Graphics.Vulkan private static long _rerouted; private static long _lastLogMs; - /// True when this clear must take the draw-based path (gated + MV-buffer shape only). public static bool ShouldReroute(FramebufferParams fb, int index) { if (!Enabled || fb == null) diff --git a/src/Ryujinx.Graphics.Vulkan/MvppHistWatchProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppHistWatchProbe.cs index 6b02f17b1..89b23d94a 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppHistWatchProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppHistWatchProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,17 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Vulkan { - /// - /// [HISTWATCH] (RYUJINX_HISTWATCH=1, inert unless set). Journal 195, read-only. - /// - /// The 194 dumps proved the R32F 640x360 depth-history input of the motion-blur builder - /// is a PERFECT CONSTANT (78,800 everywhere, 122 dumps) -- never filled -- while the - /// downstream maps saturate scene-shaped during camera motion. OMAPVENT sees ZERO draws - /// targeting it and there are no pipeline skips, so if anything is supposed to fill it, - /// it is a COPY / DrawTexture / host upload -- the exact paths the draw-level probes - /// never see. This probe logs every such operation touching any 640x360 view (or any - /// R32Sfloat destination), with source shape and path. - /// static class MvppHistWatchProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppMemAliasProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppMemAliasProbe.cs index 884e337e9..0a0b9fb5f 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppMemAliasProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppMemAliasProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,22 +5,6 @@ using System.Collections.Generic; namespace Ryujinx.Graphics.Vulkan { - /// - /// [MEMALIAS] (RYUJINX_MEMALIAS=1, inert unless set). Journal 188, read-only. - /// - /// After 187: writers, readers, bindings, masks and storage identities of the MV twin X - /// are ALL correct, yet its content is garbage -- the poison enters at the memory level, - /// below the objects. The one family a full DeviceWaitIdle per frame cannot fix (and - /// (112) measured exactly that null effect) is MEMORY ALIASING: another live resource - /// whose sub-allocated range overlaps the MV image's range, legitimately writing its own - /// data over X. It would also explain why OpenGL is clean (the GL driver manages memory, - /// no manual sub-allocation). - /// - /// Tracks every MemoryAllocation (ctor/Dispose) as live (memory handle, offset, size) - /// ranges; TextureStorage tags MV-shaped images (R10G10B10A2 1280x720). Logs any range - /// overlap between a live MV image and any other live allocation, and any mid-session - /// free of an MV range. - /// static class MvppMemAliasProbe { public static readonly bool Enabled = @@ -117,7 +98,6 @@ namespace Ryujinx.Graphics.Vulkan } } - /// Called by TextureStorage right after a successful image allocation. public static void TagMv(ulong memoryHandle, ulong offset, string desc) { if (!Enabled || memoryHandle == 0) diff --git a/src/Ryujinx.Graphics.Vulkan/MvppMvDumpProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppMvDumpProbe.cs index 79aa62a15..12a4476a0 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppMvDumpProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppMvDumpProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,43 +5,16 @@ using System.IO; namespace Ryujinx.Graphics.Vulkan { - /// - /// [MVDUMP] (RYUJINX_MVDUMP=1, inert unless set). Journal 189, read-only. - /// - /// The decisive fork after 188: every write channel into the MV twin X is measured clean - /// (FS values >= 0.5 scrubbed, clears land, no copies/DMA/compute, no aliasing, no - /// identity split, sync forced with no effect) yet the periscope -- which reads THROUGH - /// THE SAMPLER -- shows saturated masses at rest. Last untested split: is the garbage - /// really IN the image memory, or is it FABRICATED on the read path? - /// - /// This probe reads the image memory through the OTHER channel: vkCmdCopyImageToBuffer - /// via TextureView.GetData (no sampler, no guest texture cache, no re-resolution). - /// Every ~8 s, dumps each MV-shaped colour attachment (A2B10G10R10 1280x720) of the - /// currently bound framebuffer to portable-side files + logs quick pixel stats. - /// - /// Reading: dumps DIRTY (hot masses) => the poison is genuinely in memory (writers - /// below 0.5 or an unseen channel); dumps CLEAN while the periscope showed masses at - /// rest => the read path fabricates the garbage (sampler/layout/format decode). - /// static class MvppMvDumpProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_MVDUMP") == "1"; - /// [MVFLUSH] (EXP, journal 192): per-frame copy WITHOUT saving anything. - /// The intervention IS the copy: (112) recorded that a readback CHANGES the artifact, - /// and the v3 dumps read clean memory while the sampler sees garbage -- if a forced - /// copy at the end of every MV pass kills the artifact, the copy resolves what the - /// sampler cannot (driver compression-metadata family), and that is the proof. public static readonly bool FlushEnabled = Environment.GetEnvironmentVariable("RYUJINX_MVFLUSH") == "1"; private const long IntervalMs = 8000; - // [v3] One fb-switch was starving the others (a global timer only ever caught the - // first MV pass of the frame). When the timer fires, open a short WINDOW instead: - // every MV-shaped attachment leaving the pipeline within it gets dumped -- sky pass, - // material MRT and the slot-0 pass land in the same round/frame. private const long WindowMs = 400; private static bool _armedLogged; @@ -53,23 +23,15 @@ namespace Ryujinx.Graphics.Vulkan private static int _dumpRound; private static string _dir; - /// Returns true when at least one dump ran (the caller must re-begin the - /// render pass: the copy path ends it). - /// [v4] (journal 194) the whole motion-blur chain, not just the 720p twins: - /// the temporal history and the intermediate maps were never read through the copy - /// path (the (29) graph: MV 720p + R32F 640x360 history -> builder -> 320x180 - /// ping-pong -> 64x36 tile maps). private static bool IsWatched(TextureView view) { return (view.Width, view.Height, view.VkFormat) switch { (1280, 720, Silk.NET.Vulkan.Format.A2B10G10R10UnormPack32) => true, - // [v5] (journal 200) the TAA resolve's colour inputs/outputs -- including the - // history (tcb_A) and the (111) slot4 -- where the striping was measured. (1280, 720, Silk.NET.Vulkan.Format.R8G8B8A8Unorm) => true, (1280, 720, Silk.NET.Vulkan.Format.B10G11R11UfloatPack32) => true, (640, 360, Silk.NET.Vulkan.Format.R32Sfloat) => true, - (512, 288, Silk.NET.Vulkan.Format.R16G16B16A16Sfloat) => true, // [v6] cible du scatter (215) + (512, 288, Silk.NET.Vulkan.Format.R16G16B16A16Sfloat) => true, (320, 180, Silk.NET.Vulkan.Format.R8G8B8A8Unorm) => true, (64, 36, Silk.NET.Vulkan.Format.R8G8B8A8Unorm) => true, (64, 36, Silk.NET.Vulkan.Format.R8Unorm) => true, @@ -119,8 +81,6 @@ namespace Ryujinx.Graphics.Vulkan continue; } - // Only stamp the timer once we actually saw a watched framebuffer, so dumps - // happen at chain-pass moments and not on menus. if (!any) { any = true; @@ -157,8 +117,6 @@ namespace Ryujinx.Graphics.Vulkan nonZero++; } - // Any 10-bit channel >= ~0.95 (0x3CC) counts as hot (the masses are - // saturated white/yellow after the builder's normalisation). if ((px & 0x3FFu) >= 0x3CC || ((px >> 10) & 0x3FFu) >= 0x3CC || ((px >> 20) & 0x3FFu) >= 0x3CC) { hot++; @@ -181,8 +139,6 @@ namespace Ryujinx.Graphics.Vulkan return any; } - /// [MVFLUSH] copy every MV-shaped attachment of the outgoing framebuffer, - /// every single pass, and throw the bytes away. Witness = heartbeat count. private static bool FlushPass(FramebufferParams fb, long now) { bool any = false; diff --git a/src/Ryujinx.Graphics.Vulkan/MvppNoFblProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppNoFblProbe.cs index 813a2027c..45e423108 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppNoFblProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppNoFblProbe.cs @@ -1,30 +1,9 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Vulkan { - /// - /// [NOFBL] EXP 19 (RYUJINX_NOFBL=1), inert unless set. Journal 170. - /// - /// Cold re-derivation verdict: every measurement (sane written values with proven-total - /// scrub coverage, sane clears, no injection, sane objects/phase) says the MV buffers' - /// MEMORY is fine and the READ decodes garbage -- driver compression/layout metadata - /// desync on NVIDIA (the real substance of the PR #4596 family; the user runs Blackwell). - /// Our plausible trigger: AttachmentFeedbackLoopBitExt is set on EVERY attachable image - /// and the feedback-loop machinery (detection + pipeline create flags) is live, while the - /// two MV buffers are attached AND sampled every frame. - /// - /// This switch reports the VK_EXT_attachment_feedback_loop_* extensions as ABSENT, so - /// image usage, pipeline flags and detection stay consistently inert -- ONE variable. - /// flat-block counter ~0 -> trigger confirmed; real fix = only use feedback-loop - /// usage/flags when a genuine loop exists (upstream candidate); - /// unchanged -> the trigger is elsewhere in the metadata family (fast-clear tags); - /// next levers per journal 170. - /// static class MvppNoFblProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppNoFragMask0Probe.cs b/src/Ryujinx.Graphics.Vulkan/MvppNoFragMask0Probe.cs index 2613f70cc..fa59973e2 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppNoFragMask0Probe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppNoFragMask0Probe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,28 +5,6 @@ using System.Threading; namespace Ryujinx.Graphics.Vulkan { - /// - /// [NOFRAG] Temporary VALIDATION experiment (RYUJINX_NOFRAG_MASK0=1), inert unless set. - /// Tests whether fragment-less draws are the source of the XC2 stale-motion garbage. - /// - /// Measured (journal 133): XC2 issues ~6 draws per frame with the fragment stage DISABLED - /// (depth/stencil-only) while the object-MV buffer is still bound as an MRT colour target with - /// stale guest state: blend=ON, colorWriteMask=0xF. On console, no fragment shader means no - /// colour writes, period. On Vulkan the colour output of a fragment-less pipeline with a - /// non-zero write mask is UNDEFINED -- the driver may write (and here BLEND) garbage. That - /// would deposit blended garbage magnitudes into the MV buffer exactly at the geometry those - /// draws touch, every frame, visible only in motion (a garbage motion vector on a still camera - /// still decodes to 'move by v', but the whole chain is only LOOKED AT while things move). - /// - /// While this flag is set, any pipeline created WITHOUT a fragment stage gets all its colour - /// write masks forced to 0 and blending disabled -- exact console semantics, fully generic - /// (no game address, no format, no size). The counter below is the witness: 0 applications - /// logged = void, not "innocent". - /// flat-block counter ~0 -> ROOT FOUND and this IS the fix (to ship as a proper option or - /// unconditionally after regression testing -- it cannot break titles that rely on - /// defined behavior, because there is none to rely on); - /// flat blocks remain -> fragment-less draws innocent; back to the 199 material writers. - /// static class MvppNoFragMask0Probe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppOmapMaskVk.cs b/src/Ryujinx.Graphics.Vulkan/MvppOmapMaskVk.cs index 7d8286aa9..1e9f74548 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppOmapMaskVk.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppOmapMaskVk.cs @@ -1,26 +1,9 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; namespace Ryujinx.Graphics.Vulkan { - /// - /// [OMAPMASK-VK] EXP 22v2 (RYUJINX_OMAPMASK=1), inert unless set. Journal 183. - /// - /// The hardware rule the OpenGL backend already applies (Pipeline.cs:1564, - /// `_componentMasks & _fragmentOutputMap`) and the Vulkan backend never did: a fragment - /// shader that does not export to a bound colour attachment must not write it. Without it, - /// Vulkan writes UNDEFINED values -- on NVIDIA, register garbage. XC2's material pass binds - /// its second MV-shaped target at slot 3 while ~half its shaders export nothing there: - /// fresh flicker over a clean clear every frame = the block/grid artifact (proven absent - /// under the OpenGL backend, 25/07 23h13). - /// - /// Applied in PipelineBase.SetRenderTargetColorMasks (mask arrival) and re-derived at - /// SetProgram (map change) -- the exact two update points OpenGL uses. - /// static class MvppOmapMaskVk { public static readonly bool Enabled = @@ -31,7 +14,6 @@ namespace Ryujinx.Graphics.Vulkan private static long _reductions; private static long _summaryMs; - /// One target's effective mask under the hardware rule. public static uint Apply(uint componentMask, int rtIndex, int fragmentOutputMap) { if (!_armedLogged) diff --git a/src/Ryujinx.Graphics.Vulkan/MvppOmapVentProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppOmapVentProbe.cs index 5fdb7e99d..9b49cd6f6 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppOmapVentProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppOmapVentProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -11,28 +8,11 @@ using VkFormat = Silk.NET.Vulkan.Format; namespace Ryujinx.Graphics.Vulkan { - /// - /// [OMAPVENT] (RYUJINX_OMAPVENT=1, inert unless set). Journal 185, read-only. - /// - /// Journal 184's open question: the omap rule (no export = no write) applies 46% of the - /// time yet the XC2 artifact survives -- but the counter lived at mask-arrival time and - /// never said WHICH attachment the reductions hit. This probe ventilates at DRAW time, - /// where the real framebuffer and the real program are both known: per - /// (rt slot, format, size, storage identity, guest mask, omap nibble), how many draws ran. - /// A row with guest=0xF omap=0x0 on the 720p target at slot 3 is the couple (X, slot 3) - /// actually being reduced in gameplay; zero such rows means the reductions all land - /// elsewhere and the theory breathes. - /// - /// Purely observational: computes what the rule WOULD do from the raw guest masks, - /// independent of whether RYUJINX_OMAPMASK is armed. - /// static class MvppOmapVentProbe { public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_OMAPVENT") == "1"; - /// Omap value used when the bound program has no guest fragment map - /// (internal/helper programs, no fragment stage): prints as "none". private const uint NoMap = 0x10; internal sealed class Row @@ -54,9 +34,6 @@ namespace Ryujinx.Graphics.Vulkan private static long _heartbeatMs; private static long _totalDraws; - /// Rebuild the cached row set for the current (framebuffer, program, guest - /// masks) state. Called only when one of the three changed; per-draw cost is just - /// incrementing the returned rows. public static Row[] Resolve(FramebufferParams fb, ShaderCollection program, uint[] guestMasks, int guestMaskCount) { if (!_armedLogged) @@ -147,7 +124,6 @@ namespace Ryujinx.Graphics.Vulkan _rows.Values.CopyTo(snapshot, 0); } - // 720p targets first (the MV twins live there), then by traffic. Array.Sort(snapshot, (a, b) => { bool a720 = a.H == 720; diff --git a/src/Ryujinx.Graphics.Vulkan/MvppReadBarProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppReadBarProbe.cs index 8033ad04f..f94735743 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppReadBarProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppReadBarProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -8,25 +5,6 @@ using System.Threading; namespace Ryujinx.Graphics.Vulkan { - /// - /// [MVREADBAR] Temporary VALIDATION experiment (RYUJINX_MVREADBAR=1), inert unless set. - /// Tests the last strong branch of the XC2 stale-motion tree: an INTRA-frame read-during-write - /// hazard -- the motion-map builder sampling the object-MV buffer while the geometry pass's - /// writes to it are not yet visible (journal 143; the old clue "a readback CHANGES the - /// artifact" (112) is exactly the timing sensitivity this family predicts). - /// - /// Every GAL-level barrier entry (Barrier, TextureBarrier, ...) routes through the DEFERRED - /// batching system (Gd.Barriers) -- the machinery under suspicion -- so this experiment - /// deliberately bypasses it: at DRAW time (bound textures are in-order by construction at the - /// backend), if any sampled texture is the MV buffer (A2B10G10R10 1280x720), the render pass - /// is ended and an IMMEDIATE full vkCmdPipelineBarrier (AllCommands -> AllCommands, - /// MemoryWrite -> MemoryRead|Write) is recorded before the draw -- the same immediate pattern - /// the fork's optical-flow code uses. Expected hits: a handful of draws per frame. - /// flat-block counter ~0 -> ROOT: the deferred barrier system leaves the geometry->builder - /// edge unordered on this driver; the proper fix is a correct dependency at that edge; - /// flat blocks remain -> branch dead; back to the CPU-write-tracking branch with all facts. - /// The [MVREADBAR] counter is the witness: zero applications = void, never "innocent". - /// static class MvppReadBarProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppSampStoreProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppSampStoreProbe.cs index d94370efc..3339315ec 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppSampStoreProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppSampStoreProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.Shader; @@ -10,20 +7,6 @@ using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Vulkan { - /// - /// [SAMPSTORE] (RYUJINX_SAMPSTORE=1, inert unless set). Journal 187, read-only. - /// - /// The 186 run sharpened the paradox: the MV twin X receives ~15k REAL exported writes - /// per frame (attachment storage id stable all run) yet its content is garbage at rest. - /// Writers write, readers read poison -- so measure the one thing never compared: - /// the storage IDENTITY the readers actually sample. Records every sampled view shaped - /// like the MV twins (A2B10G10R10, 1280x720) by (storage id, stage, binding). - /// - /// Read TOGETHER with [OMAPVENT] rows from the SAME session (RuntimeHelpers hashes are - /// per-instance): sampled ids == attachment ids => identity split dead, poison lives - /// inside the image itself; a sampled id that matches NO attachment id => the readers - /// consume a different storage than the writers fill = the mechanism. - /// static class MvppSampStoreProbe { public static readonly bool Enabled = diff --git a/src/Ryujinx.Graphics.Vulkan/MvppStorageBarrierProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppStorageBarrierProbe.cs index 272902fd8..18cd3aa60 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppStorageBarrierProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppStorageBarrierProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using System; @@ -9,33 +6,6 @@ using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Vulkan { - /// - /// [STORAGEID] Read-only probe (RYUJINX_STORAGEID=1), inert unless set. Emits no barrier and - /// changes nothing that is rendered. - /// - /// Why. Ryujinx PR #4596 added protection against recent Nvidia GPUs clearing or rewriting a - /// render target before the previous frame finished sampling it -- the PR explicitly names - /// Xenoblade 1/2/3. The protection is a heuristic living in TextureStorage: - /// - /// QueueWriteToReadBarrier : _lastReadAccess |= dstAccessFlags (this surface was READ) - /// QueueLoadOpBarrier : if ((_lastModificationAccess | _lastReadAccess) != None) barrier - /// - /// _lastReadAccess is an INSTANCE field. The barrier therefore only fires if the surface is the - /// SAME TextureStorage instance from the frame that read it to the frame that writes it. Journal - /// (112) suspected that our surface -- a pure GPU target with empty guest memory -- does not keep a - /// stable identity across frames: a new instance starts with _lastReadAccess = None, the condition - /// is false, no barrier is emitted, and the clear races the previous frame's sampling. That would - /// explain what nothing else did: it survives full CPU/GPU serialisation (a content problem, not a - /// timing one), a readback "fixes" it (it forces the cache to re-resolve), and it only shows in - /// motion. That angle was never instrumented -- the agent assigned to it crashed. - /// - /// So this probe measures the two faces of that claim: - /// 1. how many distinct TextureStorage instances a given full-screen shape goes through, and - /// 2. how often QueueLoadOpBarrier runs with NOTHING recorded, i.e. the barrier is SKIPPED. - /// - /// Identity uses RuntimeHelpers.GetHashCode so no field has to be added to a hot class. It is - /// unique per live instance, which is exactly the lifetime we are asking about. - /// static class MvppStorageBarrierProbe { public static readonly bool Enabled = @@ -43,7 +13,6 @@ namespace Ryujinx.Graphics.Vulkan private static bool _armedLogged; - // Per shape: the set of distinct instances seen, plus barrier emitted / skipped counters. private sealed class ShapeStats { public readonly HashSet Instances = new(); @@ -68,18 +37,6 @@ namespace Ryujinx.Graphics.Vulkan Logger.Warning?.Print(LogClass.Gpu, "[STORAGEID] ARMED (RYUJINX_STORAGEID=1). Expect periodic lines for full-screen targets."); } - /// - /// Called from QueueLoadOpBarrier. is the real decision the - /// heuristic just took, not a guess: false means nothing was recorded on this instance, so the - /// protection did not fire for this write. - /// - /// - /// is the measurement that actually matters, and it is NOT the - /// same as "a barrier was emitted". srcAccessFlags = _lastModificationAccess | _lastReadAccess, - /// and _lastModificationAccess is re-armed at the end of every load op, so from the second - /// write onwards a barrier is emitted almost always -- covering the previous WRITE. Only - /// _lastReadAccess makes it cover the previous READ, which is the hazard PR #4596 is about. - /// public static void OnLoadOp(object storage, int width, int height, string format, bool barrierEmitted, bool readRecorded) { if (!Enabled) @@ -87,8 +44,6 @@ namespace Ryujinx.Graphics.Vulkan return; } - // Full-screen colour targets only: those are the surfaces journal (111)/(112) is about, and - // an unfiltered log here would be enormous and would itself perturb the frame timing. if (width != 1280 || height != 720) { return; @@ -119,8 +74,6 @@ namespace Ryujinx.Graphics.Vulkan _loadOpTotal++; - // Periodic summary rather than one line per event: what matters is the RATIO of skipped - // barriers and the NUMBER of distinct instances, not any individual occurrence. if ((_loadOpTotal % 400) == 0) { foreach (KeyValuePair kv in _stats) diff --git a/src/Ryujinx.Graphics.Vulkan/MvppVkDropProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppVkDropProbe.cs index 21b157d11..dfe3fa6a6 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppVkDropProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppVkDropProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -8,27 +5,6 @@ using System; namespace Ryujinx.Graphics.Vulkan { - /// - /// [VKDROP] Read-only probe (RYUJINX_VKDROP_PROBE=1), inert unless set. Journal 165. - /// - /// Last floor of the search: every FS value scrub was proven to cover 100% of the XC2 - /// object-MV buffer's writers (census v3) and yet the buffer holds saturated garbage at - /// rest (periscope) -- so the poison enters BETWEEN the fragment store and the image - /// memory. FramebufferParams silently treats a bound colour target whose TextureView is - /// INVALID (orphaned view after a storage swap) as a HOLE: the attachment is dropped for - /// that framebuffer, so neither draws nor clears touch the image while the game believes - /// they do -- leaving stale VkImage memory that flickers, which is exactly what the - /// periscope shows. No earlier instrument watched this boundary (they all lived at the - /// Gpu Texture level, above the view). - /// - /// This probe watches every SetRenderTargets bind: for colour targets shaped like the MV - /// buffer (R10G10B10A2Unorm 1280x720) it counts valid binds vs INVALID-view binds. - /// drops = 0, validBinds > 0 -> the view never flaps; floor closed -> next: clear - /// execution / host write mask. - /// drops > 0 -> smoking gun: frames where the MV attachment silently - /// vanished; next = find who invalidates the view. - /// Heartbeat every ~3 s even at zero (a negative must be distinguishable from a mute probe). - /// static class MvppVkDropProbe { public static readonly bool Enabled = @@ -49,7 +25,6 @@ namespace Ryujinx.Graphics.Vulkan view.Info.Format == Format.R10G10B10A2Unorm; } - /// Called at every SetRenderTargetsInternal with the raw guest-ordered colour span. public static void OnBind(Span colors) { if (!Enabled) diff --git a/src/Ryujinx.Graphics.Vulkan/MvppVkImgProbe.cs b/src/Ryujinx.Graphics.Vulkan/MvppVkImgProbe.cs index 022da63ee..127ab298c 100644 --- a/src/Ryujinx.Graphics.Vulkan/MvppVkImgProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/MvppVkImgProbe.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -10,41 +7,12 @@ using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Vulkan { - /// - /// [VKIMG] Read-only probe (RYUJINX_VKIMG_PROBE=1), inert unless set. Journal 166. - /// - /// Read-side twin check. Every write-side door is closed by measurement (values, view - /// validity, clear command/index/execution, guest uploads, copies) and the buffer is still - /// poisoned as READ. Last mechanism compatible with every constraint: the consumers' - /// descriptors point at a STALE TWIN -- a TextureView that is perfectly valid but whose - /// underlying TextureStorage (VkImage) is not the one the G-buffer pass renders into - /// (storage replacement recreates the image; a readback forces resolution, which is the - /// old (112) clue "readback CHANGES the artifact"). No instrument ever compared VkImage - /// identity between the ATTACHMENT side and the SAMPLED side. - /// - /// This probe records, for MV-shaped views (R10G10B10A2Unorm 1280x720): - /// - the TextureStorage identity of every view bound as a COLOUR ATTACHMENT; - /// - the TextureStorage identity of every view bound as a SAMPLED texture; - /// and flags sampled identities never seen on the attachment side. - /// all sampled ids ALSO seen as attachments -> one image, no twin; floor closed; - /// sampled id NEVER seen as attachment -> smoking gun: consumers read a stale twin. - /// Heartbeat every ~3 s even at zero. - /// static class MvppVkImgProbe { - // NOTE: reads the VKPHASE env var directly instead of the PhaseEnabled field -- static - // initializers run in declaration order, and reading a later field here silently yields - // false (cost one VOID run, journal 168). public static readonly bool Enabled = Environment.GetEnvironmentVariable("RYUJINX_VKIMG_PROBE") == "1" || Environment.GetEnvironmentVariable("RYUJINX_VKPHASE_PROBE") == "1"; - // [VKPHASE] (sonde 23, journal 167) VKIMG found TWO MV-shaped storages, both written and - // both sampled -- the game double-buffers its MV target. The discriminating question is - // PER-FRAME PHASE: does each frame sample the half it just wrote (healthy) or the other - // one (emulator out of phase => every measurement of the week explained)? This mode - // tags storages with stable letters (A, B, ... by first-seen order) and logs the DISTINCT - // per-frame signatures "W:A R:A" with counts, folded at present. public static readonly bool PhaseEnabled = Environment.GetEnvironmentVariable("RYUJINX_VKPHASE_PROBE") == "1"; @@ -59,7 +27,6 @@ namespace Ryujinx.Graphics.Vulkan private static readonly HashSet _sampledIds = new(); private static readonly HashSet _staleIds = new(); - // Phase mode state (all under _attachIds lock). private static readonly Dictionary _letters = new(); private static readonly HashSet _frameWrites = new(); private static readonly HashSet _frameReads = new(); @@ -96,7 +63,6 @@ namespace Ryujinx.Graphics.Vulkan } } - /// Colour-target bind (SetRenderTargetsInternal): record attachment-side storage ids. public static void OnBind(Span colors) { if (!Enabled) @@ -132,7 +98,6 @@ namespace Ryujinx.Graphics.Vulkan Heartbeat(); } - /// Sampled-texture bind (DescriptorSetUpdater, both paths): record + cross-check. public static void OnSampled(TextureView view) { if (!Enabled || !IsMvShape(view)) @@ -176,8 +141,6 @@ namespace Ryujinx.Graphics.Vulkan Heartbeat(); } - /// Frame boundary (Vulkan Window.Present): fold the per-frame write/read letter sets - /// into a signature and count it. New signatures are logged immediately. public static void OnPresent() { if (!PhaseEnabled) @@ -227,7 +190,7 @@ namespace Ryujinx.Graphics.Vulkan { if (PhaseEnabled) { - return; // phase mode reports at present, not per bind + return; } long now = Environment.TickCount64; diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs index 42a1deab8..396268fb2 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs @@ -95,15 +95,10 @@ namespace Ryujinx.Graphics.Vulkan private readonly PipelineColorBlendAttachmentState[] _storedBlend; - // [OMAPMASK-VK] (journal 183) raw guest colour masks + last applied fragment output map, - // for re-derivation when the bound program changes. See MvppOmapMaskVk. private readonly uint[] _mvppGuestColorMasks = new uint[Constants.MaxRenderTargets]; private int _mvppGuestMaskCount; private int _mvppLastOutputMap = -1; - // [OMAPVENT] (journal 185) cached ventilation rows for the current - // (framebuffer, program, guest masks) state; rebuilt lazily on the next draw - // after any of the three changes. Read-only probe, self-gated. private MvppOmapVentProbe.Row[] _mvppVentRows; private bool _mvppVentDirty = true; public ulong DrawCount { get; private set; } @@ -157,17 +152,8 @@ namespace Ryujinx.Graphics.Vulkan Gd.Barriers.QueueMemoryBarrier(); } - // [INJECT étape C3b] When the mid-frame inject ran DLSS into the real dst AND evaluate succeeded, this - // is set so the VERY NEXT draw (the guest upscale draw, queued right after the inject command, in - // order) is skipped -- letting the DLSS output survive. Default false => guest draw runs (fallback). - // Only ever set when RYUJINX_DLSS_UPSCALE_PASS is on, so the flag-off path is byte-identical. private bool _mvppSkipNextDraw; - // [INJECT étape C] Mid-frame DLSS injection hook, reached IN ORDER at the detected guest upscale draw. - // End the current render pass (mirroring ClearBuffer/compute mid-frame ops), run DLSS into the REAL - // destination RT via the Window's DlssUpscaler, and -- if it wrote the dst (evaluate OK) and - // substitution is active -- arm a skip of the next (guest) draw so the DLSS output is not overwritten. - // On any failure the skip is NOT armed => the guest draw runs = automatic fallback. Gated: no-op unless on. public void MvppInjectDlssUpscale(Ryujinx.Graphics.GAL.ITexture src, Ryujinx.Graphics.GAL.ITexture dst, Ryujinx.Graphics.GAL.ITexture depth) { if (!Ryujinx.Graphics.GAL.MvppInjectState.Enabled) @@ -262,7 +248,6 @@ namespace Ryujinx.Graphics.Vulkan ClearAttachment attachment = new(ImageAspectFlags.ColorBit, (uint)index, clearValue); ClearRect clearRect = FramebufferParams.GetClearRect(ClearScissor, layer, layerCount); - // [CLEARRECT] Read-only (gated): record the FINAL rect this clear will cover. MvppClearRectProbe.OnClear(FramebufferParams, index, clearRect, color.Red, color.Green, color.Blue, color.Alpha); Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect); @@ -365,10 +350,6 @@ namespace Ryujinx.Graphics.Vulkan Gd.Api.CmdDispatchIndirect(CommandBuffer, indirectBuffer.Get(Cbs, indirectBufferOffset, 12).Value, (ulong)indirectBufferOffset); } - // [MVREADBAR] (EXP 12, gated OFF by default) Immediate full barrier before any draw that - // samples the object-MV buffer, bypassing the deferred barrier batcher on purpose -- see - // MvppReadBarProbe. Must be recorded OUTSIDE a render pass (self-dependency rules), the - // next draw re-begins the pass automatically. private unsafe void MvppEmitReadBarrierIfNeeded() { if (!MvppReadBarProbe.Enabled || !_descriptorSetUpdater.MvppAnySampledMvBuffer()) @@ -400,12 +381,9 @@ namespace Ryujinx.Graphics.Vulkan MvppReadBarProbe.OnApplied(); } - // [OMAPVENT] (journal 185) draw-level ventilation of the omap rule by real target: - // resolves (framebuffer, program, guest masks) into aggregate rows only when one of - // them changed; steady-state per-draw cost is a few increments. Read-only. private void MvppVentOnDraw() { - MvppDofStateProbe.OnDraw(FramebufferParams, ref _newState); // [DOFSTATE] read-only, self-gated + MvppDofStateProbe.OnDraw(FramebufferParams, ref _newState); if (!MvppOmapVentProbe.Enabled) { @@ -423,8 +401,6 @@ namespace Ryujinx.Graphics.Vulkan public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance) { - // [INJECT étape C3b] The immediately-preceding inject command replaced this draw with DLSS - // (evaluate OK). Skip it once so the DLSS output survives. Only ever set under the feature flag. if (_mvppSkipNextDraw) { _mvppSkipNextDraw = false; @@ -446,7 +422,7 @@ namespace Ryujinx.Graphics.Vulkan BeginRenderPass(); DrawCount++; - MvppVentOnDraw(); // [OMAPVENT] read-only, self-gated + MvppVentOnDraw(); if (Gd.TopologyUnsupported(_topology)) { @@ -503,10 +479,8 @@ namespace Ryujinx.Graphics.Vulkan public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance) { - MvppEmitReadBarrierIfNeeded(); // [MVREADBAR] (gated) see Draw + MvppEmitReadBarrierIfNeeded(); - // [INJECT étape C3b] The immediately-preceding inject command replaced this draw with DLSS - // (evaluate OK). Skip it once so the DLSS output survives. Only ever set under the feature flag. if (_mvppSkipNextDraw) { _mvppSkipNextDraw = false; @@ -528,7 +502,7 @@ namespace Ryujinx.Graphics.Vulkan BeginRenderPass(); DrawCount++; - MvppVentOnDraw(); // [OMAPVENT] read-only, self-gated + MvppVentOnDraw(); if (_indexBufferPattern != null) { @@ -572,7 +546,7 @@ namespace Ryujinx.Graphics.Vulkan BeginRenderPass(); DrawCount++; - MvppVentOnDraw(); // [OMAPVENT] read-only, self-gated + MvppVentOnDraw(); if (_indexBufferPattern != null) { @@ -623,7 +597,7 @@ namespace Ryujinx.Graphics.Vulkan BeginRenderPass(); DrawCount++; - MvppVentOnDraw(); // [OMAPVENT] read-only, self-gated + MvppVentOnDraw(); if (_indexBufferPattern != null) { @@ -713,7 +687,7 @@ namespace Ryujinx.Graphics.Vulkan BeginRenderPass(); ResumeTransformFeedbackInternal(); DrawCount++; - MvppVentOnDraw(); // [OMAPVENT] read-only, self-gated + MvppVentOnDraw(); Gd.Api.CmdDrawIndirect(CommandBuffer, buffer, (ulong)indirectBuffer.Offset, 1, (uint)indirectBuffer.Size); } @@ -744,7 +718,7 @@ namespace Ryujinx.Graphics.Vulkan BeginRenderPass(); ResumeTransformFeedbackInternal(); DrawCount++; - MvppVentOnDraw(); // [OMAPVENT] read-only, self-gated + MvppVentOnDraw(); Gd.DrawIndirectCountApi.CmdDrawIndirectCount( CommandBuffer, @@ -758,7 +732,7 @@ namespace Ryujinx.Graphics.Vulkan public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion) { - MvppHistWatchProbe.OnDrawTexture(texture as TextureView, FramebufferParams); // [HISTWATCH] read-only, self-gated + MvppHistWatchProbe.OnDrawTexture(texture as TextureView, FramebufferParams); if (texture is TextureView srcTexture) { CullModeFlags oldCullMode = _newState.CullMode; @@ -838,7 +812,7 @@ namespace Ryujinx.Graphics.Vulkan public void SetBlendState(AdvancedBlendDescriptor blend) { Span colorBlendAttachmentStateSpan = _newState.Internal.ColorBlendAttachmentState.AsSpan(); - + for (int index = 0; index < Constants.MaxRenderTargets; index++) { ref PipelineColorBlendAttachmentState vkBlend = ref colorBlendAttachmentStateSpan[index]; @@ -1059,20 +1033,14 @@ namespace Ryujinx.Graphics.Vulkan public void SetProgram(IProgram program) { - // MV++ modprobe v2: any program change ends the "variant draw pending" span (the - // graft's own SetProgram(variant) precedes the binding-91 marker, the restore's - // SetProgram(base) follows the re-draw -- the span covers exactly our draws). _mvppVariantDrawPending = false; ShaderCollection internalProgram = (ShaderCollection)program; PipelineShaderStageCreateInfo[] stages = internalProgram.GetInfos(); _program = internalProgram; - _mvppVentDirty = true; // [OMAPVENT] + _mvppVentDirty = true; - // [OMAPMASK-VK] the effective masks depend on the fragment output map: re-derive - // them when the program's map differs from the one last applied (OpenGL does the - // same in its bind path). Terminates: the re-run updates _mvppLastOutputMap. if (MvppOmapMaskVk.Enabled && !internalProgram.IsCompute && _mvppGuestMaskCount > 0 && internalProgram.MvppFragmentOutputMap != _mvppLastOutputMap) { @@ -1127,12 +1095,10 @@ namespace Ryujinx.Graphics.Vulkan public void SetRenderTargetColorMasks(ReadOnlySpan componentMask) { - // [OMAPMASK-VK] keep the raw guest masks (re-derivation on program change) and apply - // the hardware rule: no fragment export = no write. Gated OFF by default. int guestCount = Math.Min(componentMask.Length, _mvppGuestColorMasks.Length); componentMask.Slice(0, guestCount).CopyTo(_mvppGuestColorMasks); _mvppGuestMaskCount = guestCount; - _mvppVentDirty = true; // [OMAPVENT] + _mvppVentDirty = true; Span maskedStorage = stackalloc uint[Constants.MaxRenderTargets]; scoped ReadOnlySpan effectiveMask = componentMask; @@ -1157,7 +1123,7 @@ namespace Ryujinx.Graphics.Vulkan int count = Math.Min(Constants.MaxRenderTargets, effectiveMask.Length); int writtenAttachments = 0; - + Span colorBlendAttachmentStateSpan = _newState.Internal.ColorBlendAttachmentState.AsSpan(); for (int i = 0; i < count; i++) @@ -1207,17 +1173,12 @@ namespace Ryujinx.Graphics.Vulkan private void SetRenderTargetsInternal(Span colors, ITexture depthStencil, bool filterWriteMasked) { - // [MVDUMP v2] (journal 190) dump the OUTGOING framebuffer's MV attachments at the - // moment the engine switches away from them = END-of-pass content, exactly what - // the resolve is about to sample. v1 fired at the FIRST draw of the pass and only - // ever photographed the pristine clear. The copy path ends any active render - // pass itself; the next draw re-begins its own. MvppMvDumpProbe.MaybeDump(FramebufferParams); - MvppVkDropProbe.OnBind(colors); // [VKDROP] read-only, self-gated - MvppVkImgProbe.OnBind(colors); // [VKIMG] read-only, self-gated - MvppDecompProbe.OnBind(colors); // [MVDECOMP] tracking only here, self-gated - _mvppVentDirty = true; // [OMAPVENT] + MvppVkDropProbe.OnBind(colors); + MvppVkImgProbe.OnBind(colors); + MvppDecompProbe.OnBind(colors); + _mvppVentDirty = true; CreateFramebuffer(colors, depthStencil, filterWriteMasked); CreateRenderPass(); SignalStateChange(); @@ -1335,10 +1296,6 @@ namespace Ryujinx.Graphics.Vulkan public void SetUniformBuffers(ReadOnlySpan buffers) { - // MV++ (b1) 5B3 modprobe v2: binding 91 is EXCLUSIVELY the graft's N-1 cbuf (no - // guest formula can produce it) -- an IN-BAND, same-thread, race-free marker that - // the next draw is the variant re-draw (the VariantBound gate raced cross-thread - // and mislabeled run 8: a 64x64 shadow-pass pipeline got probed). if (_mvppModProbe) { foreach (BufferAssignment ba in buffers) @@ -1367,7 +1324,7 @@ namespace Ryujinx.Graphics.Vulkan int count = Math.Min(Constants.MaxVertexAttributes, vertexAttribs.Length); uint dirtyVbSizes = 0; - + Span vertexAttributeDescriptionsSpan = _newState.Internal.VertexAttributeDescriptions.AsSpan(); for (int i = 0; i < count; i++) @@ -1418,7 +1375,7 @@ namespace Ryujinx.Graphics.Vulkan BufferHandle lastHandle = default; Auto lastBuffer = null; - + Span vertexBindingDescriptionsSpan = _newState.Internal.VertexBindingDescriptions.AsSpan(); for (int i = 0; i < count; i++) @@ -1875,21 +1832,14 @@ namespace Ryujinx.Graphics.Vulkan return true; } - // MV++ (b1) 5B3 VS-INSPECT: the silent draw-skip exits of CreatePipeline, instrumented - // one-shot (capped 30 each) -- months of "proof by construction" hid a skipped variant - // re-draw behind these logless returns. Diagnostic only, removable after the verdict. private static int _mvppSkipLinkLogs; private static int _mvppSkipNullLogs; private static int _mvppSkipStageLogs; - // MV++ (b1) 5B3 modprobe (RYUJINX_MVPP_VEL_MODHASH=1): at the VARIANT re-draw's pipeline - // creation (VariantBound raised by the graft), log the module hashes ACTUALLY bound and - // the REAL state (viewport/scissor-count/cull/topology/rasterizerDiscard/depth) -- the - // H1/H2/H3 splitter: identity of the executed module + the silent geometry killers. private static readonly bool _mvppModProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_MODHASH") == "1"; private static readonly HashSet _mvppProbedPrograms = new(); - private bool _mvppVariantDrawPending; // in-band: set by a binding-91 SetUniformBuffers + private bool _mvppVariantDrawPending; private bool CreatePipeline(PipelineBindPoint pbp) { @@ -1963,8 +1913,6 @@ namespace Ryujinx.Graphics.Vulkan } else if (_mvppSkipStageLogs < 30) { - // MV++ diag: third silent path -- no stages set, returns true WITHOUT binding - // (the draw then runs on whatever pipeline was bound before). _mvppSkipStageLogs++; Logger.Warning?.Print(LogClass.Gpu, $"MVPP pipeskip: Stages==null, no bind (program=0x{_program?.GetHashCode() ?? 0:X8}) [{_mvppSkipStageLogs}/30]."); diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs index 539446e5c..88ba80de3 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs @@ -42,8 +42,6 @@ namespace Ryujinx.Graphics.Vulkan _pendingQueryCopies.Clear(); } - // [MVDECOMP] (EXP 20, gated OFF by default) present-time hook: forced metadata - // decompression round-trip on the tracked MV storages, outside any render pass. internal void MvppDecompTick() { if (!MvppDecompProbe.Enabled) @@ -67,8 +65,6 @@ namespace Ryujinx.Graphics.Vulkan // We can't use CmdClearAttachments if not writing all components, // because on Vulkan, the pipeline state does not affect clears. // On proprietary Adreno drivers, CmdClearAttachments appears to execute out of order, so it's better to not use it at all. - // [DRAWCLEAR] (gated, default OFF) also takes this path for the XC2 MV buffer, to test - // whether CmdClearAttachments ordering is the source of the stale-motion rectangles. TextureView dstTexture = FramebufferParams.GetColorView(index); if (dstTexture == null) { diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs index 307fae8ed..92506f282 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs @@ -474,11 +474,6 @@ namespace Ryujinx.Graphics.Vulkan ScissorCount = ScissorsCount, }; - // Declared here, not inside the if: its address is chained into viewportState.PNext - // and only read ~200 lines later by vkCreateGraphicsPipelines. A local's storage is - // only guaranteed for its own block, so the previous form let the stack slot be - // reused by a later local while the driver still held a pointer to it - the depth - // range the pipeline is built with then depends on codegen rather than on DepthMode. PipelineViewportDepthClipControlCreateInfoEXT viewportDepthClipControlState = new() { SType = StructureType.PipelineViewportDepthClipControlCreateInfoExt, @@ -490,9 +485,6 @@ namespace Ryujinx.Graphics.Vulkan viewportState.PNext = &viewportDepthClipControlState; } - // Gated (RYUJINX_VK_PROVOKING_LAST=1), see ProvokingVertexOverride. Declared out here - // and not inside the if for the same reason as viewportDepthClipControlState above: - // its address is held until vkCreateGraphicsPipelines runs ~200 lines below. PipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState = new() { SType = StructureType.PipelineRasterizationProvokingVertexStateCreateInfoExt, @@ -543,17 +535,6 @@ namespace Ryujinx.Graphics.Vulkan Span colorBlendAttachmentStateSpan = Internal.ColorBlendAttachmentState.AsSpan(); - // [Beast Roofer diag] RYUJINX_NOFRAG_MASK0=1 (EXP 9, gated OFF by default): when the - // pipeline has NO fragment stage (depth/stencil-only draws -- XC2 issues ~6 per frame - // with the motion-vector MRT still bound, stale blend=ON and mask=0xF), the Vulkan - // spec leaves colour attachment output UNDEFINED, while the console guarantees no - // colour write at all. Forcing the write masks to 0 reproduces console semantics - // exactly -- fully generic (no game data). If the XC2 stale-motion artifact dies with - // this on, those undefined writes were the root; see journal (134). - // Like the MoltenVK workaround below, the change is applied only for the create call - // and RESTORED right after: `Internal` doubles as the cache key, and leaving it - // mutated would poison every later pipeline (first version of this experiment did - // exactly that -- whole screen lost its image). bool nofragSanitized = false; Span nofragSavedMasks = stackalloc ColorComponentFlags[Constants.MaxRenderTargets]; uint nofragSavedBlend = 0; @@ -708,8 +689,6 @@ namespace Ryujinx.Graphics.Vulkan Result result = gd.Api.CreateGraphicsPipelines(device, cache, 1, &pipelineCreateInfo, null, &pipelineHandle); - // [Beast Roofer diag] NOFRAG: restore the state object right after the create call, - // on every path -- it is also the pipeline cache key (see comment at the save site). if (nofragSanitized) { for (int i = 0; i < Constants.MaxRenderTargets; i++) @@ -801,7 +780,7 @@ namespace Ryujinx.Graphics.Vulkan { Span vertexBindingDescriptionsSpan = Internal.VertexBindingDescriptions.AsSpan(); - + for (int index = 0; index < VertexBindingDescriptionsCount; index++) { if (vertexBindingDescriptionsSpan[index].Binding == binding) diff --git a/src/Ryujinx.Graphics.Vulkan/ProvokingVertexOverride.cs b/src/Ryujinx.Graphics.Vulkan/ProvokingVertexOverride.cs new file mode 100644 index 000000000..f5b35ea48 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/ProvokingVertexOverride.cs @@ -0,0 +1,14 @@ +using System; + +namespace Ryujinx.Graphics.Vulkan +{ + internal static class ProvokingVertexOverride + { + internal const string ExtensionName = "VK_EXT_provoking_vertex"; + + internal static readonly bool Requested = + Environment.GetEnvironmentVariable("RYUJINX_VK_PROVOKING_LAST") == "1"; + + internal static bool Active; + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/RenderSyncSwitch.cs b/src/Ryujinx.Graphics.Vulkan/RenderSyncSwitch.cs index 022bac576..9c08c7622 100644 --- a/src/Ryujinx.Graphics.Vulkan/RenderSyncSwitch.cs +++ b/src/Ryujinx.Graphics.Vulkan/RenderSyncSwitch.cs @@ -1,5 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room diagnostic code. using Ryujinx.Common.Logging; using System; @@ -7,46 +5,17 @@ using System.Threading; namespace Ryujinx.Graphics.Vulkan { - /// - /// Discriminating test for the "render-target read-after-write synchronization hazard" hypothesis - /// for the Xenoblade camera-motion artefact. OFF by default. - /// - /// After the aliasing hypothesis was refuted by measurement (CopyToImpl never called on XC2, 0/3000 - /// presents), and the upstream family (Eden #322, present on Ryujinx 1.3.3) points at render-target - /// readback/synchronization, this switch forces MAXIMAL inter-pass synchronization to see whether a - /// missing barrier is the cause. - /// - /// RYUJINX_FORCE_RP_BARRIER=1 inserts a FULL global memory barrier (all writes -> all reads/writes, - /// all stages) at the end of EVERY render pass, so every render target's writes are made available - /// and visible before any later pass can sample them. - /// - artefact VANISHES => a missing inter-pass barrier IS the cause. We hold the right thread. - /// - artefact SURVIVES => it is NOT a missing render-pass barrier; look elsewhere (intra-pass - /// self-dependency, or a genuine driver-tiling issue). - /// Very slow (FPS is irrelevant for this test), gated, reversible, released behaviour byte-identical - /// when the variable is unset. - /// static class RenderSyncSwitch { public static readonly bool ForceRenderPassBarrier = Environment.GetEnvironmentVariable("RYUJINX_FORCE_RP_BARRIER") == "1"; - /// - /// RYUJINX_FORCE_GPU_IDLE=1 forces a full DeviceWaitIdle once per presented frame -- maximal - /// CPU/GPU serialization, the faithful mirror of what makes the screen-capture readback clean - /// (FlushAllCommands + fence wait). If the artefact vanishes, it is a CPU/GPU timing race (a - /// category, not a shippable fix). If it survives even this, synchronization is exhausted as a - /// cause. OFF by default. - /// public static readonly bool ForceGpuIdlePerFrame = Environment.GetEnvironmentVariable("RYUJINX_FORCE_GPU_IDLE") == "1"; private static int _announced; private static int _idleAnnounced; - /// - /// Logged the first time the forced barrier is inserted, so the run's log PROVES the switch is - /// armed (a diagnostic you cannot see in the log is worthless). - /// public static void AnnounceOnce() { if (Interlocked.Exchange(ref _announced, 1) == 0) @@ -57,10 +26,6 @@ namespace Ryujinx.Graphics.Vulkan } } - /// - /// Logged the first time the per-frame DeviceWaitIdle runs, so the run's log proves the switch - /// is armed. - /// public static void AnnounceIdleOnce() { if (Interlocked.Exchange(ref _idleAnnounced, 1) == 0) diff --git a/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs b/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs index 0594fb13f..a23af75e7 100644 --- a/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs @@ -6,15 +6,9 @@ namespace Ryujinx.Graphics.Vulkan { class SamplerHolder : ISampler { - // [MIPPROBE M4] see the gated log in the constructor. private static readonly bool _mipProbe = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_MIPPROBE") == "1"; - // [LODCOMP fix contrôlé, 10/07] RYUJINX_DLSS_LODBIAS_COMP=1: instead of blindly ADDING our LodBias - // on top of the guest's own MipLodBias (measured stacking: guest -1 + our -0.5 = -1.5 on the foliage - // family = the shimmer root, entry 51), samplers the game already biases NEGATIVE are brought to a - // TOTAL of 0 (simple target, A/B step 1). Never makes an existing negative bias worse. Everything - // else keeps the baseline behavior. Default OFF => byte-identical. private static readonly bool _lodComp = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_LODBIAS_COMP") == "1"; @@ -40,15 +34,12 @@ namespace Ryujinx.Graphics.Vulkan BorderColor borderColor = GetConstrainedBorderColor(info.BorderColor, out bool cantConstrain); - // [MIPPROBE M4, 10/07] Read-only: log every guest sampler's own MipLodBias at creation, with - // family hints (filters/aniso/wrap), to measure the REAL total bias (guest + our LodBias). - // Gated RYUJINX_MVPP_MIPPROBE=1, default OFF; no behavior change. float ourBias = Dlss.DlssJitter.Enabled ? Dlss.DlssJitter.LodBias : 0f; float totalBias = info.MipLodBias + ourBias; bool compApplied = _lodComp && Dlss.DlssJitter.Enabled && info.MipLodBias < 0f; if (compApplied) { - totalBias = 0f; // cible simple étape 1 : neutraliser l'empilement sur la famille biaisée + totalBias = 0f; } if (_mipProbe) @@ -69,9 +60,6 @@ namespace Ryujinx.Graphics.Vulkan AddressModeU = info.AddressU.Convert(), AddressModeV = info.AddressV.Convert(), AddressModeW = info.AddressP.Convert(), - // DLSS Mode B: while jitter is on, add a negative texture LOD bias (NVIDIA guidance) so the - // mip selection stays locked on a sharper level and does not flicker frame-to-frame under the - // sub-pixel jitter. No-op (bias 0) unless jitter is enabled, so the default path is unchanged. MipLodBias = totalBias, AnisotropyEnable = info.MaxAnisotropy != 1f, MaxAnisotropy = info.MaxAnisotropy, diff --git a/src/Ryujinx.Graphics.Vulkan/Shader.cs b/src/Ryujinx.Graphics.Vulkan/Shader.cs index ec6d14841..ff2094f5e 100644 --- a/src/Ryujinx.Graphics.Vulkan/Shader.cs +++ b/src/Ryujinx.Graphics.Vulkan/Shader.cs @@ -25,10 +25,6 @@ namespace Ryujinx.Graphics.Vulkan public ProgramLinkStatus CompileStatus { private set; get; } - // MV++ (b1) 5B3 modprobe: FNV64 of the SPIR-V bytes ACTUALLY handed to vkCreateShaderModule - // (identity check against the dumped .spv files -- the disk-cache trap hit twice already). - // Only computed under the probe: hashing every compiled shader byte-by-byte costs real time - // on a big game's first boot, and the probe log is the sole consumer. private static readonly bool _mvppModProbe = Environment.GetEnvironmentVariable("RYUJINX_MVPP_VEL_MODHASH") == "1"; public ulong SpirvHash { get; private set; } diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs index 86e99fe4f..0122f67a9 100644 --- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs @@ -54,9 +54,6 @@ namespace Ryujinx.Graphics.Vulkan private HashTableSlim> _graphicsPipelineCache; private HashTableSlim> _computePipelineCache; - // [OMAPMASK-VK] (journal 183) The guest fragment shader's output map (4 bits per target, - // from GAL ShaderInfo), set by VulkanRenderer.CreateProgram. -1 = unknown/internal - // program (helper shaders) = no masking. Mirrors OpenGL's Program.FragmentOutputMap. public int MvppFragmentOutputMap { get; set; } = -1; private readonly VulkanRenderer _gd; @@ -669,8 +666,6 @@ namespace Ryujinx.Graphics.Vulkan return false; } - // MV++ (b1) 5B3 modprobe: the per-stage FNV64 hashes of the modules this collection - // actually holds (identity check at the variant re-draw's pipeline creation). internal string GetMvppModuleHashes() { System.Text.StringBuilder sb = new(); diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag b/src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag index b60679703..177cda0da 100644 --- a/src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag +++ b/src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag @@ -4,13 +4,9 @@ layout (binding = 0, set = 2) uniform sampler2D tex; layout (std140, binding = 2) uniform hdr_params { - // x = paper white (scRGB multiplier), y = peak (scRGB multiplier), z = highlight curve exponent, w = SDR decode gamma vec4 hdr_params_data; - // highlight colour mix: 0 = luma (calm/white highlights), 1 = max channel (coloured lights pop) float hdr_blend_data; - // highlight whitening: 0 = keep colour (old look), 1 = fully desaturate extreme highlights to white float hdr_whiten_data; - // output encoding: 0 = scRGB linear (FP16 swapchain), 1 = HDR10 PQ (RGB10A2 + ST.2084 swapchain) float hdr_pq_data; float hdr_pad_data; }; @@ -18,19 +14,11 @@ layout (std140, binding = 2) uniform hdr_params layout (location = 0) in vec2 tex_coord; layout (location = 0) out vec4 colour; -// Decode the SDR image to linear light. SDR content is authored to be viewed on a display with a -// ~2.2 power gamma, NOT the exact piecewise sRGB curve. Decoding with the sRGB curve lifts the dark -// tones (sRGB is brighter than gamma 2.2 in the shadows), which raises blacks once the image is shown -// in HDR - the washed-out look we want to avoid on OLED. Decoding with gamma 2.2 keeps the blacks -// properly black and matches how the SDR image looked on a normal monitor. vec3 sdrToLinear(vec3 c, float gamma) { return pow(max(c, vec3(0.0)), vec3(gamma)); } -// First-pass SDR->HDR "inverse tone map": scale to paper white, then expand ONLY the -// highlights toward the peak. Blacks stay black (no constant lift) to avoid a washed look. -// scRGB: 1.0 == 80 nits, so paperWhite 2.0 ~= 160 nits, peak 8.0 ~= 640 nits. vec3 sdrToHdr(vec3 srgb) { float gamma = hdr_params_data.w; @@ -38,37 +26,24 @@ vec3 sdrToHdr(vec3 srgb) float paperWhite = hdr_params_data.x; float peak = hdr_params_data.y; float curve = hdr_params_data.z; - // Detect highlights by a blend of luma and the brightest channel (max RGB). Pure max over-drives - // the dominant channel of warm/saturated highlights (an orange sun looked flashy and the hue - // drifted as the panel clipped at peak); pure luma under-weights coloured lights (neon/fire/magic - // stayed dull). The 0.5 blend keeps coloured lights rising while pulling warm highlights back - // toward white. Greys are unaffected either way (max == luma). float luma = dot(lin, vec3(0.2126, 0.7152, 0.0722)); float maxc = max(max(lin.r, lin.g), lin.b); float hl = mix(luma, maxc, hdr_blend_data); float hlAmount = pow(clamp(hl, 0.0, 1.0), curve); float boost = mix(1.0, peak / paperWhite, hlAmount); vec3 outc = lin * paperWhite * boost; - // Desaturate the most extreme highlights toward white: a blinding light (sun, fire, a bright lamp) - // physically reads as white, not a saturated colour. Luminance-preserving (mix toward a grey of - // equal brightness) so only the harsh hue at the very top is removed, not the brightness itself. float t = hdr_whiten_data * hlAmount; float lumaOut = dot(outc, vec3(0.2126, 0.7152, 0.0722)); return mix(outc, vec3(lumaOut), t); } -// scRGB linear (Rec.709 primaries, 1.0 == 80 nits) -> HDR10 signal (Rec.2020 primaries + ST.2084 -// PQ curve), for an RGB10A2 swapchain in VK_COLOR_SPACE_HDR10_ST2084_EXT. Same light, different -// container: DLSS Frame Generation supports this encoding but not FP16/scRGB. vec3 scRgbToPq(vec3 scrgb) { - // Rec.709 -> Rec.2020 primaries (columns; GLSL mat3 is column-major). const mat3 rec709To2020 = mat3( 0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956); vec3 lin = max(rec709To2020 * scrgb, vec3(0.0)); - // Absolute light: scRGB 1.0 == 80 nits; PQ normalizes to 10000 nits. vec3 E = clamp(lin * (80.0 / 10000.0), vec3(0.0), vec3(1.0)); const float m1 = 0.1593017578125; const float m2 = 78.84375; diff --git a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs index f5661a5ff..0ca06f47b 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs @@ -36,7 +36,6 @@ namespace Ryujinx.Graphics.Vulkan AccessFlags.TransferReadBit | AccessFlags.TransferWriteBit; - // [BIRTHCLEAR diagnostic] Number of fresh allocations actually zeroed this session. private static int _birthClearCount; private readonly VulkanRenderer _gd; @@ -152,7 +151,6 @@ namespace Ryujinx.Graphics.Vulkan _allocationAuto = new Auto(allocation); - // [MEMALIAS] read-only, self-gated: tag MV-shaped images for overlap tracking. if (MvppMemAliasProbe.Enabled && info.Width == 1280 && info.Height == 720 && info.Format == Ryujinx.Graphics.GAL.Format.R10G10B10A2Unorm) @@ -161,21 +159,9 @@ namespace Ryujinx.Graphics.Vulkan } _imageAuto = new Auto(new DisposableImage(_gd.Api, device, _image), null, _allocationAuto); - // [BIRTHCLEAR] Fresh allocations only: the suballocator recycles freed device memory - // within the process, and on the DLSS-SR fractional path a render target can reach - // DLSS before the game's writes cover the quantized allocation -- the recycled bytes - // are then an earlier frame (the startup ghost image in Quality). Never on the - // foreign branch below: that memory aliases live content. - // RYUJINX_BIRTHCLEAR_ALL lifts the SrFractional gate for diagnosis (21/07 XC2 - // artifact on camera rotation, DLSS proven off) -- off by default, so the - // released paths keep their exact current behaviour. bool birthClear = (Dlss.DlssIntegration.SrFractional || Dlss.DlssIntegration.BirthClearAll) && !Dlss.DlssIntegration.BirthClearDisabled; - // A diagnostic switch that cannot be SEEN in the log is worse than no switch: - // "the artefact is still there" then means either "mechanism cleared" or "my - // variable did nothing", and there is no way to tell the two apart. Counted and - // reported so the negative result is trustworthy. if (birthClear) { int n = System.Threading.Interlocked.Increment(ref _birthClearCount); @@ -342,9 +328,6 @@ namespace Ryujinx.Graphics.Vulkan 1, in barrier); - // [BIRTHCLEAR] Zero the whole image so the first read sees defined data instead of the - // recycled allocation's previous contents. Transfer clears are forbidden on compressed - // formats (and those textures are always fully written from guest data anyway). if (zeroFill && !_info.IsCompressed) { if ((aspectFlags & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit)) != 0) @@ -562,9 +545,6 @@ namespace Ryujinx.Graphics.Vulkan AccessFlags.DepthStencilAttachmentWriteBit | AccessFlags.DepthStencilAttachmentReadBit : AccessFlags.ColorAttachmentWriteBit | AccessFlags.ColorAttachmentReadBit; - // [STORAGEID, READ-ONLY] Record whether the PR #4596 protection actually fires for this - // write, and how many distinct storage instances this shape goes through. Gated; the - // decision below is untouched. MvppStorageBarrierProbe.ReportArmed(); MvppStorageBarrierProbe.OnLoadOp(this, Info.Width, Info.Height, Info.Format.ToString(), srcAccessFlags != AccessFlags.None, _lastReadAccess != AccessFlags.None); diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index e1eb80b20..902da60fb 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -36,8 +36,6 @@ namespace Ryujinx.Graphics.Vulkan public int Width => Info.Width; public int Height => Info.Height; - // DLSS-SR fractional-render: active sub-rect the scene was actually rendered into. - // Falls back to Width/Height for normal textures (TextureCreateInfo ctor guarantees != 0). public int ActiveWidth => Info.ActiveWidth; public int ActiveHeight => Info.ActiveHeight; @@ -218,7 +216,7 @@ namespace Ryujinx.Graphics.Vulkan public void CopyTo(ITexture destination, int firstLayer, int firstLevel) { - MvppHistWatchProbe.OnCopy(this, destination as TextureView, "full"); // [HISTWATCH] read-only, self-gated + MvppHistWatchProbe.OnCopy(this, destination as TextureView, "full"); TextureView src = this; TextureView dst = (TextureView)destination; @@ -281,7 +279,7 @@ namespace Ryujinx.Graphics.Vulkan public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel) { - MvppHistWatchProbe.OnCopy(this, destination as TextureView, "layer"); // [HISTWATCH] read-only, self-gated + MvppHistWatchProbe.OnCopy(this, destination as TextureView, "layer"); TextureView src = this; TextureView dst = (TextureView)destination; @@ -339,7 +337,7 @@ namespace Ryujinx.Graphics.Vulkan public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter) { - MvppHistWatchProbe.OnCopy(this, destination as TextureView, "blit"); // [HISTWATCH] read-only, self-gated + MvppHistWatchProbe.OnCopy(this, destination as TextureView, "blit"); TextureView dst = (TextureView)destination; if (_gd.CommandBufferPool.OwnedByCurrentThread) @@ -781,7 +779,7 @@ namespace Ryujinx.Graphics.Vulkan /// public void SetData(MemoryOwner data) { - MvppHistWatchProbe.OnSetData(this); // [HISTWATCH] read-only, self-gated + MvppHistWatchProbe.OnSetData(this); SetData(data.Span, 0, 0, Info.GetLayers(), Info.Levels, singleSlice: false); data.Dispose(); } @@ -789,7 +787,7 @@ namespace Ryujinx.Graphics.Vulkan /// public void SetData(MemoryOwner data, int layer, int level) { - MvppHistWatchProbe.OnSetData(this); // [HISTWATCH] read-only, self-gated + MvppHistWatchProbe.OnSetData(this); SetData(data.Span, layer, level, 1, 1, singleSlice: true); data.Dispose(); } @@ -797,7 +795,7 @@ namespace Ryujinx.Graphics.Vulkan /// public void SetData(MemoryOwner data, int layer, int level, Rectangle region) { - MvppHistWatchProbe.OnSetData(this); // [HISTWATCH] read-only, self-gated + MvppHistWatchProbe.OnSetData(this); SetData(data.Span, layer, level, 1, 1, singleSlice: true, region); data.Dispose(); } diff --git a/src/Ryujinx.Graphics.Vulkan/UnsafeBlitProbe.cs b/src/Ryujinx.Graphics.Vulkan/UnsafeBlitProbe.cs index 63e477b66..f3bcf6054 100644 --- a/src/Ryujinx.Graphics.Vulkan/UnsafeBlitProbe.cs +++ b/src/Ryujinx.Graphics.Vulkan/UnsafeBlitProbe.cs @@ -1,5 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room diagnostic code. using Ryujinx.Common.Logging; using System; @@ -8,32 +6,8 @@ using System.Threading; namespace Ryujinx.Graphics.Vulkan { - /// - /// Diagnostic for the Xenoblade "camera-motion" artefact (XC2/XC3/XCX), OFF by default. - /// - /// HYPOTHESIS (21/07, after four host-path readers cleared every CPU layout-conversion path): - /// the corruption autocorrelates at 8 lines = the height of ONE host Nvidia GOB tile (64B x 8), - /// NOT the guest Tegra swizzle -- which never runs on the composed scene target, whose guest - /// memory is empty. The one surviving host mechanism that reinterprets a render target's bytes - /// under a DIFFERENT format sharing the SAME device memory is the UseUnsafeBlit path in - /// TextureView.CopyToImpl: it builds a second, format-aliased VkImage over the live allocation - /// (CreateAliasedStorageUnsafe). A missing aliasing barrier there is a per-vendor hazard -- which - /// matches "only when the camera moves", "builds up", intermittent, clean under readback (GetData - /// forces flush + fence), and the upstream 'amd-vendor-bug' classification on stock Ryujinx and - /// Eden since 2024. - /// - /// TWO SWITCHES, both OFF by default so released behaviour is byte-identical when unset: - /// - RYUJINX_UNSAFE_BLIT_PROBE=1 : log only. Announces itself ARMED, counts EVERY CopyToImpl - /// call and splits safe-vs-unsafe, so a null result is unambiguous. (SONDE -- run first.) - /// - RYUJINX_NO_UNSAFE_BLIT=1 : forces the SAFE HelperShader.Blit path instead. If the artefact - /// dies, the hazard is confirmed; if it survives, refuted -- with nothing broken. (BOUTON A/B.) - /// - /// Generic by construction: no game name, no hardcoded resolution. It logs EVERY distinct blit - /// signature and lets a human read which shapes appear. - /// static class UnsafeBlitProbe { - /// RYUJINX_NO_UNSAFE_BLIT=1 forces CopyToImpl onto the safe shader-blit path. public static readonly bool KillSwitch = Environment.GetEnvironmentVariable("RYUJINX_NO_UNSAFE_BLIT") == "1"; @@ -50,13 +24,6 @@ namespace Ryujinx.Graphics.Vulkan private static long _presents; private static int _presentReported; - /// - /// Called unconditionally from Window.Present (which is guaranteed to run). Its FIRST line - /// prints the env value as the rendering process actually sees it -- so a silent probe can be - /// told apart from "env never reached the process" (A) versus "CopyToImpl never called" (B). - /// Then, if armed, it periodically reports the CopyToImpl totals independently of whether the - /// unsafe branch ever fired. - /// public static void PresentTick() { long p = Interlocked.Increment(ref _presents); @@ -84,11 +51,6 @@ namespace Ryujinx.Graphics.Vulkan $"{dst.Info.Format} {dst.Width}x{dst.Height}(store={dstStore})"; } - /// - /// Called at the TOP of CopyToImpl on EVERY call. Announces the probe armed the first time (so - /// "no unsafe blit" can never be confused with "probe was not running"), counts all calls, and - /// periodically reports how many took the UNSAFE format-aliased branch. - /// public static void NoteEntry(TextureView src, TextureView dst, bool srcUsesStorageFormat, bool dstUsesStorageFormat) { if (!_probe) @@ -119,10 +81,6 @@ namespace Ryujinx.Graphics.Vulkan } } - /// - /// Called when the UNSAFE format-aliased branch is actually taken. Records each distinct shape - /// once, loudly, so we can read whether the corrupt-buffer shape ever passes through here. - /// public static void Note(TextureView src, TextureView dst, bool srcUsesStorageFormat, bool dstUsesStorageFormat) { if (!_probe) @@ -139,10 +97,6 @@ namespace Ryujinx.Graphics.Vulkan } } - /// - /// Called the first time the kill-switch actually diverts a blit, so the A/B run's log PROVES - /// the switch is armed (a diagnostic you cannot see in the log is worse than no diagnostic). - /// public static void AnnounceKillOnce() { if (Interlocked.Exchange(ref _killAnnounced, 1) == 0) diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index d9273f200..9b3fbb6ad 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -271,12 +271,6 @@ namespace Ryujinx.Graphics.Vulkan internal static Device CreateDevice(Vk api, VulkanPhysicalDevice physicalDevice, uint queueFamilyIndex, uint queueCount, out uint opticalFlowQueueFamilyIndex) { - // DLSS-G (RYUJINX_DLSS_FG) needs its OWN command queues in this family, which Streamline - // fetches at vkGetDeviceQueue(family, hostQueueCount + i) during swapchain setup. Ryujinx - // normally caps the family at QueuesCount (2, both host-used: present + background), leaving - // NO room -> SL's createCommandQueue fails on index 2 and crashes at slHookVkCreateSwapchainKHR. - // When FG is requested, keep ALL of the family's queues so SL's queues (indices >= 2) exist. - // The extra queues are otherwise untouched by Ryujinx. Flag off -> capped at QueuesCount (unchanged). int cap = Dlss.DlssIntegration.FrameGenRequested ? (int)queueCount : QueuesCount; if (queueCount > cap) { @@ -290,10 +284,6 @@ namespace Ryujinx.Graphics.Vulkan queuePriorities[i] = 1f; } - // NVOFA (B2 multi-queue): vkCmdOpticalFlowExecuteNV is only legal on a queue family that - // advertises VK_QUEUE_OPTICAL_FLOW_BIT_NV (on NVIDIA a dedicated family, NOT graphics). When - // DLSS is on and such a family exists, request a second queue from it so optical flow work can - // be submitted on the correct family. uint.MaxValue means "no OFA queue". opticalFlowQueueFamilyIndex = uint.MaxValue; if (Dlss.DlssIntegration.IsEnabled && physicalDevice.IsDeviceExtensionPresent("VK_NV_optical_flow")) { @@ -303,12 +293,6 @@ namespace Ryujinx.Graphics.Vulkan { opticalFlowQueueFamilyIndex = i; - // Diagnostic (NVOFA): dump the chosen optical-flow family's properties. When a user's - // vkCreateDevice REJECTS this dedicated queue (the INITIALIZATION_FAILED some NVIDIA - // configs hit), their log then shows WHAT is different about their OFA family - // (queueCount / flags / timestampValidBits) versus a working one -- the lever for a - // real fix instead of the queue-drop fallback. One Info line; the driver version is - // printed by PrintGpuInformation right after. QueueFamilyProperties ofaProps = physicalDevice.QueueFamilyProperties[i]; QueueFamilyProperties gfxProps = physicalDevice.QueueFamilyProperties[queueFamilyIndex]; Logger.Info?.Print(LogClass.Gpu, @@ -459,10 +443,6 @@ namespace Ryujinx.Graphics.Vulkan SType = StructureType.PhysicalDeviceOpticalFlowFeaturesNV, }; - // Query support for the NVOFA (optical flow) features, so we only ENABLE them below when the - // device actually advertises them. Every other feature block already does this; this pair was - // the lone exception. Chained only when DLSS is on and the extension is present, so non-DLSS - // device creation is left unchanged. if (Dlss.DlssIntegration.IsEnabled && physicalDevice.IsDeviceExtensionPresent("VK_NV_optical_flow")) { supportedFeaturesSync2.PNext = features2.PNext; @@ -588,8 +568,6 @@ namespace Ryujinx.Graphics.Vulkan if (Dlss.DlssIntegration.IsEnabled) { - // DLSS/NGX requires these Vulkan 1.2 features. Enable them only if the device - // advertises support, mirroring how the other features above are gated. featuresVk12.TimelineSemaphore = supportedPhysicalDeviceVulkan12Features.TimelineSemaphore; featuresVk12.BufferDeviceAddress = supportedPhysicalDeviceVulkan12Features.BufferDeviceAddress; } @@ -656,8 +634,6 @@ namespace Ryujinx.Graphics.Vulkan pExtendedFeatures = &featuresDepthClipControl; } - // Gated (RYUJINX_VK_PROVOKING_LAST=1), see ProvokingVertexOverride. Nothing is chained and - // no extension is enabled when the gate is closed, so device creation is byte-identical. PhysicalDeviceProvokingVertexFeaturesEXT featuresProvokingVertex; if (ProvokingVertexOverride.Requested && @@ -676,8 +652,6 @@ namespace Ryujinx.Graphics.Vulkan ProvokingVertexOverride.Active = true; } - // Witness line. Without it, a request that could not be honoured is indistinguishable - // from a request that changed nothing, and "no visible change" becomes uninterpretable. if (ProvokingVertexOverride.Requested) { if (ProvokingVertexOverride.Active) @@ -722,17 +696,6 @@ namespace Ryujinx.Graphics.Vulkan pExtendedFeatures = &featuresDynamicAttachmentFeedbackLoopLayout; } - // NVOFA (B2): the DEDICATED optical-flow queue (a 2nd VkDeviceQueue on the NVIDIA OFA family) - // is OUR OWN addition, used only by NvOpticalFlow for hardware motion vectors. Some NVIDIA - // GPU/driver combos reject a device created with it (VK_ERROR_INITIALIZATION_FAILED at - // vkCreateDevice), which killed the renderer at startup for users who turned DLSS on. We now - // (a) enable the OFA / sync2 FEATURES only when the device reports them supported (queried - // above), and (b) if creation WITH the dedicated queue fails, retry WITHOUT that queue (see the - // device-creation block below). The extension and features stay enabled either way: Streamline's - // DLSS-G (Frame Generation) needs them and submits its own optical flow on the GRAPHICS queue, - // not ours; and NvOpticalFlow falls back to its Lucas-Kanade estimator on its own when the queue - // is absent (its _queueReady stays false). RYUJINX_DLSS_NO_OFA forces the no-queue path so it can - // be validated on hardware that does NOT crash (e.g. an RTX 5090). bool forceNoOpticalFlowQueue = Environment.GetEnvironmentVariable("RYUJINX_DLSS_NO_OFA") is "1" or "true" or "on"; @@ -741,8 +704,6 @@ namespace Ryujinx.Graphics.Vulkan bool sync2Supported = opticalFlowExtPresent && supportedFeaturesSync2.Synchronization2; bool opticalFlowSupported = opticalFlowExtPresent && supportedFeaturesOpticalFlow.OpticalFlow; - // Whether to request OUR dedicated OFA queue. Gated on support + a distinct OFA family existing - // + not forced off. If we won't use it, drop the family index so the caller skips the queue. bool useOpticalFlowQueue = !forceNoOpticalFlowQueue && opticalFlowQueueFamilyIndex != uint.MaxValue && sync2Supported && opticalFlowSupported; @@ -758,9 +719,6 @@ namespace Ryujinx.Graphics.Vulkan PhysicalDeviceSynchronization2Features featuresSync2; PhysicalDeviceOpticalFlowFeaturesNV featuresOpticalFlow; - // Enable the OFA / sync2 features whenever the device supports them -- KEPT even if we drop our - // dedicated queue, because Streamline's DLSS-G relies on them. synchronization2 is also a hard - // dependency of VK_NV_optical_flow. if (sync2Supported) { featuresSync2 = new() @@ -794,21 +752,11 @@ namespace Ryujinx.Graphics.Vulkan if (Dlss.DlssIntegration.IsEnabled) { - // Add the device extensions NGX/DLSS needs (only those the device actually supports). enabledExtensions = enabledExtensions .Union(Dlss.DlssIntegration.DeviceExtensions.Intersect(physicalDevice.DeviceExtensions)) .ToArray(); } - // NVOFA DIAG (temporary instrument, read-only): the crash on some NVIDIA configs is a - // device-creation failure (ErrorInitializationFailed) when optical flow is enabled, and it - // persists even without our dedicated OFA queue -> the FEATURE/EXTENSION is what the driver - // rejects, not the queue. VK_NV_optical_flow REQUIRES VK_KHR_synchronization2 AND - // VK_KHR_format_feature_flags2; the .Intersect above silently drops any dependency the driver - // does not advertise as a listed extension (they are core since Vulkan 1.3, so some drivers - // stop listing them), which would enable OFA WITHOUT its dependency. This one line makes an - // affected user's log name the exact missing piece before the crash: device Vulkan version, - // driver version, and, for each dependency, whether it is advertised vs actually enabled. if (opticalFlowExtPresent) { uint apiVer = physicalDevice.PhysicalDeviceProperties.ApiVersion; @@ -830,8 +778,6 @@ namespace Ryujinx.Graphics.Vulkan ppEnabledExtensions[i] = Marshal.StringToHGlobalAnsi(enabledExtensions[i]); } - // The extensions and the feature (pNext) chain are IDENTICAL across both attempts; only our - // dedicated OFA queue (the 2nd DeviceQueueCreateInfo) is dropped on the fallback attempt. DeviceCreateInfo deviceCreateInfo = new() { SType = StructureType.DeviceCreateInfo, @@ -847,11 +793,6 @@ namespace Ryujinx.Graphics.Vulkan if (result != Result.Success && useOpticalFlowQueue) { - // Some NVIDIA GPU/driver combos reject the device when our dedicated optical-flow queue is - // requested (seen as VK_ERROR_INITIALIZATION_FAILED). Retry WITHOUT that queue so DLSS -- and - // Streamline's Frame Generation, which uses the graphics queue and keeps the extension -- - // still run; NvOpticalFlow falls back to Lucas-Kanade. The result code names the reason in - // the affected user's log. Logger.Warning?.Print(LogClass.Gpu, $"NVOFA: device creation with the dedicated optical-flow queue failed ({result}); retrying without it. " + "DLSS and Frame Generation stay on; hardware optical flow (NvOpticalFlow) is disabled."); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 585f9a245..f385db21b 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -37,13 +37,10 @@ namespace Ryujinx.Graphics.Vulkan internal HardwareCapabilities Capabilities; internal Vk Api { get; private set; } - internal PhysicalDevice PhysicalDevice => _physicalDevice.PhysicalDevice; // for NVOFA raw image memory-type selection + internal PhysicalDevice PhysicalDevice => _physicalDevice.PhysicalDevice; internal KhrSurface SurfaceApi { get; private set; } internal KhrSwapchain SwapchainApi { get; private set; } - // Porte B.1: non-null ONLY when RYUJINX_DLSS_FG=1, the DLSS_G probe succeeded and every - // mandatory Streamline Vulkan proxy resolved. Window.cs then routes swapchain/present - // through it; null (the default) keeps the present path exactly as it is today. private Dlss.DlssFrameGenHooks _fgHooks; internal Dlss.DlssFrameGenHooks FgHooks => _fgHooks; internal ExtConditionalRendering ConditionalRenderingApi { get; private set; } @@ -56,7 +53,7 @@ namespace Ryujinx.Graphics.Vulkan internal uint QueueFamilyIndex { get; private set; } internal Queue Queue { get; private set; } internal Queue BackgroundQueue { get; private set; } - internal Queue OpticalFlowQueue { get; private set; } // NVOFA dedicated queue (B2 multi-queue); default null + internal Queue OpticalFlowQueue { get; private set; } internal uint OpticalFlowQueueFamilyIndex { get; private set; } = uint.MaxValue; internal Lock BackgroundQueueLock { get; private set; } internal Lock QueueLock { get; private set; } @@ -89,9 +86,6 @@ namespace Ryujinx.Graphics.Vulkan public IWindow Window => _window; - // [INJECT étape C] Bridge from the mid-frame pipeline hook to the DLSS driver owned by the Window. - // Carries the frame's own source + destination handles (from the command, not a side-channel). - // Returns true if DLSS wrote the real dst and the guest draw should be skipped (C3b). internal bool MvppRunInjectMidFrame(CommandBufferScoped cbs, GAL.ITexture src, GAL.ITexture dst, GAL.ITexture depth) { return (_window as Window)?.MvppRunInjectMidFrame(cbs, src, dst, depth) ?? false; @@ -327,9 +321,6 @@ namespace Ryujinx.Graphics.Vulkan bool supportsAttachmentFeedbackLoop = _physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_layout"); - // [NOFBL] (EXP 19, journal 170, gated OFF by default): report the feedback-loop - // extensions as absent so image usage bits, pipeline create flags and the detection - // machinery stay consistently inert -- one switch, one variable. if (MvppNoFblProbe.Enabled && supportsAttachmentFeedbackLoop) { MvppNoFblProbe.ReportStripped(); @@ -547,8 +538,6 @@ namespace Ryujinx.Graphics.Vulkan QueueFamilyIndex = queueFamilyIndex; - // NVOFA (B2 multi-queue): acquire the dedicated optical-flow queue if one was requested at - // device creation. vkCmdOpticalFlowExecuteNV must be submitted here, not on the graphics queue. if (opticalFlowQueueFamilyIndex != uint.MaxValue) { Api.GetDeviceQueue(_device, opticalFlowQueueFamilyIndex, 0, out Queue opticalFlowQueue); @@ -619,8 +608,6 @@ namespace Ryujinx.Graphics.Vulkan program = new ShaderCollection(this, _device, sources, info.ResourceLayout); } - // [OMAPMASK-VK] carry the fragment output map so the pipeline can apply the - // hardware write rule (mirrors what OpenGL's Program has always stored). program.MvppFragmentOutputMap = info.FragmentOutputMap; return program; @@ -970,8 +957,6 @@ namespace Ryujinx.Graphics.Vulkan private void ProbeDlssSupport() { - // Opt-in only (RYUJINX_DLSS=1), and DLSS only runs on NVIDIA RTX hardware. When the flag - // is unset nothing here runs, so the default path never touches Streamline. if (!Dlss.DlssIntegration.IsEnabled || Vendor != Vendor.Nvidia) { return; @@ -979,8 +964,6 @@ namespace Ryujinx.Graphics.Vulkan try { - // The user drops the Streamline DLLs (MIT) and their own nvngx_dlss.dll into a - // "dlss" folder next to the executable. Nothing proprietary is ever shipped. string dlssFolder = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "dlss"); string ngx = Dlss.DlssBinaries.LocateNgxDlss(dlssFolder); @@ -1001,9 +984,6 @@ namespace Ryujinx.Graphics.Vulkan return; } - // Porte A (RYUJINX_DLSS_FG=1): pre-flight the FG binaries with DISTINCT logs, so a - // failed probe is attributable - SL plugin absent (bundlable, MIT) vs proprietary - // NGX binary absent (BYO) vs device/queue rejection (logged by the probe itself). bool fgRequested = Dlss.DlssIntegration.FrameGenRequested; if (fgRequested) { @@ -1015,9 +995,6 @@ namespace Ryujinx.Graphics.Vulkan "(MIT, bundlable - drop them next to sl.interposer.dll)."); } - // Porte B.1: NGX only loads nvngx_dlssg.dll from the plugin folder, so stage the - // DriverStore copy in automatically (previously a manual copy). Runs BEFORE - // slInit so the DLSS_G plugin finds it on its very first load. string ngxG = Dlss.DlssBinaries.StageNgxDlssG(dlssFolder, out string ngxGAction); Logger.Info?.Print(LogClass.Gpu, ngxG != null ? $"DLSS-FG: nvngx_dlssg.dll {ngxGAction}." @@ -1026,39 +1003,25 @@ namespace Ryujinx.Graphics.Vulkan if (Dlss.Streamline.Initialize(streamlineFolder, fgRequested)) { - // Register our natively-created device with SL (mandatory, since we don't use - // SL's vkCreate* proxies), then query DLSS support for this physical device. Dlss.Streamline.SetVulkanInfo( (IntPtr)_instance.Instance.Handle, (IntPtr)_physicalDevice.PhysicalDevice.Handle, (IntPtr)_device.Handle, QueueFamilyIndex, - // Porte B.2: index where SL's own queues start = the count of queues the HOST uses in - // this family (index 0 = present, index 1 = background if created). 0 made SL treat - // our present queue as unknown ("Invalid VK app queue"). FG-only; SR keeps 0 (no-op). fgRequested ? (BackgroundQueue.Handle != 0 ? 2u : 1u) : 0u); Dlss.Streamline.IsDlssSupported((IntPtr)_physicalDevice.PhysicalDevice.Handle); if (fgRequested) { - // After SetVulkanInfo on purpose: the answer must reflect our real - // device/queue mapping, not a default adapter. bool fgSupported = Dlss.Streamline.IsDlssGSupported((IntPtr)_physicalDevice.PhysicalDevice.Handle); - // Porte B.1: arm the manual-hooking proxies (ManualHooking guide §4.2). - // TryCreate logs, per entry point, whether SL actually installed a proxy, - // and refuses to arm on a partial set - in which case the present path - // stays native and this run's logs say exactly which hook was missing. if (fgSupported) { _fgHooks = Dlss.DlssFrameGenHooks.TryCreate(Api, _device); if (_fgHooks != null) { - // The first swapchain predates slInit (created in SetupContext), so - // it was created natively; force a recreation so the next present - // rebuilds it THROUGH the SL proxy and hands SL the present path. (_window as Window)?.MarkSwapchainDirty(); Logger.Info?.Print(LogClass.Gpu, @@ -1199,9 +1162,6 @@ namespace Ryujinx.Graphics.Vulkan _counters.Dispose(); _window.Dispose(); - // Release Streamline/NGX cleanly while the device is still alive. Without this slShutdown the NGX - // state was never torn down, and the NEXT launch crashed in VulkanInitialization.CreateDevice - // (access violation) trying to re-create a DLSS/optical-flow device over the leftover state. Dlss.Streamline.Shutdown(); HelperShader.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 7dddd3e7d..02894f1a3 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -29,23 +29,9 @@ namespace Ryujinx.Graphics.Vulkan private int _frameIndex; - // DLSS-FG Porte B.1: true when the CURRENT swapchain was created through the Streamline - // manual-hooking proxy. Every call on a swapchain must go through the API that created it - // (SL tracks its own handles), so this is recorded at creation and consulted everywhere. private bool _swapchainViaFgProxy; private bool _fgPresentLogged; - // [FGFEED 02/08] Nourrir la FG pendant les doublons (journal (353), suite du diagnostic - // (348)-(352)) : quand la presentation invitee ne porte AUCUN dessin (sequence - // RenderedFrameSeq immobile, produite par Gpu/MvppCameraCapture), on AVALE le present - // hote en entier — rien n'est acquis, rien n'est evalue, la fonction retourne. Streamline - // ne voit alors QUE les vraies images (~25-30/s en rotation BOTW) et la FG interpole - // entre elles avec de vrais MVs : des intermediaires a la place des fantomes. Surete : - // le backend Vulkan n'appelle jamais swapBuffersCallback et le release de la texture - // invitee se fait au retour du Present quoi qu'il arrive (Gpu/Window.cs:531-533) — avaler - // est invisible pour l'invite. Exige la FG armee (proxy SL), sinon INERTE : sans FG, - // avaler = saccade nue (lecon DUPSKIP (352)). Plafond de 8 avalages consecutifs - // (~130 ms) : un menu statique continue de presenter ~7 img/s, jamais un gel. private static readonly bool _fgFeedEnabled = System.Environment.GetEnvironmentVariable("RYUJINX_DLSS_FGFEED") == "1"; private const int FgFeedMaxConsecutive = 8; @@ -61,7 +47,7 @@ namespace Ryujinx.Graphics.Vulkan private VSyncMode _vSyncMode; private bool _swapchainIsDirty; private VkFormat _format; - private bool _isHdrPq; // swapchain is RGB10A2 + HDR10/ST.2084: present shaders must PQ-encode + private bool _isHdrPq; private bool _pqFilterBypassLogged; private AntiAliasing _currentAntiAliasing; private bool _updateEffect; @@ -70,80 +56,53 @@ namespace Ryujinx.Graphics.Vulkan private Dlss.DlssUpscaler _dlss; private bool _dlssCropLogged; - // [COLORCAP snapshot prototype] held copies of the native scene color, keyed by (w,h,fmt), - // parallel to DlssUpscaler._heldDepthCache. Gated on RYUJINX_MVPP_COLORCAP_SNAP; nothing wired - // to DLSS. Proves the native color survives to present via a GPU copy. private readonly System.Collections.Generic.Dictionary<(int, int, GAL.Format), TextureView> _heldColorCache = new(); private long _colorSnapLastSig = -1; - private long _colorSnapDiagMs; // [COLORCAP snapshot] throttled bridge-state diagnostic + private long _colorSnapDiagMs; private readonly bool _colorDumpEnabled = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_COLORDUMP") == "1"; - private bool _colorDumped; // [COLORCAP dump step 3] one-shot native+presented readback + private bool _colorDumped; - // [PRESENTCAP 18/07 — dossier cadre noir] Capture de la frame PRÉSENTÉE : la toute - // fin de chaîne (post-DLSS, post-blit final, post-encodage PQ), le seul segment - // qu'aucun instrument ne voyait — 200+ frames amont scannées sans le cadre pendant - // que l'écran le montre. Gate RYUJINX_MVPP_PRESENTCAP=1 (banc seulement) ; - // 1 frame / 2 s dès que DLSS évalue, cap 40 fichiers pres_* dans mvpp-dump\. - // Exige TransferSrc sur la swapchain : ajouté sous le MÊME gate et vérifié contre - // les capacités surface (leçon STORAGE_BIT du jour : jamais d'usage non déclaré). private static readonly bool _presentCapArmed = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_PRESENTCAP") == "1"; private bool _presentCapUsable; private int _presentCapCount; private long _presentCapLastMs; - private BufferHandle _presentCapBuffer; // staging réutilisé (la vue swapchain n'a PAS de storage : copie brute) + private BufferHandle _presentCapBuffer; private int _presentCapBufferSize; - private bool _presentCapPending; // copie enregistrée dans le cbs, lecture après le flush du present + private bool _presentCapPending; - // [21/07 v2] Rafale d'images CONSÉCUTIVES + capture appariée de la texture invitée. private static readonly int _presentCapBurst = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_PRESENTCAP_BURST"), out int pcb) && pcb > 0 ? pcb : 8; private static readonly int _presentCapGapMs = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_PRESENTCAP_GAP"), out int pcg) && pcg > 0 ? pcg : 3000; private static readonly int _presentCapMax = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_PRESENTCAP_MAX"), out int pcm) && pcm > 0 ? pcm : 48; - // Délai avant la PREMIÈRE rafale. Sans lui, les 48 captures partent pendant le chargement du jeu - // et le run entier est perdu -- exactement ce qui est arrivé au run 14:27 (captures 00:07 -> 00:24). private static readonly int _presentCapStartSec = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_PRESENTCAP_START"), out int pcs) && pcs > 0 ? pcs : 45; private long _presentCapArmMs; private bool _presentCapAnnounced; private int _presentCapBurstLeft; - private BufferHandle _presentCapGuestBuffer; // staging de la texture invitée, appariée à la même image + private BufferHandle _presentCapGuestBuffer; private int _presentCapGuestSize; private bool _presentCapGuestPending; private int _presentCapGuestW, _presentCapGuestH; private string _presentCapGuestFmt; - // [FLASHCAP / Mesure A] Detect flashes on the PRESENTED frame (readable) and auto-dump full frames on - // a spike, to SEE where the artifact is. Throttled sampling; default OFF. private readonly bool _flashCap = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP") == "1"; private byte[] _flashPrevSamples; private float _flashAvg, _flashMax; private int _flashDumpCount, _flashDumpNext; private long _flashLogMs, _flashLastTickMs; - // [FLASHCAP timed] Periodic mode for the global-shift measurement: every N seconds dump a triplet of - // CONSECUTIVE presented frames (spike-independent, unbiased sampling). Spike dumps are disabled in - // this mode so the captures are not biased toward artifact moments. Default OFF. private readonly int _flashTimedSec = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP_TIMED"), out int flashTimedSec) ? flashTimedSec : 0; private long _flashTimedNextMs; - // [FLASHCAP timed] Burst length (frames per trigger) and total dump cap, env-tunable so the sampling - // pattern (dense triplets vs sparse 1 Hz trajectory) can change without a rebuild. private readonly int _flashTimedBurst = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP_BURST"), out int flashTimedBurst) && flashTimedBurst > 0 ? flashTimedBurst : 3; private readonly int _flashTimedCap = int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP_CAP"), out int flashTimedCap) && flashTimedCap > 0 ? flashTimedCap : 45; - // [FLASHCAP FORCE, 10/07] Alternate arming gate so runs WITHOUT the inject path (e.g. ABMODE=B, - // where LastInjectMs is never written) can still capture. Dump-arming ONLY: no render change, - // no DLSS input change, no reconstruction change. Default OFF. private static readonly bool _flashForce = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_FLASHCAP_FORCE") == "1"; - // [SYNCFRAME, 21/07] Banc artefact XC2, piste n°1 « synchronisation » : force une attente GPU - // complète après la soumission du command buffer de present, À CHAQUE IMAGE. Attente pure : - // aucun état de rendu touché, aucune passe ajoutée, aucun buffer lu. Banc uniquement (perte de - // FPS assumée), à ne jamais migrer vers le daily. Default OFF. private static readonly bool _syncFrame = System.Environment.GetEnvironmentVariable("RYUJINX_MVPP_SYNCFRAME") == "1"; private static bool _syncFrameLive = _syncFrame; @@ -175,36 +134,19 @@ namespace Ryujinx.Graphics.Vulkan CreateSwapchain(); } - // DLSS-FG Porte B.1: the swapchain API matching the CURRENT swapchain. Flag off (or hooks - // not armed) => always the native SwapchainApi, i.e. exactly today's path. private KhrSwapchain ActiveSwapchainApi => _swapchainViaFgProxy && _gd.FgHooks != null ? _gd.FgHooks.SwapchainProxy : _gd.SwapchainApi; - /// - /// DLSS-FG Porte B.1: forces the swapchain to be recreated at the next present - used once - /// when the Streamline proxies get armed, so the present path moves under SL's hooks. - /// public void MarkSwapchainDirty() { _swapchainIsDirty = true; } - // [INJECT étape C2b] Entry point for the mid-frame DLSS injection, reached in command order from - // PipelineBase.MvppInjectDlssUpscale via VulkanRenderer. Reuses the single present-path DlssUpscaler - // instance (viewport 1); no-op until DLSS has been created on the first present. The source handle + - // destination dims are the frame's own, carried through the command. public bool MvppRunInjectMidFrame(CommandBufferScoped cbs, GAL.ITexture src, GAL.ITexture dst, GAL.ITexture depth) { return _dlss?.RunInjectMidFrame(cbs, src, dst, depth) ?? false; } - // [CANARY v2 19/07 — DeviceLost au recreate, SR sans FG] Les 5 logs Leleta (4 jeux) : sortie - // de plein écran → 2-3 recreates sous contexte DLSS-SR vivant → DeviceLost au flush d'une - // frame ULTÉRIEURE. Cause : les vues de swapchain (la SORTIE que NGX consomme et référence - // entre les frames) et la swapchain elle-même étaient détruites IMMÉDIATEMENT — le même - // défaut que _fgRetired a déjà corrigé pour nos textures internes. Remède canonique Vulkan : - // OldSwapchain chaîné à la création + destruction DIFFÉRÉE (4 presents) de l'ancienne - // chaîne et de ses vues, une fois la profondeur de file de tout consommateur drainée. private readonly System.Collections.Generic.List<(SwapchainKHR Chain, KhrSwapchain Api, TextureView[] Views, int RetireAfter)> _retiredSwapchains = new(); private void DrainRetiredSwapchains(bool all) @@ -229,30 +171,16 @@ namespace Ryujinx.Graphics.Vulkan SwapchainKHR oldSwapchain = _swapchain; _swapchainIsDirty = false; - // [SWAPDIAG -- Canary probe, read-only] Swapchain recreation is the DeviceLost trigger - // (fullscreen toggle / in-game resolution change under DLSS). Log the size transition and the - // DLSS/FG state so the crash log pinpoints the moment. Behaviour is unchanged. int diagOldW = _width, diagOldH = _height; Logger.Info?.Print(LogClass.Gpu, $"SWAPDIAG recreate BEGIN: old {_width}x{_height} dlss={Dlss.DlssIntegration.IsEnabled} fgProxy={_swapchainViaFgProxy} dlssCtx={_dlss != null} retire=deferred"); - // [CANARY v2] Vues et chaîne PARQUÉES, plus de destruction immédiate — capture de - // l'API et des vues de l'ANCIENNE chaîne avant que CreateSwapchain ne les remplace. - // [v2b] Périmètre : SR-SEUL uniquement. Sous le proxy FG, une vieille chaîne retenue - // étrangle les workers dlfg (hang GPU -> syncpoint/query timeouts -> DeviceLost, - // run MK8+FG 08:17) ; le chemin FG garde son protocole éprouvé (teardown + waits + - // destruction immédiate). Le trou de Leleta/principale est fgProxy=False. TextureView[] oldViews = _swapchainImageViews; KhrSwapchain oldApi = ActiveSwapchainApi; bool oldWasProxy = _swapchainViaFgProxy; - // DLSS-FG Porte B.1: when the old swapchain lives under SL's proxy, the wait must go - // through eVulkan_DeviceWaitIdle so Streamline retires in-flight FG work before the - // teardown (ManualHooking guide, sl_hooks.h). Otherwise: the native wait, as always. if (_swapchainViaFgProxy && _gd.FgHooks != null) { - // Resize protocol: DLSS-G must be eOff BEFORE the old swapchain is destroyed, - // or dlfg's present workers hang the device (fullscreen-toggle crash). Dlss.StreamlineFrameGen.OnSwapchainTeardown(Dlss.DlssUpscaler.ViewportId); _gd.FgHooks.DeviceWaitIdle(); @@ -275,13 +203,6 @@ namespace Ryujinx.Graphics.Vulkan } } - // [CANARY v2c] Le périmètre se décide AVANT la création (leçon v2b : sur le chemin FG, - // créer la nouvelle chaîne pendant que l'ancienne vit encore SANS chaînage OldSwapchain - // = ErrorInitializationFailed, surface exclusive — bascule VRR 08:23). Chemin FG (ancien - // OU nouveau côté proxy) : ordre HISTORIQUE, destruction AVANT CreateSwapchain. Chemin - // SR-seul : l'ancienne vit à travers la création (liée via OldSwapchain) puis part au - // parc — destruction dans 4 presents, quand plus aucun consommateur (NGX SR, présents - // en vol) ne peut la référencer. bool parkable = oldSwapchain.Handle != 0 && !oldWasProxy && _gd.FgHooks == null; if (oldSwapchain.Handle != 0 && !parkable) @@ -298,8 +219,6 @@ namespace Ryujinx.Graphics.Vulkan if (parkable) { - // [v2e] +4 -> +8 : la validation a vu des vues détruites encore référencées par - // des descriptor sets (caches plus longs qu'un cycle de cbs). Marge doublée. _retiredSwapchains.Add((oldSwapchain, oldApi, oldViews, _frameIndex + 8)); } @@ -354,8 +273,6 @@ namespace Ryujinx.Graphics.Vulkan $"HDR PQ: swapchain {surfaceFormat.Format} + HDR10/ST.2084 (FG-compatible HDR container)."); } - // [PRESENTCAP] TransferSrc demandé sous gate seulement ; si la surface ne - // l'offre pas, la capture reste désarmée — jamais d'usage non déclaré. _presentCapUsable = _presentCapArmed && capabilities.SupportedUsageFlags.HasFlag(ImageUsageFlags.TransferSrcBit); if (_presentCapArmed && !_presentCapUsable) { @@ -402,30 +319,14 @@ namespace Ryujinx.Graphics.Vulkan SwizzleComponent.Blue, SwizzleComponent.Alpha); - // DLSS-FG Porte B.1: once the Streamline proxies are armed, the swapchain is created - // THROUGH them so SL owns the presentation path (eVulkan_CreateSwapchainKHR ... Present). - // GetSwapchainImages must use the same API: SL may hand back its own image set for FG - // injection. Flag off => FgHooks is null => native API, byte-for-byte today's behavior. bool viaFgProxy = _gd.FgHooks != null; KhrSwapchain swapchainApi = viaFgProxy ? _gd.FgHooks.SwapchainProxy : _gd.SwapchainApi; - // [CANARY v2b] Chaîner l'ancienne swapchain (pattern resize canonique Vulkan) SEULEMENT - // sur le chemin natif SR-seul : ni la nouvelle ni l'ancienne sous le proxy FG - // (_swapchainViaFgProxy décrit encore l'ANCIENNE chaîne ici). L'ancienne — « retired » — - // reste vivante jusqu'à son passage au parc _retiredSwapchains. Null au premier lancement. if (!viaFgProxy && !_swapchainViaFgProxy && oldSwapchain.Handle != 0) { swapchainCreateInfo.OldSwapchain = oldSwapchain; } - // DLSS-FG Porte B.2 (blocage n°2): DLSS-G refuses to run under VSync - // (FeatureRequirementFlags::eVSyncOffRequired, sl_core_types.h; sl.log: "VSync with - // FG: not supported"), and the default VRR choice is FifoRelaxed - still the FIFO - // family. Measured: under Mailbox, DLSSGState reported vsyncOK=0 and FG did NOT generate - // (framesPresentedSinceLastPoll ~= 0) - Mailbox still paces to refresh, so DLSS-G does not - // treat it as VSync-off. Prefer IMMEDIATE (true VSync-off, what DLSS-G wants); it is also - // the classic "G-Sync + VSync off" pairing, so with the user's VRR it engages G-Sync and - // does not tear. Fall back to Mailbox only if Immediate is unavailable. Flag off => untouched. if (viaFgProxy) { PresentModeKHR fgMode = @@ -452,9 +353,6 @@ namespace Ryujinx.Graphics.Vulkan if (swapchainCreateInfo.PresentMode != PresentModeKHR.ImmediateKhr && swapchainCreateInfo.PresentMode != PresentModeKHR.MailboxKhr) { - // sl_core_types.h: FeatureRequirementFlags::eVSyncOffRequired - DLSS-G refuses - // to interpolate under VSync ("VSync with FG: not supported" in sl.log). B.1 only - // warns; actually forcing the mode is a later, deliberate step. Ryujinx.Common.Logging.Logger.Warning?.Print(Ryujinx.Common.Logging.LogClass.Gpu, $"DLSS-FG: present mode {swapchainCreateInfo.PresentMode} implies VSync; DLSS-G requires VSync OFF (eVSyncOffRequired). Set VSync to Unbounded before enabling FG."); } @@ -469,15 +367,6 @@ namespace Ryujinx.Graphics.Vulkan swapchainApi.GetSwapchainImages(_device, _swapchain, &imageCount, pSwapchainImages); } - // [BIRTHCLEAR-SWAPCHAIN] Presentable images start as recycled device memory, and on - // the DLSS-FG path the Streamline proxy can present before our first blit covers - // them -- the seconds-long startup ghost image (user repro 13/07: FG on = ghost, - // FG off = gone). Zero them once at creation; the daily (no DLSS) is untouched. - // [CANARY v2e 19/07] Scopé au chemin FG-PROXY seulement : toucher une image - // présentable AVANT son premier acquire est illégal (violation nommée par la - // validation, run 08:42 — la classe « has not been acquired »), et le chemin SR - // n'en a pas besoin (le blit pleine-image couvre l'image dès le premier present). - // Le repro du 13/07 était FG-on uniquement ; le remède reste où le mal vivait. if (viaFgProxy) { BirthClearSwapchainImages(); @@ -485,8 +374,6 @@ namespace Ryujinx.Graphics.Vulkan if (viaFgProxy) { - // DLSS-FG Porte B.2: DLSSGOptions wants numBackBuffers + the swapchain format - // (sl_dlss_g.h); pushed here where the ACTUAL image count is known. Dlss.StreamlineFrameGen.SwapchainImageCount = imageCount; Dlss.StreamlineFrameGen.SwapchainFormat = (uint)surfaceFormat.Format; Dlss.StreamlineFrameGen.SwapchainWidth = extent.Width; @@ -520,11 +407,6 @@ namespace Ryujinx.Graphics.Vulkan } } - /// - /// [BIRTHCLEAR-SWAPCHAIN] Zeroes every swapchain image once, right after creation, so - /// anything presented before the first real blit shows black instead of recycled VRAM. - /// Same command-buffer acquisition pattern as TextureStorage.InitialTransition. - /// private unsafe void BirthClearSwapchainImages() { CommandBufferScoped cbs; @@ -573,8 +455,6 @@ namespace Ryujinx.Graphics.Vulkan _gd.Api.CmdClearColorImage(cbs.CommandBuffer, image, ImageLayout.TransferDstOptimal, in zeroColor, 1, in range); - // PresentSrc so an early present (the FG proxy path) finds a legal layout; our own - // present path transitions from Undefined anyway, so this never blocks the blit. ImageMemoryBarrier toPresent = new() { SType = StructureType.ImageMemoryBarrier, @@ -628,18 +508,11 @@ namespace Ryujinx.Graphics.Vulkan return new TextureView(_gd, _device, new DisposableImageView(_gd.Api, _device, imageView), info, format); } - // HDR output container: RGB10A2 + ST.2084 (PQ) instead of the default FP16 + scRGB. - // Same light, different encoding - needed because DLSS Frame Generation does not support - // FP16/scRGB swapchains (DLSS-G guide 11.0). Dev-gated while the PQ path is validated. private static readonly bool _hdrPqRequested = Environment.GetEnvironmentVariable("RYUJINX_HDR_PQ") is "1" or "true" or "on"; private static SurfaceFormatKHR ChooseSwapSurfaceFormat(SurfaceFormatKHR[] availableFormats, bool colorSpacePassthroughEnabled, bool hdrEnabled) { - // HDR output (scRGB inverse tone mapping): when HDR is enabled and the surface advertises the scRGB - // (extended sRGB linear) HDR colorspace, select the 16-bit float swapchain. That - // colorspace is only enumerated on a genuinely HDR-capable display/OS, so on an SDR - // display this branch never triggers and the normal SDR format is used unchanged. if (hdrEnabled) { if (_hdrPqRequested) @@ -721,9 +594,6 @@ namespace Ryujinx.Graphics.Vulkan private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, VSyncMode vSyncMode) { - // VRR: FifoRelaxed lets the display's variable refresh rate (G-Sync/FreeSync) drive the refresh - // in fullscreen, smoothing a fluctuating frame rate without tearing. Immediate (full VSync off) - // was tried first but could hang the GPU under load, so FifoRelaxed is used instead. if (vSyncMode == VSyncMode.Vrr && availablePresentModes.Contains(PresentModeKHR.FifoRelaxedKhr)) { return PresentModeKHR.FifoRelaxedKhr; @@ -757,10 +627,6 @@ namespace Ryujinx.Graphics.Vulkan public unsafe override void Present(ITexture texture, ITexture depthTexture, ImageCrop crop, Action swapBuffersCallback) { - // [FGFEED 02/08] Avalage des presentations-doublons — TOUT EN TETE : un present avale - // n'existe pour aucune couche Vulkan (pas d'acquire, pas de flush, pas de DLSS, pas - // de marqueurs PCL — la sequence de marqueurs par vraie image reste correcte pour la - // FG). Temoin d'armement a la PREMIERE presentation, decouple de l'usage ((348)). if (_fgFeedEnabled) { bool fgArmed = _swapchainViaFgProxy && _gd.FgHooks != null; @@ -777,9 +643,6 @@ namespace Ryujinx.Graphics.Vulkan } } - // Fenetre imprimee toutes les ~5 s DES QUE le gate est arme, avalages ou pas : - // « 0 avale » en plaine 60 fps doit se LIRE, pas se deduire d'un silence - // (un temoin absent est indiscernable d'un temoin casse — lecon (348)). long fgFeedNow = Environment.TickCount64; if (fgFeedNow - _fgFeedLogMs >= 5000) @@ -813,25 +676,16 @@ namespace Ryujinx.Graphics.Vulkan _fgFeedPresented++; } - MvppVkImgProbe.OnPresent(); // [VKPHASE] frame boundary, read-only, self-gated - _gd.PipelineInternal.MvppDecompTick(); // [MVDECOMP] EXP 20, self-gated + MvppVkImgProbe.OnPresent(); + _gd.PipelineInternal.MvppDecompTick(); _gd.PipelineInternal.AutoFlush.Present(); - // [CANARY v2] Détruit les swapchains parquées dont le délai de retrait est écoulé. DrainRetiredSwapchains(all: false); uint nextImage = 0; int semaphoreIndex = _frameIndex++ % _imageAvailableSemaphores.Length; - // [CANARY v2d — run validation 08:42] Deux violations nommées par la couche : (1) - // transition de layout sur une image présentable jamais (re-)acquise, (2) sémaphore - // d'acquire signalé jamais attendu. Même racine : recréer la swapchain APRÈS un - // acquire réussi (Suboptimal livre une image !) = image + sémaphore abandonnés en - // vol -> comportement indéfini -> le DeviceLost probabiliste au flush suivant. - // Pattern canonique : un recreate n'arrive qu'AVANT tout acquire (dirty en tête de - // boucle ; OutOfDate ne livre pas d'image) ; Suboptimal = on PRÉSENTE cette frame - // sur la chaîne sous-optimale (légal) et on recrée proprement au tour suivant. while (true) { if (_swapchainIsDirty) @@ -840,8 +694,6 @@ namespace Ryujinx.Graphics.Vulkan semaphoreIndex = (_frameIndex - 1) % _imageAvailableSemaphores.Length; } - // DLSS-FG: ActiveSwapchainApi re-evaluates each iteration on purpose - the - // RecreateSwapchain above can move the swapchain under the SL proxy mid-loop. Result acquireResult = ActiveSwapchainApi.AcquireNextImage( _device, _swapchain, @@ -858,8 +710,6 @@ namespace Ryujinx.Graphics.Vulkan if (acquireResult == Result.SuboptimalKhr) { - // Image bel et bien acquise : on la garde pour CETTE frame, la chaîne - // se refait au prochain passage (avant l'acquire), sans rien abandonner. _swapchainIsDirty = true; break; } @@ -876,7 +726,6 @@ namespace Ryujinx.Graphics.Vulkan } catch (System.Exception swapdiagEx) { - // [SWAPDIAG] Surface the DeviceLost context, then let it propagate unchanged. Logger.Error?.Print(LogClass.Gpu, $"SWAPDIAG DEVICELOST at present flush #1: {_width}x{_height} dlss={Dlss.DlssIntegration.IsEnabled} fgProxy={_swapchainViaFgProxy} -- {swapdiagEx.Message}"); throw; @@ -894,16 +743,11 @@ namespace Ryujinx.Graphics.Vulkan TextureView view = (TextureView)texture; - // [FLASHCAP / Mesure A] Per-frame flash detection on the PRESENTED frame (readable). Auto-dumps - // full frames on a spike so we can SEE where the artifact is. Gated, default OFF. if (_flashCap && view.Width > 0) { FlashCapPresent(view); } - // [COLORCAP snapshot] Copy the bridged NATIVE scene color into a held texture and log. Runs at - // PRESENT (after the game's composite), so a successful copy with matching dims proves the - // native 2666x1500 color still exists and is snapshot-able. Gated; nothing wired to DLSS. if (GAL.MvppColorSnapshot.Enabled) { GAL.ITexture snapHost = GAL.MvppColorSnapshot.SceneColorBackend; @@ -936,8 +780,6 @@ namespace Ryujinx.Graphics.Vulkan cH = held.Height; success = cW == sW && cH == sH; - // [HELDCOLOR] Publish the present-time (tonemapped) copy so the mid-frame inject can - // feed it instead of the live raw scene. 1-frame stale by construction. if (success) { GAL.MvppColorSnapshot.SceneColorHeld = held; @@ -958,11 +800,6 @@ namespace Ryujinx.Graphics.Vulkan $"copiedSize={cW}x{cH} frame={GAL.MvppColorSnapshot.FrameId} success={success}"); } - // [COLORDUMP step 3] one-shot readback of the NATIVE scene color (srcColor) and the - // PRESENTED color (view, before any effect) for offline content comparison (HUD / bloom / - // tonemap signatures). Synced to the mid-frame dump's DumpArmed flag so BOTH dumps come from - // the SAME confirmed-gameplay (bright) frame -- never a loading screen. Fallback: a very high - // frame count if the mid-frame dump is disabled. Main HDR pass sW>=2000. if (_colorDumpEnabled && !_colorDumped && success && sW >= 2000 && (GAL.MvppColorSnapshot.DumpArmed || GAL.MvppColorSnapshot.FrameId > 5000)) { @@ -1020,11 +857,6 @@ namespace Ryujinx.Graphics.Vulkan srcY1 = crop.Bottom; } - // Logged on every CHANGE, not once: the "screen shrinks/grows by itself at startup" - // report (quality mode, 04/07) needs the present-geometry timeline -- a once-only - // probe hid whatever bounces during the boot/dyn-res ramp. Steady state = one line. - // Compared as values: building the description string on every present cost 60-120 - // allocations/second for a probe that almost never fires. (int, int, int, int, int, int, int, int, int, int, Type, Type) presentSig = (view.Width, view.Height, view.ActiveWidth, view.ActiveHeight, srcX0, srcY0, srcX1, srcY1, _width, _height, @@ -1091,15 +923,9 @@ namespace Ryujinx.Graphics.Vulkan if (!_dlssCropLogged) { _dlssCropLogged = true; - // Evidence for the MV++ y-convention auto-detection hypothesis: per game, does - // the present crop's FlipY match the RYUJINX_MVPP_FLIPY value the eye test - // validated? (TOTK: the flipped NDC mapping won on vertical pans.) Once the - // correlation holds on a second game, wire flipY to the crop and drop the knob. Logger.Info?.Print(LogClass.Gpu, $"DLSS present crop: FlipY={crop.FlipY} FlipX={crop.FlipX}."); } - // DLSS upscales the frame and blits straight to the swapchain; on any failure it - // returns false and we fall through to the normal scaling/blit path below. dlssHandled = _dlss.TryRun( view, depthTexture as TextureView, @@ -1117,27 +943,15 @@ namespace Ryujinx.Graphics.Vulkan _hdrWhiten, _isHdrPq); - // [INJECT étape C2b] The present-time throwaway path (RunInjectPass, étape B) is RETIRED here: - // DLSS now runs at the REAL injection point via the mid-frame hook (MvppRunInjectMidFrame, - // driven from PipelineBase.MvppInjectDlssUpscale). Running both would evaluate viewport 1 twice - // per frame. RunInjectPass is kept in DlssUpscaler for reference; C4 removes it. } - // DLSS (when it ran) wrote the swapchain directly -- pure DLSS, no hybrid hand-off, nothing more - // to do. Otherwise (DLSS disabled, or it could not run this frame) render the configured spatial - // path: the NIS scaling filter, or a plain HDR-aware blit. if (!dlssHandled) { RenderSpatial(_swapchainImageViews[nextImage]); } - // Renders the configured spatial path (NIS scaling filter, or a plain HDR-aware blit) into the - // given destination -- the swapchain in steady state, or the cross-fade scratch target during a - // DLSS<->NIS transition. Both inputs to the final blend are therefore in the same display space. void RenderSpatial(TextureView spatialDst) { - // NIS and FSR have PQ variants of their HDR output; Area does not (SDR-only - // shader). On a PQ swapchain, route Area through the plain HDR blit instead. bool filterCanEncode = !_isHdrPq || _scalingFilter is Effects.NisScalingFilter or Effects.FsrScalingFilter; if (_isHdrPq && _scalingFilter != null && !filterCanEncode && !_pqFilterBypassLogged) @@ -1190,23 +1004,6 @@ namespace Ryujinx.Graphics.Vulkan } } - // [PRESENTCAP v2] capture AVANT la transition PresentSrc : l'image est COMPLÈTE - // (DLSS + blit + encodage PQ écrits) et encore en General — layout source accepté - // par CmdCopyImageToBuffer, zéro transition. v1 (GetData sur la vue swapchain) - // = NRE : cette vue est un wrapper SANS TextureStorage → copie Vulkan brute vers - // un staging, LUE après le flush du present (drapeau différé + DeviceWaitIdle, - // banc seulement). dlssHandled seulement : les menus ne brûlent pas les slots. - // [21/07] `dlssHandled` retiré : il rendait cette capture INARMABLE sans DLSS, donc jamais - // utilisable dans les conditions de repro de l'artefact XC2 (DLSS off). C'est la seule capture - // du dépôt qui regarde l'image de swapchain APRÈS le blit d'agrandissement -- l'autre sonde - // (Gpu/Window.cs:281) capture la texture invitée AVANT tout le chemin de présentation, et trois - // de ses images relues à l'œil sont propres alors que l'artefact est décrit comme continu. - // La capture reste gated par RYUJINX_MVPP_PRESENTCAP, off par défaut. - // [21/07 v2] L'artefact est TEMPOREL : il s'accumule, disparaît, revient. Une image toutes les - // 2 s ne peut rien en dire. On capture donc des images CONSÉCUTIVES par rafales, et surtout on - // capture la texture INVITÉE et l'image de SWAPCHAIN sur LA MÊME IMAGE, dans le même command - // buffer -- c'est la seule façon d'attribuer une différence à l'étage de présentation plutôt - // qu'à deux instants différents. Rafale et pause réglables sans rebuild. if (_presentCapUsable && _presentCapCount < _presentCapMax) { long presNowMs = System.Environment.TickCount64; @@ -1253,7 +1050,6 @@ namespace Ryujinx.Graphics.Vulkan if (presSize == _presentCapBufferSize) { - // Écritures (compute/blit) rendues visibles au transfert, General→General. Transition(cbs.CommandBuffer, swapchainImage, AccessFlags.MemoryWriteBit, AccessFlags.TransferReadBit, ImageLayout.General, ImageLayout.General); @@ -1265,10 +1061,6 @@ namespace Ryujinx.Graphics.Vulkan _gd.Api.CmdCopyImageToBuffer(cbs.CommandBuffer, swapchainImage, ImageLayout.General, presBuf, 1, in presRegion); _presentCapPending = true; - // MÊME IMAGE, MÊME command buffer : la texture invitée telle qu'elle entre dans - // le chemin de présentation. C'est le seul appariement qui permette d'attribuer - // une différence à l'étage de présentation et non à deux instants distincts. - // (Avec DLSS off et effect=none, `view` est encore la texture du jeu.) int guestBpp = view.Info.BytesPerPixel; int guestSize = view.Width * view.Height * guestBpp; @@ -1319,14 +1111,11 @@ namespace Ryujinx.Graphics.Vulkan } catch (System.Exception swapdiagEx) { - // [SWAPDIAG] Surface the DeviceLost context, then let it propagate unchanged. Logger.Error?.Print(LogClass.Gpu, $"SWAPDIAG DEVICELOST at present flush #2: {_width}x{_height} dlss={Dlss.DlssIntegration.IsEnabled} fgProxy={_swapchainViaFgProxy} -- {swapdiagEx.Message}"); throw; } - // [PRESENTCAP v2] la copie est partie dans le flush ci-dessus ; on attend le GPU - // (stall ~ms toutes les 2 s, banc seulement) puis on lit le staging → fichier. if (_presentCapPending) { _presentCapPending = false; @@ -1340,8 +1129,6 @@ namespace Ryujinx.Graphics.Vulkan $"pres_{_presentCapCount:00}_{_width}x{_height}_{_format}.bin"); System.IO.File.WriteAllBytes(presFile, presData.Get().ToArray()); - // La moitié invitée de la PAIRE, écrite avec le MÊME index : pres_NN et guest_NN sont - // la même image, capturées dans le même command buffer. if (_presentCapGuestPending) { _presentCapGuestPending = false; @@ -1367,15 +1154,10 @@ namespace Ryujinx.Graphics.Vulkan [PipelineStageFlags.ColorAttachmentOutputBit], [_renderFinishedSemaphores[semaphoreIndex]]); - // [SYNCFRAME] Banc XC2 : sérialise CPU/GPU une fois par image. Ici et pas ailleurs — c'est le - // seul instant où le command buffer de l'image est déjà soumis (le Return ci-dessus) et où la - // présentation n'a pas encore été demandée (après QueuePresent on mesurerait le vsync). if (_syncFrameLive) { if (!_syncFrameAnnounced) { - // Armement prouvé AVANT le premier wait : un interrupteur qu'on ne voit pas dans le - // log ne vaut rien, y compris quand le wait échoue du premier coup. _syncFrameAnnounced = true; Logger.Info?.Print(LogClass.Gpu, "MVPP SYNCFRAME: ON -- DeviceWaitIdle après chaque present (banc uniquement, perte de FPS attendue)."); @@ -1385,10 +1167,6 @@ namespace Ryujinx.Graphics.Vulkan try { - // vkDeviceWaitIdle exige la synchro externe de TOUTES les VkQueue : on prend le même - // verrou que QueuePresent plus bas. Et sous le proxy Streamline, l'attente doit passer - // par les hooks FG (même règle qu'à la recréation de swapchain, l.199-210) : un - // DeviceWaitIdle natif sous le proxy peut geler le device. lock (_gd.QueueLock) { if (_swapchainViaFgProxy && _gd.FgHooks != null) @@ -1404,8 +1182,6 @@ namespace Ryujinx.Graphics.Vulkan _syncFrameCount++; _syncFrameStallTicks += System.Diagnostics.Stopwatch.GetTimestamp() - syncStart; - // Battement toutes les 5 s : c'est LUI qui prouve que le wait tourne par image, et le - // stall moyen qui dit ce que le test coûte (à lire avec les FPS du run). long syncNowMs = System.Environment.TickCount64; if (syncNowMs >= _syncFrameNextLogMs) { @@ -1418,7 +1194,6 @@ namespace Ryujinx.Graphics.Vulkan } catch (System.Exception syncEx) { - // Un instrument de banc ne doit pas tuer le processus : on désarme et on le dit. _syncFrameLive = false; Logger.Warning?.Print(LogClass.Gpu, $"MVPP SYNCFRAME: désarmé après erreur inattendue -- {syncEx.Message}"); @@ -1442,9 +1217,6 @@ namespace Ryujinx.Graphics.Vulkan PResults = &result, }; - // DLSS-FG Porte B.2: PCL pacing markers bracketing the present. No-ops unless FG is - // armed AND this frame ran the SR evaluate (which set the frame token); they never - // touch the B.1 queue-routing logic above. if (_swapchainViaFgProxy) { Dlss.StreamlineFrameGen.BeforePresent(); @@ -1455,7 +1227,6 @@ namespace Ryujinx.Graphics.Vulkan ActiveSwapchainApi.QueuePresent(_gd.Queue, in presentInfo); } - // [SWAPDIAG] The present result is normally ignored; surface it when it is not Success/Suboptimal. if (result != Result.Success && result != Result.SuboptimalKhr) { Logger.Warning?.Print(LogClass.Gpu, @@ -1464,14 +1235,9 @@ namespace Ryujinx.Graphics.Vulkan if (_swapchainViaFgProxy) { - // ePresentEnd + Reflex sleep + periodic slDLSSGGetState poll (the actual - // "is FG injecting frames?" measurement). Dlss.StreamlineFrameGen.AfterPresent(Dlss.DlssUpscaler.ViewportId); } - // DLSS-FG Porte B.1 attribution: one line proving the present actually went through - // SL's eVulkan_Present hook (i.e. sl.common's presentCommon() is now running per frame, - // as the ManualHooking guide §2.0 requires) and what the swapchain said about it. if (_swapchainViaFgProxy && !_fgPresentLogged) { _fgPresentLogged = true; @@ -1480,16 +1246,11 @@ namespace Ryujinx.Graphics.Vulkan } } - // [FLASHCAP / Mesure A] Sample the PRESENTED frame (readable), measure the frame-to-frame change, and - // on a sudden spike (a flash) dump the full frames to mvpp-colordump/flashNNN_*.bin so we can SEE where - // the artifact is. Throttled (~33 Hz) to limit the readback stall. Diagnostic only, gated. private void FlashCapPresent(TextureView view) { try { long now = System.Environment.TickCount64; - // During a dump burst, sample EVERY present so the triplet is truly consecutive frames - // (the 28 ms throttle would skip every other frame at 60 fps). if (_flashDumpNext == 0 && now - _flashLastTickMs < 28) { return; @@ -1503,7 +1264,6 @@ namespace Ryujinx.Graphics.Vulkan return; } - // Format-agnostic change detector: subsample ~4096 raw bytes at a fixed stride. const int NSamp = 4096; int step = System.Math.Max(1, packed.Length / NSamp); int count = (packed.Length + step - 1) / step; @@ -1517,9 +1277,6 @@ namespace Ryujinx.Graphics.Vulkan cur[s] = packed[i]; } - // Only detect during REAL 3D gameplay: the mid-frame DLSS inject ran within the last ~150 ms. - // Loading/map/menu screens do NOT run the inject, so they are excluded (brightness alone did - // not work -- a bright loading map still passed and polluted the captures). bool warm = _flashForce || (now - GAL.MvppColorSnapshot.LastInjectMs) < 150; if (_flashPrevSamples != null && _flashPrevSamples.Length == count) @@ -1529,7 +1286,7 @@ namespace Ryujinx.Graphics.Vulkan { diff += System.Math.Abs(cur[s] - _flashPrevSamples[s]); } - float metric = (float)diff / count; // avg raw-byte change (0..255) + float metric = (float)diff / count; _flashAvg = _flashAvg <= 0f ? metric : 0.9f * _flashAvg + 0.1f * metric; if (metric > _flashMax) { @@ -1538,18 +1295,14 @@ namespace Ryujinx.Graphics.Vulkan int dumpCap = _flashTimedSec > 0 ? _flashTimedCap : 15; - // Spike = big change AND well above the local baseline (so steady motion doesn't false-fire; - // an immobile-camera flash trips it easily). Disabled in timed mode (unbiased sampling). bool spike = _flashTimedSec == 0 && warm && metric > 8f && metric > 3f * _flashAvg; if (spike && _flashDumpNext == 0 && _flashDumpCount < dumpCap) { Logger.Info?.Print(LogClass.Gpu, $"MVPP-FLASHCAP: SPIKE metric={metric:0.0} baseline={_flashAvg:0.0} -> dumping presented flash{_flashDumpCount:D3}"); - _flashDumpNext = 3; // this sample + next 2, to see the flash unfold + _flashDumpNext = 3; } - // [FLASHCAP timed] Periodic trigger: one consecutive-frame triplet every N seconds of - // active gameplay (inject warm), regardless of spikes. if (_flashTimedSec > 0 && warm && _flashDumpNext == 0 && _flashDumpCount < dumpCap && now >= _flashTimedNextMs) { @@ -1621,13 +1374,8 @@ namespace Ryujinx.Graphics.Vulkan { _updateEffect = false; - // RYUJINX_TAA=1 forces the native temporal filter regardless of the UI selection (dev override). AntiAliasing antiAliasing = Effects.TemporalFilter.IsEnabled ? AntiAliasing.Taa : _currentAntiAliasing; - // Guard (user report 12/07, crash at launch): TAA and DLSS together stack two - // temporal accumulators over the same present path. DLSS already does the - // temporal work, so it wins; the TAA choice stays untouched in the config and - // comes back the moment DLSS is off. if (antiAliasing == AntiAliasing.Taa && Dlss.DlssIntegration.IsEnabled) { Ryujinx.Common.Logging.Logger.Warning?.Print(Ryujinx.Common.Logging.LogClass.Gpu, @@ -1788,7 +1536,6 @@ namespace Ryujinx.Graphics.Vulkan if (_hdrEnabled != enabled) { _hdrEnabled = enabled; - // The swapchain surface format (SDR vs scRGB float) changes, so recreate it. _swapchainIsDirty = true; } } @@ -1799,9 +1546,6 @@ namespace Ryujinx.Graphics.Vulkan { unsafe { - // DLSS-FG: same teardown protocol as RecreateSwapchain -- DLSS-G must be eOff and - // its in-flight present work retired BEFORE this swapchain dies, or dlfg's present - // workers hang the device on exit (close-with-FG freeze). if (_swapchainViaFgProxy && _gd.FgHooks != null) { Dlss.StreamlineFrameGen.OnSwapchainTeardown(Dlss.DlssUpscaler.ViewportId); @@ -1809,8 +1553,6 @@ namespace Ryujinx.Graphics.Vulkan _gd.FgHooks.DeviceWaitIdle(); } - // [CANARY v2] Vider le parc AVANT la chaîne courante (fermeture = plus aucun - // present à attendre, destruction immédiate de tout ce qui traînait). DrainRetiredSwapchains(all: true); for (int i = 0; i < _swapchainImageViews.Length; i++) @@ -1828,7 +1570,6 @@ namespace Ryujinx.Graphics.Vulkan _gd.Api.DestroySemaphore(_device, _renderFinishedSemaphores[i], null); } - // DLSS-FG: destroyed with the API that created it (SL proxy when routed). ActiveSwapchainApi.DestroySwapchain(_device, _swapchain, null); } @@ -1836,8 +1577,6 @@ namespace Ryujinx.Graphics.Vulkan _scalingFilter?.Dispose(); _dlss?.Dispose(); - // [COLORCAP snapshot] the held per-size color copies -- same cleanup loop as the - // twin _heldDepthCache in DlssUpscaler.Dispose. foreach (TextureView held in _heldColorCache.Values) { held?.Dispose(); diff --git a/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs b/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs index 144fb65bb..ccd257ddb 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs @@ -12,9 +12,9 @@ namespace Ryujinx.HLE.Debugger { private void MainLoop() { - IPEndPoint endpoint = new(IPAddress.Any, GdbStubPort); + IPEndPoint endpoint = new(IPAddress.Loopback, GdbStubPort); _listenerSocket = new TcpListener(endpoint); - + try { _listenerSocket.Start(); @@ -36,7 +36,7 @@ namespace Ryujinx.HLE.Debugger } catch (SocketException se) { - Logger.Error?.Print(LogClass.GdbStub, + Logger.Error?.Print(LogClass.GdbStub, $"Failed to accept incoming GDB client connection: {Enum.GetName(se.SocketErrorCode)}"); return; } diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs index ff928aa90..ddce7d59f 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -10,7 +10,6 @@ using System.Net.Sockets; using System.Threading; using IExecutionContext = Ryujinx.Cpu.IExecutionContext; - namespace Ryujinx.HLE.Debugger { public partial class Debugger : IDisposable @@ -27,7 +26,7 @@ namespace Ryujinx.HLE.Debugger private Socket _clientSocket; private NetworkStream _readStream; private NetworkStream _writeStream; - + private GdbCommands _commands; private bool _shuttingDown; @@ -37,7 +36,7 @@ namespace Ryujinx.HLE.Debugger internal ulong? GThreadId; internal KThread CThread => CThreadId?.Into(DebugProcess.GetThread); - + internal KThread GThread => GThreadId?.Into(DebugProcess.GetThread); public readonly BreakpointManager BreakpointManager; @@ -98,8 +97,79 @@ namespace Ryujinx.HLE.Debugger } } + private static readonly bool _watchNullX0 = + System.Environment.GetEnvironmentVariable("RYUJINX_WATCH_NULLX0") == "1"; + + private long _watchVus; + + private readonly ulong[] _dernieresCles = new ulong[32]; + private readonly ulong[] _dernieresAdr = new ulong[32]; + private int _curseurCles; + + public void VidangerCles() + { + if (!_watchNullX0) + { + return; + } + + System.Text.StringBuilder sb = new(); + sb.AppendLine($"WATCH: {_watchVus} recherches vues. 32 dernieres cles (la plus recente en premier) :"); + + for (int k = 0; k < _dernieresCles.Length; k++) + { + int i = ((_curseurCles - 1 - k) % _dernieresCles.Length + _dernieresCles.Length) % _dernieresCles.Length; + + if (_dernieresCles[i] == 0) + { + continue; + } + + sb.AppendLine($" cle=0x{_dernieresCles[i]:X16} appelee depuis 0x{_dernieresAdr[i]:X16}"); + } + + Logger.Error?.Print(LogClass.GdbStub, sb.ToString()); + } + public void BreakHandler(IExecutionContext ctx, ulong address, int imm) { + if (_watchNullX0) + { + ulong x0 = ctx.GetX(0); + ulong x1 = ctx.GetX(1); + + _watchVus++; + + _dernieresCles[_curseurCles] = x1; + _dernieresAdr[_curseurCles] = ctx.GetX(30); + _curseurCles = (_curseurCles + 1) % _dernieresCles.Length; + + if (x0 != 0 || x1 == 0) + { + return; + } + + System.Text.StringBuilder sb = new(); + sb.AppendLine($"WATCH: APPEL FATAL a 0x{address:X16} apres {_watchVus} passages valides."); + + for (int r = 0; r <= 30; r++) + { + sb.Append($" X{r}=0x{ctx.GetX(r):X16}"); + + if (r % 4 == 3) + { + sb.AppendLine(); + } + } + + Logger.Error?.Print(LogClass.GdbStub, sb.ToString()); + Logger.Error?.Print(LogClass.GdbStub, + $"WATCH: X30 (adresse de retour REELLE) = 0x{ctx.GetX(30):X16}"); + Logger.Flush(); + + return; + } + DebugProcess.DebugInterruptHandler(ctx); _breakHandlerEvent.Reset(); diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index d98f7e9ad..e15203426 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -222,8 +222,6 @@ namespace Ryujinx.HLE.FileSystem FileStream file = new(aoc.ContainerPath, FileMode.Open, FileAccess.Read); using UniqueRef ncaFile = new(); - // [ROBUSTESSE 20/07] Case-insensitive: a container named "FILE.NSP" (uppercase, seen in - // the wild) silently fell into the default arm and the AOC was never served to the game. switch (Path.GetExtension(aoc.ContainerPath).ToLowerInvariant()) { case ".xci": @@ -427,11 +425,6 @@ namespace Ryujinx.HLE.FileSystem private LocationEntry GetLocation(ulong titleId, NcaContentType contentType, StorageId storageId) { - // [ROBUSTESSE 20/07] A guest can pass a StorageId we have no entries for (seen in the wild: - // Pokemon Scarlet requesting its AOC data with StorageId.None after GetAocDataStorage failed - // on a scene-made DLC NSP). The callers already handle an empty LocationEntry (same shape - // FirstOrDefault produces when the title is absent), so answer "not found" instead of - // killing the emulator with a KeyNotFoundException. if (!_locationEntries.TryGetValue(storageId, out LinkedList locationList)) { return default; diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index 20c8b116c..a7c6ce9d8 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -1092,11 +1092,299 @@ namespace Ryujinx.HLE.HOS.Kernel.Process MemoryManager = new KPageTable(KernelContext, CpuMemory, Context.AddressSpaceSize); } + private static readonly bool _faultProbe = + System.Environment.GetEnvironmentVariable("RYUJINX_FAULTPROBE") == "1"; + + private static readonly string _faultCle = + System.Environment.GetEnvironmentVariable("RYUJINX_FAULTCLE"); + + private static int _faultCleVus; + + private static void DumpWords(IVirtualMemoryManager mem, string quoi, ulong centre, int de, int a, ulong pc, ulong va) + { + System.Text.StringBuilder sb = new(); + sb.Append($"FAULTPROBE {quoi}=0x{centre:X16} pc=0x{pc:X16} va=0x{va:X16} mots:"); + + for (int k = de; k <= a; k++) + { + ulong addr = (ulong)((long)centre + k * 4); + string txt; + + try + { + txt = mem.Read(addr).ToString("X8"); + } + catch + { + txt = "........"; + } + + sb.Append(k == 0 ? $" [{txt}]" : $" {txt}"); + } + + Logger.Error?.Print(LogClass.Cpu, sb.ToString()); + } + + private static void DumpTexte(IVirtualMemoryManager mem, string quoi, ulong adresse, int octets) + { + for (int ligne = 0; ligne * 16 < octets; ligne++) + { + System.Text.StringBuilder hex = new(); + System.Text.StringBuilder txt = new(); + + for (int k = 0; k < 16; k++) + { + try + { + byte b = mem.Read(adresse + (ulong)(ligne * 16 + k)); + hex.Append($"{b:X2} "); + txt.Append(b >= 0x20 && b < 0x7F ? (char)b : '.'); + } + catch + { + hex.Append(".. "); + txt.Append(' '); + } + } + + Logger.Error?.Print(LogClass.Cpu, + $"FAULTCLE {quoi} +0x{ligne * 16:X2} : {hex}| {txt}"); + } + } + private static bool InvalidAccessHandler(ulong va) { KernelStatic.GetCurrentThread()?.PrintGuestStackTrace(); KernelStatic.GetCurrentThread()?.PrintGuestRegisterPrintout(); + if (_faultProbe) + { + try + { + KThread thread = KernelStatic.GetCurrentThread(); + ulong pc = thread?.Context?.Pc ?? 0; + + IVirtualMemoryManager mem = thread?.Owner?.CpuMemory; + + if (pc != 0 && mem != null) + { + for (int bloc = 0; bloc < 4; bloc++) + { + DumpWords(mem, $"PC+{bloc * 64:X2}", pc + (ulong)(bloc * 64), 0, 15, pc, va); + } + + ulong lr = thread.Context.GetX(30); + + if (lr > 0x40) + { + DumpWords(mem, "LR", lr, -16, 15, pc, va); + DumpWords(mem, "LR+40", lr + 0x40, 0, 15, pc, va); + } + + if (!string.IsNullOrEmpty(_faultCle) && _faultCleVus++ < 3) + { + string[] champs = _faultCle.Split(':'); + + if (champs.Length == 2 + && int.TryParse(champs[0], out int reg) && reg >= 0 && reg <= 30 + && ulong.TryParse(champs[1], System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, out ulong dep)) + { + ulong bas = thread.Context.GetX(reg); + + Logger.Error?.Print(LogClass.Cpu, + $"FAULTCLE source X{reg}=0x{bas:X16} decalage +0x{dep:X}"); + + try + { + ulong cle = mem.Read(bas + dep); + + Logger.Error?.Print(LogClass.Cpu, $"FAULTCLE valeur = 0x{cle:X16}"); + + if (cle > 0x1000) + { + DumpTexte(mem, "cle", cle, 96); + + ulong vtable = mem.Read(cle); + + if (vtable > 0x1000) + { + try + { + ulong typeInfo = mem.Read(vtable - 8); + + if (typeInfo > 0x1000) + { + ulong nom = mem.Read(typeInfo + 8); + + if (nom > 0x1000) + { + DumpTexte(mem, "CLASSE", nom, 64); + } + } + } + catch + { + } + } + + for (int champ = 0; champ < 6; champ++) + { + try + { + ulong p = mem.Read(cle + (ulong)(champ * 8)); + + if (p > 0x1000) + { + DumpTexte(mem, $"cle-champ{champ}", p, 48); + } + } + catch + { + } + } + } + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.Cpu, $"FAULTCLE illisible : {ex.Message}"); + } + } + else + { + Logger.Error?.Print(LogClass.Cpu, + $"FAULTCLE mal forme : {_faultCle} (attendu :, ex. 23:10)"); + } + } + + ulong sp = thread.Context.GetX(31); + + if (sp > 0x1000) + { + for (int bloc = 0; bloc < 4; bloc++) + { + System.Text.StringBuilder ss = new(); + ss.Append($"FAULTSTACK sp+0x{bloc * 0x40:X3} :"); + + for (int k = 0; k < 8; k++) + { + ulong addr = sp + (ulong)(bloc * 0x40 + k * 8); + + try + { + ss.Append($" {mem.Read(addr):X16}"); + } + catch + { + ss.Append(" ................"); + } + } + + Logger.Error?.Print(LogClass.Cpu, ss.ToString()); + } + } + + if (sp > 0x1000) + { + int trouves = 0; + + for (int k = 0; k < 48 && trouves < 4; k++) + { + ulong val; + + try + { + val = mem.Read(sp + (ulong)(k * 8)); + } + catch + { + continue; + } + + if (val < 0x0800_0000UL || val > 0x1200_0000UL || (val & 3) != 0) + { + continue; + } + + trouves++; + DumpWords(mem, $"RETOUR@sp+0x{k * 8:X2} 0x{val:X}", val, -10, 1, pc, va); + } + } + + for (int r = 19; r <= 26; r++) + { + ulong obj = thread.Context.GetX(r); + + if (obj < 0x1000_0000UL || obj > 0x1_0000_0000_0000UL) + { + continue; + } + + try + { + ulong vtable = mem.Read(obj); + + if (vtable < 0x0800_0000UL || vtable > 0x1200_0000UL) + { + continue; + } + + ulong methode = mem.Read(vtable + 0x2E0); + + if (methode < 0x0800_0000UL || methode > 0x1200_0000UL) + { + continue; + } + + DumpWords(mem, $"RELAIS X{r} vtbl=0x{vtable:X} +0x2E0", methode, 0, 7, pc, va); + } + catch + { + } + } + + for (int r = 0; r <= 30; r++) + { + ulong val = thread.Context.GetX(r); + + if (val < 0x1000_0000UL || val > 0x1_0000_0000_0000UL) + { + continue; + } + + System.Text.StringBuilder so = new(); + so.Append($"FAULTOBJ X{r}=0x{val:X12} :"); + + for (int k = 0; k < 8; k++) + { + try + { + so.Append($" {mem.Read(val + (ulong)(k * 8)):X16}"); + } + catch + { + so.Append(" ................"); + } + } + + Logger.Error?.Print(LogClass.Cpu, so.ToString()); + } + } + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.Cpu, $"FAULTPROBE indisponible : {ex.Message}"); + } + } + + try + { + KernelStatic.GetCurrentThread()?.Owner?.KernelContext?.Device?.Debugger?.VidangerCles(); + } + catch + { + } + Logger.Error?.Print(LogClass.Cpu, $"Invalid memory access at virtual address 0x{va:X16}."); Logger.Flush(); @@ -1283,7 +1571,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process { return false; } - + _kernelContext.CriticalSection.Enter(); _steppingThread = target; bool waiting = target.MutexOwner != null || target.WaitingSync || target.WaitingInArbitration; diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs index 480899ae1..c2d286674 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs @@ -23,10 +23,25 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy private const string Xc2GlobalTitleId = "0100e95004038000"; private static bool IsXc2 => TitleIDs.CurrentApplication.Value.OrDefault() is Xc2GlobalTitleId or Xc2JpTitleId; + private static readonly int _slowFsUs = + int.TryParse(System.Environment.GetEnvironmentVariable("RYUJINX_SLOWFS"), out int us) && us > 0 ? us : 0; + + private static readonly long _slowFsTicks = + (long)(System.Diagnostics.Stopwatch.Frequency * (_slowFsUs / 1_000_000.0)); + [CommandCmif(0)] // Read(u64 offset, u64 length) -> buffer buffer public ResultCode Read(ServiceCtx context) { + if (_slowFsTicks > 0) + { + long fin = System.Diagnostics.Stopwatch.GetTimestamp() + _slowFsTicks; + + while (System.Diagnostics.Stopwatch.GetTimestamp() < fin) + { + } + } + ulong offset = context.RequestData.ReadUInt64(); ulong size = context.RequestData.ReadUInt64(); @@ -50,7 +65,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy } catch (HorizonResultException hre) when (hre.IsOfResultType(ResultFs.NonRealDataVerificationFailed)) { - Logger.Error?.Print(LogClass.ServiceFs, + Logger.Error?.Print(LogClass.ServiceFs, $"Encountered corrupted data in filesystem storage @ offset 0x{offset:X8}, size 0x{size:X8}. " + "Please redump the current game and/or update from your console."); result = ResultFs.NonRealDataVerificationFailed; diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs index e90414114..691b6b7ed 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs @@ -863,10 +863,6 @@ namespace Ryujinx.HLE.HOS.Services.Fs } } - // [ROBUSTESSE 20/07] A guest asking for a data title we cannot resolve (seen in the wild: - // Pokemon Scarlet requesting an AOC whose scene-made NSP has no openable data section) must - // get a filesystem error back, not kill the emulator. The game decides what to do with it - // (most titles handle a missing AOC gracefully). Logger.Warning?.Print(LogClass.ServiceFs, $"Data title {titleId:x16} requested on Storage {storageId} could not be resolved; returning PathDoesNotExist to the guest."); diff --git a/src/Ryujinx.HLE/HOS/Services/IpcService.cs b/src/Ryujinx.HLE/HOS/Services/IpcService.cs index c7dee64fb..048d54fc3 100644 --- a/src/Ryujinx.HLE/HOS/Services/IpcService.cs +++ b/src/Ryujinx.HLE/HOS/Services/IpcService.cs @@ -13,6 +13,14 @@ namespace Ryujinx.HLE.HOS.Services { abstract class IpcService { + private static readonly string _ipcProbe = + Environment.GetEnvironmentVariable("RYUJINX_IPCPROBE") is { Length: > 0 } s ? s : null; + + private static readonly string[] _ipcProbeSvc = + Environment.GetEnvironmentVariable("RYUJINX_IPCPROBE_SVC") is { Length: > 0 } sv + ? sv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + : null; + public IReadOnlyDictionary CmifCommands { get; } public IReadOnlyDictionary TipcCommands { get; } @@ -149,9 +157,39 @@ namespace Ryujinx.HLE.HOS.Services { Logger.Trace?.Print(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Name}"); + bool probeThis = _ipcProbe != null; + + if (probeThis) + { + ulong uid = context.Thread?.ThreadUid ?? 0; + probeThis = _ipcProbe == "*" || _ipcProbe == uid.ToString(); + + if (probeThis && _ipcProbeSvc != null) + { + string full = service.GetType().Name + "." + processRequest.Name; + probeThis = false; + + foreach (string motif in _ipcProbeSvc) + { + if (full.Contains(motif, StringComparison.OrdinalIgnoreCase)) + { + probeThis = true; + break; + } + } + } + } + _parameters[0] = context; - + result = (ResultCode)processRequest.Invoke(service, _parameters); + + if (probeThis) + { + Logger.Info?.Print(LogClass.KernelIpc, + $"IPCPROBE [fil {context.Thread?.ThreadUid ?? 0}] " + + $"{service.GetType().Name}.{processRequest.Name} -> {result}"); + } } else { @@ -202,7 +240,7 @@ namespace Ryujinx.HLE.HOS.Services Logger.Debug?.Print(LogClass.KernelIpc, $"{GetType().Name}: {processRequest.Name}"); _parameters[0] = context; - + result = (ResultCode)processRequest.Invoke(this, _parameters); } else diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs index 99e49af71..dcb9c1cd3 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs @@ -378,10 +378,6 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger if (vrrCapped) { - // VRR with a per-game FPS cap: force a paced interval (swap interval 1 -> - // _ticksPerFrame derived from TargetVSyncInterval) instead of running - // unbounded. The present mode stays FifoRelaxed (chosen from VSyncMode.Vrr - // in Window.cs), so the display still follows the framerate, just capped. if (_swapInterval != 1 || _device.TargetVSyncInterval != _targetVSyncInterval || _vSyncMode != _device.VSyncMode) diff --git a/src/Ryujinx.HLE/HleConfiguration.cs b/src/Ryujinx.HLE/HleConfiguration.cs index f9999118a..e1819d11b 100644 --- a/src/Ryujinx.HLE/HleConfiguration.cs +++ b/src/Ryujinx.HLE/HleConfiguration.cs @@ -94,7 +94,6 @@ namespace Ryujinx.HLE internal readonly int CustomVSyncInterval; /// - /// Whether the custom VSync interval is enabled. Also acts as the per-game VRR FPS cap toggle. /// internal readonly bool EnableCustomVSyncInterval; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index f22dfd1dd..8d8c88978 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -411,6 +411,46 @@ namespace Ryujinx.HLE.Loaders.Processes } } + string instr = Environment.GetEnvironmentVariable("RYUJINX_PATCHINSTR"); + + if (!string.IsNullOrEmpty(instr)) + { + foreach (string paire in instr.Split(',')) + { + string[] bouts = paire.Split('='); + + if (bouts.Length != 2 + || !ulong.TryParse(bouts[0].Trim(), System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, out ulong adr) + || !uint.TryParse(bouts[1].Trim(), System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, out uint mot)) + { + Logger.Error?.Print(LogClass.Loader, + $"PATCHINSTR mal forme : {paire} (attendu =)"); + + continue; + } + + try + { + uint avant = process.CpuMemory.Read(adr); + + process.CpuMemory.Write(adr, mot); + + uint apres = process.CpuMemory.Read(adr); + + Logger.Warning?.Print(LogClass.Loader, + apres == mot + ? $"PATCHINSTR 0x{adr:X16} : {avant:X8} -> {apres:X8} APPLIQUE" + : $"PATCHINSTR 0x{adr:X16} : ECHEC, lu {apres:X8} au lieu de {mot:X8}"); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.Loader, $"PATCHINSTR 0x{adr:X16} impossible : {ex.Message}"); + } + } + } + process.DefaultCpuCore = meta.DefaultCpuId; context.Processes.TryAdd(process.Pid, process); diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index d60d7518f..39d3f847e 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -105,7 +105,7 @@ namespace Ryujinx.HLE System.EnablePtc = Configuration.EnablePtc; System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; - + UpdateVSyncInterval(); #pragma warning restore IDE0055 @@ -149,8 +149,6 @@ namespace Ryujinx.HLE TargetVSyncInterval = 1; break; case VSyncMode.Vrr: - // VRR runs unbounded by default (interval 1). If the per-game FPS cap is - // enabled, pace to that value instead (e.g. TOTK capped at 60). TargetVSyncInterval = (CustomVSyncIntervalEnabled && CustomVSyncInterval > 0) ? CustomVSyncInterval : 1; diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Cmif/ServiceDispatchTableBase.cs b/src/Ryujinx.Horizon/Sdk/Sf/Cmif/ServiceDispatchTableBase.cs index df1472560..6d44da87d 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/Cmif/ServiceDispatchTableBase.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/Cmif/ServiceDispatchTableBase.cs @@ -12,6 +12,9 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif { private const uint MaxCmifVersion = 1; + private static readonly string _ipcProbe = + Environment.GetEnvironmentVariable("RYUJINX_IPCPROBE") is { Length: > 0 } s ? s : null; + public abstract Result ProcessMessage(ref ServiceDispatchContext context, ReadOnlySpan inRawData); protected static Result ProcessMessageImpl(ref ServiceDispatchContext context, ReadOnlySpan inRawData, IReadOnlyDictionary entries, string objectName) @@ -62,6 +65,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif Result commandResult = commandHandler.Invoke(ref outHeader, ref context, inMessageRawData); + if (_ipcProbe != null && (_ipcProbe == "*" || objectName.Contains(_ipcProbe, StringComparison.OrdinalIgnoreCase))) + { + Logger.Info?.Print(LogClass.KernelIpc, + $"IPCPROBE-HZN {objectName}.{commandHandler.MethodName} -> {commandResult}"); + } + if (commandResult.Module is SfResult.ModuleId or HipcResult.ModuleId) { diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index e35b29748..1790da3e3 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -49,9 +49,6 @@ namespace Ryujinx.Ava [STAThread] public static int Main(string[] args) { - // [DLSS restart] The cold-restart child is asked (via this env var) to wait until the dying - // parent has fully exited: launching immediately raced the single-instance guard, which killed - // the child ~400 ms after boot (ghost logs, settings that never applied - caught 11/07). string restartDelay = Environment.GetEnvironmentVariable("RYUJINX_RESTART_DELAY_MS"); if (!string.IsNullOrEmpty(restartDelay) && int.TryParse(restartDelay, out int delayMs)) { @@ -129,9 +126,9 @@ namespace Ryujinx.Ava "The Beast Roofer Edition is not intended to be run as administrator.", "Ok"); return 0; } - + string downloadFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); - + if (Environment.CurrentDirectory.StartsWithIgnoreCase(downloadFiles)) { Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Downloads folder. Exiting..."); @@ -139,9 +136,9 @@ namespace Ryujinx.Ava "The Beast Roofer Edition is not intended to be run from the Downloads folder.", "Ok"); return 0; } - + string icloudFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Mobile Documents/com~apple~CloudDocs"); - + if (Environment.CurrentDirectory.StartsWithIgnoreCase(icloudFiles)) { Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the iCloud folder. Exiting..."); @@ -159,9 +156,9 @@ namespace Ryujinx.Ava LinuxSDLInterop.SimpleMessageBox($"{RyujinxApp.FullAppName} {Version}", "The Beast Roofer Edition is not intended to be run as administrator."); return 0; } - + string container = Environment.GetEnvironmentVariable("container"); - + if (container is not null && container.EqualsIgnoreCase("flatpak")) { Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); @@ -196,8 +193,6 @@ namespace Ryujinx.Ava Initialize(args); - // DLSS: translate the graphics-settings UI choice into the RYUJINX_DLSS_* env vars the backend - // reads, before any game/renderer starts (no-op if a launcher .bat already set them). DlssUiSettings.ApplyAtStartup(); LoggerAdapter.Register(); @@ -212,12 +207,12 @@ namespace Ryujinx.Ava public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - + // Vulkan UI rendering performs better, but its unpolished, and as such it lacks effective transparency. // https://github.com/AvaloniaUI/Avalonia/issues/19378 // https://github.com/AvaloniaUI/Avalonia/issues/9610 // X11RenderingMode.Glx && X11RenderingMode.Egl, Win32RenderingMode.Vulkan have these issues. - + .With(new X11PlatformOptions { EnableMultiTouch = true, @@ -332,7 +327,6 @@ namespace Ryujinx.Ava string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); - if (!isRunGameWithCustomConfig) // To return settings from the game folder if the user configuration exists { // Now load the configuration as the other subsystems are now registered @@ -425,7 +419,7 @@ namespace Ryujinx.Ava // Check if PPTC was overridden. if (CommandLineState.OverridePPTC is not null) - if (Enum.TryParse(CommandLineState.OverridePPTC, true, out bool result)) + if (bool.TryParse(CommandLineState.OverridePPTC, out bool result)) { ConfigurationState.Instance.System.EnablePtc.Value = result; } @@ -457,7 +451,6 @@ namespace Ryujinx.Ava Logger.Notice.Print(LogClass.Application, @"/_/|_| \_, / \_,_/ /_.__//_/ /_//_/ \_, / "); Logger.Notice.Print(LogClass.Application, " /___/ /___/ "); - Logger.Notice.Print(LogClass.Application, $"{RyujinxApp.FullAppName} Version: {Version}"); Logger.Notice.Print(LogClass.Application, $".NET Runtime: {RuntimeInformation.FrameworkDescription}"); SystemInfo.Gather().Print(); @@ -499,7 +492,6 @@ namespace Ryujinx.Ava log.PrintMsg(LogClass.Application, message); } - if (isTerminating) { Logger.Flush(); diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 0ee240479..917f022a3 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -328,8 +328,6 @@ namespace Ryujinx.Ava.Systems ConfigurationState.Instance.Graphics.HdrHighlightWhiten / 100f); } - // Maps the user-facing HDR intensity (0-100, higher = more pop) to the highlight curve - // exponent: 0 -> 6.0 (only near-white pops), 50 -> 4.0 (default), 100 -> 2.0 (whole scene pops). private static float HdrIntensityToCurve(int intensity) { return 6.0f - (Math.Clamp(intensity, 0, 100) * 0.04f); @@ -611,7 +609,7 @@ namespace Ryujinx.Ava.Systems _isActive = false; _viewModel.IsPaused = false; _playTimer.Stop(); - + GCSettings.LatencyMode = GCLatencyMode.Interactive; if (ConfigurationState.Instance.System.GCLowLatency) { @@ -654,7 +652,7 @@ namespace Ryujinx.Ava.Systems NpadManager.Dispose(); TouchScreenManager.Dispose(); Device.Dispose(); - + // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. // We only need to wait for all commands submitted during the main gpu loop to be processed. // If the GPU has no work and is cancelled, we need to handle that as well. @@ -667,18 +665,12 @@ namespace Ryujinx.Ava.Systems Device.Gpu.WaitUntilGpuReady(); } - // Dispose the event only when the render loop actually signalled it. When WaitAny - // returned on CANCELLATION, the loop's finally is still flushing and will Set() this - // event afterwards -- disposing here crashed the process at every "unlucky" close - // (ObjectDisposedException on the render thread, the long-standing close crash, - // stack finally captured 13/07). Skipping the dispose on that path leaks one handle - // into process exit, which the runtime reclaims. if (doneIndex == 0) { _gpuDoneEvent.Dispose(); } _gpuCancellationTokenSource.Dispose(); - + DisposeGpu(); AppExit?.Invoke(this, EventArgs.Empty); } @@ -688,10 +680,10 @@ namespace Ryujinx.Ava.Systems { if (Device.Processes != null) { - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText, + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText, _playTimer.Elapsed); } - + ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; @@ -716,7 +708,7 @@ namespace Ryujinx.Ava.Systems _chrono.Stop(); _playTimer.Stop(); - + GCSettings.LatencyMode = GCLatencyMode.Interactive; if (ConfigurationState.Instance.System.GCLowLatency) { @@ -749,7 +741,7 @@ namespace Ryujinx.Ava.Systems { Device.Gpu.WaitUntilGpuReady(); } - + Device.DisposeGpu(); } } @@ -973,7 +965,7 @@ namespace Ryujinx.Ava.Systems ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); - + _playTimer.Start(); if (ConfigurationState.Instance.System.GCLowLatency) @@ -991,7 +983,7 @@ namespace Ryujinx.Ava.Systems _playTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed."); - + if (ConfigurationState.Instance.System.GCLowLatency) { GCSettings.LatencyMode = GCLatencyMode.LowLatency; @@ -1007,7 +999,7 @@ namespace Ryujinx.Ava.Systems _playTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); Logger.Info?.Print(LogClass.Emulation, "Emulation was paused."); - + GCSettings.LatencyMode = GCLatencyMode.Interactive; if (ConfigurationState.Instance.System.GCLowLatency) { @@ -1235,8 +1227,6 @@ namespace Ryujinx.Ava.Systems } catch (ObjectDisposedException) { - // Close race second belt: the disposer stopped waiting (cancellation) and - // already tore the event down; there is nobody left to signal. } } }); @@ -1273,12 +1263,9 @@ namespace Ryujinx.Ava.Systems if (GraphicsConfig.ResScale != 1) { - dockedMode += $" ({GraphicsConfig.ResScale}x)"; + dockedMode += $" ({GraphicsConfig.ResScale:0.##}x)"; } - // DLSS-FG status chip (user request): "FG x2" green while interpolating, "FG x1" - // neutral while armed-but-passthrough (before, the FPS suffix just vanished at x1 - // and read as a malfunction), absent when FG is off. string frameGen = string.Empty; if (Graphics.Vulkan.Dlss.StreamlineFrameGen.UiArmed) { @@ -1286,8 +1273,6 @@ namespace Ryujinx.Ava.Systems frameGen = fgMult >= 2 ? $"FG ×{fgMult}" : "FG ×1"; } - // DLSS chip (user request): the active mode's label, green while super resolution - // is really evaluating frames. Absent when DLSS is off. string dlss = string.Empty; if (Graphics.Vulkan.Dlss.StreamlineDlss.UiActive) { @@ -1322,9 +1307,6 @@ namespace Ryujinx.Ava.Systems ? LocaleManager.GetFormatted(LocaleKeys.FpsTurboStatusBarText, frameRate, frameTime, Device.TickScalar) : LocaleManager.GetFormatted(LocaleKeys.FpsStatusBarText, frameRate, frameTime); - // DLSS frame generation: show what the display actually receives. The multiplier - // is 1 unless FG is measurably interpolating (present-side counter), so the - // default status bar stays byte-identical with FG off. int fgMultiplier = Graphics.Vulkan.Dlss.StreamlineFrameGen.UiMultiplier; if (fgMultiplier >= 2) { diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index 098cf11c7..3cdea6ff8 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -596,10 +596,6 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (nca.Header.ContentType == NcaContentType.PublicData) { - // [ROBUSTESSE 20/07] Reject AOC NCAs whose data section cannot actually be - // opened (seen in the wild: scene-made "unlocker" DLC NSPs). Registering them - // only defers the failure to game runtime; skipping here makes the existing - // "no valid DLC in this file" dialog fire at ADD time, while the user watches. try { _ = nca.OpenStorage(NcaSectionType.Data, IntegrityCheckLevel.None); @@ -1183,12 +1179,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary Logger.Warning?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid."); return null; } - + string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); string metadataFile = Path.Combine(metadataFolder, "metadata.json"); ApplicationMetadata appMetadata; - + if (!File.Exists(metadataFile)) { Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}..."); diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index 4a7e5a1b1..8a04097e8 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -70,37 +70,30 @@ namespace Ryujinx.Ava.Systems.Configuration public int ScalingFilterLevel { get; set; } /// - /// Enables or disables HDR output (scRGB) via SDR->HDR inverse tone mapping (AutoHDR-style retrofit, not native per-game HDR). /// public bool EnableHdr { get; set; } /// - /// HDR paper white (reference white) brightness, in nits. /// public int HdrPaperWhite { get; set; } /// - /// HDR peak (highlight) brightness target, in nits. /// public int HdrPeakBrightness { get; set; } /// - /// HDR highlight intensity (0-100): how far down the brightness range the highlight expansion reaches. /// public int HdrIntensity { get; set; } /// - /// HDR SDR-decode gamma x100 (e.g. 220 = gamma 2.20). Higher = deeper blacks on OLED. /// public int HdrGamma { get; set; } /// - /// HDR highlight colour mix (0-100): 0 = luma (calm/white highlights), 100 = max channel (coloured lights pop). 50 = balanced. /// public int HdrHighlightMix { get; set; } /// - /// HDR highlight whitening (0-100): desaturate the brightest highlights toward white. 0 = old look (keep colour). /// public int HdrHighlightWhiten { get; set; } @@ -148,7 +141,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public bool LoggingEnableFsAccessLog { get; set; } - + /// /// Enables printing network log messages /// @@ -505,7 +498,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// Uses Hypervisor over JIT if available /// public bool UseHypervisor { get; set; } - + /// /// Enable or disable low-latency mode for garbage collection /// diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index 5af922bbb..8a38ca2e6 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -257,7 +257,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public ReactiveObject EnableFsAccessLog { get; private set; } - + /// /// Enables printing network log messages /// @@ -333,7 +333,6 @@ namespace Ryujinx.Ava.Systems.Configuration /// public ReactiveObject MatchSystemTime { get; private set; } - /// /// Enable or disable use global input config (Independent from controllers binding) /// @@ -417,7 +416,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// Uses Hypervisor over JIT if available /// public ReactiveObject UseHypervisor { get; private set; } - + /// /// Enable or disable low-latency garbage collection /// @@ -621,40 +620,30 @@ namespace Ryujinx.Ava.Systems.Configuration public ReactiveObject ScalingFilterLevel { get; private set; } /// - /// Enables or disables HDR output (scRGB) via SDR->HDR inverse tone mapping (AutoHDR-style retrofit, not native per-game HDR). /// public ReactiveObject EnableHdr { get; private set; } /// - /// HDR paper white (reference white) brightness, in nits. /// public ReactiveObject HdrPaperWhite { get; private set; } /// - /// HDR peak (highlight) brightness target, in nits. /// public ReactiveObject HdrPeakBrightness { get; private set; } /// - /// HDR highlight intensity (0-100): how far down the brightness range the highlight - /// expansion reaches. Higher = more of the scene pops, lower = only near-white pops. /// public ReactiveObject HdrIntensity { get; private set; } /// - /// HDR SDR-decode gamma x100 (e.g. 220 = gamma 2.20). Higher = deeper blacks on OLED. /// public ReactiveObject HdrGamma { get; private set; } /// - /// HDR highlight colour mix (0-100): 0 = luma (calm/white highlights), 100 = max - /// channel (coloured lights pop toward peak). 50 = balanced. /// public ReactiveObject HdrHighlightMix { get; private set; } /// - /// HDR highlight whitening (0-100): desaturate the brightest highlights toward white. - /// 0 = keep full colour (old look), 100 = strong whitening of extreme highlights. /// public ReactiveObject HdrHighlightWhiten { get; private set; } diff --git a/src/Ryujinx/Systems/DlssRestart.cs b/src/Ryujinx/Systems/DlssRestart.cs index 5bf6d2e65..587520814 100644 --- a/src/Ryujinx/Systems/DlssRestart.cs +++ b/src/Ryujinx/Systems/DlssRestart.cs @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code. -// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only. using Ryujinx.Ava.Utilities; using Ryujinx.Common.Logging; @@ -11,33 +8,9 @@ using System.Threading; namespace Ryujinx.Ava.Systems { - /// - /// NVIDIA NGX (the DLSS runtime) only supports a SINGLE slInit per process: re-initializing it in the - /// same process -- which happens whenever a second game is loaded -- fail-fasts natively (0xc0000409 / - /// 0xc0000005, not catchable from managed). The clean, hardware-respecting answer is "one slInit per - /// process": whenever DLSS has been active and we would otherwise re-init it (stopping a game, or the - /// user changing the DLSS mode), we cold-restart the executable instead so every game always gets a - /// fresh process. - /// - /// Overlap safety: callers must invoke this only after the GPU/renderer has already been disposed (so - /// the old process has already run slShutdown and released the NGX/GPU state). A relaunch that overlaps - /// a still-live NGX state is exactly what crashed the earlier naive self-relaunch. - /// internal static class DlssRestart { - // [1.2.3] L'ancienne liste en dur des variables à retirer au restart a été remplacée par le - // balayage par préfixe dans RestartCold (elle oubliait EDGE/BORDERFIX/SKYDRIFT/SKYGRID/ - // STATS_STRIDE — l'état partiel hérité éteignait la moitié de la pile MV++ chez l'enfant). - /// - /// Cold-restarts the application (no game loaded, back to the game list) and exits the current - /// process. Returns only if the relaunch could not be started (the caller then stays open). - /// - /// - /// When true, the inherited DLSS environment is cleared so the new process picks up the mode the user - /// just saved in the UI (via DlssUiSettings.ApplyAtStartup). When false, the environment is inherited - /// unchanged so the new process keeps the exact same DLSS state (used when stopping a game). - /// public static void RestartCold(bool applyUiMode) { string exe = Environment.ProcessPath; @@ -52,33 +25,19 @@ namespace Ryujinx.Ava.Systems ProcessStartInfo startInfo = new(exe) { - // UseShellExecute must be false so we can edit the child's environment block. UseShellExecute = false, WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory, }; - // Preserve launcher arguments (data dir / profile) but deliberately add NO game path: a cold - // start lands on the game list, matching "reopen from the shortcut". foreach (string arg in CommandLineState.Arguments) { startInfo.ArgumentList.Add(arg); } - // The child waits (in Program.Main, via this env var) until the dying parent has fully - // exited: launching immediately raced the single-instance guard, which killed the child - // ~400 ms after boot (ghost logs / wrong data dir / settings that never applied, 11/07). startInfo.Environment["RYUJINX_RESTART_DELAY_MS"] = "3000"; if (applyUiMode) { - // [1.2.3] Balayage par PRÉFIXE au lieu de la liste en dur : la liste oubliait - // EDGE/BORDERFIX/SKYDRIFT/SKYGRID/STATS_STRIDE — après un changement de réglage - // DLSS dans l'UI, l'enfant héritait d'un état PARTIEL (early-return du profil - // avec la moitié de la pile morte : nuages qui reghostent, attrapé par l'œil - // sur le paquet 1.2.3 AVANT expédition). Tout RYUJINX_DLSS*/RYUJINX_MVPP_*/ - // HDR_PQ est retiré ; l'ApplyAtStartup du fils re-dérive TOUT depuis l'UI. - // (RESTART_DELAY_MS est posé APRÈS ce balayage, il survit.) Toute nouvelle - // variable de la famille est couverte d'office — la classe de bug meurt. foreach (string name in startInfo.Environment.Keys .Where(k => k.StartsWith("RYUJINX_DLSS") || k.StartsWith("RYUJINX_MVPP_") || k == "RYUJINX_HDR_PQ") .ToArray()) @@ -101,18 +60,8 @@ namespace Ryujinx.Ava.Systems Logger.Info?.Print(LogClass.Application, "DLSS restart: relaunching cold (one slInit per process)."); - // Environment.Exit waits on foreground threads + finalizers. After a real DLSS upscale pass (Quality - // mode), the GPU render thread can be stuck on a fence, which hangs the clean exit -- the window - // freezes and Windows force-closes it. The GPU/NGX is already disposed and the child has launched, so - // guard the exit with a watchdog: if the clean shutdown does not complete quickly, force-terminate - // this process so the relaunch always proceeds instead of hanging. The log line tells us, after the - // fact, whether the clean exit hung (watchdog fired) or succeeded (it did not). Thread watchdog = new(() => { - // 5 s, not 1 s: the clean exit is also what flushes the shader cache TOC and the - // config to disk. A 1 s fuse could Kill() mid-write and corrupt them; the hang - // this watchdog exists for is indefinite, so the extra seconds only cost time in - // the rare stuck case. Thread.Sleep(5000); Logger.Warning?.Print(LogClass.Application, "DLSS restart: clean exit hung >5s -- force-terminating the old process to complete the relaunch."); diff --git a/src/Ryujinx/Systems/DlssUiSettings.cs b/src/Ryujinx/Systems/DlssUiSettings.cs index 08176c503..a79f67870 100644 --- a/src/Ryujinx/Systems/DlssUiSettings.cs +++ b/src/Ryujinx/Systems/DlssUiSettings.cs @@ -1,6 +1,4 @@ -// 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.Configuration; using Ryujinx.Common.Logging; @@ -10,30 +8,14 @@ using System.IO; namespace Ryujinx.Ava.Systems { - /// - /// Persists the user's NVIDIA DLSS / DLAA choice from the graphics settings UI in a small standalone - /// file -- deliberately NOT in the main Ryujinx config, so its version is never touched. At startup it - /// translates the saved choice into the RYUJINX_DLSS_* environment variables the (env-var-driven) DLSS - /// backend reads, UNLESS those are already set externally (a launcher .bat / dev override wins). - /// - /// The DLSS render preset IS a user choice (user decision 05/07: "ça ne doit pas être - /// verrouillé"): the second token stores the nvngx preset value (11=K default, 13=M, 12=L, - /// 10=J, 6=F), applied as RYUJINX_DLSS_PRESET at startup; 0/absent = engine defaults. - /// - /// File format: a "mode [preset]" line. Mode index: 0 = Off, 1 = DLAA, 2 = Quality, - /// 3 = Balanced, 4 = Performance. A change applies on the next application launch. - /// public static class DlssUiSettings { - // Mode index -> RYUJINX_DLSS_MODE string accepted by DlssIntegration.ParseQualityMode (0 = Off). private static readonly string[] _modeEnv = { "", "dlaa", "quality", "balanced", "performance" }; - // UI combo index -> nvngx preset value. Order matches the settings ComboBox. - public static readonly int[] PresetValues = { 11, 13, 12, 10, 6 }; // K, M, L, J, F + public static readonly int[] PresetValues = { 11, 13, 12, 10, 6 }; private static string FilePath => Path.Combine(AppDataManager.BaseDirPath, "dlss_mode.cfg"); - /// Reads the saved DLSS quality-mode index (0 = Off if absent or invalid). public static int Load() { try @@ -59,7 +41,6 @@ namespace Ryujinx.Ava.Systems return 0; } - /// Reads the saved nvngx preset VALUE (0 = unset -> engine defaults). public static int LoadPresetValue() { try @@ -85,7 +66,6 @@ namespace Ryujinx.Ava.Systems return 0; } - /// Reads the saved Frame Generation display multiplier (1 = off, 2 = 2x, 3 = 3x). public static int LoadFgMultiplier() { try @@ -111,7 +91,6 @@ namespace Ryujinx.Ava.Systems return 1; } - /// Reads the saved runtime mipmap generation choice (independent of the DLSS mode). public static bool LoadForceMips() { try @@ -132,8 +111,6 @@ namespace Ryujinx.Ava.Systems return false; } - /// [SHARPEN 02/08] Reads the saved post-DLSS sharpening level (0 = off, 1-100 = RCAS strength; - /// 5th token, absent for pre-sharpening-era files). public static int LoadSharpenLevel() { try @@ -158,8 +135,6 @@ namespace Ryujinx.Ava.Systems return 0; } - /// [XC2STACK 03/08] Reads a boolean token by index (6th = advanced motion stack, - /// 7th = motion-blur skip). Absent = false, so pre-existing files keep today's behaviour. private static bool LoadFlag(int index) { try @@ -180,16 +155,8 @@ namespace Ryujinx.Ava.Systems return false; } - /// [DOFSKIP UI 03/08] The ONE option that TAKES SOMETHING AWAY, hence the only - /// one behind a checkbox (user decision): skipping the Xenoblade-family motion-blur - /// scatter pass removes its Vulkan block artifact AND the blur with it (see MvppDofSkip - - /// journal 226..246 eliminated every shader, input, constant and path by measurement; - /// this is a trade, not a repair). Everything else in the motion stack only changes which - /// camera data is trusted, removes nothing, and ships in the profile. public static bool LoadDofSkip() => LoadFlag(5); - /// Writes mode index + preset value + FG multiplier + mip-generation flag + sharpening level - /// + XC2 stack flag. Takes effect on the next application launch. public static void Save(int mode, int presetValue, int fgMultiplier, bool forceMips, int sharpenLevel, bool dofSkipFlag) { try @@ -205,17 +172,6 @@ namespace Ryujinx.Ava.Systems } } - /// - /// Applies the saved DLSS mode as environment variables so the DLSS backend picks it up when a game - /// launches. Skipped entirely if RYUJINX_DLSS is already set (a launcher .bat / dev override wins). - /// Off leaves DLSS disabled. Sets the validated profile: clip-space jitter 1.0 + LOD -0.5. The - /// render preset follows the user's UI choice (second token); unset = engine defaults (K). - /// - /// [ENVDUMP 03/08] Journalise TOUTES les variables RYUJINX_* presentes dans le - /// processus, triees. Permet de diffuser deux journaux (lancement UI vs lancement bat) - /// et de voir mecaniquement ce qui differe, au lieu de le deduire du code. - // [ENVDUMP] Diagnostic seulement : RYUJINX_ENVDUMP=1. Eteint par defaut - une release - // ne crache pas 24 lignes de variables a chaque demarrage (parti par erreur en 1.2.4). private static readonly bool _envDump = Environment.GetEnvironmentVariable("RYUJINX_ENVDUMP") == "1"; @@ -254,8 +210,6 @@ namespace Ryujinx.Ava.Systems public static void ApplyAtStartup() { - // Runtime mip generation is independent of the DLSS mode; an externally set variable - // (launcher .bat / dev override) always wins. if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS")) && LoadForceMips()) { Environment.SetEnvironmentVariable("RYUJINX_MVPP_FORCEMIPS", "1"); @@ -268,8 +222,6 @@ namespace Ryujinx.Ava.Systems if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RYUJINX_DLSS"))) { - // [ENVDUMP] Lancement par bat : le profil UI se retire, mais on journalise quand - // meme l'environnement pour pouvoir le comparer a un lancement par l'interface. DumpEnvironment("lancement par bat, profil UI ignore"); return; @@ -289,64 +241,26 @@ namespace Ryujinx.Ava.Systems Environment.SetEnvironmentVariable("RYUJINX_DLSS_MODE", _modeEnv[mode]); Environment.SetEnvironmentVariable("RYUJINX_DLSS_SR", "1"); - // The validated motion-vector stack: camera-reprojection MVs + sky/cloud handling. - // NO jitter in the released profile (user decision 11/07: it amplifies the known - // foliage shimmer; it returns once that dossier is closed). + Environment.SetEnvironmentVariable("RYUJINX_DLSS_AUTO_RES", "1"); + Environment.SetEnvironmentVariable("RYUJINX_MVPP_CAP", "1"); Environment.SetEnvironmentVariable("RYUJINX_MVPP_REPROJ", "2"); Environment.SetEnvironmentVariable("RYUJINX_MVPP_FLIPY", "1"); Environment.SetEnvironmentVariable("RYUJINX_MVPP_VPFIFO", "1"); Environment.SetEnvironmentVariable("RYUJINX_MVPP_SKYROT", "1"); - // Leading-edge disocclusion = clamp to nearest valid history (user A/B 13/07 at x3: - // 0 zero-MV = fine black lines on pans, 1 clamp = far less, >=2 raw = three lines). - // The engine default stays 0 so diagnostic baselines are unchanged; only the shipped - // UI profile opts in. - Environment.SetEnvironmentVariable("RYUJINX_MVPP_EDGE", "1"); + Environment.SetEnvironmentVariable("RYUJINX_MVPP_EDGE", "6"); - // v1.2.3: black-frame killer. The game's scaled-resolution compose leaves a - // pure-black texel border; DLSS's temporal history drags it inward on pans (the - // long-standing "black frame" report). The pass detects and fills it before DLSS - // ever sees it (BorderClamp, generic, any game). Proven at the bench (ECHO A/B) - // and approved in-game 18/07. Environment.SetEnvironmentVariable("RYUJINX_MVPP_BORDERFIX", "1"); - // v1.2.3: living clouds. Global sky-content drift (1 Hz EMA of the optical-flow - // mean) + per-zone residuals on an 8x4 grid, fed back so DLSS understands cloud - // drift instead of fighting it. Stats sampled 1 frame in 4 (the validated cadence; - // 1-in-8 measured too noisy -- zone EMAs breathe). +0.4 ms. Approved 17-18/07. Environment.SetEnvironmentVariable("RYUJINX_MVPP_SKYDRIFT", "1"); Environment.SetEnvironmentVariable("RYUJINX_MVPP_SKYGRID", "1"); - // [03/08, corrige le meme jour] RYUJINX_MVPP_STATS_STRIDE reste a 4. Retire un moment - // pour gagner en qualite de nuages, il a immediatement coute le seuil vsync sur XC2 - // (60 -> 30 des que la camera bouge, avec Frame Generation) et un tremblement general - // jamais vu auparavant. C'est un reglage de PERFORMANCE : on ne le change pas sans - // mesurer ce qu'il coute, sur un jeu qui tourne deja pres de la limite. Verdict a - // l'oeil de l'utilisateur, dans sa configuration reelle : 4 = parfait, 1 = casse. Environment.SetEnvironmentVariable("RYUJINX_MVPP_STATS_STRIDE", "4"); - // v1.2.4: camera-cut handling, validated at length on Xenoblade Chronicles 2 (01/08: - // election churn 35 -> 4-9, "elastic" mechanism 55% -> 13%, cinematics approved) and - // eye-checked on BOTW + TOTK before shipping (02/08). - // - ELECT_HYST: the camera election holds through a scene change instead of re-electing - // on the first frames of the new scene (the "ghost double image" after cuts). - // - ELECT_WARP: a hard camera warp (teleport) resolves as a clean history reset instead - // of a brief flash of distorted image. - // - SCENECUT_MAX_MOTION: a scene cut is only declared when the frame is nearly still, - // so fast camera motion is never mistaken for a cut. Environment.SetEnvironmentVariable("RYUJINX_MVPP_ELECT_HYST", "500"); Environment.SetEnvironmentVariable("RYUJINX_MVPP_ELECT_WARP", "1"); Environment.SetEnvironmentVariable("RYUJINX_DLSS_SCENECUT_MAX_MOTION", "0.01"); - // [XC2STACK 03/08] Opt-in stack, off unless the user ticks it. Ten refinements of the - // solo-camera path (the one used when a game stores its camera outside the canonical - // triplet), validated over days on Xenoblade Chronicles 2 but unproven elsewhere - - // hence a checkbox rather than a default. Nothing visual is removed by these; they - // only change WHICH camera data the motion-vector stack trusts and when. - // [XC2STACK 03/08] The whole Xenoblade-validated stack behind ONE switch (user decision: - // "un seul bouton avec tout"), off by default so nobody is exposed without asking. - // Ten solo-camera refinements that remove nothing from the image, plus the - // motion-blur scatter skip which IS a trade (blur removed, not repaired). bool dofSkip = LoadDofSkip(); if (dofSkip) { @@ -362,12 +276,8 @@ namespace Ryujinx.Ava.Systems Environment.SetEnvironmentVariable("RYUJINX_MVPP_SNAPFLOOR", "0.5"); Environment.SetEnvironmentVariable("RYUJINX_MVPP_NOLDR", "1"); - // [03/08] RYUJINX_MVPP_DOF_SCATTER_SKIP retire de cette case : les blocs noirs - // qu'il masquait etaient causes par NOTRE bloc de jitter (lecture d'une sortie - // Position non ecrite). Le vrai correctif les supprime a la racine, donc sauter - // la passe de flou ne sert plus a rien - et couter au joueur le flou de mouvement - // du jeu pour masquer un bug qu'on a repare serait absurde. Le gate existe - // toujours (RYUJINX_MVPP_DOF_SCATTER_SKIP=1) pour qui en aurait encore besoin. + Environment.SetEnvironmentVariable("RYUJINX_MVPP_ELECT_MAXSTEP", "1000"); + } int fgMultiplier = LoadFgMultiplier(); @@ -376,10 +286,6 @@ namespace Ryujinx.Ava.Systems Environment.SetEnvironmentVariable("RYUJINX_DLSS_FG", "1"); Environment.SetEnvironmentVariable("RYUJINX_DLSS_FG_FRAMES", (fgMultiplier - 1).ToString(CultureInfo.InvariantCulture)); - // DLSS-G cannot present on the FP16/scRGB HDR swapchain (DLSS-G guide 11.0, see - // Window.ChooseSwapSurfaceFormat): with HDR enabled, switch the container to - // RGB10A2+PQ -- same light, different encoding -- so frame generation can engage. - // No-op on SDR displays (neither HDR branch triggers there). Environment.SetEnvironmentVariable("RYUJINX_HDR_PQ", "1"); } @@ -389,9 +295,6 @@ namespace Ryujinx.Ava.Systems Environment.SetEnvironmentVariable("RYUJINX_DLSS_PRESET", preset.ToString(CultureInfo.InvariantCulture)); } - // [SHARPEN 02/08] Post-DLSS RCAS sharpening (journal (372)): 0 = off, 1-100 = the UI - // slider, same strength mapping as the FSR sharpening slider. Backend gate: - // DlssSharpenPass (linear pre-pass before the final tone-mapped blit). int sharpen = LoadSharpenLevel(); if (sharpen > 0) { @@ -399,10 +302,6 @@ namespace Ryujinx.Ava.Systems Environment.SetEnvironmentVariable("RYUJINX_DLSS_SHARPEN_LEVEL", sharpen.ToString(CultureInfo.InvariantCulture)); } - // [ENVDUMP 03/08] Ce que le PROCESSUS a reellement en memoire, quelle que soit - // l'origine (profil UI ou bat). Comparer deux journaux vaut mieux que comparer du - // code : c'est la seule facon de prouver qu'un lancement par l'interface et un - // lancement par bat sont identiques - ou de voir exactement ce qui manque. DumpEnvironment("apres profil UI"); Logger.Info?.Print(LogClass.Application, diff --git a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs index a485f8fbf..5282c3920 100644 --- a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs +++ b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs @@ -1,7 +1,3 @@ -// The Beast Roofer Edition -- update check. -// Original Ryubing flow talked to their UpdateServer via a client library. -// This fork talks to our OWN update server (server.py) with a simple JSON -// response: { "version": "...", "url": "...", "notes": "..." }. using Gommon; using Ryujinx.Ava.Common.Locale; @@ -18,13 +14,10 @@ namespace Ryujinx.Ava.Systems { internal static partial class Updater { - // Our own update server (fronted by Caddy on the Beast Roofer host). private const string UpdateEndpoint = "https://thebeastroofer-edition.com/updates/latest"; - // The download URL of the latest build artifact, set by CheckVersionAsync. private static string _artifactUrl; - // Minimal shape returned by server.py /latest. private sealed class BeastUpdateInfo { [JsonPropertyName("version")] public string Version { get; set; } @@ -66,7 +59,6 @@ namespace Ryujinx.Ava.Systems return default; } - // If no artifact URL was returned, assume no update is available. if (info == null || string.IsNullOrEmpty(info.Url)) { if (showVersionUpToDate) @@ -100,7 +92,6 @@ namespace Ryujinx.Ava.Systems _artifactUrl = info.Url; - // Single download stream -- simplest and most reliable. _connectionCount = 1; return (currentVersion, newVersion); diff --git a/src/Ryujinx/Systems/Updater/Updater.cs b/src/Ryujinx/Systems/Updater/Updater.cs index 66577d52c..e15da8e3b 100644 --- a/src/Ryujinx/Systems/Updater/Updater.cs +++ b/src/Ryujinx/Systems/Updater/Updater.cs @@ -32,14 +32,14 @@ namespace Ryujinx.Ava.Systems private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory; private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish"); - + private static int _connectionCount = 1; private static long _buildSize; private static bool _updateSuccessful; private static bool _running; private static readonly string[] _windowsDependencyDirs = []; - + public static async Task BeginUpdateAsync(bool showVersionUpToDate = false) { if (_running) @@ -62,7 +62,7 @@ namespace Ryujinx.Ava.Systems { await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], - string.Empty, + string.Empty, changelogUrl: ReleaseInformation.GetChangelogUrl(currentVersion, currentVersion)); } @@ -75,8 +75,6 @@ namespace Ryujinx.Ava.Systems await Dispatcher.UIThread.InvokeAsync(async () => { - // Surface a persistent "update available" indicator in the status bar, so the - // user can still grab this update later even if they dismiss this prompt. RyujinxApp.MainWindow.ViewModel.UpdateAvailable = true; string newVersionString = ReleaseInformation.IsCanaryBuild @@ -129,11 +127,11 @@ namespace Ryujinx.Ava.Systems // If we get a .zip url switch it to the preferred .7z file instead // The update server still returns the .zip url by default for legacy support downloadUrl = downloadUrl.Replace(".zip", ".7z"); - + // If we get a .tar.gz url switch it to the preferred .tar.xz file instead // The update server still returns the .tar.gz url by default for legacy support downloadUrl = downloadUrl.Replace(".tar.gz", ".tar.xz"); - + string updateFile = Path.Combine(_updateDir, "update.bin"); TaskDialog taskDialog = new() @@ -144,13 +142,13 @@ namespace Ryujinx.Ava.Systems ShowProgressBar = true, XamlRoot = RyujinxApp.MainWindow, }; - + // Fetch build size information to learn chunk sizes. using HttpClient buildSizeClient = ConstructHttpClient(); try { buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0"); - + // Forgejo instance is located in Ukraine. Connection times will vary across the world. buildSizeClient.Timeout = TimeSpan.FromSeconds(10); @@ -393,10 +391,6 @@ namespace Ryujinx.Ava.Systems updateFileStream.Write(buffer, 0, readSize); } - // Install on the UI thread: InstallUpdate touches the task dialog, and - // Avalonia UI calls must run on the UI thread. (The multi-threaded path - // already runs InstallUpdate on the UI thread via its WebClient callback; - // this keeps the single-threaded fallback consistent and crash-free.) Dispatcher.UIThread.Post(() => InstallUpdate(taskDialog, updateFile)); } @@ -406,12 +400,6 @@ namespace Ryujinx.Ava.Systems return max == 0 ? 0 : value / max * 100; } - /// - /// Runs a file operation that can transiently fail because the freshly-downloaded - /// update archive is momentarily locked by another process. On Windows this is almost - /// always the antivirus (e.g. Defender) scanning the just-written file; the lock clears - /// within a second or two. Retries briefly before giving up. - /// private static T RetryOnTransientFileLock(Func operation) { const int maxAttempts = 20; @@ -443,8 +431,6 @@ namespace Ryujinx.Ava.Systems } catch (Exception ex) { - // An unhandled exception on this background thread would otherwise hard-crash - // the whole application. Surface it as a normal error dialog instead. Logger.Error?.Print(LogClass.Application, $"Update failed: {ex}"); _running = false; @@ -470,17 +456,17 @@ namespace Ryujinx.Ava.Systems { using FileStream inStream = File.OpenRead(archivePath); using GZipStream gzipStream = new(inStream, CompressionMode.Decompress); - + TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true); } - + [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] private static void ExtractTarXzipFile(string archivePath, string outputDirectoryPath) { using FileStream inStream = File.OpenRead(archivePath); using XZStream gzipStream = new(inStream); - + TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true); } @@ -488,12 +474,9 @@ namespace Ryujinx.Ava.Systems { ZipFile.ExtractToDirectory(archivePath, outputDirectoryPath); } - + private static void Extract7ZipFile(string archivePath, string outputDirectoryPath) { - // Retry the open: a freshly-downloaded archive is often momentarily locked by - // antivirus (e.g. Windows Defender) scanning it. `using` releases the handle so - // the file can be deleted afterwards (the previous code leaked it). using IArchive archive = RetryOnTransientFileLock(() => ArchiveFactory.OpenArchive(archivePath)); archive.WriteToDirectory(outputDirectoryPath); } @@ -516,7 +499,7 @@ namespace Ryujinx.Ava.Systems { throw new NotSupportedException(); } - + // The new decompression implementations don't have a way to show progress // so the progressbar is just set to 100% after the decompression is done taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal); diff --git a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs index 48c824500..fade57808 100644 --- a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs +++ b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs @@ -103,7 +103,7 @@ namespace Ryujinx.Ava.UI.Helpers return await ShowContentDialog(title, content, primaryButton, secondaryButton, closeButton, primaryButtonResult, deferResetEvent, deferCloseAction); } - + public async static Task ShowTextDialogWithButton( string title, string primaryText, @@ -258,7 +258,7 @@ namespace Ryujinx.Ava.UI.Helpers buttonContent.Children.Add(new TextBlock { - Text = buttonName, + Text = buttonName, Margin = new Thickness(2) }); @@ -485,13 +485,6 @@ namespace Ryujinx.Ava.UI.Helpers isTopDialog = false; } - // When no game is running, host the dialog INLINE on the main window's OverlayLayer - // (FluentAvalonia's native hosting -- the same ShowAsync(visual) API used below). This - // avoids spawning a separate transparent top-level window per dialog, which is what - // produces the brief compositor flash at cold startup (the empty first frame of a fresh - // transparent OS window). The dedicated overlay window is only required WHILE A GAME IS - // RUNNING, because the native Vulkan/OpenGL render surface draws on top of any inline - // Avalonia overlay and would hide the dialog. if (isTopDialog && parent is MainWindow inlineHost && !inlineHost.ViewModel.IsGameRunning) { inlineHost.Activate(); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 66d595962..eae8348e3 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -113,14 +113,10 @@ namespace Ryujinx.Ava.UI.ViewModels [ObservableProperty] public partial Brush VSyncModeColor { get; set; } - // DLSS-FG status chip: green (VRR resource) while interpolating, gray while armed at x1, - // hidden when frame generation is off. [ObservableProperty] public partial Brush FgStatusColor { get; set; } [ObservableProperty] public partial string FgStatusText { get; set; } [ObservableProperty] public partial bool ShowFgStatus { get; set; } - // DLSS chip: the active mode's label (DLAA / DLSS Q / ...), green while super - // resolution is really evaluating; hidden when DLSS is off. [ObservableProperty] public partial Brush DlssStatusColor { get; set; } [ObservableProperty] public partial string DlssStatusText { get; set; } [ObservableProperty] public partial bool ShowDlssStatus { get; set; } @@ -1388,9 +1384,6 @@ namespace Ryujinx.Ava.UI.ViewModels _ => "VSync" }; - // DLSS-FG chip: green (the VRR color) while interpolating, gray while armed - // at x1 (visible on purpose: the old FPS suffix vanished at x1 and read as a - // malfunction), hidden when FG is off. ShowFgStatus = !string.IsNullOrEmpty(args.FrameGen); if (ShowFgStatus) { @@ -1409,7 +1402,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // DLSS chip: green (the VRR color) while evaluating, hidden otherwise. ShowDlssStatus = !string.IsNullOrEmpty(args.Dlss); if (ShowDlssStatus) { @@ -1809,9 +1801,9 @@ namespace Ryujinx.Ava.UI.ViewModels private async Task?> PickFolders(LocaleKeys titleKey) { - return (await StorageProvider.OpenMultiFolderPickerAsync(new FolderPickerOpenOptions - { - Title = LocaleManager.Instance[titleKey] + return (await StorageProvider.OpenMultiFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[titleKey] })).TryGet(out IReadOnlyList folders) ? folders.Select(f => f.Path.LocalPath).ToList() : null; @@ -1843,12 +1835,12 @@ namespace Ryujinx.Ava.UI.ViewModels string backendThreadingInit = Program.BackendThreadingArg ?? ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString(); - // If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting. + // If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting. string idGame = application.IdBaseString; if (ConfigurationFileFormat.TryLoad(Program.GetDirGameUserConfig(idGame), out ConfigurationFileFormat configurationFileFormat)) { - // Loads the user configuration, having previously changed the global configuration to the user configuration + // Loads the user configuration, having previously changed the global configuration to the user configuration ConfigurationState.Instance.Load(configurationFileFormat, Program.GetDirGameUserConfig(idGame, true), idGame); @@ -1915,7 +1907,7 @@ namespace Ryujinx.Ava.UI.ViewModels UserChannelPersistence, this, TopLevel); - + CancellationTokenSource cts = new CancellationTokenSource(); try @@ -1934,11 +1926,11 @@ namespace Ryujinx.Ava.UI.ViewModels { cts.Dispose(); } - + CanUpdate = false; application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; - + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); @@ -1964,9 +1956,9 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId, TimeSpan playTime) + public static void UpdateGameMetadata(string titleId, TimeSpan playTime) => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); - + public void RefreshFirmwareStatus() { SystemVersion version = null; @@ -2004,11 +1996,6 @@ namespace Ryujinx.Ava.UI.ViewModels IsGameRunning = false; - // NGX (DLSS) only supports a single slInit per process. If DLSS ran this session, returning to the - // game list and loading another game would re-initialize NGX in-process and fail-fast. The GPU and - // renderer are already disposed here (DisposeContext ran slShutdown before firing AppExit), so a - // cold restart is overlap-safe and guarantees the next game gets a fresh process. Skipped during a - // full app close (IsClosing already handled above) so quitting Ryujinx does not relaunch it. if (Ryujinx.Graphics.Vulkan.Dlss.Streamline.WasEverInitialized) { DlssRestart.RestartCold(applyUiMode: false); diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 224328e89..9e5f656c0 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -97,7 +97,6 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsGameTitleNotNull => !string.IsNullOrEmpty(GameTitle); public double PanelOpacity => IsGameTitleNotNull ? 0.5 : 1; - public int ResolutionScale { get => _resolutionScale; @@ -181,7 +180,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } public bool EnableMouse { get; set; } - public bool DisableInputWhenOutOfFocus { get; set; } + public bool DisableInputWhenOutOfFocus { get; set; } public int FocusLostActionType { get; set; } public bool UseGlobalInputConfig @@ -200,7 +199,7 @@ namespace Ryujinx.Ava.UI.ViewModels public VSyncMode VSyncMode { - get => _vSyncMode; + get => _vSyncMode; set { if (value is VSyncMode.Custom or VSyncMode.Switch or VSyncMode.Unbounded) @@ -261,9 +260,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // --- Clean VSync / VRR UI (Roofer Edition) --- - // Dropdown positions: 0 = Switch, 1 = Unbounded, 2 = VRR. We translate position <-> enum - // here so we can skip the legacy "Custom" enum value (2) and still bind by SelectedIndex. public int VSyncModeIndex { get => _vSyncMode switch @@ -285,10 +281,8 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // The VRR FPS cap picker is only relevant when VRR is the selected mode. public bool ShowVrrCapPicker => _vSyncMode == VSyncMode.Vrr; - // 0 = Off (unbounded VRR), 1 = 30, 2 = 60, 3 = 120. public int VrrFpsCapIndex { get; @@ -302,7 +296,6 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnablePptc { get; set; } public bool EnableLowPowerPptc { get; set; } - public long TurboMultiplier { get; @@ -485,7 +478,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // Index of the "DLSS" entry in the Scaling Filter dropdown (after Bilinear/Nearest/Fsr/Area/NIS). private const int DlssScalingIndex = 5; public int ScalingFilter @@ -499,30 +491,24 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(IsDlssSelected)); OnPropertyChanged(nameof(IsDlssMenuEnabled)); - // Couple the two menus: choosing DLSS in the filter list enables the quality sub-menu - // (defaulting to DLAA); any other filter forces the sub-menu back to Off. if (value == DlssScalingIndex) { if (DlssMode == 0) { - DlssMode = 1; // DLAA + DlssMode = 1; } } else { - DlssMode = 0; // Off + DlssMode = 0; } } } - /// True when "DLSS" is the selected scaling filter. public bool IsDlssSelected => _scalingFilter == DlssScalingIndex; - /// The DLSS quality sub-menu is active only when DLSS is selected and no game is running. public bool IsDlssMenuEnabled => IsDlssSelected && !IsGameRunning; - // NVIDIA DLSS / DLAA mode (0 = Off, 1 = DLAA, 2 = Quality, 3 = Balanced, 4 = Performance). Stored - // outside the main config (see DlssUiSettings); applied as env vars on the next app launch. private int _dlssMode; public int DlssMode { @@ -534,7 +520,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // DLSS Frame Generation display multiplier as combo index (0 = Off, 1 = 2x, 2 = 3x). private int _dlssFgIndex; public int DlssFgIndex { @@ -546,7 +531,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // Runtime mipmap generation (independent of the DLSS mode; applies on next launch). private bool _enableForceMips; public bool EnableForceMips { @@ -558,8 +542,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // DLSS render preset -- a USER choice (decision 05/07: "ça ne doit pas être verrouillé"). - // Combo index into DlssUiSettings.PresetValues: 0=K (default), 1=M, 2=L, 3=J, 4=F. private int _dlssPresetIndex; public int DlssPresetIndex { @@ -567,19 +549,13 @@ namespace Ryujinx.Ava.UI.ViewModels set { _dlssPresetIndex = value; - _dlssPresetWasUnset = false; // an explicit pick replaces "engine default" + _dlssPresetWasUnset = false; OnPropertyChanged(); } } - // True when the saved preset token was absent/0 (engine default) or unknown: the combo - // displays K (which IS the engine default) but saving without touching it must preserve - // the default token -- silently writing an explicit K also tripped a false restart - // prompt for every pre-preset-era user (save wrote 11, the file said 0). private bool _dlssPresetWasUnset; - // [SHARPEN 02/08] Nettete post-DLSS (RCAS maison), 0 = off, 1-100 = force du slider. - // Applique au prochain lancement, comme le reste du panneau DLSS. private int _dlssSharpenLevel; public int DlssSharpenLevel { @@ -591,8 +567,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // [XC2STACK 03/08] Pile de mouvement validee sur Xenoblade Chronicles 2, en un seul - // interrupteur (coche = tout marche, decoche = rien). Applique au prochain lancement. private bool _enableDofSkip; public bool EnableDofSkip { @@ -604,8 +578,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - // Set true by SaveSettings when the DLSS mode actually changed, so the settings window can offer a - // one-click self-restart (the DLSS env vars are only read at app startup). public bool DlssModeRestartPending { get; set; } public int PreferredGpuIndex { get; set; } @@ -625,7 +597,7 @@ namespace Ryujinx.Ava.UI.ViewModels [ObservableProperty] public partial bool MatchSystemTime { get; set; } - + public DateTimeOffset CurrentDate { get; set; } public TimeSpan CurrentTime { get; set; } @@ -935,9 +907,6 @@ namespace Ryujinx.Ava.UI.ViewModels CustomVSyncInterval = config.Graphics.CustomVSyncInterval; - // VSync / VRR (Roofer clean UI): set the backing field directly so VRR loads (the - // legacy VSyncMode setter rejects it), and derive the VRR cap picker from the stored - // custom-interval values. _vSyncMode = config.Graphics.VSyncMode.Value; VrrFpsCapIndex = config.Graphics.EnableCustomVSyncInterval.Value ? config.Graphics.CustomVSyncInterval.Value switch @@ -992,11 +961,11 @@ namespace Ryujinx.Ava.UI.ViewModels int presetSaved = Ryujinx.Ava.Systems.DlssUiSettings.LoadPresetValue(); int presetIndex = Array.IndexOf(Ryujinx.Ava.Systems.DlssUiSettings.PresetValues, presetSaved); DlssPresetIndex = Math.Max(0, presetIndex); - _dlssPresetWasUnset = presetIndex < 0; // after the property write (the setter clears it) + _dlssPresetWasUnset = presetIndex < 0; DlssFgIndex = Math.Clamp(Ryujinx.Ava.Systems.DlssUiSettings.LoadFgMultiplier() - 1, 0, 2); EnableForceMips = Ryujinx.Ava.Systems.DlssUiSettings.LoadForceMips(); - DlssSharpenLevel = Ryujinx.Ava.Systems.DlssUiSettings.LoadSharpenLevel(); // [SHARPEN] - EnableDofSkip = Ryujinx.Ava.Systems.DlssUiSettings.LoadDofSkip(); // [DOFSKIP] + DlssSharpenLevel = Ryujinx.Ava.Systems.DlssUiSettings.LoadSharpenLevel(); + EnableDofSkip = Ryujinx.Ava.Systems.DlssUiSettings.LoadDofSkip(); // Audio AudioBackend = (int)config.System.AudioBackend.Value; @@ -1098,8 +1067,6 @@ namespace Ryujinx.Ava.UI.ViewModels // Graphics config.Graphics.VSyncMode.Value = _vSyncMode; - // VRR FPS cap -> reuse the custom-interval plumbing the engine already reads - // (EnableCustomVSyncInterval = cap on, CustomVSyncInterval = the capped fps). bool vrrCapOn = _vSyncMode == VSyncMode.Vrr && VrrFpsCapIndex > 0; config.Graphics.EnableCustomVSyncInterval.Value = vrrCapOn; if (vrrCapOn) @@ -1130,14 +1097,11 @@ namespace Ryujinx.Ava.UI.ViewModels config.Graphics.MaxAnisotropy.Value = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy); config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio; config.Graphics.AntiAliasing.Value = (AntiAliasing)AntiAliasingEffect; - // "DLSS" in the filter list is not a real spatial scaling filter (it is a separate pipeline); - // when it is selected, store a neutral spatial filter (DLSS bypasses it) and persist the chosen - // DLSS quality mode separately. Otherwise DLSS is Off and the real filter is stored. int dlssMode; if (ScalingFilter == DlssScalingIndex) { config.Graphics.ScalingFilter.Value = Ryujinx.Common.Configuration.ScalingFilter.Bilinear; - dlssMode = DlssMode == 0 ? 1 : DlssMode; // at least DLAA + dlssMode = DlssMode == 0 ? 1 : DlssMode; } else { @@ -1147,15 +1111,15 @@ namespace Ryujinx.Ava.UI.ViewModels config.Graphics.ScalingFilterLevel.Value = ScalingFilterLevel; int dlssPresetValue = _dlssPresetWasUnset && DlssPresetIndex == 0 - ? 0 // untouched engine default stays the engine default + ? 0 : Ryujinx.Ava.Systems.DlssUiSettings.PresetValues[ Math.Clamp(DlssPresetIndex, 0, Ryujinx.Ava.Systems.DlssUiSettings.PresetValues.Length - 1)]; DlssModeRestartPending = dlssMode != Ryujinx.Ava.Systems.DlssUiSettings.Load() || dlssPresetValue != Ryujinx.Ava.Systems.DlssUiSettings.LoadPresetValue() || DlssFgIndex + 1 != Ryujinx.Ava.Systems.DlssUiSettings.LoadFgMultiplier() || EnableForceMips != Ryujinx.Ava.Systems.DlssUiSettings.LoadForceMips() || - DlssSharpenLevel != Ryujinx.Ava.Systems.DlssUiSettings.LoadSharpenLevel() || // [SHARPEN] - EnableDofSkip != Ryujinx.Ava.Systems.DlssUiSettings.LoadDofSkip(); // [DOFSKIP] + DlssSharpenLevel != Ryujinx.Ava.Systems.DlssUiSettings.LoadSharpenLevel() || + EnableDofSkip != Ryujinx.Ava.Systems.DlssUiSettings.LoadDofSkip(); Ryujinx.Ava.Systems.DlssUiSettings.Save(dlssMode, dlssPresetValue, DlssFgIndex + 1, EnableForceMips, DlssSharpenLevel, EnableDofSkip); if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) @@ -1223,7 +1187,7 @@ namespace Ryujinx.Ava.UI.ViewModels { // Restores settings for a custom configuration during a game, if the condition is met. // If the condition is not met (parameter is false), restores global (default) configuration instead. - Program.ReloadConfig(isCustomConfig && isGameRunning); + Program.ReloadConfig(isCustomConfig && isGameRunning); } public void ApplyButton() diff --git a/src/Ryujinx/UI/Windows/GameSpecificSettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/GameSpecificSettingsWindow.axaml.cs index 2bbc49db9..ac1ecaeab 100644 --- a/src/Ryujinx/UI/Windows/GameSpecificSettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/GameSpecificSettingsWindow.axaml.cs @@ -14,9 +14,9 @@ namespace Ryujinx.Ava.UI.Windows //Fix compiler warning public GameSpecificSettingsWindow() { - + } - + public GameSpecificSettingsWindow(MainWindowViewModel viewModel, bool findUserConfigDir = true) { Title = string.Format(LocaleManager.Instance[LocaleKeys.SettingsWithInfo], viewModel.SelectedApplication.Name, viewModel.SelectedApplication.IdString); @@ -44,10 +44,6 @@ namespace Ryujinx.Ava.UI.Windows { InputPage.InputView?.SaveCurrentProfile(); - // The DLSS choice is global (dlss_mode.cfg) and applies on restart -- the game-specific - // window shares the same graphics page, so it must offer the same one-click restart the - // global settings window does (hole caught by the user 11/07: no prompt ever appeared - // because he edits settings from the per-game window). if (ViewModel.DlssModeRestartPending && !ViewModel.IsGameRunning) { ViewModel.DlssModeRestartPending = false; diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 558790233..e74eae703 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -418,8 +418,6 @@ namespace Ryujinx.Ava.UI.Windows switch (ConfigurationState.Instance.UpdateCheckerType.Value) { case UpdaterType.PromptAtStartup: - // Only auto-prompt once per launch. The persistent status-bar indicator is the - // fallback if the user dismisses the prompt. if (_startupUpdatePromptShown) { break; @@ -579,8 +577,8 @@ namespace Ryujinx.Ava.UI.Windows public static void UpdateGraphicsConfig() { #pragma warning disable IDE0055 // Disable formatting - GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 - ? ConfigurationState.Instance.Graphics.ResScaleCustom + GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 + ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale; GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy; GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath; @@ -589,10 +587,6 @@ namespace Ryujinx.Ava.UI.Windows GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; #pragma warning restore IDE0055 - // Give the DLSS integration (in the Vulkan backend, which cannot reference GraphicsConfig) - // access to the guest resolution scale, then let the selected quality mode apply its - // render-scale preset at config time. DLSS never drives the resolution at runtime -- this - // just scales the configured value once here, exactly as if the user had moved their slider. Ryujinx.Graphics.Vulkan.Dlss.DlssIntegration.ResolutionScaleGetter ??= () => GraphicsConfig.ResScale; Ryujinx.Graphics.Vulkan.Dlss.DlssIntegration.ResolutionScaleSetter ??= value => GraphicsConfig.ResScale = value; Ryujinx.Graphics.Vulkan.Dlss.DlssIntegration.ApplyModeResolutionScale(); diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index 3e1b44e85..a1cf21b1a 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -58,9 +58,6 @@ namespace Ryujinx.Ava.UI.Windows window.LoadApplications(); } - // The DLSS mode is read from env vars at app startup, so applying a change needs a restart. The - // ComboBox is locked while a game runs (IsGameRunning), so this can only fire from the main menu - // -- safe to relaunch. Offer a one-click self-restart via the same mechanism as the theme change. if (ViewModel.DlssModeRestartPending && !ViewModel.IsGameRunning) { ViewModel.DlssModeRestartPending = false; @@ -68,11 +65,6 @@ namespace Ryujinx.Ava.UI.Windows } } - // The DLSS mode is read from env vars at startup, so it applies on the NEXT launch -> a cold restart. - // This is overlap-safe: the mode ComboBox is locked while a game runs, and stopping a DLSS game already - // cold-restarts the app (AppHost_AppExit), so we only ever reach here with NGX in a clean state (no live - // slInit to collide with). RestartCold(applyUiMode: true) drops the inherited DLSS env so the new - // process picks up the mode just saved in dlss_mode.cfg. internal static void PromptDlssRestart() { _ = Dispatcher.UIThread.InvokeAsync(async () =>