diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl
new file mode 100644
index 000000000..4713174c5
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl
@@ -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));
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.spv b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.spv
new file mode 100644
index 000000000..979a458b8
Binary files /dev/null and b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.spv differ
diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl
index 39aac9860..6c1d43864 100644
--- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl
+++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.glsl
@@ -1,16 +1,18 @@
// Temporal Anti-Aliasing (clean-room).
//
-// Phase 2: motion-vector reprojection. The history is no longer read at the fixed pixel but at the
-// position the pixel's content occupied last frame, following our reconstructed motion field:
+// 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.
//
-// prevPos = loc + motionSign * mv // motionSign = +1 (calibrated): follow the field to the history
-// history = bilinear sample of HistoryRead at prevPos
-// result = mix(current, history, blend)
+// 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)
//
-// This makes the accumulated history track moving geometry, so the massive ghosting from Phase 1
-// collapses while the camera turns. Pixels whose previous position falls off-screen (disocclusion)
-// fall back to the current frame so freshly revealed areas do not smear. Neighborhood clamping for
-// the remaining ghosting/fireflies comes in Phase 3.
+// prevPos = loc + motionSign * mv // motionSign = +1 (calibrated)
#version 430 core
@@ -19,8 +21,8 @@ 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 accumulated result
-layout (binding = 5, set = 2) uniform sampler2D Motion; // rg16f motion field, render-res pixels
+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;
@@ -29,31 +31,103 @@ layout (binding = 2) uniform params {
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);
- if (loc.x >= int(width) || loc.y >= int(height))
+ 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);
- // This pixel's motion (in render-resolution pixels); reproject the history read by it.
- vec2 mv = hasMotion > 0.5 ? texelFetch(Motion, loc, 0).xy : vec2(0.0);
+ // 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)));
- vec4 history = texture(HistoryRead, uv); // bilinear: prevPos is sub-pixel
+ bool valid = hasHistory > 0.5 && onScreen;
- // No history yet, or reprojected off-screen (disocclusion): take the current frame, no ghost.
- float w = (hasHistory > 0.5 && onScreen) ? blend : 0.0;
- vec4 result = mix(current, history, w);
+ vec4 histSample = texture(HistoryRead, uv);
+ float prevVSmooth = valid ? histSample.a : 0.0;
- imageStore(imgOutput, loc, result);
- imageStore(imgHistoryWrite, loc, result);
+ // 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));
}
diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.spv b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.spv
index f1eebbd8e..42a84a8ce 100644
Binary files a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.spv and b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/Temporal.spv differ
diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs
index 6aa291853..9260b3dff 100644
--- a/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Effects/TemporalFilter.cs
@@ -14,16 +14,16 @@ namespace Ryujinx.Graphics.Vulkan.Effects
///
/// Clean-room native Temporal Anti-Aliasing.
///
- /// Phase 2: fixed-weight temporal accumulation with motion-vector reprojection. Each frame it
- /// reconstructs a motion field from the current and previous color (the same Lucas-Kanade pass + 3x3
- /// median filter used by the DLSS path -- self-contained, so TAA works with DLSS off), then blends the
- /// current frame with the history sampled at the reprojected position (prevPos = loc - mv). Static
- /// content stays rock-steady and the Phase 1 ghosting collapses while the camera moves. Neighborhood
- /// clamping for the remaining ghosting/fireflies is Phase 3.
+ /// 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.
///
/// Gated entirely on RYUJINX_TAA=1: when unset the present path never constructs or runs this, so the
- /// default render is byte-identical. RYUJINX_TAA_BLEND overrides the history weight (default 0.90);
- /// RYUJINX_TAA_MV_SIGN flips the reprojection direction (default +1) for sign calibration.
+ /// default render is byte-identical. Tunables (no rebuild): RYUJINX_TAA_BLEND (history weight, default
+ /// 0.90), RYUJINX_TAA_MV_SIGN (reprojection direction, default +1), RYUJINX_TAA_CLAMP (variance-box
+ /// half-width in std-devs, default 1.0), RYUJINX_TAA_MV_DOWNSCALE (flow resolution divisor 1/2/4, default 2).
///
internal class TemporalFilter : IPostProcessingEffect
{
@@ -31,17 +31,20 @@ namespace Ryujinx.Graphics.Vulkan.Effects
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 render-resolution pixels
+ 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; // motion estimate + median filter passes
+ 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;
@@ -55,11 +58,17 @@ namespace Ryujinx.Graphics.Vulkan.Effects
private bool _readFromHistory0;
private bool _hasHistory;
- // Optical-flow inputs/outputs: previous color (input format), raw and median-filtered motion (rg16f).
- private TextureView _prevColor;
+ // 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;
@@ -92,6 +101,33 @@ namespace Ryujinx.Graphics.Vulkan.Effects
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.0f;
+ }
+
+ 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();
@@ -114,6 +150,17 @@ namespace Ryujinx.Graphics.Vulkan.Effects
new ShaderSource(blendShader, ShaderStage.Compute, TargetLanguage.Spirv)
], blendLayout);
+ // Colour downsample to the flow working resolution: source (b1), params (b2), output image (b0/set3).
+ byte[] downsampleShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.spv");
+ ResourceLayout downsampleLayout = new ResourceLayoutBuilder()
+ .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2)
+ .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");
@@ -173,20 +220,27 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_output?.Dispose();
_history0?.Dispose();
_history1?.Dispose();
- _prevColor?.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;
- _prevColor = _renderer.CreateTexture(MakeInfo(view.Info, view.Width, view.Height, view.Info.Format, view.Info.BytesPerPixel)) as TextureView;
- _motion = _renderer.CreateTexture(MakeInfo(view.Info, view.Width, view.Height, Format.R16G16Float, 4)) as TextureView;
- _motionFiltered = _renderer.CreateTexture(MakeInfo(view.Info, view.Width, view.Height, Format.R16G16Float, 4)) 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;
}
@@ -198,14 +252,20 @@ namespace Ryujinx.Graphics.Vulkan.Effects
{
_activeLogged = true;
Logger.Info?.Print(LogClass.Gpu,
- $"TAA: active (reprojection, history blend {HistoryBlend:0.00}, mv sign {MotionSign:+0;-0}).");
+ $"TAA: active (YCoCg clamp, blend {HistoryBlend:0.00}, mv sign {MotionSign:+0;-0}, clamp {ClampGamma:0.00}, " +
+ $"flow 1/{MotionDownscale} = {_motionWidth}x{_motionHeight}).");
}
- // Reconstruct this frame's motion field from (current, previous), then median-filter it.
+ // 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(view, cbs);
- RunMotionFilterPass(view, cbs);
+ RunMotionPass(curHalf, prevHalf, cbs);
+ RunMotionFilterPass(cbs);
}
// Ping-pong: read last frame's result, write this frame's result to the other target.
@@ -226,6 +286,8 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_hasHistory ? 1f : 0f,
MotionSign,
_hasPrev ? 1f : 0f,
+ ClampGamma,
+ _mvScale,
];
int rangeSize = paramsBuffer.Length * sizeof(float);
using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize);
@@ -242,27 +304,43 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_pipeline.Finish();
- // Keep this frame's color as the "previous" frame for next time's motion estimate.
- CopyToPrev(view, cbs);
-
- // Next frame reads the target we just wrote and has both a history and a previous frame.
+ // 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 RunMotionPass(TextureView input, CommandBufferScoped cbs)
+ private void RunDownsamplePass(TextureView source, TextureView dst, CommandBufferScoped cbs)
+ {
+ _motionPipeline.SetCommandBuffer(cbs);
+ _motionPipeline.SetProgram(_downsampleProgram);
+ _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 1, source, _sampler);
+
+ ReadOnlySpan 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, input, _sampler);
- _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _prevColor, _sampler);
+ _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 1, current, _sampler);
+ _motionPipeline.SetTextureAndSampler(ShaderStage.Compute, 3, previous, _sampler);
// width/height/maxMotion/metrics + dejitterX/Y + 2 pad. No clip-space jitter on the TAA input
// (that is a DLSS-only feature), so de-jitter is zero; metrics off.
- ReadOnlySpan p = [input.Width, input.Height, MaxMotion, 0f, 0f, 0f, 0f, 0f];
+ ReadOnlySpan p = [_motionWidth, _motionHeight, MaxMotion, 0f, 0f, 0f, 0f, 0f];
using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, p.Length * sizeof(float));
buffer.Holder.SetDataUnchecked(buffer.Offset, p);
@@ -270,17 +348,17 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_motionPipeline.SetStorageBuffers([new BufferAssignment(0, new BufferRange(_sceneChangeBuffer, 0, CounterCount * sizeof(uint), true))]);
_motionPipeline.SetImage(0, _motion.GetImageView());
- _motionPipeline.DispatchCompute(BitUtils.DivRoundUp(input.Width, 16), BitUtils.DivRoundUp(input.Height, 16), 1);
+ _motionPipeline.DispatchCompute(BitUtils.DivRoundUp(_motionWidth, 16), BitUtils.DivRoundUp(_motionHeight, 16), 1);
_motionPipeline.ComputeBarrier();
_motionPipeline.Finish();
}
- private void RunMotionFilterPass(TextureView input, CommandBufferScoped cbs)
+ private void RunMotionFilterPass(CommandBufferScoped cbs)
{
_motionPipeline.SetCommandBuffer(cbs);
_motionPipeline.SetProgram(_motionFilterProgram);
- ReadOnlySpan p = [input.Width, input.Height, MaxMotion, 0f, 0f, 0f, 0f, 0f];
+ ReadOnlySpan p = [_motionWidth, _motionHeight, MaxMotion, 0f, 0f, 0f, 0f, 0f];
using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, p.Length * sizeof(float));
buffer.Holder.SetDataUnchecked(buffer.Offset, p);
@@ -289,50 +367,25 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_motionPipeline.SetImage(0, _motionFiltered.GetImageView());
_motionPipeline.SetImage(1, _motion.GetImageView());
- _motionPipeline.DispatchCompute(BitUtils.DivRoundUp(input.Width, 16), BitUtils.DivRoundUp(input.Height, 16), 1);
+ _motionPipeline.DispatchCompute(BitUtils.DivRoundUp(_motionWidth, 16), BitUtils.DivRoundUp(_motionHeight, 16), 1);
_motionPipeline.ComputeBarrier();
_motionPipeline.Finish();
}
- 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),
- };
-
- _renderer.Api.CmdCopyImage(
- cbs.CommandBuffer,
- input.GetImage().Get(cbs).Value,
- ImageLayout.General,
- _prevColor.GetImage().Get(cbs).Value,
- ImageLayout.General,
- 1,
- in region);
- }
-
public void Dispose()
{
_pipeline.Dispose();
_motionPipeline.Dispose();
_program.Dispose();
+ _downsampleProgram.Dispose();
_motionProgram.Dispose();
_motionFilterProgram.Dispose();
_sampler.Dispose();
_output?.Dispose();
_history0?.Dispose();
_history1?.Dispose();
- _prevColor?.Dispose();
+ _colorHalf0?.Dispose();
+ _colorHalf1?.Dispose();
_motion?.Dispose();
_motionFiltered?.Dispose();
_renderer.BufferManager.Delete(_sceneChangeBuffer);
diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
index 2b5fe917d..964d8fd68 100644
--- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
+++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
@@ -25,6 +25,7 @@
+