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:
@@ -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**.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<->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, ©);
|
||||
}
|
||||
|
||||
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 -> 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, ®ion, Filter.Nearest);
|
||||
}
|
||||
|
||||
// 3c-i: parse the readback (R16G16_SFLOAT) and log the RMS / outlier rate of the hardware flow so
|
||||
// it can be compared to the Lucas-Kanade metrics line. Throttled to ~every 120 frames.
|
||||
private void MeasureFlow()
|
||||
{
|
||||
if (_readbackPtr == null)
|
||||
{
|
||||
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;
|
||||
|
||||
Binary file not shown.
@@ -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));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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));
|
||||
}
|
||||
Binary file not shown.
@@ -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));
|
||||
}
|
||||
Binary file not shown.
@@ -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));
|
||||
}
|
||||
Binary file not shown.
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,37 +452,22 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
int dstY0 = crop.FlipY ? dstPaddingY : _height - dstPaddingY;
|
||||
int dstY1 = crop.FlipY ? _height - dstPaddingY : dstPaddingY;
|
||||
|
||||
if (_scalingFilter != null && _scalingFilter.IsResolutionSupported(view.Width, view.Height, _width, _height))
|
||||
bool dlssHandled = false;
|
||||
|
||||
if (Dlss.DlssIntegration.IsEnabled)
|
||||
{
|
||||
_scalingFilter.Run(
|
||||
_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].GetImageViewForAttachment(),
|
||||
_format,
|
||||
_swapchainImageViews[nextImage],
|
||||
_width,
|
||||
_height,
|
||||
new Extents2D(srcX0, srcY0, srcX1, srcY1),
|
||||
new Extents2D(dstX0, dstY0, dstX1, dstY1),
|
||||
_format == VkFormat.R16G16B16A16Sfloat,
|
||||
_hdrPaperWhite / 80f,
|
||||
_hdrPeak / 80f,
|
||||
_hdrCurve,
|
||||
_hdrGamma,
|
||||
_hdrBlend,
|
||||
_hdrWhiten
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gd.HelperShader.BlitColor(
|
||||
_gd,
|
||||
cbs,
|
||||
view,
|
||||
_swapchainImageViews[nextImage],
|
||||
new Extents2D(srcX0, srcY0, srcX1, srcY1),
|
||||
new Extents2D(dstX0, dstY1, dstX1, dstY0),
|
||||
_isLinear,
|
||||
true,
|
||||
_format == VkFormat.R16G16B16A16Sfloat,
|
||||
_hdrPaperWhite / 80f,
|
||||
_hdrPeak / 80f,
|
||||
@@ -491,6 +477,60 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
_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,
|
||||
spatialDst.GetImageViewForAttachment(),
|
||||
_format,
|
||||
_width,
|
||||
_height,
|
||||
new Extents2D(srcX0, srcY0, srcX1, srcY1),
|
||||
new Extents2D(dstX0, dstY0, dstX1, dstY1),
|
||||
_format == VkFormat.R16G16B16A16Sfloat,
|
||||
_hdrPaperWhite / 80f,
|
||||
_hdrPeak / 80f,
|
||||
_hdrCurve,
|
||||
_hdrGamma,
|
||||
_hdrBlend,
|
||||
_hdrWhiten
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gd.HelperShader.BlitColor(
|
||||
_gd,
|
||||
cbs,
|
||||
view,
|
||||
spatialDst,
|
||||
new Extents2D(srcX0, srcY0, srcX1, srcY1),
|
||||
new Extents2D(dstX0, dstY1, dstX1, dstY0),
|
||||
_isLinear,
|
||||
true,
|
||||
_format == VkFormat.R16G16B16A16Sfloat,
|
||||
_hdrPaperWhite / 80f,
|
||||
_hdrPeak / 80f,
|
||||
_hdrCurve,
|
||||
_hdrGamma,
|
||||
_hdrBlend,
|
||||
_hdrWhiten);
|
||||
}
|
||||
}
|
||||
|
||||
Transition(
|
||||
cbs.CommandBuffer,
|
||||
swapchainImage,
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,9 +496,54 @@ 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; }
|
||||
|
||||
public float Volume
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user