Merge dlss-wip-snapshot into master: native TAA + clean-room DLSS/DLAA/NIS + credits/bilingual docs. Tree matches the in-game-validated work branch; v1.1.6 (NIS/HDR) kept in history.

This commit is contained in:
2026-06-28 23:48:11 -04:00
49 changed files with 4599 additions and 46 deletions
+33
View File
@@ -124,6 +124,32 @@ If you are planning to contribute or just want to learn more about this project
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
You can configure all of them through the graphical interface or manually through the config file, `Config.json`, found in the Ryujinx data folder which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
## Beast Roofer Edition — graphics extras / extras graphiques
### 🇬🇧 Native TAA (Temporal Anti-Aliasing)
Available in **Settings > Graphics > Anti-Aliasing > TAA**. Soft, cinematic, low-flicker image; works on **all GPUs** with no extra files. It looks best at higher internal resolution scales; at low resolution it may flicker a little more.
### 🇫🇷 TAA natif (anti-crénelage temporel)
Disponible dans **Paramètres > Graphismes > Anti-crénelage > TAA**. Rendu doux, cinématographique, peu de scintillement ; fonctionne sur **toutes les cartes graphiques**, aucun fichier à ajouter. Meilleur en haute résolution interne ; peut scintiller un peu plus en basse résolution.
### 🇬🇧 NVIDIA DLSS / DLAA (experimental, RTX only, opt-in)
DLSS/DLAA is **off by default** and **experimental**. No NVIDIA files are bundled (legal reasons). To enable it:
1. Create a folder named **`dlss`** next to **`Ryujinx.exe`**.
2. Put the **NVIDIA Streamline** files in it: `sl.interposer.dll`, `sl.common.dll`, `sl.dlss.dll` (MIT-licensed; from the public Streamline SDK).
3. The DLSS runtime (`nvngx_dlss.dll`) is **located automatically** from your own installed games/driver. If it isn't found, drop your own copy into the same `dlss` folder.
4. In **Settings > Graphics**, set the **Scaling Filter** to **DLSS** and pick a mode (**DLAA** is the most stable). Applies on the next launch (the app restarts).
⚠️ Experimental: may be unstable depending on the game/GPU. With no `dlss` folder or no DLSS file present, DLSS simply stays unavailable (no crash). Requires a recent NVIDIA RTX GPU.
### 🇫🇷 NVIDIA DLSS / DLAA (expérimental, RTX uniquement, optionnel)
DLSS/DLAA est **désactivé par défaut** et **expérimental**. Aucun fichier NVIDIA n'est inclus (raisons légales). Pour l'activer :
1. Crée un dossier nommé **`dlss`** à côté de **`Ryujinx.exe`**.
2. Mets-y les fichiers **NVIDIA Streamline** : `sl.interposer.dll`, `sl.common.dll`, `sl.dlss.dll` (sous licence MIT ; du SDK Streamline public).
3. Le runtime DLSS (`nvngx_dlss.dll`) est **localisé automatiquement** depuis tes propres jeux/pilote installés. S'il n'est pas trouvé, dépose ta propre copie dans le même dossier `dlss`.
4. Dans **Paramètres > Graphismes**, mets le **Filtre de mise à l'échelle** sur **DLSS** et choisis un mode (**DLAA** = le plus stable). S'applique au prochain lancement (l'app redémarre).
⚠️ Expérimental : peut être instable selon le jeu/la carte. Sans dossier `dlss` ou sans fichier DLSS, DLSS reste simplement indisponible (pas de plantage). Nécessite une carte NVIDIA RTX récente.
## License
This software is licensed under the terms of the [MIT license](https://git.ryujinx.app/projects/Ryubing/src/branch/master/LICENSE.txt).
@@ -136,3 +162,10 @@ See [LICENSE.txt](https://git.ryujinx.app/projects/Ryubing/src/branch/master/LIC
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
### Beast Roofer Edition
- **Original code in this fork** by **The Roofer Dev** (© 2026), MIT-licensed: native TAA, clean-room NVIDIA DLSS/DLAA/NIS integration, native scRGB HDR, per-game VRR cap. *Code/integration only — DLSS, DLAA and NIS technologies belong to NVIDIA.*
- **🇫🇷 Code original de ce fork** par **The Roofer Dev** (© 2026), sous licence MIT : intégration du TAA natif, intégration clean-room de NVIDIA DLSS/DLAA/NIS, HDR scRGB natif, cap VRR par jeu. *Code/intégration uniquement — les technologies DLSS, DLAA et NIS appartiennent à NVIDIA.*
- This fork bundles **no proprietary NVIDIA files**. [NVIDIA Streamline](https://github.com/NVIDIA-RTX/Streamline) and NVIDIA Image Scaling (NIS) are MIT-licensed (© NVIDIA). The DLSS runtime (`nvngx`) is proprietary and **never bundled** — it is located on the user's own machine.
- NVIDIA, DLSS, DLAA and NIS are trademarks of NVIDIA Corporation. This is an independent project, **not affiliated with or endorsed by NVIDIA**.
+58
View File
@@ -711,3 +711,61 @@
SOFTWARE.
```
</details>
# NVIDIA Image Scaling - NIS (MIT)
<details>
<summary>See License</summary>
```
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.
```
</details>
# NVIDIA Streamline (MIT)
<details>
<summary>See License</summary>
```
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.
```
</details>
@@ -11,5 +11,6 @@ namespace Ryujinx.Common.Configuration
SmaaMedium,
SmaaHigh,
SmaaUltra,
Taa,
}
}
@@ -0,0 +1,32 @@
namespace Ryujinx.Graphics.GAL
{
/// <summary>
/// Cross-layer hand-off for DLSS "Mode B" sub-pixel jitter. The DLSS backend generates the Halton
/// offset and publishes the NEXT frame's value in <see cref="OffsetX"/>/<see cref="OffsetY"/>; the GPU
/// layer (StateUpdater) reads it to shift the resolution-scaled viewport of the frame it is rendering.
///
/// The hard part is handing DLSS the offset the PRESENTED frame was actually rendered with. Matching by
/// texture reference failed (the presented framebuffer is a different texture than the jittered 3D
/// target), so instead the offset is carried THROUGH the present frame queue: it is snapshotted onto
/// each frame when it is enqueued for presentation, then written to <see cref="PresentX"/>/<see
/// cref="PresentY"/> when that exact frame is dequeued and presented. The DLSS backend reads those, so
/// the offset always matches the frame on screen regardless of frame-queue depth or frames-in-flight.
///
/// When <see cref="Enabled"/> is false the GPU layer skips all of this, so the path is byte-identical.
/// This lives in GAL because the GPU layer cannot reference the Vulkan backend.
/// </summary>
public static class DlssJitterState
{
public static bool Enabled;
/// <summary>The next frame's jitter offset, in internal-render pixels, published by the DLSS backend
/// and read by the GPU layer to jitter the scaled viewport.</summary>
public static float OffsetX;
public static float OffsetY;
/// <summary>The jitter offset the frame currently being presented was rendered with. Set by the GPU
/// present from the value carried alongside that frame through the queue; read by the DLSS backend.</summary>
public static float PresentX;
public static float PresentY;
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Ryujinx.Graphics.GAL
{
public interface IWindow
{
void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback);
void Present(ITexture texture, ITexture depthTexture, ImageCrop crop, Action swapBuffersCallback);
void SetSize(int width, int height);
@@ -8,12 +8,14 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Window
{
public readonly CommandType CommandType => CommandType.WindowPresent;
private TableRef<ThreadedTexture> _texture;
private TableRef<ThreadedTexture> _depthTexture;
private ImageCrop _crop;
private TableRef<Action> _swapBuffersCallback;
public void Set(TableRef<ThreadedTexture> texture, ImageCrop crop, TableRef<Action> swapBuffersCallback)
public void Set(TableRef<ThreadedTexture> texture, TableRef<ThreadedTexture> depthTexture, ImageCrop crop, TableRef<Action> swapBuffersCallback)
{
_texture = texture;
_depthTexture = depthTexture;
_crop = crop;
_swapBuffersCallback = swapBuffersCallback;
}
@@ -21,7 +23,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Window
public static void Run(ref WindowPresentCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
threaded.SignalFrame();
renderer.Window.Present(command._texture.Get(threaded)?.Base, command._crop, command._swapBuffersCallback.Get(threaded));
renderer.Window.Present(command._texture.Get(threaded)?.Base, command._depthTexture.Get(threaded)?.Base, command._crop, command._swapBuffersCallback.Get(threaded));
}
}
}
@@ -17,13 +17,17 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_impl = impl;
}
public unsafe void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback)
public unsafe void Present(ITexture texture, ITexture depthTexture, ImageCrop crop, Action swapBuffersCallback)
{
// If there's already a frame in the pipeline, wait for it to be presented first.
// This is a multithread rate limit - we can't be more than one frame behind the command queue.
_renderer.WaitForFrame();
_renderer.New<WindowPresentCommand>()->Set(new TableRef<ThreadedTexture>(_renderer, texture as ThreadedTexture), crop, new TableRef<Action>(_renderer, swapBuffersCallback));
_renderer.New<WindowPresentCommand>()->Set(
new TableRef<ThreadedTexture>(_renderer, texture as ThreadedTexture),
new TableRef<ThreadedTexture>(_renderer, depthTexture as ThreadedTexture),
crop,
new TableRef<Action>(_renderer, swapBuffersCallback));
_renderer.QueueCommand();
}
@@ -739,6 +739,25 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
Span<ViewportTransform> viewportTransformSpan = _state.State.ViewportTransform.AsSpan();
Span<ViewportExtents> viewportExtentsSpan = _state.State.ViewportExtents.AsSpan();
// DLSS Mode B: hand the shader this frame's sub-pixel jitter as a clip-space NDC offset, which it
// adds to gl_Position scaled by w (the correct, native-DLSS way to jitter -- not a viewport shift).
// Only the scaled (main) 3D pass; native passes (UI) keep RenderTargetScale 1 and are NOT jittered.
// The offset is 0 unless jitter is enabled, so the default path is unchanged.
float jitterNdcX = 0f, jitterNdcY = 0f;
if (DlssJitterState.Enabled && _channel.TextureManager.RenderTargetScale != 1f)
{
ref ViewportTransform vp0 = ref viewportTransformSpan[0];
float jScale = _channel.TextureManager.RenderTargetScale;
float vpWidth = MathF.Abs(vp0.ScaleX) * 2f * jScale;
float vpHeight = MathF.Abs(vp0.ScaleY) * 2f * jScale;
if (vpWidth > 0f && vpHeight > 0f)
{
jitterNdcX = DlssJitterState.OffsetX * 2f / vpWidth;
jitterNdcY = DlssJitterState.OffsetY * 2f / vpHeight;
}
}
_context.SupportBufferUpdater.SetJitter(jitterNdcX, jitterNdcY);
for (int index = 0; index < Constants.TotalViewports; index++)
{
if (disableTransform)
+7
View File
@@ -46,6 +46,13 @@ namespace Ryujinx.Graphics.Gpu
/// </summary>
public Window Window { get; }
/// <summary>
/// Most recently bound depth-stencil render target, captured so the presentation path can hand a
/// real depth buffer to temporal upscalers (DLSS). May be stale or a UI depth, so consumers must
/// validate it (e.g. by dimensions) before use. Written and read on the GPU thread only.
/// </summary>
internal Image.Texture LastPresentDepthStencil { get; set; }
/// <summary>
/// Internal sequence number, used to avoid needless resource data updates
/// in the middle of a command buffer before synchronizations.
@@ -210,6 +210,13 @@ 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;
}
return changesScale || ScaleNeedsUpdated(depthStencil);
}
@@ -88,6 +88,23 @@ namespace Ryujinx.Graphics.Gpu.Memory
MarkDirty(SupportBuffer.ViewportSizeOffset, SupportBuffer.FieldSize);
}
/// <summary>
/// Sets the DLSS Mode B clip-space jitter offset (in NDC) the shader adds to vertex positions
/// (scaled by w). Zero unless jitter is enabled, so the default path is unaffected.
/// </summary>
/// <param name="x">Jitter X in normalized device coordinates</param>
/// <param name="y">Jitter Y in normalized device coordinates</param>
public void SetJitter(float x, float y)
{
if (_data.JitterOffset.X != x || _data.JitterOffset.Y != y)
{
_data.JitterOffset.X = x;
_data.JitterOffset.Y = y;
MarkDirty(SupportBuffer.JitterOffsetOffset, SupportBuffer.FieldSize);
}
}
/// <summary>
/// Sets the scale of all output render targets (they should all have the same scale).
/// </summary>
+29 -3
View File
@@ -56,6 +56,13 @@ namespace Ryujinx.Graphics.Gpu
/// </summary>
public object UserObj { get; }
/// <summary>
/// DLSS Mode B sub-pixel jitter offset this frame was rendered with, snapshotted at enqueue and
/// carried through the queue so the present can hand the matching offset to DLSS.
/// </summary>
public float JitterX { get; }
public float JitterY { get; }
/// <summary>
/// Creates a new instance of the presentation texture.
/// </summary>
@@ -73,7 +80,9 @@ namespace Ryujinx.Graphics.Gpu
ImageCrop crop,
Action<GpuContext, object> acquireCallback,
Action<object> releaseCallback,
object userObj)
object userObj,
float jitterX,
float jitterY)
{
Cache = cache;
Info = info;
@@ -82,6 +91,8 @@ namespace Ryujinx.Graphics.Gpu
AcquireCallback = acquireCallback;
ReleaseCallback = releaseCallback;
UserObj = userObj;
JitterX = jitterX;
JitterY = jitterY;
}
}
@@ -176,6 +187,12 @@ 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;
_frameQueue.Enqueue(new PresentationTexture(
physicalMemory.TextureCache,
info,
@@ -183,7 +200,9 @@ namespace Ryujinx.Graphics.Gpu
crop,
acquireCallback,
releaseCallback,
userObj));
userObj,
jitterX,
jitterY));
return true;
}
@@ -238,7 +257,14 @@ namespace Ryujinx.Graphics.Gpu
crop = new ImageCrop(left, right, top, bottom, crop.FlipX, crop.FlipY, crop.IsStretched, crop.AspectRatioX, crop.AspectRatioY);
}
_context.Renderer.Window.Present(texture.HostTexture, crop, swapBuffersCallback);
// 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;
_context.Renderer.Window.Present(texture.HostTexture, _context.LastPresentDepthStencil?.HostTexture, crop, swapBuffersCallback);
pt.ReleaseCallback(pt.UserObj);
}
+3 -1
View File
@@ -38,8 +38,9 @@ namespace Ryujinx.Graphics.OpenGL
_renderer = renderer;
}
public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback)
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();
@@ -323,6 +324,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.
_antiAliasing?.Dispose();
_antiAliasing = null;
break;
+6 -1
View File
@@ -24,6 +24,7 @@ namespace Ryujinx.Graphics.Shader
RenderScale,
TfeOffset,
TfeVertexCount,
JitterOffset,
}
public struct SupportBuffer
@@ -42,6 +43,7 @@ namespace Ryujinx.Graphics.Shader
public static readonly int ComputeRenderScaleOffset;
public static readonly int TfeOffsetOffset;
public static readonly int TfeVertexCountOffset;
public static readonly int JitterOffsetOffset;
public const int FragmentIsBgraCount = 8;
// One for the render target, 64 for the textures, and 8 for the images.
@@ -68,6 +70,7 @@ namespace Ryujinx.Graphics.Shader
ComputeRenderScaleOffset = GraphicsRenderScaleOffset + FieldSize;
TfeOffsetOffset = OffsetOf(ref instance, ref instance.TfeOffset);
TfeVertexCountOffset = OffsetOf(ref instance, ref instance.TfeVertexCount);
JitterOffsetOffset = OffsetOf(ref instance, ref instance.JitterOffset);
}
internal static StructureType GetStructureType()
@@ -80,7 +83,8 @@ namespace Ryujinx.Graphics.Shader
new StructureField(AggregateType.S32, "frag_scale_count"),
new StructureField(AggregateType.Array | AggregateType.FP32, "render_scale", RenderScaleMaxCount),
new StructureField(AggregateType.Vector4 | AggregateType.S32, "tfe_offset"),
new StructureField(AggregateType.S32, "tfe_vertex_count")
new StructureField(AggregateType.S32, "tfe_vertex_count"),
new StructureField(AggregateType.Vector4 | AggregateType.FP32, "jitter_offset")
]);
}
@@ -95,5 +99,6 @@ namespace Ryujinx.Graphics.Shader
public Vector4<int> TfeOffset;
public Vector4<int> TfeVertexCount;
public Vector4<float> JitterOffset;
}
}
@@ -292,6 +292,20 @@ 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.
{
Operand jpx = this.Load(StorageKind.Output, IoVariable.Position, null, Const(0));
Operand jpy = this.Load(StorageKind.Output, IoVariable.Position, null, Const(1));
Operand jpw = this.Load(StorageKind.Output, IoVariable.Position, null, Const(3));
Operand jox = this.Load(StorageKind.ConstantBuffer, SupportBuffer.Binding, Const((int)SupportBufferField.JitterOffset), Const(0));
Operand joy = this.Load(StorageKind.ConstantBuffer, SupportBuffer.Binding, Const((int)SupportBufferField.JitterOffset), Const(1));
this.Store(StorageKind.Output, IoVariable.Position, null, Const(0), this.FPFusedMultiplyAdd(jox, jpw, jpx));
this.Store(StorageKind.Output, IoVariable.Position, null, Const(1), this.FPFusedMultiplyAdd(joy, jpw, jpy));
}
if (TranslatorContext.Definitions.ViewportTransformDisable)
{
Operand x = this.Load(StorageKind.Output, IoVariable.Position, null, Const(0));
@@ -0,0 +1,229 @@
// 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;
using System.Diagnostics;
using System.IO;
namespace Ryujinx.Graphics.Vulkan.Dlss
{
/// <summary>
/// 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.
/// </summary>
public static class DlssBinaries
{
public const string NgxDlssDll = "nvngx_dlss.dll";
public const string StreamlineInterposer = "sl.interposer.dll";
/// <summary>
/// Returns the path to a usable nvngx_dlss.dll, or null if none could be found.
/// </summary>
/// <param name="byoFolder">Explicit folder the user can drop the DLL into (highest priority).</param>
/// <param name="autoScan">When true, scan common game/driver locations if the BYO folder is empty.</param>
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);
if (File.Exists(explicitPath))
{
return explicitPath;
}
}
if (!autoScan)
{
return null;
}
// 2. Auto-scan known locations and pick the newest version found.
string best = null;
Version bestVersion = null;
foreach (string root in GetScanRoots())
{
foreach (string candidate in SafeEnumerate(root, NgxDlssDll))
{
Version version = GetFileVersion(candidate);
if (best == null || (version != null && (bestVersion == null || version > bestVersion)))
{
best = candidate;
bestVersion = version;
}
}
}
return best;
}
/// <summary>
/// Returns the folder containing sl.interposer.dll (bundled or in the BYO folder), or null.
/// </summary>
public static string LocateStreamlineFolder(string byoFolder, string bundledFolder)
{
foreach (string folder in new[] { byoFolder, bundledFolder })
{
if (!string.IsNullOrEmpty(folder) && File.Exists(Path.Combine(folder, StreamlineInterposer)))
{
return folder;
}
}
return null;
}
private static IEnumerable<string> GetScanRoots()
{
// Common launcher install roots, per drive. Kept curated so the scan stays fast
// instead of walking entire drives.
string[] relativeRoots =
{
@"SteamLibrary\steamapps\common",
@"Program Files (x86)\Steam\steamapps\common",
@"XboxGames",
@"Program Files\Epic Games",
@"Epic Games",
@"Program Files\EA Games",
@"Program Files\Ubisoft\Ubisoft Game Launcher\games",
};
foreach (DriveInfo drive in GetReadyDrives())
{
foreach (string relative in relativeRoots)
{
string root = Path.Combine(drive.RootDirectory.FullName, relative);
if (Directory.Exists(root))
{
yield return root;
}
}
}
// 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");
if (Directory.Exists(driverStore))
{
yield return driverStore;
}
}
private static IEnumerable<DriveInfo> GetReadyDrives()
{
DriveInfo[] drives;
try
{
drives = DriveInfo.GetDrives();
}
catch
{
yield break;
}
foreach (DriveInfo drive in drives)
{
bool ready;
try
{
ready = drive.IsReady && drive.DriveType == DriveType.Fixed;
}
catch
{
ready = false;
}
if (ready)
{
yield return drive;
}
}
}
// 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<string> SafeEnumerate(string root, string fileName)
{
Queue<string> pending = new();
pending.Enqueue(root);
while (pending.Count > 0)
{
string dir = pending.Dequeue();
string[] matches = null;
try
{
matches = Directory.GetFiles(dir, fileName);
}
catch
{
// ignored - unreadable directory
}
if (matches != null)
{
foreach (string match in matches)
{
yield return match;
}
}
string[] subDirs = null;
try
{
subDirs = Directory.GetDirectories(dir);
}
catch
{
// ignored
}
if (subDirs != null)
{
foreach (string subDir in subDirs)
{
pending.Enqueue(subDir);
}
}
}
}
private static Version GetFileVersion(string path)
{
try
{
FileVersionInfo info = FileVersionInfo.GetVersionInfo(path);
if (info.FileMajorPart != 0 || info.FileMinorPart != 0 || info.FileBuildPart != 0 || info.FilePrivatePart != 0)
{
return new Version(info.FileMajorPart, info.FileMinorPart, info.FileBuildPart, info.FilePrivatePart);
}
}
catch
{
// ignored - treat as unknown version
}
return null;
}
}
}
@@ -0,0 +1,171 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
public static class DlssIntegration
{
private static readonly bool _enabled = IsTruthy(Environment.GetEnvironmentVariable("RYUJINX_DLSS"));
/// <summary>True when the user opted into DLSS via RYUJINX_DLSS=1.</summary>
public static bool IsEnabled => _enabled;
/// <summary>
/// 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.
/// </summary>
public static readonly bool MvFilterEnabled =
Environment.GetEnvironmentVariable("RYUJINX_DLSS_MV_FILTER") is not ("0" or "false" or "off");
/// <summary>
/// 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.
/// </summary>
public static readonly bool MetricsEnabled =
Environment.GetEnvironmentVariable("RYUJINX_DLSS_METRICS") is "1" or "true" or "on";
/// <summary>
/// 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&lt;-&gt;NIS pumping or history churn.
/// OFF by default (the build is byte-identical); intended only for A/B measurement runs.
/// </summary>
public static readonly bool ForceDlss =
Environment.GetEnvironmentVariable("RYUJINX_DLSS_FORCE_DLSS") is "1" or "true" or "on";
/// <summary>
/// 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.
/// </summary>
public static readonly string[] DeviceExtensions =
{
"VK_NVX_binary_import",
"VK_NVX_image_view_handle",
"VK_KHR_buffer_device_address",
"VK_EXT_buffer_device_address",
"VK_NV_optical_flow", // NVOFA (B2): hardware optical flow accelerator for accurate motion vectors
"VK_KHR_synchronization2", // required dependency of VK_NV_optical_flow
};
/// <summary>
/// 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.
/// </summary>
public static Func<float> ResolutionScaleGetter;
public static Action<float> ResolutionScaleSetter;
/// <summary>The user's configured resolution scale, captured before a mode preset is applied.</summary>
public static float UserBaseResScale = 1f;
private static bool _modeLogged;
/// <summary>
/// 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 <see cref="ApplyModeResolutionScale"/>; DLSS itself picks the matching preset
/// from whatever internal resolution results, and never steers the resolution at runtime.
/// </summary>
public static readonly DlssQualityMode Mode = ParseQualityMode(Environment.GetEnvironmentVariable("RYUJINX_DLSS_MODE")) ?? DlssQualityMode.Native;
/// <summary>
/// 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.
/// </summary>
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
_ => 1f, // Native: leave the user's resolution alone.
};
}
/// <summary>
/// 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.
/// </summary>
public static void ApplyModeResolutionScale()
{
if (!_enabled || ResolutionScaleGetter == null || ResolutionScaleSetter == null)
{
return;
}
float baseScale = ResolutionScaleGetter();
if (baseScale <= 0f)
{
baseScale = 1f;
}
UserBaseResScale = baseScale;
float factor = RenderScaleFactor(Mode);
if (factor == 1f)
{
return;
}
float scaled = baseScale * factor;
ResolutionScaleSetter(scaled);
if (!_modeLogged)
{
_modeLogged = true;
Logger.Info?.Print(LogClass.Gpu,
$"DLSS: quality mode {Mode} -> resolution scale {baseScale:0.00} x {factor:0.00} = {scaled:0.00}.");
}
}
private static DlssQualityMode? ParseQualityMode(string value)
{
return value?.Trim().ToLowerInvariant() switch
{
"native" or "dlaa" => DlssQualityMode.Native,
"quality" => DlssQualityMode.Quality,
"balanced" => DlssQualityMode.Balanced,
"performance" or "perf" => DlssQualityMode.Performance,
_ => null,
};
}
private static bool IsTruthy(string value)
{
return value is "1" or "true" or "TRUE" or "True";
}
}
/// <summary>
/// User-facing DLSS quality mode. Under the "no automatic piloting" design these act only as a
/// one-shot Resolution Scale preset (see <see cref="DlssIntegration.ApplyModeResolutionScale"/>):
/// 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.
/// </summary>
public enum DlssQualityMode
{
Native,
Quality,
Balanced,
Performance,
}
}
@@ -0,0 +1,110 @@
// 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;
using System.Globalization;
namespace Ryujinx.Graphics.Vulkan.Dlss
{
/// <summary>
/// Generates the deterministic Halton(2,3) sub-pixel jitter for DLSS "Mode B" and publishes the
/// NEXT frame's offset to the cross-layer <see cref="DlssJitterState"/>, 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.
/// </summary>
public static class DlssJitter
{
private static readonly bool _flag =
Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER") is "1" or "true" or "on";
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;
private static float ParseScale()
{
string value = Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_SCALE");
if (value != null &&
float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float scale) &&
scale > 0f)
{
return scale;
}
return 1.0f; // real +-0.5px sub-pixel jitter by default
}
// 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;
private static float ParseLodBias()
{
string value = Environment.GetEnvironmentVariable("RYUJINX_DLSS_JITTER_LOD_BIAS");
if (value != null &&
float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float bias))
{
return bias;
}
return -0.5f;
}
private const uint SequenceLength = 16; // a short Halton phase set, classic for DLSS
private static uint _index;
/// <summary>
/// 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.
/// </summary>
public static void Advance()
{
if (!Enabled)
{
DlssJitterState.Enabled = false;
DlssJitterState.OffsetX = 0f;
DlssJitterState.OffsetY = 0f;
return;
}
_index = _index % SequenceLength + 1; // 1..N (Halton is undefined at 0)
DlssJitterState.Enabled = true;
DlssJitterState.OffsetX = (Halton(_index, 2) - 0.5f) * _scale;
DlssJitterState.OffsetY = (Halton(_index, 3) - 0.5f) * _scale;
}
private static float Halton(uint index, uint radix)
{
float result = 0f;
float fraction = 1f;
uint i = index;
while (i > 0)
{
fraction /= radix;
result += fraction * (i % radix);
i /= radix;
}
return result;
}
}
}
@@ -0,0 +1,635 @@
// 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;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
using Silk.NET.Vulkan;
using System;
using Extents2D = Ryujinx.Graphics.GAL.Extents2D;
using Format = Ryujinx.Graphics.GAL.Format;
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
using VkFormat = Silk.NET.Vulkan.Format;
namespace Ryujinx.Graphics.Vulkan.Dlss
{
/// <summary>
/// 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 <see cref="DlssIntegration.IsEnabled"/>; on any failure TryRun returns
/// false so the caller falls back to the normal blit/scaling path.
/// </summary>
internal sealed class DlssUpscaler : IDisposable
{
private const uint ViewportId = 0;
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)
private readonly VulkanRenderer _gd;
private TextureView _output;
private TextureView _depth;
private TextureView _motion;
private TextureView _motionFiltered;
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;
private int _framesSinceReset;
// 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
// 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.
private TextureView _heldDepth;
private bool _hasHeldDepth;
private readonly PipelineHelperShader _motionPipeline;
private readonly ShaderCollection _motionProgram;
private readonly ShaderCollection _motionFilterProgram;
private readonly ISampler _sampler;
private int _inW, _inH, _outW, _outH;
private uint _frame;
private bool _modeLogged;
private bool _modeWarned;
// DLSS modes from highest quality (smallest upscale) to most aggressive (largest upscale).
private static readonly StreamlineDlss.DlssMode[] ModesByQuality =
{
StreamlineDlss.DlssMode.Dlaa,
StreamlineDlss.DlssMode.MaxQuality,
StreamlineDlss.DlssMode.Balanced,
StreamlineDlss.DlssMode.MaxPerformance,
StreamlineDlss.DlssMode.UltraPerformance,
};
public DlssUpscaler(VulkanRenderer gd, Device device)
{
_gd = gd;
_motionPipeline = new PipelineHelperShader(gd, device);
_motionPipeline.Initialize();
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)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 3)
.Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build();
_sampler = gd.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear));
_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)
.Add(ResourceStages.Compute, ResourceType.StorageBuffer, 0)
.Add(ResourceStages.Compute, ResourceType.Image, 0, true)
.Add(ResourceStages.Compute, ResourceType.Image, 1, true).Build();
_motionFilterProgram = gd.CreateProgramWithMinimalLayout([
new ShaderSource(filterShader, ShaderStage.Compute, TargetLanguage.Spirv)
], filterLayout);
_sceneChangeBuffer = gd.BufferManager.CreateWithHandle(gd, CounterCount * sizeof(uint));
}
public bool TryRun(
TextureView input,
TextureView depth,
CommandBufferScoped cbs,
TextureView dst,
int outW,
int outH,
Extents2D dstRegion,
bool hdr,
float paperWhite,
float peak,
float curve,
float gamma,
float blend,
float whiten)
{
if (!DlssIntegration.IsEnabled || !Streamline.IsInitialized)
{
return false;
}
if (input.Width == 0 || input.Height == 0)
{
return 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.
const float JitterSignX = -1f;
const float JitterSignY = -1f;
DlssJitter.Advance();
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).
_dejitterX = JitterSignX * (frameJitterX - _lastJitterX);
_dejitterY = JitterSignY * (frameJitterY - _lastJitterY);
_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++;
Logger.Info?.Print(LogClass.Gpu,
$"DLSS jitter probe: carrier=({frameJitterX:0.000},{frameJitterY:0.000}) " +
$"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);
if (contentH > outH)
{
contentH = outH;
contentW = (int)Math.Round(outH * inAspect);
}
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.
if (input.Width >= contentW || input.Height >= contentH)
{
return false;
}
if (!TryPickMode(input.Width, input.Height, contentW, contentH, out StreamlineDlss.DlssMode mode))
{
if (!_modeWarned)
{
_modeWarned = true;
Logger.Warning?.Print(LogClass.Gpu,
$"DLSS: no preset fits internal {input.Width}x{input.Height} -> {contentW}x{contentH} " +
"(a global NVIDIA DLSS override may be forcing a fixed mode); falling back to normal scaling.");
}
return false;
}
int dlssOutW = contentW;
int dlssOutH = contentH;
EnsureResources(input, dlssOutW, dlssOutH, cbs);
if (!StreamlineDlss.SetOptions(ViewportId, mode, (uint)dlssOutW, (uint)dlssOutH, hdr))
{
return false;
}
if (!_modeLogged)
{
_modeLogged = true;
Logger.Info?.Print(LogClass.Gpu, $"DLSS: using mode {mode} for internal {input.Width}x{input.Height} -> {dlssOutW}x{dlssOutH} (content {contentW}x{contentH}).");
}
// 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++;
bool sceneCut = _hasPrev && changing && !_wasChanging
&& motionFraction < SceneCutMaxMotion
&& _framesSinceReset >= ResetCooldownFrames;
_wasChanging = changing;
bool reset = !_hasPrev || sceneCut;
if (reset)
{
_framesSinceReset = 0;
}
if (sceneCut)
{
Logger.Info?.Print(LogClass.Gpu,
$"DLSS: history reset (changed={100f * unexplained / total:0.#}% motion={100f * motion / total:0.#}%).");
}
// 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<uint> zero = stackalloc uint[] { 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u };
_gd.BufferManager.SetData<uint>(_sceneChangeBuffer, 0, zero);
if (_hasPrev)
{
RunMotionPass(input, cbs);
RunMotionFilterPass(input, cbs);
}
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;
if (depthMatches)
{
if (_heldDepth == null ||
_heldDepth.Width != input.Width ||
_heldDepth.Height != input.Height ||
_heldDepth.Info.Format != depth.Info.Format)
{
_heldDepth?.Dispose();
_heldDepth = _gd.CreateTexture(MakeInfo(depth.Info, input.Width, input.Height, depth.Info.Format, depth.Info.BytesPerPixel)) as TextureView;
}
CopyDepth(depth, _heldDepth, cbs);
_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.
TextureView depthSource = depthMatches ? depth : (_hasHeldDepth ? _heldDepth : _depth);
int depthState = depthMatches ? 2 : (_hasHeldDepth ? 1 : 0);
if (_depthState != depthState)
{
_depthState = depthState;
Logger.Info?.Print(LogClass.Gpu,
$"DLSS depth -> {(depthState == 2 ? "REAL" : depthState == 1 ? "HELD (last good)" : "dummy")} " +
$"(input {input.Width}x{input.Height}, capture {(depth != null ? $"{depth.Width}x{depth.Height}" : "null")}).");
}
StreamlineDlss.DlssTexture depthTex = Describe(depthSource, cbs);
TextureView mvSource = DlssIntegration.MvFilterEnabled ? _motionFiltered : _motion;
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 = frameJitterX * JitterSignX;
float evalJitterY = frameJitterY * JitterSignY;
bool ok = StreamlineDlss.Evaluate(
(IntPtr)cbs.CommandBuffer.Handle,
ViewportId,
_frame++,
reset,
evalJitterX,
evalJitterY,
in inTex,
in outTex,
in depthTex,
in mvTex);
if (!ok)
{
return false;
}
// 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,
_output,
dst,
new Extents2D(0, 0, _output.Width, _output.Height),
dstRegion,
true,
true,
hdr,
paperWhite,
peak,
curve,
gamma,
blend,
whiten);
// Keep this frame's color as the "previous" frame for next time's motion estimate.
CopyToPrev(input, cbs);
_hasPrev = true;
return true;
}
private void RunMotionPass(TextureView input, CommandBufferScoped cbs)
{
_motionPipeline.SetCommandBuffer(cbs);
_motionPipeline.SetProgram(_motionProgram);
_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<float> 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);
_motionPipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]);
_motionPipeline.SetStorageBuffers([new BufferAssignment(0, new BufferRange(_sceneChangeBuffer, 0, CounterCount * sizeof(uint), true))]);
_motionPipeline.SetImage(0, _motion.GetImageView());
_motionPipeline.DispatchCompute((input.Width + 15) / 16, (input.Height + 15) / 16, 1);
_motionPipeline.ComputeBarrier();
_motionPipeline.Finish();
}
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<float> 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);
_motionPipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]);
_motionPipeline.SetStorageBuffers([new BufferAssignment(0, new BufferRange(_sceneChangeBuffer, 0, CounterCount * sizeof(uint), true))]);
_motionPipeline.SetImage(0, _motionFiltered.GetImageView());
_motionPipeline.SetImage(1, _motion.GetImageView());
_motionPipeline.DispatchCompute((input.Width + 15) / 16, (input.Height + 15) / 16, 1);
_motionPipeline.ComputeBarrier();
_motionPipeline.Finish();
}
/// <summary>
/// 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.
/// </summary>
private void ReadCounters(out uint changed, out uint motion)
{
using PinnedSpan<byte> data = _gd.BufferManager.GetData(_sceneChangeBuffer, 0, CounterCount * sizeof(uint));
ReadOnlySpan<byte> bytes = data.Get();
changed = bytes.Length >= 1 * sizeof(uint) ? BitConverter.ToUInt32(bytes[(0 * sizeof(uint))..]) : 0u;
motion = bytes.Length >= 2 * sizeof(uint) ? BitConverter.ToUInt32(bytes[(1 * sizeof(uint))..]) : 0u;
}
private void CopyToPrev(TextureView input, CommandBufferScoped cbs)
{
ImageSubresourceLayers layers = new()
{
AspectMask = ImageAspectFlags.ColorBit,
MipLevel = 0,
BaseArrayLayer = 0,
LayerCount = 1,
};
ImageCopy region = new()
{
SrcSubresource = layers,
DstSubresource = layers,
Extent = new Extent3D((uint)input.Width, (uint)input.Height, 1),
};
_gd.Api.CmdCopyImage(
cbs.CommandBuffer,
input.GetImage().Get(cbs).Value,
ImageLayout.General,
_prevColor.GetImage().Get(cbs).Value,
ImageLayout.General,
1,
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()
{
AspectMask = ImageAspectFlags.DepthBit,
MipLevel = 0,
BaseArrayLayer = 0,
LayerCount = 1,
};
ImageCopy region = new()
{
SrcSubresource = layers,
DstSubresource = layers,
Extent = new Extent3D((uint)src.Width, (uint)src.Height, 1),
};
_gd.Api.CmdCopyImage(
cbs.CommandBuffer,
src.GetImage().Get(cbs).Value,
ImageLayout.General,
dst.GetImage().Get(cbs).Value,
ImageLayout.General,
1,
in region);
}
private StreamlineDlss.DlssTexture Describe(TextureView tex, CommandBufferScoped cbs)
{
return new StreamlineDlss.DlssTexture
{
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.
Width = (uint)tex.Width,
Height = (uint)tex.Height,
};
}
private static bool TryPickMode(int inW, int inH, int outW, int outH, out StreamlineDlss.DlssMode mode)
{
foreach (StreamlineDlss.DlssMode candidate in ModesByQuality)
{
if (StreamlineDlss.GetRenderRange(candidate, (uint)outW, (uint)outH, out uint minW, out uint minH, out uint maxW, out uint maxH) &&
inW >= minW && inW <= maxW && inH >= minH && inH <= maxH)
{
mode = candidate;
return true;
}
}
mode = StreamlineDlss.DlssMode.MaxPerformance;
return false;
}
private void EnsureResources(TextureView input, int outW, int outH, CommandBufferScoped cbs)
{
if (_output != null && _inW == input.Width && _inH == input.Height && _outW == outW && _outH == outH)
{
return;
}
_output?.Dispose();
_depth?.Dispose();
_motion?.Dispose();
_motionFiltered?.Dispose();
_prevColor?.Dispose(); // was leaked on every reallocation (dynamic-resolution games churn this)
_heldDepth?.Dispose();
_heldDepth = null;
_hasHeldDepth = false; // the held depth is stale at a new resolution; recapture before reuse
_inW = input.Width;
_inH = input.Height;
_outW = outW;
_outH = outH;
_output = _gd.CreateTexture(MakeInfo(input.Info, outW, outH, input.Info.Format, input.Info.BytesPerPixel)) as TextureView;
_depth = _gd.CreateTexture(MakeInfo(input.Info, input.Width, input.Height, Format.D32Float, 4)) as TextureView;
_motion = _gd.CreateTexture(MakeInfo(input.Info, input.Width, input.Height, Format.R16G16Float, 4)) as TextureView;
_motionFiltered = _gd.CreateTexture(MakeInfo(input.Info, input.Width, input.Height, Format.R16G16Float, 4)) as TextureView;
_prevColor = _gd.CreateTexture(MakeInfo(input.Info, input.Width, input.Height, input.Info.Format, input.Info.BytesPerPixel)) as TextureView;
_hasPrev = false;
ClearResources(cbs);
}
private static TextureCreateInfo MakeInfo(TextureCreateInfo b, int w, int h, Format format, int bytesPerPixel)
{
return new TextureCreateInfo(
w,
h,
1,
1,
1,
1,
1,
bytesPerPixel,
format,
b.DepthStencilMode,
Target.Texture2D,
b.SwizzleR,
b.SwizzleG,
b.SwizzleB,
b.SwizzleA);
}
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,
BaseMipLevel = 0,
LevelCount = 1,
BaseArrayLayer = 0,
LayerCount = 1,
};
ImageSubresourceRange depthRange = new()
{
AspectMask = ImageAspectFlags.DepthBit,
BaseMipLevel = 0,
LevelCount = 1,
BaseArrayLayer = 0,
LayerCount = 1,
};
ClearColorValue zeroColor = default;
_gd.Api.CmdClearColorImage(cbs.CommandBuffer, _output.GetImage().Get(cbs).Value, ImageLayout.General, in zeroColor, 1, in colorRange);
_gd.Api.CmdClearColorImage(cbs.CommandBuffer, _motion.GetImage().Get(cbs).Value, ImageLayout.General, in zeroColor, 1, in colorRange);
_gd.Api.CmdClearColorImage(cbs.CommandBuffer, _motionFiltered.GetImage().Get(cbs).Value, ImageLayout.General, in zeroColor, 1, in colorRange);
_gd.Api.CmdClearColorImage(cbs.CommandBuffer, _prevColor.GetImage().Get(cbs).Value, ImageLayout.General, in zeroColor, 1, in colorRange);
ClearDepthStencilValue zeroDepth = new() { Depth = 0f, Stencil = 0 };
_gd.Api.CmdClearDepthStencilImage(cbs.CommandBuffer, _depth.GetImage().Get(cbs).Value, ImageLayout.General, in zeroDepth, 1, in depthRange);
}
public void Dispose()
{
_output?.Dispose();
_depth?.Dispose();
_motion?.Dispose();
_motionFiltered?.Dispose();
_prevColor?.Dispose();
_heldDepth?.Dispose();
_motionProgram?.Dispose();
_motionFilterProgram?.Dispose();
_motionPipeline?.Dispose();
_sampler?.Dispose();
_gd.BufferManager.Delete(_sceneChangeBuffer);
}
}
}
@@ -0,0 +1,792 @@
// 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;
using Silk.NET.Vulkan;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Vulkan.Dlss
{
/// <summary>
/// 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
/// <see cref="Available"/>/<see cref="SessionReady"/>; on any failure the caller keeps Lucas-Kanade.
/// </summary>
internal sealed unsafe class NvOpticalFlow
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate Result CreateSessionDelegate(Device device, OpticalFlowSessionCreateInfoNV* createInfo, AllocationCallbacks* allocator, OpticalFlowSessionNV* session);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void DestroySessionDelegate(Device device, OpticalFlowSessionNV session, AllocationCallbacks* allocator);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate Result BindImageDelegate(Device device, OpticalFlowSessionNV session, OpticalFlowSessionBindingPointNV bindingPoint, ImageView view, ImageLayout layout);
[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;
private CreateSessionDelegate _createSession;
private DestroySessionDelegate _destroySession;
private BindImageDelegate _bindImage;
private ExecuteDelegate _execute;
private Vk _api;
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;
private CommandPool _cmdPool;
private CommandBuffer _cmdBuffer;
private Fence _fence;
private bool _queueReady;
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;
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;
/// <summary>True only when all four entry points resolved, i.e. the device really exposes NVOFA.</summary>
public bool Available { get; private set; }
/// <summary>True when a session + its images are created and bound, ready to execute.</summary>
public bool SessionReady { get; private set; }
/// <summary>
/// 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, <see cref="Available"/> stays false
/// and the caller falls back to the Lucas-Kanade motion estimator.
/// </summary>
public void TryBind(Vk api, Device device, PhysicalDevice physicalDevice, Queue opticalFlowQueue, uint opticalFlowQueueFamily, uint graphicsQueueFamily)
{
_api = api;
_device = device;
_physicalDevice = physicalDevice;
_ofaQueue = opticalFlowQueue;
_ofaFamily = opticalFlowQueueFamily;
_graphicsFamily = graphicsQueueFamily;
_createSession = Load<CreateSessionDelegate>(api, device, "vkCreateOpticalFlowSessionNV");
_bindImage = Load<BindImageDelegate>(api, device, "vkBindOpticalFlowSessionImageNV");
_execute = Load<ExecuteDelegate>(api, device, "vkCmdOpticalFlowExecuteNV");
_destroySession = Load<DestroySessionDelegate>(api, device, "vkDestroyOpticalFlowSessionNV");
Available = _createSession != null && _bindImage != null && _execute != null && _destroySession != null;
Logger.Info?.Print(LogClass.Gpu,
$"NVOFA probe: vkCreateOpticalFlowSessionNV={(_createSession != null ? "solved" : "NULL")}, " +
$"vkBindOpticalFlowSessionImageNV={(_bindImage != null ? "solved" : "NULL")}, " +
$"vkCmdOpticalFlowExecuteNV={(_execute != null ? "solved" : "NULL")}, " +
$"vkDestroyOpticalFlowSessionNV={(_destroySession != null ? "solved" : "NULL")} -> " +
(Available
? "AVAILABLE (hardware optical flow ready; session setup may proceed)."
: "UNAVAILABLE (falling back to Lucas-Kanade motion estimation)."));
if (Available)
{
QueryHardware();
SetupOfaQueue(device);
}
}
/// <summary>
/// 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.
/// </summary>
private void SetupOfaQueue(Device device)
{
if (_ofaFamily == uint.MaxValue)
{
Logger.Warning?.Print(LogClass.Gpu, "NVOFA queue setup: no dedicated optical-flow queue was created; OFA execute disabled.");
return;
}
CommandPoolCreateInfo pci = new()
{
SType = StructureType.CommandPoolCreateInfo,
QueueFamilyIndex = _ofaFamily,
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
};
if (_api.CreateCommandPool(device, &pci, null, out _cmdPool) != Result.Success)
{
Logger.Warning?.Print(LogClass.Gpu, "NVOFA queue setup: command pool creation failed.");
return;
}
CommandBufferAllocateInfo cbi = new()
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _cmdPool,
Level = CommandBufferLevel.Primary,
CommandBufferCount = 1,
};
CommandBuffer cb;
if (_api.AllocateCommandBuffers(device, &cbi, &cb) != Result.Success)
{
Logger.Warning?.Print(LogClass.Gpu, "NVOFA queue setup: command buffer allocation failed.");
return;
}
_cmdBuffer = cb;
FenceCreateInfo fci = new() { SType = StructureType.FenceCreateInfo };
if (_api.CreateFence(device, &fci, null, out _fence) != Result.Success)
{
Logger.Warning?.Print(LogClass.Gpu, "NVOFA queue setup: fence creation failed.");
return;
}
_queueReady = true;
Logger.Info?.Print(LogClass.Gpu, $"NVOFA queue setup: command pool + buffer + fence ready on optical-flow family {_ofaFamily}.");
}
/// <summary>
/// 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.
/// </summary>
private void QueryHardware()
{
PhysicalDeviceOpticalFlowPropertiesNV ofaProps = new() { SType = StructureType.PhysicalDeviceOpticalFlowPropertiesNV };
PhysicalDeviceProperties2 props2 = new() { SType = StructureType.PhysicalDeviceProperties2, PNext = &ofaProps };
_api.GetPhysicalDeviceProperties2(_physicalDevice, &props2);
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;
else if ((grids & 1) != 0) _outputGrid = 1;
Logger.Info?.Print(LogClass.Gpu,
$"NVOFA HW Query: supportedOutputGridSizes bitmask=0x{grids:X} " +
$"(1x1={(grids & 1) != 0}, 2x2={(grids & 2) != 0}, 4x4={(grids & 4) != 0}, 8x8={(grids & 8) != 0}), " +
$"min={ofaProps.MinWidth}x{ofaProps.MinHeight}, max={ofaProps.MaxWidth}x{ofaProps.MaxHeight} -> chosen grid {_outputGrid}x{_outputGrid}.");
uint count = 0;
_api.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, ref count, null);
if (count != 0)
{
QueueFamilyProperties[] fams = new QueueFamilyProperties[count];
fixed (QueueFamilyProperties* p = fams)
{
_api.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, ref count, p);
}
const uint OfaQueueBit = 0x100; // VK_QUEUE_OPTICAL_FLOW_BIT_NV
string s = "";
for (int i = 0; i < count; i++)
{
uint flags = (uint)fams[i].QueueFlags;
s += $"[fam{i}: flags=0x{flags:X} gfx={(flags & (uint)QueueFlags.GraphicsBit) != 0} compute={(flags & (uint)QueueFlags.ComputeBit) != 0} OFA={(flags & OfaQueueBit) != 0}] ";
}
Logger.Info?.Print(LogClass.Gpu, $"NVOFA HW Query: queue families = {s}(vkCmdOpticalFlowExecuteNV is only legal on a family with OFA=true).");
}
}
/// <summary>
/// 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 <paramref name="cmd"/>.
/// Any failure is reported and leaves <see cref="SessionReady"/> false (caller keeps Lucas-Kanade);
/// a wrong format/parameter surfaces as a VkResult error rather than a device loss.
/// </summary>
public bool CreateSession(Device device, CommandBuffer cmd, uint width, uint height)
{
if (!Available)
{
return false;
}
DestroySession(device);
OpticalFlowSessionCreateInfoNV info = new()
{
SType = StructureType.OpticalFlowSessionCreateInfoNV,
Width = width,
Height = height,
ImageFormat = Format.R8Unorm,
FlowVectorFormat = Format.R16G16Sfloat,
CostFormat = Format.Undefined,
OutputGridSize = (OpticalFlowGridSizeFlagsNV)_outputGrid, // chosen from supportedOutputGridSizes (QueryHardware)
HintGridSize = (OpticalFlowGridSizeFlagsNV)0,
PerformanceLevel = (OpticalFlowPerformanceLevelNV)2, // MEDIUM
};
OpticalFlowSessionNV session;
Result r = _createSession(device, &info, null, &session);
if (r != Result.Success || session.Handle == 0)
{
Logger.Warning?.Print(LogClass.Gpu, $"NVOFA Session: vkCreateOpticalFlowSessionNV failed (VkResult={r}); keeping Lucas-Kanade.");
SessionReady = false;
return false;
}
_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;
if (!CreateImage(device, width, height, Format.R8Unorm, UsageInput, out _imgInput, out _memInput, out _viewInput) ||
!CreateImage(device, width, height, Format.R8Unorm, UsageInput, out _imgRef, out _memRef, out _viewRef) ||
!CreateImage(device, flowW, flowH, Format.R16G16Sfloat, UsageOutput, out _imgFlow, out _memFlow, out _viewFlow))
{
Logger.Warning?.Print(LogClass.Gpu, "NVOFA Session: optical flow image creation failed; keeping Lucas-Kanade.");
DestroySession(device);
return false;
}
// UNDEFINED -> GENERAL + clear (defined contents) for all three.
TransitionToGeneral(cmd, _imgInput);
TransitionToGeneral(cmd, _imgRef);
TransitionToGeneral(cmd, _imgFlow);
ClearImage(cmd, _imgInput);
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);
_bound = rIn == Result.Success && rRef == Result.Success && rFlow == Result.Success;
if (!_bound)
{
Logger.Warning?.Print(LogClass.Gpu, $"NVOFA Session: image bind failed (input={rIn}, reference={rRef}, flowVector={rFlow}); keeping Lucas-Kanade.");
DestroySession(device);
return false;
}
_flowW = flowW;
_flowH = flowH;
CreateReadbackBuffer(device, flowW, flowH);
SessionReady = true;
Logger.Info?.Print(LogClass.Gpu,
$"NVOFA Session: vkCreateOpticalFlowSessionNV initialized successfully ({width}x{height} input, {flowW}x{flowH} flow @ {_outputGrid}x{_outputGrid} grid, R8_UNORM -> R16G16_SFLOAT, pNext images bound).");
return true;
}
/// <summary>Destroys the current session and its images, if any.</summary>
public void DestroySession(Device device)
{
DestroyImage(device, ref _imgInput, ref _memInput, ref _viewInput);
DestroyImage(device, ref _imgRef, ref _memRef, ref _viewRef);
DestroyImage(device, ref _imgFlow, ref _memFlow, ref _viewFlow);
if (_readbackBuffer.Handle != 0)
{
if (_readbackPtr != null)
{
_api.UnmapMemory(device, _readbackMemory);
_readbackPtr = null;
}
_api.DestroyBuffer(device, _readbackBuffer, null);
_readbackBuffer = default;
}
if (_readbackMemory.Handle != 0)
{
_api.FreeMemory(device, _readbackMemory, null);
_readbackMemory = default;
}
if (_session.Handle != 0 && _destroySession != null)
{
_destroySession(device, _session, null);
_session = default;
}
SessionReady = false;
_bound = false;
}
/// <summary>
/// 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.
/// </summary>
public void Execute(CommandBuffer cmd)
{
if (!Available || !SessionReady || !_bound)
{
return;
}
Barrier(cmd);
OpticalFlowExecuteInfoNV info = new()
{
SType = StructureType.OpticalFlowExecuteInfoNV,
// Flags = 0 (forward flow), RegionCount = 0 (whole image).
};
_execute(cmd, _session, &info);
Barrier(cmd);
if (_execLogCount < 3)
{
_execLogCount++;
Logger.Info?.Print(LogClass.Gpu, "NVOFA Execute: Dummy hardware motion vectors generated (vkCmdOpticalFlowExecuteNV recorded, no error).");
}
}
/// <summary>
/// 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).
/// </summary>
public void ExecuteOnOfaQueue()
{
if (!Available || !SessionReady || !_bound || !_queueReady)
{
return;
}
_api.ResetCommandBuffer(_cmdBuffer, 0);
CommandBufferBeginInfo begin = new()
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
};
if (_api.BeginCommandBuffer(_cmdBuffer, &begin) != Result.Success)
{
return;
}
Execute(_cmdBuffer); // records the barrier + vkCmdOpticalFlowExecuteNV into the OFA command buffer
// 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()
{
ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
ImageExtent = new Extent3D(_flowW, _flowH, 1),
};
_api.CmdCopyImageToBuffer(_cmdBuffer, _imgFlow, ImageLayout.General, _readbackBuffer, 1, &copy);
}
if (_api.EndCommandBuffer(_cmdBuffer) != Result.Success)
{
return;
}
CommandBuffer cb = _cmdBuffer;
SubmitInfo submit = new()
{
SType = StructureType.SubmitInfo,
CommandBufferCount = 1,
PCommandBuffers = &cb,
};
_api.ResetFences(_device, 1, in _fence);
_api.QueueSubmit(_ofaQueue, 1, &submit, _fence);
_api.WaitForFences(_device, 1, in _fence, true, ulong.MaxValue);
MeasureFlow();
}
/// <summary>
/// Uploads the real current (frame N) and previous (frame N-1) colour images into the OFA R8 inputs
/// via a transfer blit (RGBA -&gt; 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.
/// </summary>
public void UploadFrames(CommandBuffer graphicsCmd, Image current, Image previous, uint width, uint height)
{
if (!Available || !SessionReady)
{
return;
}
BlitToR8(graphicsCmd, current, _imgInput, width, height);
BlitToR8(graphicsCmd, previous, _imgRef, width, height);
}
private void BlitToR8(CommandBuffer cmd, Image src, Image dst, uint width, uint height)
{
ImageBlit region = new()
{
SrcSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
DstSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
};
region.SrcOffsets.Element0 = new Offset3D(0, 0, 0);
region.SrcOffsets.Element1 = new Offset3D((int)width, (int)height, 1);
region.DstOffsets.Element0 = new Offset3D(0, 0, 0);
region.DstOffsets.Element1 = new Offset3D((int)width, (int)height, 1);
_api.CmdBlitImage(cmd, src, ImageLayout.General, dst, ImageLayout.General, 1, &region, 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)
{
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;
double maxSq = 0.0;
int outliers = 0;
for (int i = 0; i < count; i++)
{
float x = data[(i * 2) + 0] / 32.0f;
float y = data[(i * 2) + 1] / 32.0f;
float magSq = (x * x) + (y * y);
sumSq += magSq;
if (magSq > maxSq)
{
maxSq = magSq;
}
if (magSq > 4.0f) // |mv| > 2px
{
outliers++;
}
}
if ((_measureLogCount++ % 120) == 0 && count > 0)
{
float rms = (float)Math.Sqrt(sumSq / count);
float max = (float)Math.Sqrt(maxSq);
Logger.Info?.Print(LogClass.Gpu,
$"NVOFA Flow Measure: HW vectors (S10.5) RMS={rms:0.000}px outliers={100f * outliers / count:0.0}% max={max:0.0}px ({_flowW}x{_flowH} @ {_outputGrid}x grid) -- compare vs the 'DLSS motion-field' Lucas-Kanade line.");
}
}
private void CreateReadbackBuffer(Device device, uint flowW, uint flowH)
{
ulong size = (ulong)flowW * flowH * 4; // R16G16_SFLOAT = 4 bytes/texel
BufferCreateInfo bci = new()
{
SType = StructureType.BufferCreateInfo,
Size = size,
Usage = BufferUsageFlags.TransferDstBit,
SharingMode = SharingMode.Exclusive,
};
if (_api.CreateBuffer(device, &bci, null, out _readbackBuffer) != Result.Success)
{
_readbackBuffer = default;
return;
}
_api.GetBufferMemoryRequirements(device, _readbackBuffer, out MemoryRequirements req);
uint memType = FindMemoryType(req.MemoryTypeBits, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit);
if (memType == uint.MaxValue)
{
_api.DestroyBuffer(device, _readbackBuffer, null);
_readbackBuffer = default;
return;
}
MemoryAllocateInfo ai = new()
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = req.Size,
MemoryTypeIndex = memType,
};
if (_api.AllocateMemory(device, &ai, null, out _readbackMemory) != Result.Success)
{
_api.DestroyBuffer(device, _readbackBuffer, null);
_readbackBuffer = default;
return;
}
_api.BindBufferMemory(device, _readbackBuffer, _readbackMemory, 0);
void* ptr = null;
_api.MapMemory(device, _readbackMemory, 0, size, 0, ref ptr);
_readbackPtr = ptr;
}
/// <summary>Full teardown: session + images, then the queue command pool/buffer and fence.</summary>
public void Dispose(Device device)
{
DestroySession(device);
if (_fence.Handle != 0)
{
_api.DestroyFence(device, _fence, null);
_fence = default;
}
if (_cmdPool.Handle != 0)
{
_api.DestroyCommandPool(device, _cmdPool, null);
_cmdPool = default;
}
_queueReady = false;
}
private bool CreateImage(Device device, uint width, uint height, Format format, uint ofaUsage, out Image image, out DeviceMemory memory, out ImageView view)
{
image = default;
memory = default;
view = default;
OpticalFlowImageFormatInfoNV ofaInfo = new()
{
SType = StructureType.OpticalFlowImageFormatInfoNV,
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;
bool concurrent = _queueReady && _graphicsFamily != _ofaFamily;
ImageCreateInfo ci = new()
{
SType = StructureType.ImageCreateInfo,
PNext = &ofaInfo,
ImageType = ImageType.Type2D,
Format = format,
Extent = new Extent3D(width, height, 1),
MipLevels = 1,
ArrayLayers = 1,
Samples = SampleCountFlags.Count1Bit,
Tiling = ImageTiling.Optimal,
Usage = ImageUsageFlags.SampledBit | ImageUsageFlags.TransferDstBit,
SharingMode = concurrent ? SharingMode.Concurrent : SharingMode.Exclusive,
QueueFamilyIndexCount = concurrent ? 2u : 0u,
PQueueFamilyIndices = concurrent ? families : null,
InitialLayout = ImageLayout.Undefined,
};
Image img;
if (_api.CreateImage(device, &ci, null, &img) != Result.Success)
{
return false;
}
_api.GetImageMemoryRequirements(device, img, out MemoryRequirements req);
uint memType = FindMemoryType(req.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit);
if (memType == uint.MaxValue)
{
_api.DestroyImage(device, img, null);
return false;
}
MemoryAllocateInfo ai = new()
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = req.Size,
MemoryTypeIndex = memType,
};
DeviceMemory mem;
if (_api.AllocateMemory(device, &ai, null, &mem) != Result.Success)
{
_api.DestroyImage(device, img, null);
return false;
}
_api.BindImageMemory(device, img, mem, 0);
ImageViewCreateInfo vi = new()
{
SType = StructureType.ImageViewCreateInfo,
Image = img,
ViewType = ImageViewType.Type2D,
Format = format,
SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, 1),
};
ImageView v;
if (_api.CreateImageView(device, &vi, null, &v) != Result.Success)
{
_api.FreeMemory(device, mem, null);
_api.DestroyImage(device, img, null);
return false;
}
image = img;
memory = mem;
view = v;
return true;
}
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags props)
{
_api.GetPhysicalDeviceMemoryProperties(_physicalDevice, out PhysicalDeviceMemoryProperties memProps);
for (uint i = 0; i < memProps.MemoryTypeCount; i++)
{
if ((typeBits & (1u << (int)i)) != 0 && (memProps.MemoryTypes[(int)i].PropertyFlags & props) == props)
{
return i;
}
}
return uint.MaxValue;
}
private void TransitionToGeneral(CommandBuffer cmd, Image image)
{
ImageMemoryBarrier b = new()
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = 0,
DstAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit,
OldLayout = ImageLayout.Undefined,
NewLayout = ImageLayout.General,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, 1),
};
_api.CmdPipelineBarrier(cmd, PipelineStageFlags.TopOfPipeBit, PipelineStageFlags.AllCommandsBit, 0, 0, null, 0, null, 1, &b);
}
private void ClearImage(CommandBuffer cmd, Image image)
{
ClearColorValue zero = default;
ImageSubresourceRange range = new(ImageAspectFlags.ColorBit, 0, 1, 0, 1);
_api.CmdClearColorImage(cmd, image, ImageLayout.General, &zero, 1, &range);
}
private void DestroyImage(Device device, ref Image image, ref DeviceMemory memory, ref ImageView view)
{
if (view.Handle != 0)
{
_api.DestroyImageView(device, view, null);
view = default;
}
if (image.Handle != 0)
{
_api.DestroyImage(device, image, null);
image = default;
}
if (memory.Handle != 0)
{
_api.FreeMemory(device, memory, null);
memory = default;
}
}
// 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()
{
SType = StructureType.MemoryBarrier,
SrcAccessMask = AccessFlags.MemoryWriteBit,
DstAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit,
};
_api.CmdPipelineBarrier(cmd, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.AllCommandsBit, 0, 1, &mb, 0, null, 0, null);
}
private static T Load<T>(Vk api, Device device, string name) where T : Delegate
{
PfnVoidFunction pfn = api.GetDeviceProcAddr(device, name);
if (pfn.Handle == null)
{
Logger.Warning?.Print(LogClass.Gpu, $"NVOFA: {name} could not be resolved (vkGetDeviceProcAddr returned null).");
return null;
}
return Marshal.GetDelegateForFunctionPointer<T>((IntPtr)pfn.Handle);
}
}
}
@@ -0,0 +1,404 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
using Ryujinx.Common.Logging;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Vulkan.Dlss
{
/// <summary>
/// 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 <see cref="DlssBinaries"/>.
///
/// 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` -> <see cref="byte"/> (1 byte, NOT marshalled bool which is 4)
/// - C++ enums -> <see cref="uint"/> (underlying type uint32_t)
/// - C++ `size_t` -> <see cref="nuint"/> (8 bytes on x64)
/// - C++ pointers -> <see cref="IntPtr"/>
/// </summary>
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 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;
/// <summary>sl::Result - sl_result.h. eOk == 0; everything else is an error/warning.</summary>
public enum Result
{
Ok = 0,
ErrorIO,
ErrorDriverOutOfDate,
ErrorOSOutOfDate,
ErrorOSDisabledHWS,
ErrorDeviceNotCreated,
ErrorNoSupportedAdapterFound,
ErrorAdapterNotSupported,
ErrorNoPlugins,
ErrorVulkanAPI,
ErrorDXGIAPI,
ErrorD3DAPI,
ErrorNRDAPI,
ErrorNVAPI,
ErrorReflexAPI,
ErrorNGXFailed,
ErrorJSONParsing,
ErrorMissingProxy,
ErrorMissingResourceState,
ErrorInvalidIntegration,
ErrorMissingInputParameter,
ErrorNotInitialized,
ErrorComputeFailed,
ErrorInitNotCalled,
ErrorExceptionHandler,
ErrorInvalidParameter,
ErrorMissingConstants,
ErrorDuplicatedConstants,
ErrorMissingOrInvalidAPI,
ErrorCommonConstantsMissing,
ErrorUnsupportedInterface,
ErrorFeatureMissing,
ErrorFeatureNotSupported,
ErrorFeatureMissingHooks,
ErrorFeatureFailedToLoad,
ErrorFeatureWrongPriority,
ErrorFeatureMissingDependency,
ErrorFeatureManagerInvalidState,
ErrorInvalidState,
WarnOutOfVRAM,
}
// sl::StructType - a 16-byte GUID. Alignment 4.
[StructLayout(LayoutKind.Sequential)]
private struct SlStructType
{
public uint Data1;
public ushort Data2;
public ushort Data3;
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)
}
// 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*)
}
// 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
}
// 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);
private static IntPtr _interposerHandle;
private static IntPtr _logPathPtr;
private static IntPtr _featuresPtr;
private static bool _initialized;
private static bool _everInitialized;
/// <summary>True once slInit has succeeded and the device has been registered.</summary>
public static bool IsInitialized => _initialized;
/// <summary>
/// True once slInit has succeeded at least once in this process. Unlike <see cref="IsInitialized"/>
/// this is NEVER reset by <see cref="Shutdown"/>. 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.
/// </summary>
public static bool WasEverInitialized => _everInitialized;
/// <summary>
/// Loads sl.interposer.dll from <paramref name="streamlineFolder"/> and calls slInit,
/// requesting the DLSS plugin. Returns true only if slInit succeeds. Logs the outcome.
/// </summary>
public static unsafe bool Initialize(string streamlineFolder)
{
if (_initialized)
{
return true;
}
if (!OperatingSystem.IsWindows())
{
Logger.Info?.Print(LogClass.Gpu, "DLSS: Streamline is Windows-only; skipping.");
return false;
}
string interposerPath = Path.Combine(streamlineFolder ?? string.Empty, InterposerDll);
if (!File.Exists(interposerPath))
{
Logger.Info?.Print(LogClass.Gpu, $"DLSS: {InterposerDll} not found at \"{interposerPath}\"; DLSS unavailable.");
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}\".");
return false;
}
Preferences pref = default;
pref.StructType = MakeGuid(0x1ca10965, 0xbf8e, 0x432b, 0x8d, 0xa1, 0x67, 0x16, 0xd8, 0x79, 0xfb, 0x14);
pref.StructVersion = StructVersion1;
pref.ShowConsole = 0;
// Dev bring-up: verbose log written to sl.log next to the interposer for diagnostics.
pref.LogLevel = LogLevelVerbose;
_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 = (1UL << 0);
// 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.
_featuresPtr = Marshal.AllocHGlobal(sizeof(uint));
Marshal.WriteInt32(_featuresPtr, (int)FeatureDLSS);
pref.FeaturesToLoad = _featuresPtr;
pref.NumFeaturesToLoad = 1;
Result result;
try
{
result = slInit(in pref, KSdkVersion);
}
catch (DllNotFoundException ex)
{
Logger.Warning?.Print(LogClass.Gpu, $"DLSS: slInit could not be resolved: {ex.Message}");
return false;
}
if (result != Result.Ok)
{
Logger.Info?.Print(LogClass.Gpu, $"DLSS: slInit failed ({result}); DLSS unavailable.");
return false;
}
_initialized = true;
_everInitialized = true;
Logger.Info?.Print(LogClass.Gpu, "DLSS: Streamline initialized.");
return true;
}
/// <summary>
/// Asks Streamline whether DLSS is supported on the given Vulkan physical device.
/// Logs "DLSS: available" / the failure reason. Requires <see cref="Initialize"/> first.
/// </summary>
/// <param name="vkPhysicalDevice">Native VkPhysicalDevice handle of the device in use.</param>
public static bool IsDlssSupported(IntPtr vkPhysicalDevice)
{
if (!_initialized)
{
return false;
}
AdapterInfo adapter = default;
adapter.StructType = MakeGuid(0x0677315f, 0xa746, 0x4492, 0x9f, 0x42, 0xcb, 0x61, 0x42, 0xc9, 0xc3, 0xd4);
adapter.StructVersion = StructVersion1;
adapter.VkPhysicalDevice = vkPhysicalDevice;
Result result = slIsFeatureSupported(FeatureDLSS, in adapter);
if (result == Result.Ok)
{
Logger.Info?.Print(LogClass.Gpu, "DLSS: available.");
return true;
}
Logger.Info?.Print(LogClass.Gpu, $"DLSS: not available ({result}).");
return false;
}
/// <summary>
/// Registers our natively-created Vulkan device/instance with Streamline. Mandatory when not
/// using SL's vkCreate* proxies. Must be called after <see cref="Initialize"/> and after the
/// device is created, before any feature evaluation.
/// </summary>
public static bool SetVulkanInfo(IntPtr instance, IntPtr physicalDevice, IntPtr device, uint queueFamily, uint queueIndex)
{
if (!_initialized)
{
return false;
}
VulkanInfo info = default;
info.StructType = MakeGuid(0x0eed6fd5, 0x82cd, 0x43a9, 0xbd, 0xb5, 0x47, 0xa5, 0xba, 0x2f, 0x45, 0xd6);
info.StructVersion = StructVersion3;
info.Device = device;
info.Instance = instance;
info.PhysicalDevice = physicalDevice;
// DLSS-SR requests no extra queues, so all of SL's queues map to Ryujinx's single queue.
info.GraphicsQueueFamily = queueFamily;
info.GraphicsQueueIndex = queueIndex;
info.ComputeQueueFamily = queueFamily;
info.ComputeQueueIndex = queueIndex;
info.OpticalFlowQueueFamily = queueFamily;
info.OpticalFlowQueueIndex = queueIndex;
Result result = slSetVulkanInfo(in info);
if (result == Result.Ok)
{
Logger.Info?.Print(LogClass.Gpu, "DLSS: Vulkan device registered with Streamline.");
return true;
}
Logger.Warning?.Print(LogClass.Gpu, $"DLSS: slSetVulkanInfo failed ({result}).");
return false;
}
/// <summary>Shuts Streamline down if it was initialized.</summary>
public static void Shutdown()
{
if (_initialized)
{
slShutdown();
_initialized = false;
}
// Free the unmanaged buffers slInit was given, now that Streamline has been shut down.
if (_logPathPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(_logPathPtr);
_logPathPtr = IntPtr.Zero;
}
if (_featuresPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(_featuresPtr);
_featuresPtr = IntPtr.Zero;
}
if (_interposerHandle != IntPtr.Zero)
{
NativeLibrary.Free(_interposerHandle);
_interposerHandle = IntPtr.Zero;
}
}
private static SlStructType MakeGuid(uint d1, ushort d2, ushort d3, byte b0, byte b1, byte b2, byte b3, byte b4, byte b5, byte b6, byte b7)
{
return new SlStructType
{
Data1 = d1,
Data2 = d2,
Data3 = d3,
B0 = b0, B1 = b1, B2 = b2, B3 = b3, B4 = b4, B5 = b5, B6 = b6, B7 = b7,
};
}
}
}
@@ -0,0 +1,531 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 The Roofer Dev - Beast Roofer Edition. Clean-room integration code.
// Built on Ryujinx (MIT). DLSS, DLAA and NIS are NVIDIA technologies; this is integration code only.
using Ryujinx.Common.Logging;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Vulkan.Dlss
{
/// <summary>
/// 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.
/// </summary>
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;
// 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;
/// <summary>One image to hand to DLSS (input/output/depth/motion).</summary>
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 uint Width;
public uint Height;
}
public enum DlssMode : uint
{
Off = 0,
MaxPerformance,
Balanced,
MaxQuality,
UltraPerformance,
UltraQuality,
Dlaa,
}
[StructLayout(LayoutKind.Sequential)]
private struct StructType
{
public uint Data1;
public ushort Data2;
public ushort Data3;
public byte B0, B1, B2, B3, B4, B5, B6, B7;
}
[StructLayout(LayoutKind.Sequential)]
private struct ViewportHandle
{
public IntPtr Next;
public StructType Type;
public nuint Version;
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
}
[StructLayout(LayoutKind.Sequential)]
private struct Extent
{
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 uint BufferType;
public uint Lifecycle;
public Extent Extent;
}
[StructLayout(LayoutKind.Sequential)]
private struct Mat4
{
public float M00, M01, M02, M03;
public float M10, M11, M12, M13;
public float M20, M21, M22, M23;
public float M30, M31, M32, M33;
public static Mat4 Identity()
{
Mat4 m = default;
m.M00 = m.M11 = m.M22 = m.M33 = 1f;
return m;
}
}
// sl::Constants (kStructVersion2) - sl_consts.h. sizeof == 456 on x64.
[StructLayout(LayoutKind.Sequential)]
private struct Constants
{
public IntPtr Next;
public StructType Type;
public nuint Version;
public Mat4 CameraViewToClip;
public Mat4 ClipToCameraView;
public Mat4 ClipToLensClip;
public Mat4 ClipToPrevClip;
public Mat4 PrevClipToClip;
public float JitterOffsetX, JitterOffsetY;
public float MvecScaleX, MvecScaleY;
public float CameraPinholeOffsetX, CameraPinholeOffsetY;
public float CameraPosX, CameraPosY, CameraPosZ;
public float CameraUpX, CameraUpY, CameraUpZ;
public float CameraRightX, CameraRightY, CameraRightZ;
public float CameraFwdX, CameraFwdY, CameraFwdZ;
public float CameraNear;
public float CameraFar;
public float CameraFOV;
public float CameraAspectRatio;
public float MotionVectorsInvalidValue;
public byte DepthInverted;
public byte CameraMotionIncluded;
public byte MotionVectors3D;
public byte Reset;
public byte OrthographicProjection;
public byte MotionVectorsDilated;
public byte MotionVectorsJittered;
public float MinRelativeLinearDepthObjectSeparation;
}
// sl::DLSSOptions (kStructVersion3) - sl_dlss.h. sizeof == 88.
[StructLayout(LayoutKind.Sequential)]
private struct DlssOptions
{
public IntPtr Next;
public StructType Type;
public nuint Version;
public uint Mode;
public uint OutputWidth;
public uint OutputHeight;
public float Sharpness;
public float PreExposure;
public float ExposureScale;
public byte ColorBuffersHDR;
public byte IndicatorInvertAxisX;
public byte IndicatorInvertAxisY;
public uint DlaaPreset;
public uint QualityPreset;
public uint BalancedPreset;
public uint PerformancePreset;
public uint UltraPerformancePreset;
public uint UltraQualityPreset;
public byte UseAutoExposure;
public byte AlphaUpscalingEnabled;
}
// sl::DLSSOptimalSettings (kStructVersion1) - sl_dlss.h. sizeof == 64.
[StructLayout(LayoutKind.Sequential)]
private struct DlssOptimalSettings
{
public IntPtr Next;
public StructType Type;
public nuint Version;
public uint OptimalRenderWidth;
public uint OptimalRenderHeight;
public float OptimalSharpness;
public uint RenderWidthMin;
public uint RenderHeightMin;
public uint RenderWidthMax;
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);
[DllImport(Interposer, EntryPoint = "slSetConstants", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
private static extern int slSetConstants(in Constants values, IntPtr frameToken, in ViewportHandle viewport);
[DllImport(Interposer, EntryPoint = "slEvaluateFeature", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
private static extern int slEvaluateFeature(uint feature, IntPtr frameToken, IntPtr* inputs, uint numInputs, IntPtr cmdBuffer);
[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]<in ViewportHandle, in DlssOptions, int> _slDLSSSetOptions;
private static delegate* unmanaged[Cdecl]<in DlssOptions, ref DlssOptimalSettings, int> _slDLSSGetOptimalSettings;
private static bool _functionsBound;
private static bool _loggedSizes;
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)
{
return new StructType
{
Data1 = d1, Data2 = d2, Data3 = d3,
B0 = b0, B1 = b1, B2 = b2, B3 = b3, B4 = b4, B5 = b5, B6 = b6, B7 = b7,
};
}
private static bool BindFunctions()
{
if (_functionsBound)
{
return true;
}
if (slGetFeatureFunction(FeatureDLSS, "slDLSSSetOptions", out IntPtr setOptions) != 0 ||
slGetFeatureFunction(FeatureDLSS, "slDLSSGetOptimalSettings", out IntPtr getOptimal) != 0 ||
setOptions == IntPtr.Zero || getOptimal == IntPtr.Zero)
{
Logger.Warning?.Print(LogClass.Gpu, "DLSS: could not bind DLSS feature functions.");
return false;
}
_slDLSSSetOptions = (delegate* unmanaged[Cdecl]<in ViewportHandle, in DlssOptions, int>)setOptions;
_slDLSSGetOptimalSettings = (delegate* unmanaged[Cdecl]<in DlssOptions, ref DlssOptimalSettings, int>)getOptimal;
_functionsBound = true;
if (!_loggedSizes)
{
_loggedSizes = true;
Logger.Info?.Print(LogClass.Gpu, $"DLSS: struct sizes Resource={Marshal.SizeOf<Resource>()}(112) Constants={Marshal.SizeOf<Constants>()}(456) DLSSOptions={Marshal.SizeOf<DlssOptions>()}(88) Tag={Marshal.SizeOf<ResourceTag>()}(64)");
}
return true;
}
private static ViewportHandle MakeViewport(uint id)
{
ViewportHandle vp = default;
vp.Type = Guid(0x171b6435, 0x9b3c, 0x4fc8, 0x99, 0x94, 0xfb, 0xe5, 0x25, 0x69, 0xaa, 0xa4);
vp.Version = 1;
vp.Value = id;
return vp;
}
/// <summary>Sets DLSS options for the viewport (mode + final output size). Call when size/mode changes.</summary>
public static bool SetOptions(uint viewportId, DlssMode mode, uint outputWidth, uint outputHeight, bool hdr)
{
if (!BindFunctions())
{
return false;
}
DlssOptions opt = default;
opt.Type = Guid(0x6ac826e4, 0x4c61, 0x4101, 0xa9, 0x2d, 0x63, 0x8d, 0x42, 0x10, 0x57, 0xb8);
opt.Version = 3;
opt.Mode = (uint)mode;
opt.OutputWidth = outputWidth;
opt.OutputHeight = outputHeight;
opt.PreExposure = 1.0f;
opt.ExposureScale = 1.0f;
opt.ColorBuffersHDR = hdr ? BoolTrue : BoolFalse;
opt.UseAutoExposure = 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 PresetF = 6; // CNN ultra-stability, for the upscaling modes
opt.DlaaPreset = PresetK;
opt.QualityPreset = PresetF;
opt.BalancedPreset = PresetF;
opt.PerformancePreset = PresetF;
opt.UltraPerformancePreset = PresetF;
opt.UltraQualityPreset = PresetF;
ViewportHandle vp = MakeViewport(viewportId);
int r = _slDLSSSetOptions(in vp, in opt);
if (r != 0)
{
Logger.Warning?.Print(LogClass.Gpu, $"DLSS: slDLSSSetOptions failed ({r}).");
return false;
}
return true;
}
/// <summary>Queries DLSS's optimal render resolution for a given output size + mode.</summary>
public static bool GetOptimalRenderSize(DlssMode mode, uint outputWidth, uint outputHeight, out uint renderWidth, out uint renderHeight)
{
renderWidth = outputWidth;
renderHeight = outputHeight;
if (!BindFunctions())
{
return false;
}
DlssOptions opt = default;
opt.Type = Guid(0x6ac826e4, 0x4c61, 0x4101, 0xa9, 0x2d, 0x63, 0x8d, 0x42, 0x10, 0x57, 0xb8);
opt.Version = 3;
opt.Mode = (uint)mode;
opt.OutputWidth = outputWidth;
opt.OutputHeight = outputHeight;
DlssOptimalSettings settings = default;
settings.Type = Guid(0xef1d0957, 0xfd58, 0x4df7, 0xb5, 0x04, 0x8b, 0x69, 0xd8, 0xaa, 0x6b, 0x76);
settings.Version = 1;
if (_slDLSSGetOptimalSettings(in opt, ref settings) != 0 || settings.OptimalRenderWidth == 0)
{
return false;
}
renderWidth = settings.OptimalRenderWidth;
renderHeight = settings.OptimalRenderHeight;
return true;
}
/// <summary>
/// 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.
/// </summary>
public static bool GetRenderRange(DlssMode mode, uint outputWidth, uint outputHeight,
out uint minWidth, out uint minHeight, out uint maxWidth, out uint maxHeight)
{
minWidth = minHeight = maxWidth = maxHeight = 0;
if (!BindFunctions())
{
return false;
}
DlssOptions opt = default;
opt.Type = Guid(0x6ac826e4, 0x4c61, 0x4101, 0xa9, 0x2d, 0x63, 0x8d, 0x42, 0x10, 0x57, 0xb8);
opt.Version = 3;
opt.Mode = (uint)mode;
opt.OutputWidth = outputWidth;
opt.OutputHeight = outputHeight;
DlssOptimalSettings settings = default;
settings.Type = Guid(0xef1d0957, 0xfd58, 0x4df7, 0xb5, 0x04, 0x8b, 0x69, 0xd8, 0xaa, 0x6b, 0x76);
settings.Version = 1;
if (_slDLSSGetOptimalSettings(in opt, ref settings) != 0 || settings.OptimalRenderWidth == 0)
{
return false;
}
minWidth = settings.RenderWidthMin;
minHeight = settings.RenderHeightMin;
maxWidth = settings.RenderWidthMax;
maxHeight = settings.RenderHeightMax;
return true;
}
private static Resource MakeResource(in DlssTexture tex)
{
Resource r = default;
r.Type = Guid(0x3a9d70cf, 0x2418, 0x4b72, 0x83, 0x91, 0x13, 0xf8, 0x72, 0x1c, 0x72, 0x61);
r.Version = 1;
r.ResType = ResourceTypeTex2d;
r.Native = tex.Image;
r.View = tex.View;
r.State = tex.Layout;
r.Width = tex.Width;
r.Height = tex.Height;
r.NativeFormat = tex.NativeFormat;
r.MipLevels = 1;
r.ArrayLayers = 1;
return r;
}
private static ResourceTag MakeTag(Resource* resource, uint bufferType, uint width, uint height)
{
ResourceTag t = default;
t.Type = Guid(0x4c6a5aad, 0xb445, 0x496c, 0x87, 0xff, 0x1a, 0xf3, 0x84, 0x5b, 0xe6, 0x53);
t.Version = 1;
t.ResourcePtr = (IntPtr)resource;
t.BufferType = bufferType;
t.Lifecycle = LifecycleValidUntilEvaluate;
t.Extent = new Extent { Top = 0, Left = 0, Width = width, Height = height };
return t;
}
private static Constants BuildConstants(uint outW, uint outH, uint renderW, uint renderH, bool reset, float jitterX, float jitterY)
{
Constants c = default;
c.Type = Guid(0xdcd35ad7, 0x4e4a, 0x4bad, 0xa9, 0x0c, 0xe0, 0xc4, 0x9e, 0xb2, 0x3a, 0xfe);
c.Version = 2;
c.CameraViewToClip = Mat4.Identity();
c.ClipToCameraView = Mat4.Identity();
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.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;
c.CameraRightX = 1f;
c.CameraFwdZ = 1f;
c.CameraNear = 0.1f;
c.CameraFar = 10000f;
c.CameraFOV = 1.0f;
c.CameraAspectRatio = outH != 0 ? (float)outW / outH : 1.7777f;
c.MotionVectorsInvalidValue = 0f;
c.DepthInverted = BoolFalse;
c.CameraMotionIncluded = BoolTrue;
c.MotionVectors3D = BoolFalse;
c.Reset = reset ? BoolTrue : BoolFalse; // history off only on the first frame / scene cut
c.OrthographicProjection = BoolFalse;
c.MotionVectorsDilated = BoolFalse;
c.MotionVectorsJittered = BoolFalse;
c.MinRelativeLinearDepthObjectSeparation = 40f;
return c;
}
/// <summary>
/// Runs DLSS for one frame: sets constants, tags the four buffers and evaluates into the
/// output image, recording into <paramref name="cmdBuffer"/>. The caller submits that buffer.
/// </summary>
public static bool Evaluate(
IntPtr cmdBuffer,
uint viewportId,
uint frameIndex,
bool reset,
float jitterX,
float jitterY,
in DlssTexture input,
in DlssTexture output,
in DlssTexture depth,
in DlssTexture motion)
{
if (!_functionsBound)
{
return false;
}
if (slGetNewFrameToken(out IntPtr token, in frameIndex) != 0 || token == IntPtr.Zero)
{
Logger.Warning?.Print(LogClass.Gpu, "DLSS: slGetNewFrameToken failed.");
return false;
}
ViewportHandle vp = MakeViewport(viewportId);
Constants constants = BuildConstants(output.Width, output.Height, motion.Width, motion.Height, reset, jitterX, jitterY);
int rc = slSetConstants(in constants, token, in vp);
if (rc != 0)
{
Logger.Warning?.Print(LogClass.Gpu, $"DLSS: slSetConstants failed ({rc}).");
return false;
}
Resource rInput = MakeResource(input);
Resource rOutput = MakeResource(output);
Resource rDepth = MakeResource(depth);
Resource rMotion = MakeResource(motion);
ResourceTag tInput = MakeTag(&rInput, BufferTypeScalingInputColor, input.Width, input.Height);
ResourceTag tOutput = MakeTag(&rOutput, BufferTypeScalingOutputColor, output.Width, output.Height);
ResourceTag tDepth = MakeTag(&rDepth, BufferTypeDepth, depth.Width, depth.Height);
ResourceTag tMotion = MakeTag(&rMotion, BufferTypeMotionVectors, motion.Width, motion.Height);
// Inputs to slEvaluateFeature: the viewport plus the four local resource tags.
IntPtr* inputs = stackalloc IntPtr[5];
inputs[0] = (IntPtr)(&vp);
inputs[1] = (IntPtr)(&tDepth);
inputs[2] = (IntPtr)(&tMotion);
inputs[3] = (IntPtr)(&tInput);
inputs[4] = (IntPtr)(&tOutput);
int re = slEvaluateFeature(FeatureDLSS, token, inputs, 5, cmdBuffer);
if (re != 0)
{
Logger.Warning?.Print(LogClass.Gpu, $"DLSS: slEvaluateFeature failed ({re}).");
return false;
}
return true;
}
}
}
@@ -1,3 +1,7 @@
// 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;
using Ryujinx.Graphics.Shader;
@@ -0,0 +1,96 @@
// 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 (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 dejitterY;
};
layout (std430, set = 1, binding = 0) buffer SceneChange {
uint changedCount;
uint motionCount;
uint statSamples;
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
};
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;
for (int j = i + 1; j < 9; ++j)
{
if (a[j] < a[m])
{
m = j;
}
}
float t = a[i];
a[i] = a[m];
a[m] = t;
}
return a[4];
}
void main()
{
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
int w = int(width);
int h = int(height);
if (p.x >= w || p.y >= h)
{
return;
}
ivec2 maxc = ivec2(w - 1, h - 1);
float xs[9];
float ys[9];
int k = 0;
for (int dy = -1; dy <= 1; ++dy)
{
for (int dx = -1; dx <= 1; ++dx)
{
vec2 m = imageLoad(imgIn, clamp(p + ivec2(dx, dy), ivec2(0), maxc)).rg;
xs[k] = m.x;
ys[k] = m.y;
++k;
}
}
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);
if (mag > 2.0)
{
atomicAdd(outlierCountF, 1u);
}
float magStat = min(mag, 4.0);
atomicAdd(sumMagFxF, uint(magStat * 256.0 + 0.5));
atomicAdd(sumMagSqFxF, uint(magStat * magStat * 1024.0 + 0.5));
}
}
@@ -0,0 +1,159 @@
// 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;
layout (rg16f, binding = 0, set = 3) uniform image2D imgMotion;
layout (binding = 1, set = 2) uniform sampler2D CurrentColor;
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 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
};
float luma(vec3 c)
{
return dot(c, vec3(0.299, 0.587, 0.114));
}
float lumaAt(ivec2 p, ivec2 maxc)
{
ivec2 q = clamp(p, ivec2(0), maxc);
return luma(texelFetch(CurrentColor, q, 0).rgb);
}
void main()
{
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
int w = int(width);
int h = int(height);
if (p.x >= w || p.y >= h)
{
return;
}
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
for (int dy = -R; dy <= R; ++dy)
{
for (int dx = -R; dx <= R; ++dx)
{
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);
Sxx += ix * ix;
Sxy += ix * iy;
Syy += iy * iy;
Sxt += ix * it;
Syt += iy * it;
Stt += it * it;
}
}
// 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);
if (mag > 2.0)
{
atomicAdd(outlierCount, 1u);
}
float magStat = min(mag, 4.0);
atomicAdd(statSamples, 1u);
atomicAdd(sumMagFx, uint(magStat * 256.0 + 0.5));
atomicAdd(sumMagSqFx, uint(magStat * magStat * 1024.0 + 0.5));
}
imageStore(imgMotion, p, vec4(mv, 0.0, 0.0));
}
@@ -0,0 +1,31 @@
// 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 (binding = 2) uniform params {
float outWidth;
float outHeight;
};
void main()
{
ivec2 loc = ivec2(gl_GlobalInvocationID.xy);
if (loc.x >= int(outWidth) || loc.y >= int(outHeight))
{
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));
}
@@ -0,0 +1,133 @@
// 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
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 = 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
};
// 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
vec3 RGBToYCoCg(vec3 c)
{
return vec3(
0.25 * c.r + 0.5 * c.g + 0.25 * c.b,
0.5 * c.r - 0.5 * c.b,
-0.25 * c.r + 0.5 * c.g - 0.25 * c.b);
}
vec3 YCoCgToRGB(vec3 c)
{
float y = c.x;
float co = c.y;
float cg = c.z;
return vec3(y + co - cg, y + cg, y - co - cg);
}
void main()
{
ivec2 loc = ivec2(gl_GlobalInvocationID.xy);
int iw = int(width);
int ih = int(height);
if (loc.x >= iw || loc.y >= ih)
{
return;
}
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);
for (int dy = -1; dy <= 1; ++dy)
{
for (int dx = -1; dx <= 1; ++dx)
{
ivec2 p = clamp(loc + ivec2(dx, dy), ivec2(0), ivec2(iw - 1, ih - 1));
vec3 c = RGBToYCoCg(texelFetch(Source, p, 0).rgb);
m1 += c;
m2 += c * c;
}
}
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;
vec2 uv = prevPos / vec2(width, height);
bool onScreen = all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));
bool valid = hasHistory > 0.5 && onScreen;
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);
vec3 boxMin = mean - gamma * sigma;
vec3 boxMax = mean + gamma * sigma;
vec3 historyY = clamp(RGBToYCoCg(histSample.rgb), boxMin, boxMax);
float w = valid ? wBlend : 0.0;
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));
}
@@ -0,0 +1,398 @@
// 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;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
using Silk.NET.Vulkan;
using System;
using System.Globalization;
using Format = Ryujinx.Graphics.GAL.Format;
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
namespace Ryujinx.Graphics.Vulkan.Effects
{
/// <summary>
/// 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).
/// </summary>
internal class TemporalFilter : IPostProcessingEffect
{
/// <summary>True when the user opted into the experimental TAA via RYUJINX_TAA=1.</summary>
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 static readonly float HistoryBlend = ParseBlend();
private static readonly float MotionSign = ParseSign();
private static readonly float ClampGamma = ParseClamp();
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 ISampler _sampler;
private ShaderCollection _program;
private ShaderCollection _downsampleProgram;
private ShaderCollection _motionProgram;
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;
private TextureView _motionFiltered;
private bool _readColorHalf0;
private bool _hasPrev;
private int _motionWidth;
private int _motionHeight;
private float _mvScale;
private bool _activeLogged;
public TemporalFilter(VulkanRenderer renderer, Device device)
{
_renderer = renderer;
_pipeline = new PipelineHelperShader(renderer, device);
_motionPipeline = new PipelineHelperShader(renderer, device);
Initialize();
}
private static float ParseBlend()
{
string value = Environment.GetEnvironmentVariable("RYUJINX_TAA_BLEND");
if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float blend) &&
blend >= 0f && blend <= 0.99f)
{
return blend;
}
return 0.935f; // validated in-game default (soft, cinematic, very low flicker); RYUJINX_TAA_BLEND overrides.
}
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) &&
gamma >= 0.25f && gamma <= 8f)
{
return gamma;
}
return 1.25f; // validated in-game default (lets native texture detail through); RYUJINX_TAA_CLAMP overrides.
}
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,
"4" => 4,
_ => 2,
};
}
private void Initialize()
{
_pipeline.Initialize();
_motionPipeline.Initialize();
_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)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 3)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 5)
.Add(ResourceStages.Compute, ResourceType.Image, 0, true)
.Add(ResourceStages.Compute, ResourceType.Image, 1, true).Build();
_program = _renderer.CreateProgramWithMinimalLayout([
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)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1)
.Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build();
_downsampleProgram = _renderer.CreateProgramWithMinimalLayout([
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)
.Add(ResourceStages.Compute, ResourceType.StorageBuffer, 0)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1)
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 3)
.Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build();
_motionProgram = _renderer.CreateProgramWithMinimalLayout([
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)
.Add(ResourceStages.Compute, ResourceType.StorageBuffer, 0)
.Add(ResourceStages.Compute, ResourceType.Image, 0, true)
.Add(ResourceStages.Compute, ResourceType.Image, 1, true).Build();
_motionFilterProgram = _renderer.CreateProgramWithMinimalLayout([
new ShaderSource(filterShader, ShaderStage.Compute, TargetLanguage.Spirv)
], filterLayout);
_sceneChangeBuffer = _renderer.BufferManager.CreateWithHandle(_renderer, CounterCount * sizeof(uint));
}
private static TextureCreateInfo MakeInfo(TextureCreateInfo b, int w, int h, Format format, int bytesPerPixel)
{
return new TextureCreateInfo(
w,
h,
1,
1,
1,
1,
1,
bytesPerPixel,
format,
b.DepthStencilMode,
Target.Texture2D,
b.SwizzleR,
b.SwizzleG,
b.SwizzleB,
b.SwizzleA);
}
private void EnsureResources(TextureView view)
{
if (_output != null && _output.Width == view.Width && _output.Height == view.Height)
{
return;
}
_output?.Dispose();
_history0?.Dispose();
_history1?.Dispose();
_colorHalf0?.Dispose();
_colorHalf1?.Dispose();
_motion?.Dispose();
_motionFiltered?.Dispose();
_motionWidth = Math.Max(1, view.Width / MotionDownscale);
_motionHeight = Math.Max(1, view.Height / MotionDownscale);
_mvScale = (float)view.Width / _motionWidth;
_output = _renderer.CreateTexture(view.Info) as TextureView;
_history0 = _renderer.CreateTexture(MakeInfo(view.Info, view.Width, view.Height, Format.R16G16B16A16Float, 8)) as TextureView;
_history1 = _renderer.CreateTexture(MakeInfo(view.Info, view.Width, view.Height, Format.R16G16B16A16Float, 8)) as TextureView;
_colorHalf0 = _renderer.CreateTexture(MakeInfo(view.Info, _motionWidth, _motionHeight, Format.R16G16B16A16Float, 8)) as TextureView;
_colorHalf1 = _renderer.CreateTexture(MakeInfo(view.Info, _motionWidth, _motionHeight, Format.R16G16B16A16Float, 8)) as TextureView;
_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;
_hasPrev = false;
}
public TextureView Run(TextureView view, CommandBufferScoped cbs, int width, int height)
{
EnsureResources(view);
if (!_activeLogged)
{
_activeLogged = true;
Logger.Info?.Print(LogClass.Gpu,
$"TAA: active (YCoCg clamp, blend {HistoryBlend:0.00}, mv sign {MotionSign:+0;-0}, clamp {ClampGamma:0.00}, " +
$"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;
_pipeline.SetCommandBuffer(cbs);
_pipeline.SetProgram(_program);
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _sampler);
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, historyRead, _sampler);
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 5, _motionFiltered, _sampler);
ReadOnlySpan<float> paramsBuffer =
[
view.Width,
view.Height,
HistoryBlend,
_hasHistory ? 1f : 0f,
MotionSign,
_hasPrev ? 1f : 0f,
ClampGamma,
_mvScale,
];
int rangeSize = paramsBuffer.Length * sizeof(float);
using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize);
buffer.Holder.SetDataUnchecked(buffer.Offset, paramsBuffer);
_pipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]);
_pipeline.SetImage(ShaderStage.Compute, 0, _output.GetView(FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)));
_pipeline.SetImage(1, historyWrite.GetImageView());
int dispatchX = BitUtils.DivRoundUp(view.Width, 16);
int dispatchY = BitUtils.DivRoundUp(view.Height, 16);
_pipeline.DispatchCompute(dispatchX, dispatchY, 1);
_pipeline.ComputeBarrier();
_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;
_hasPrev = true;
return _output;
}
private void RunDownsamplePass(TextureView source, TextureView dst, CommandBufferScoped cbs)
{
_motionPipeline.SetCommandBuffer(cbs);
_motionPipeline.SetProgram(_downsampleProgram);
_motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 1, source, _sampler);
ReadOnlySpan<float> p = [dst.Width, dst.Height];
using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, p.Length * sizeof(float));
buffer.Holder.SetDataUnchecked(buffer.Offset, p);
_motionPipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]);
_motionPipeline.SetImage(0, dst.GetImageView());
_motionPipeline.DispatchCompute(BitUtils.DivRoundUp(dst.Width, 16), BitUtils.DivRoundUp(dst.Height, 16), 1);
_motionPipeline.ComputeBarrier();
_motionPipeline.Finish();
}
private void RunMotionPass(TextureView current, TextureView previous, CommandBufferScoped cbs)
{
_motionPipeline.SetCommandBuffer(cbs);
_motionPipeline.SetProgram(_motionProgram);
_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<float> 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);
_motionPipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]);
_motionPipeline.SetStorageBuffers([new BufferAssignment(0, new BufferRange(_sceneChangeBuffer, 0, CounterCount * sizeof(uint), true))]);
_motionPipeline.SetImage(0, _motion.GetImageView());
_motionPipeline.DispatchCompute(BitUtils.DivRoundUp(_motionWidth, 16), BitUtils.DivRoundUp(_motionHeight, 16), 1);
_motionPipeline.ComputeBarrier();
_motionPipeline.Finish();
}
private void RunMotionFilterPass(CommandBufferScoped cbs)
{
_motionPipeline.SetCommandBuffer(cbs);
_motionPipeline.SetProgram(_motionFilterProgram);
ReadOnlySpan<float> 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);
_motionPipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]);
_motionPipeline.SetStorageBuffers([new BufferAssignment(0, new BufferRange(_sceneChangeBuffer, 0, CounterCount * sizeof(uint), true))]);
_motionPipeline.SetImage(0, _motionFiltered.GetImageView());
_motionPipeline.SetImage(1, _motion.GetImageView());
_motionPipeline.DispatchCompute(BitUtils.DivRoundUp(_motionWidth, 16), BitUtils.DivRoundUp(_motionHeight, 16), 1);
_motionPipeline.ComputeBarrier();
_motionPipeline.Finish();
}
public void Dispose()
{
_pipeline.Dispose();
_motionPipeline.Dispose();
_program.Dispose();
_downsampleProgram.Dispose();
_motionProgram.Dispose();
_motionFilterProgram.Dispose();
_sampler.Dispose();
_output?.Dispose();
_history0?.Dispose();
_history1?.Dispose();
_colorHalf0?.Dispose();
_colorHalf1?.Dispose();
_motion?.Dispose();
_motionFiltered?.Dispose();
_renderer.BufferManager.Delete(_sceneChangeBuffer);
}
}
}
@@ -484,6 +484,71 @@ namespace Ryujinx.Graphics.Vulkan
_pipeline.Finish(gd, cbs);
}
/// <summary>
/// Blits <paramref name="src"/> over <paramref name="dst"/> 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.
/// </summary>
public void BlitColorWithAlpha(
VulkanRenderer gd,
CommandBufferScoped cbs,
TextureView src,
TextureView dst,
Extents2D srcRegion,
Extents2D dstRegion,
float alpha)
{
_pipeline.SetCommandBuffer(cbs);
const int RegionBufferSize = 16;
_pipeline.SetTextureAndSamplerIdentitySwizzle(ShaderStage.Fragment, 0, src, _samplerLinear);
Span<float> region = stackalloc float[RegionBufferSize / sizeof(float)];
region[0] = (float)srcRegion.X1 / src.Width;
region[1] = (float)srcRegion.X2 / src.Width;
region[2] = (float)srcRegion.Y1 / src.Height;
region[3] = (float)srcRegion.Y2 / src.Height;
using ScopedTemporaryBuffer buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, RegionBufferSize);
buffer.Holder.SetDataUnchecked<float>(buffer.Offset, region);
_pipeline.SetUniformBuffers([new BufferAssignment(1, buffer.Range)]);
Span<Viewport> viewports = stackalloc Viewport[1];
Rectangle<float> rect = new(
MathF.Min(dstRegion.X1, dstRegion.X2),
MathF.Min(dstRegion.Y1, dstRegion.Y2),
MathF.Abs(dstRegion.X2 - dstRegion.X1),
MathF.Abs(dstRegion.Y2 - dstRegion.Y1));
viewports[0] = new Viewport(rect, ViewportSwizzle.PositiveX, ViewportSwizzle.PositiveY, ViewportSwizzle.PositiveZ, ViewportSwizzle.PositiveW, 0f, 1f);
_pipeline.SetProgram(_programColorBlit);
_pipeline.SetRenderTarget(dst, (uint)dst.Width, (uint)dst.Height);
_pipeline.SetRenderTargetColorMasks([0xf]);
_pipeline.SetScissors([new Rectangle<int>(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),
Ryujinx.Graphics.GAL.BlendOp.Add, Ryujinx.Graphics.GAL.BlendFactor.ConstantAlpha, Ryujinx.Graphics.GAL.BlendFactor.OneMinusConstantAlpha,
Ryujinx.Graphics.GAL.BlendOp.Add, Ryujinx.Graphics.GAL.BlendFactor.ConstantAlpha, Ryujinx.Graphics.GAL.BlendFactor.OneMinusConstantAlpha));
_pipeline.SetViewports(viewports);
_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),
Ryujinx.Graphics.GAL.BlendOp.Add, Ryujinx.Graphics.GAL.BlendFactor.One, Ryujinx.Graphics.GAL.BlendFactor.Zero,
Ryujinx.Graphics.GAL.BlendOp.Add, Ryujinx.Graphics.GAL.BlendFactor.One, Ryujinx.Graphics.GAL.BlendFactor.Zero));
_pipeline.Finish(gd, cbs);
}
private void BlitDepthStencil(
VulkanRenderer gd,
CommandBufferScoped cbs,
@@ -18,10 +18,14 @@
<EmbeddedResource Include="Effects\Shaders\AreaScaling.spv" />
<EmbeddedResource Include="Effects\Shaders\NisScaling.spv" />
<EmbeddedResource Include="Effects\Shaders\NisScalingHdr.spv" />
<EmbeddedResource Include="Effects\Shaders\MotionVectors.spv" />
<EmbeddedResource Include="Effects\Shaders\MotionFilter.spv" />
<EmbeddedResource Include="Effects\Shaders\FsrScaling.spv" />
<EmbeddedResource Include="Effects\Shaders\FsrSharpening.spv" />
<EmbeddedResource Include="Effects\Shaders\FsrSharpeningHdr.spv" />
<EmbeddedResource Include="Effects\Shaders\Fxaa.spv" />
<EmbeddedResource Include="Effects\Shaders\Temporal.spv" />
<EmbeddedResource Include="Effects\Shaders\TaaDownsample.spv" />
<EmbeddedResource Include="Effects\Shaders\SmaaBlend.spv" />
<EmbeddedResource Include="Effects\Shaders\SmaaEdge.spv" />
<EmbeddedResource Include="Effects\Shaders\SmaaNeighbour.spv" />
+4 -1
View File
@@ -37,7 +37,10 @@ namespace Ryujinx.Graphics.Vulkan
AddressModeU = info.AddressU.Convert(),
AddressModeV = info.AddressV.Convert(),
AddressModeW = info.AddressP.Convert(),
MipLodBias = info.MipLodBias,
// 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 = info.MipLodBias + (Dlss.DlssJitter.Enabled ? Dlss.DlssJitter.LodBias : 0f),
AnisotropyEnable = info.MaxAnisotropy != 1f,
MaxAnisotropy = info.MaxAnisotropy,
CompareEnable = info.CompareMode == CompareMode.CompareRToTexture,
@@ -269,7 +269,7 @@ namespace Ryujinx.Graphics.Vulkan
return InvalidIndex;
}
internal static Device CreateDevice(Vk api, VulkanPhysicalDevice physicalDevice, uint queueFamilyIndex, uint queueCount)
internal static Device CreateDevice(Vk api, VulkanPhysicalDevice physicalDevice, uint queueFamilyIndex, uint queueCount, out uint opticalFlowQueueFamilyIndex)
{
if (queueCount > QueuesCount)
{
@@ -283,7 +283,28 @@ namespace Ryujinx.Graphics.Vulkan
queuePriorities[i] = 1f;
}
DeviceQueueCreateInfo queueCreateInfo = new()
// 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"))
{
for (uint i = 0; i < physicalDevice.QueueFamilyProperties.Length; i++)
{
if (i != queueFamilyIndex && ((uint)physicalDevice.QueueFamilyProperties[i].QueueFlags & 0x100u) != 0)
{
opticalFlowQueueFamilyIndex = i;
break;
}
}
}
float ofaPriority = 1f;
uint queueCreateInfoCount = opticalFlowQueueFamilyIndex != uint.MaxValue ? 2u : 1u;
DeviceQueueCreateInfo* queueCreateInfos = stackalloc DeviceQueueCreateInfo[2];
queueCreateInfos[0] = new()
{
SType = StructureType.DeviceQueueCreateInfo,
QueueFamilyIndex = queueFamilyIndex,
@@ -291,6 +312,17 @@ namespace Ryujinx.Graphics.Vulkan
PQueuePriorities = queuePriorities,
};
if (queueCreateInfoCount == 2)
{
queueCreateInfos[1] = new()
{
SType = StructureType.DeviceQueueCreateInfo,
QueueFamilyIndex = opticalFlowQueueFamilyIndex,
QueueCount = 1,
PQueuePriorities = &ofaPriority,
};
}
bool useRobustBufferAccess = VendorUtils.FromId(physicalDevice.PhysicalDeviceProperties.VendorID) == Vendor.Nvidia;
PhysicalDeviceFeatures2 features2 = new()
@@ -498,6 +530,14 @@ namespace Ryujinx.Graphics.Vulkan
ShaderStorageImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderStorageImageArrayNonUniformIndexing,
};
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;
}
pExtendedFeatures = &featuresVk12;
PhysicalDeviceIndexTypeUint8FeaturesEXT featuresIndexU8;
@@ -590,8 +630,43 @@ namespace Ryujinx.Graphics.Vulkan
pExtendedFeatures = &featuresDynamicAttachmentFeedbackLoopLayout;
}
PhysicalDeviceSynchronization2Features featuresSync2;
PhysicalDeviceOpticalFlowFeaturesNV featuresOpticalFlow;
if (Dlss.DlssIntegration.IsEnabled && physicalDevice.IsDeviceExtensionPresent("VK_NV_optical_flow"))
{
// NVOFA (B2): VK_NV_optical_flow needs synchronization2 as a dependency. Enable both
// features, only when DLSS is on and the device advertises the extension, so the default
// (non-DLSS) device creation is left byte-identical to upstream.
featuresSync2 = new()
{
SType = StructureType.PhysicalDeviceSynchronization2Features,
PNext = pExtendedFeatures,
Synchronization2 = true,
};
pExtendedFeatures = &featuresSync2;
featuresOpticalFlow = new()
{
SType = StructureType.PhysicalDeviceOpticalFlowFeaturesNV,
PNext = pExtendedFeatures,
OpticalFlow = true,
};
pExtendedFeatures = &featuresOpticalFlow;
}
string[] enabledExtensions = _requiredExtensions.Union(_desirableExtensions.Intersect(physicalDevice.DeviceExtensions)).ToArray();
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();
}
nint* ppEnabledExtensions = stackalloc nint[enabledExtensions.Length];
for (int i = 0; i < enabledExtensions.Length; i++)
@@ -603,8 +678,8 @@ namespace Ryujinx.Graphics.Vulkan
{
SType = StructureType.DeviceCreateInfo,
PNext = pExtendedFeatures,
QueueCreateInfoCount = 1,
PQueueCreateInfos = &queueCreateInfo,
QueueCreateInfoCount = queueCreateInfoCount,
PQueueCreateInfos = queueCreateInfos,
PpEnabledExtensionNames = (byte**)ppEnabledExtensions,
EnabledExtensionCount = (uint)enabledExtensions.Length,
PEnabledFeatures = &features,
+76 -1
View File
@@ -37,6 +37,7 @@ 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 KhrSurface SurfaceApi { get; private set; }
internal KhrSwapchain SwapchainApi { get; private set; }
internal ExtConditionalRendering ConditionalRenderingApi { get; private set; }
@@ -49,6 +50,8 @@ 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 uint OpticalFlowQueueFamilyIndex { get; private set; } = uint.MaxValue;
internal Lock BackgroundQueueLock { get; private set; }
internal Lock QueueLock { get; private set; }
@@ -501,7 +504,7 @@ namespace Ryujinx.Graphics.Vulkan
uint queueFamilyIndex = VulkanInitialization.FindSuitableQueueFamily(Api, _physicalDevice, _surface, out uint maxQueueCount);
_device = VulkanInitialization.CreateDevice(Api, _physicalDevice, queueFamilyIndex, maxQueueCount);
_device = VulkanInitialization.CreateDevice(Api, _physicalDevice, queueFamilyIndex, maxQueueCount, out uint opticalFlowQueueFamilyIndex);
if (Api.TryGetDeviceExtension(_instance.Instance, _device, out KhrSwapchain swapchainApi))
{
@@ -516,6 +519,17 @@ 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);
OpticalFlowQueue = opticalFlowQueue;
OpticalFlowQueueFamilyIndex = opticalFlowQueueFamilyIndex;
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Gpu,
$"NVOFA: dedicated optical-flow queue acquired (family {opticalFlowQueueFamilyIndex}); device created with 2 queue families.");
}
_window = new Window(this, _surface, _physicalDevice.PhysicalDevice, _device);
_initialized = true;
@@ -916,11 +930,66 @@ namespace Ryujinx.Graphics.Vulkan
Logger.Notice.Print(LogClass.Gpu, $"GPU Memory: {GetTotalGPUMemory() / (1024 * 1024)} MiB");
}
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;
}
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);
if (ngx != null)
{
Logger.Info?.Print(LogClass.Gpu, $"DLSS: located nvngx_dlss.dll at \"{ngx}\".");
}
else
{
Logger.Info?.Print(LogClass.Gpu, "DLSS: nvngx_dlss.dll not found (bring your own - drop one in the 'dlss' folder or install a DLSS game).");
}
string streamlineFolder = Dlss.DlssBinaries.LocateStreamlineFolder(dlssFolder, dlssFolder);
if (streamlineFolder == null)
{
Logger.Info?.Print(LogClass.Gpu, "DLSS: sl.interposer.dll (Streamline) not found; DLSS disabled.");
return;
}
if (Dlss.Streamline.Initialize(streamlineFolder))
{
// 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,
0);
Dlss.Streamline.IsDlssSupported((IntPtr)_physicalDevice.PhysicalDevice.Handle);
}
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Gpu, $"DLSS: support probe failed: {ex.Message}");
}
}
public void Initialize(GraphicsDebugLevel logLevel)
{
SetupContext(logLevel);
PrintGpuInformation();
ProbeDlssSupport();
}
internal bool NeedsVertexBufferAlignment(int attrScalarAlignment, out int alignment)
@@ -1038,6 +1107,12 @@ namespace Ryujinx.Graphics.Vulkan
BackgroundResources.Dispose();
_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();
_pipeline.Dispose();
BufferManager.Dispose();
+55 -4
View File
@@ -37,6 +37,7 @@ namespace Ryujinx.Graphics.Vulkan
private bool _updateEffect;
private IPostProcessingEffect _effect;
private IScalingFilter _scalingFilter;
private Dlss.DlssUpscaler _dlss;
private bool _isLinear;
private float _scalingFilterLevel;
private bool _updateScalingFilter;
@@ -339,7 +340,7 @@ namespace Ryujinx.Graphics.Vulkan
return new Extent2D(width, height);
}
public unsafe override void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback)
public unsafe override void Present(ITexture texture, ITexture depthTexture, ImageCrop crop, Action swapBuffersCallback)
{
_gd.PipelineInternal.AutoFlush.Present();
@@ -451,12 +452,50 @@ namespace Ryujinx.Graphics.Vulkan
int dstY0 = crop.FlipY ? dstPaddingY : _height - dstPaddingY;
int dstY1 = crop.FlipY ? _height - dstPaddingY : dstPaddingY;
bool dlssHandled = false;
if (Dlss.DlssIntegration.IsEnabled)
{
_dlss ??= new Dlss.DlssUpscaler(_gd, _device);
// 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,
cbs,
_swapchainImageViews[nextImage],
_width,
_height,
new Extents2D(dstX0, dstY1, dstX1, dstY0),
_format == VkFormat.R16G16B16A16Sfloat,
_hdrPaperWhite / 80f,
_hdrPeak / 80f,
_hdrCurve,
_hdrGamma,
_hdrBlend,
_hdrWhiten);
}
// 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)
{
if (_scalingFilter != null && _scalingFilter.IsResolutionSupported(view.Width, view.Height, _width, _height))
{
_scalingFilter.Run(
view,
cbs,
_swapchainImageViews[nextImage].GetImageViewForAttachment(),
spatialDst.GetImageViewForAttachment(),
_format,
_width,
_height,
@@ -477,7 +516,7 @@ namespace Ryujinx.Graphics.Vulkan
_gd,
cbs,
view,
_swapchainImageViews[nextImage],
spatialDst,
new Extents2D(srcX0, srcY0, srcX1, srcY1),
new Extents2D(dstX0, dstY1, dstX1, dstY0),
_isLinear,
@@ -490,6 +529,7 @@ namespace Ryujinx.Graphics.Vulkan
_hdrBlend,
_hdrWhiten);
}
}
Transition(
cbs.CommandBuffer,
@@ -566,12 +606,22 @@ namespace Ryujinx.Graphics.Vulkan
{
_updateEffect = false;
switch (_currentAntiAliasing)
// RYUJINX_TAA=1 forces the native temporal filter regardless of the UI selection (dev override).
AntiAliasing antiAliasing = Effects.TemporalFilter.IsEnabled ? AntiAliasing.Taa : _currentAntiAliasing;
switch (antiAliasing)
{
case AntiAliasing.Fxaa:
_effect?.Dispose();
_effect = new FxaaPostProcessingEffect(_gd, _device);
break;
case AntiAliasing.Taa:
if (_effect is not Effects.TemporalFilter)
{
_effect?.Dispose();
_effect = new Effects.TemporalFilter(_gd, _device);
}
break;
case AntiAliasing.None:
_effect?.Dispose();
_effect = null;
@@ -742,6 +792,7 @@ namespace Ryujinx.Graphics.Vulkan
_effect?.Dispose();
_scalingFilter?.Dispose();
_dlss?.Dispose();
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace Ryujinx.Graphics.Vulkan
public bool ScreenCaptureRequested { get; set; }
public abstract void Dispose();
public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback);
public abstract void Present(ITexture texture, ITexture depthTexture, ImageCrop crop, Action swapBuffersCallback);
public abstract void SetSize(int width, int height);
public abstract void ChangeVSyncMode(VSyncMode vSyncMode);
public abstract void SetAntiAliasing(AntiAliasing effect);
+4
View File
@@ -186,6 +186,10 @@ 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();
IconProvider.Current
+97
View File
@@ -0,0 +1,97 @@
// 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;
using System;
using System.Diagnostics;
namespace Ryujinx.Ava.Systems
{
/// <summary>
/// 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.
/// </summary>
internal static class DlssRestart
{
// The environment variables DlssUiSettings.ApplyAtStartup sets. When the user changes the mode in the
// UI we drop these from the child so it re-derives the new mode from dlss_mode.cfg at startup.
private static readonly string[] _dlssEnvVars =
{
"RYUJINX_DLSS",
"RYUJINX_DLSS_MODE",
"RYUJINX_DLSS_JITTER",
"RYUJINX_DLSS_JITTER_SCALE",
"RYUJINX_DLSS_JITTER_LOD_BIAS",
};
/// <summary>
/// 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).
/// </summary>
/// <param name="applyUiMode">
/// 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).
/// </param>
public static void RestartCold(bool applyUiMode)
{
string exe = Environment.ProcessPath;
if (string.IsNullOrEmpty(exe))
{
Logger.Warning?.Print(LogClass.Application,
"DLSS restart: could not resolve the executable path; restart skipped.");
return;
}
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);
}
if (applyUiMode)
{
foreach (string name in _dlssEnvVars)
{
startInfo.Environment.Remove(name);
}
}
try
{
Process.Start(startInfo);
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Application,
$"DLSS restart: failed to relaunch ({ex.Message}); staying open.");
return;
}
Logger.Info?.Print(LogClass.Application, "DLSS restart: relaunching cold (one slInit per process).");
Environment.Exit(0);
}
}
}
+101
View File
@@ -0,0 +1,101 @@
// 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;
using System;
using System.Globalization;
using System.IO;
namespace Ryujinx.Ava.Systems
{
/// <summary>
/// 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 no longer a user choice: it is auto-mapped per mode in the engine
/// (StreamlineDlss.SetOptions -- DLAA gets Preset K, the upscaling modes get Preset F).
///
/// File format: a "mode [preset]" line; only the mode (first token) is read now. Mode index: 0 = Off,
/// 1 = DLAA, 2 = Quality, 3 = Balanced, 4 = Performance. A change applies on the next application launch.
/// </summary>
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" };
private static string FilePath => Path.Combine(AppDataManager.BaseDirPath, "dlss_mode.cfg");
/// <summary>Reads the saved DLSS quality-mode index (0 = Off if absent or invalid).</summary>
public static int Load()
{
try
{
if (File.Exists(FilePath))
{
string[] parts = File.ReadAllText(FilePath).Trim()
.Split(new[] { ' ', ',', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 0 &&
int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int mode) &&
mode >= 0 && mode < _modeEnv.Length)
{
return mode;
}
}
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"Failed to read DLSS UI setting: {ex.Message}");
}
return 0;
}
/// <summary>Writes the chosen mode index. Takes effect on the next application launch.</summary>
public static void Save(int mode)
{
try
{
File.WriteAllText(FilePath, mode.ToString(CultureInfo.InvariantCulture));
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"Failed to write DLSS UI setting: {ex.Message}");
}
}
/// <summary>
/// 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 is chosen by the engine per mode, not here.
/// </summary>
public static void ApplyAtStartup()
{
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RYUJINX_DLSS")))
{
return;
}
int mode = Load();
if (mode <= 0 || mode >= _modeEnv.Length)
{
return;
}
Environment.SetEnvironmentVariable("RYUJINX_DLSS", "1");
Environment.SetEnvironmentVariable("RYUJINX_DLSS_MODE", _modeEnv[mode]);
Environment.SetEnvironmentVariable("RYUJINX_DLSS_JITTER", "1");
Environment.SetEnvironmentVariable("RYUJINX_DLSS_JITTER_SCALE", "1.0");
Environment.SetEnvironmentVariable("RYUJINX_DLSS_JITTER_LOD_BIAS", "-0.5");
Logger.Info?.Print(LogClass.Application,
$"DLSS UI profile applied: mode={_modeEnv[mode]} (clip-space jitter 1.0, LOD -0.5; preset auto-mapped per mode).");
}
}
}
@@ -1958,6 +1958,18 @@ 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);
return;
}
Dispatcher.UIThread.InvokeAsync(async () =>
{
ShowMenuAndStatusBar = true;
+68 -2
View File
@@ -485,6 +485,9 @@ 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
{
get => _scalingFilter;
@@ -493,8 +496,53 @@ namespace Ryujinx.Ava.UI.ViewModels
_scalingFilter = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsScalingFilterActive));
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
}
}
else
{
DlssMode = 0; // Off
}
}
}
/// <summary>True when "DLSS" is the selected scaling filter.</summary>
public bool IsDlssSelected => _scalingFilter == DlssScalingIndex;
/// <summary>The DLSS quality sub-menu is active only when DLSS is selected and no game is running.</summary>
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
{
get => _dlssMode;
set
{
_dlssMode = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DlssPresetDisplayIndex));
}
}
// Read-only indicator of the engine's auto-mapped render preset: DLAA -> Preset K (index 0), the
// upscaling modes (Quality/Balanced/Performance) -> Preset F (index 1). The user cannot change it;
// it just tracks the selected mode.
public int DlssPresetDisplayIndex => DlssMode == 1 ? 0 : 1;
// 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; }
@@ -873,8 +921,10 @@ namespace Ryujinx.Ava.UI.ViewModels
GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value;
ShaderDumpPath = config.Graphics.ShadersDumpPath;
AntiAliasingEffect = (int)config.Graphics.AntiAliasing.Value;
ScalingFilter = (int)config.Graphics.ScalingFilter.Value;
int dlssSaved = Ryujinx.Ava.Systems.DlssUiSettings.Load();
ScalingFilter = dlssSaved > 0 ? DlssScalingIndex : (int)config.Graphics.ScalingFilter.Value;
ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value;
DlssMode = dlssSaved;
// Audio
AudioBackend = (int)config.System.AudioBackend.Value;
@@ -1008,8 +1058,24 @@ 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;
config.Graphics.ScalingFilter.Value = (ScalingFilter)ScalingFilter;
// "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
}
else
{
config.Graphics.ScalingFilter.Value = (Ryujinx.Common.Configuration.ScalingFilter)ScalingFilter;
dlssMode = 0;
}
config.Graphics.ScalingFilterLevel.Value = ScalingFilterLevel;
DlssModeRestartPending = dlssMode != Ryujinx.Ava.Systems.DlssUiSettings.Load();
Ryujinx.Ava.Systems.DlssUiSettings.Save(dlssMode);
if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex)
{
@@ -290,6 +290,8 @@
Content="{ext:Locale SmaaHigh}" />
<ComboBoxItem
Content="{ext:Locale SmaaUltra}" />
<ComboBoxItem
Content="TAA" />
</ComboBox>
</StackPanel>
</StackPanel>
@@ -316,6 +318,8 @@
Content="{ext:Locale GraphicsScalingFilterArea}" />
<ComboBoxItem
Content="NIS" />
<ComboBoxItem
Content="DLSS" />
</ComboBox>
<controls:SliderScroll Value="{Binding ScalingFilterLevel}"
ToolTip.Tip="{ext:Locale GraphicsScalingFilterLevelTooltip}"
@@ -337,6 +341,35 @@
Text="{Binding ScalingFilterLevelText}"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="NVIDIA DLSS / DLAA (RTX). Reconstruction par IA. S'applique au prochain lancement de l'app. / AI reconstruction; applied on next app launch."
Text="NVIDIA DLSS / DLAA (experimental)"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
IsEnabled="{Binding IsDlssMenuEnabled}"
SelectedIndex="{Binding DlssMode}">
<ComboBoxItem Content="Off" />
<ComboBoxItem Content="DLAA" />
<ComboBoxItem Content="Quality" />
<ComboBoxItem Content="Balanced" />
<ComboBoxItem Content="Performance" />
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="Preset de modèle DLSS auto-associé au mode (lecture seule) : DLAA = Preset K (Transformer), Upscaling = Preset F (stabilité). / DLSS model preset, auto-mapped per mode (read-only)."
Text="DLSS Model Preset (auto)"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
IsEnabled="False"
SelectedIndex="{Binding DlssPresetDisplayIndex}">
<ComboBoxItem Content="Preset K (Transformer)" />
<ComboBoxItem Content="Preset F (Ultra-Stability)" />
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale AnisotropyTooltip}"
@@ -588,6 +588,14 @@ namespace Ryujinx.Ava.UI.Windows
GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression;
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();
}
private void VolumeStatus_CheckedChanged(object sender, RoutedEventArgs e)
@@ -1,7 +1,11 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Input;
@@ -53,6 +57,41 @@ 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;
PromptDlssRestart();
}
}
// 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.
private static void PromptDlssRestart()
{
_ = Dispatcher.UIThread.InvokeAsync(async () =>
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
"Mode DLSS modifié / DLSS mode changed",
"Le mode s'applique au redémarrage. Redémarrer Beast Roofer DEV maintenant ? / The mode applies on restart. Restart Beast Roofer DEV now?",
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
string.Empty);
if (result == UserResult.Yes)
{
Ryujinx.Ava.Systems.DlssRestart.RestartCold(applyUiMode: true);
}
}
});
}
private void Load()