TAA Phase 3+4 + velocity-drop hysteresis

Phase 3 -- YCoCg neighbourhood variance clamp: the reprojected history is forced
into the colour box (mean +/- gamma*sigma) of the current frame's 3x3
neighbourhood in YCoCg, crushing the doubling/smearing the reconstructed motion
field leaves while preserving the marble-steady smoothing at rest.
RYUJINX_TAA_CLAMP tunes the box half-width (default 1.0).

Phase 4 -- reduced-resolution optical flow: the colour is bilinear-downsampled to
1/N (default half) and Lucas-Kanade + the 3x3 median run there (~N^2 less work,
the dominant cost), then the field is bilinear-upscaled and scaled by N in the
blend. The per-frame full-res previous-color copy is gone. RYUJINX_TAA_MV_DOWNSCALE
sets the divisor (1/2/4, default 2). New TaaDownsample shader. Validated: 60 FPS
at x3 with the Phase 3 quality preserved.

Velocity-drop hysteresis -- removes the single-frame flash on a hard stop: a
per-pixel smoothed velocity (carried in the history alpha) rises instantly with
motion but decays over several frames, and both the clamp width and blend weight
are continuous functions of it, so the static 'marble' reasserts gradually
instead of snapping. Decay 0.85 for large distant surfaces (waterfalls).

All gated on RYUJINX_TAA=1; default render path unchanged when off.
This commit is contained in:
2026-06-28 20:20:17 -04:00
parent ae19925e8e
commit df3240c17f
6 changed files with 239 additions and 80 deletions
@@ -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));
}
@@ -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));
}
@@ -14,16 +14,16 @@ namespace Ryujinx.Graphics.Vulkan.Effects
/// <summary>
/// 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 (<c>prevPos = loc - mv</c>). 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).
/// </summary>
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<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, 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<float> p = [input.Width, input.Height, MaxMotion, 0f, 0f, 0f, 0f, 0f];
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);
@@ -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<float> p = [input.Width, input.Height, MaxMotion, 0f, 0f, 0f, 0f, 0f];
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);
@@ -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);
@@ -25,6 +25,7 @@
<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" />