From df3240c17f5ab34993c3dd4b8465d4ee4435a113 Mon Sep 17 00:00:00 2001 From: The Roofer Dev Date: Sun, 28 Jun 2026 20:20:17 -0400 Subject: [PATCH] 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. --- .../Effects/Shaders/TaaDownsample.glsl | 31 ++++ .../Effects/Shaders/TaaDownsample.spv | Bin 0 -> 1880 bytes .../Effects/Shaders/Temporal.glsl | 114 ++++++++++-- .../Effects/Shaders/Temporal.spv | Bin 3912 -> 9272 bytes .../Effects/TemporalFilter.cs | 173 ++++++++++++------ .../Ryujinx.Graphics.Vulkan.csproj | 1 + 6 files changed, 239 insertions(+), 80 deletions(-) create mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.glsl create mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/TaaDownsample.spv 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 0000000000000000000000000000000000000000..979a458b89b3ba1c8281463482af60ec611b4625 GIT binary patch literal 1880 zcmZvcTTfF_5QR78E(#(y5yb*3DoDHlq6id4OBKb02YoZO5NJ}UftDvn^V&> zH?8OljY*+3U7YBaa^OBTsyf+ zwM4Eh(cG@Q$ycgxi6M&7W=NaU)((itk#mmP@!6{y((|kcJw1} z-|(wj&)4{fYfAQ~p1fYi8zbWGdVl}&f?_nL*x8Qnm!r}hjQ|p}gzp=KZSzns1{I$oXc}_Ky33 zyfxnO1>igI?yd1Whk^WFI_F}_ePNDoF$>g@|3r=42*`qO^f88Y{3Ito3gkQo{n+1S zeEaj0.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 f1eebbd8e831c82b3057557d3f3c66028d0726a1..42a84a8ce26f41d203c12712ac556c699e1762aa 100644 GIT binary patch literal 9272 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 QeJ0o%om0E#E$4av7cx*;y8r+H literal 3912 zcmZ{lX>(Ln5QcAp2_T#7B8mZ2R4|1Ih$0|@Bq&QHDkv%plOY+M%!FA8C?dEZDxxCp zKR`d}cgyl$SuRyp`8@aD)>|n&Rqu57+kN`<>2uDVY2B-*Cp}%stYk*=ZIX@oNq5qP zIxFc-<-oq-ef_QWNdLx7>y4P3OiKfenU~B?dXP7P@lv&}V-C0tYzBKk6I=z?!7b!8 zc7CdBC+Ql~$k~nlBT0LRDCXP3>8g6u&kX;gUnKQtj@&0 zoM#|sFC)%5ax)5?XCODbz&S_GI}~&6z1$*TP4;~yFlX4S&>hQBD|tbIS9>Fy8!f<}}_On@fIcZhN+oe=fT5=3m6`0_55}??QBkc|3!t zWeM4rrhJmDpd#;s_Ko0N_iKLRtU=qelyfcjw;VW(_s;1vU(|9Jx;01qn%s8Je#ngV z4mtlpq_xT~<7@K}Qor+yBkbkuXCn^doWCg#lCxfI@0P>$wx%+x(fG%(T~9vh+m7x% z({}BhfUzU;dG{RQ-=CSuI%b*Y0FV=r^Evd`V;S8wJuhoInd{M0*Q|jgxv(R0j^~_w z)Zra>xDP%(X-?s>W?}5*bxxI~g-Qzqk2OK1e@7Vpz2}c(= zN33hi2l}_ObN8velimBS7X!J4z}&vaB|zR7-{T#?H@h4d(Fra@(2*L9yn&*v3T7Poc{h7d3l7{iZz)^b1E9a6h|{`aA>c zvIc7j-+?JUW311d2Z8gA^?hpVv({&TwKnqDLpe7@Pn zH)aipnB(a3vCk5^^Nxedd3`6)T~D9297W1SEfsXRUx`1790hWo#dq}6w=o9fy-%+q z9o{GTDvvDQ;+rEMtA*-Al82eUEX@nBem_<1*G%M6F&D<^cfR*-a~i3 zv6Dz`ee03$gAE1yGP=EqxgVfAXDILIhv;%e%;ulfTpxih&=1UOF5_MI5>mf;uOL4G wS95zh@>4J~x6Sn#a88+cWA)29{~GcOa6Px3|0O8S{|Y?T=Z}t}O50n%t6aWAK 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 @@ +