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 @@ +