TAA Phase 1: fixed-weight temporal accumulation (ping-pong history)
Blends each frame with the previous accumulated frame (result = mix(current, history, blend); default 0.90, RYUJINX_TAA_BLEND override) using two ping-pong rgba16f history targets. No motion vectors yet, so history is read at the same pixel: static/slow content converges to a steady image, fast motion ghosts (by design; Phase 2 reprojects). First frame seeds history (no flash). Validated in game: static smoothing clean, expected ghosting on motion, no errors.
This commit is contained in:
@@ -1,19 +1,29 @@
|
||||
// Temporal Anti-Aliasing (clean-room).
|
||||
//
|
||||
// Phase 0: pure pass-through copy (current -> output). This mirrors the FXAA effect's exact
|
||||
// read/store path (a no-op FXAA is already an identity pass), so the result is visually identical
|
||||
// to the input -- it only proves the slot and resources. Later phases reproject the ping-pong
|
||||
// history with our motion vectors, neighborhood-clamp it and accumulate it exponentially.
|
||||
// Phase 1: fixed-weight temporal accumulation with ping-pong history. No motion vectors yet, so the
|
||||
// history is read at the SAME pixel -- static and slow-moving content converges to a rock-steady image
|
||||
// (cascades/scenery freeze like marble), while fast camera motion ghosts. That ghosting is expected and
|
||||
// goes away in Phase 2, which reprojects the history with our reconstructed motion vectors.
|
||||
//
|
||||
// result = mix(current, history, blend) // blend = history weight (e.g. 0.90)
|
||||
//
|
||||
// The result is written both to the present output (input format) and to the write-side history target
|
||||
// (rgba16f), which becomes next frame's read-side history.
|
||||
|
||||
#version 430 core
|
||||
|
||||
layout (local_size_x = 16, local_size_y = 16) in;
|
||||
|
||||
layout (rgba8, binding = 0, set = 3) uniform image2D imgOutput;
|
||||
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;
|
||||
layout (binding = 2) uniform dimensions {
|
||||
layout (binding = 3, set = 2) uniform sampler2D HistoryRead;
|
||||
|
||||
layout (binding = 2) uniform params {
|
||||
float width;
|
||||
float height;
|
||||
float blend; // history weight in [0,1]; current weight is (1 - blend)
|
||||
float hasHistory; // >0.5 once the read-side history holds a valid previous frame
|
||||
};
|
||||
|
||||
void main()
|
||||
@@ -25,6 +35,13 @@ void main()
|
||||
return;
|
||||
}
|
||||
|
||||
// Integer fetch, no filtering: the stored value is exactly the source texel.
|
||||
imageStore(imgOutput, loc, texelFetch(Source, loc, 0));
|
||||
vec4 current = texelFetch(Source, loc, 0);
|
||||
vec4 history = texelFetch(HistoryRead, loc, 0);
|
||||
|
||||
// First frame after (re)allocation has no valid history: pass current through and seed the history.
|
||||
float w = hasHistory > 0.5 ? blend : 0.0;
|
||||
vec4 result = mix(current, history, w);
|
||||
|
||||
imageStore(imgOutput, loc, result);
|
||||
imageStore(imgHistoryWrite, loc, result);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -5,6 +5,7 @@ using Ryujinx.Graphics.Shader;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
using Silk.NET.Vulkan;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Format = Ryujinx.Graphics.GAL.Format;
|
||||
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
|
||||
|
||||
@@ -13,13 +14,14 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
/// <summary>
|
||||
/// Clean-room native Temporal Anti-Aliasing.
|
||||
///
|
||||
/// Phase 0 is pure scaffolding: it allocates the ping-pong history targets (rgba16f, for the precise
|
||||
/// accumulation later phases need) and runs a pass-through copy whose output is visually identical to
|
||||
/// the input -- so the present slot and the resources are proven without changing a single pixel.
|
||||
/// Later phases reproject the history with our motion vectors, neighborhood-clamp it and accumulate it.
|
||||
/// Phase 1: fixed-weight temporal accumulation. Each frame blends the current frame with the previous
|
||||
/// accumulated frame (<c>result = mix(current, history, blend)</c>) using two ping-pong rgba16f history
|
||||
/// targets (one read, one written, swapped every frame). With no motion vectors yet the history is read
|
||||
/// at the same pixel, so static/slow content converges to a rock-steady image while fast motion ghosts
|
||||
/// (resolved in Phase 2 by reprojecting the history with our reconstructed motion vectors).
|
||||
///
|
||||
/// Gated entirely on RYUJINX_TAA=1: when it is unset the present path never constructs or runs this,
|
||||
/// so the default render is byte-identical to before.
|
||||
/// Gated entirely on RYUJINX_TAA=1: when unset the present path never constructs or runs this, so the
|
||||
/// default render is byte-identical to before. RYUJINX_TAA_BLEND overrides the history weight (default 0.90).
|
||||
/// </summary>
|
||||
internal class TemporalFilter : IPostProcessingEffect
|
||||
{
|
||||
@@ -27,6 +29,9 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
public static readonly bool IsEnabled =
|
||||
Environment.GetEnvironmentVariable("RYUJINX_TAA") is "1" or "true" or "TRUE" or "True";
|
||||
|
||||
// History weight (fraction of the accumulated history kept each frame). Tunable for bring-up.
|
||||
private static readonly float HistoryBlend = ParseBlend();
|
||||
|
||||
private readonly VulkanRenderer _renderer;
|
||||
private readonly PipelineHelperShader _pipeline;
|
||||
private ISampler _sampler;
|
||||
@@ -35,9 +40,11 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
// Output keeps the input format so the downstream blit/scaling filter sees an identical texture.
|
||||
private TextureView _output;
|
||||
|
||||
// Ping-pong history, rgba16f. Allocated now to prove the resources; consumed from Phase 1 onward.
|
||||
// Ping-pong history, rgba16f. Each frame one is read (previous result) and one is written (new result).
|
||||
private TextureView _history0;
|
||||
private TextureView _history1;
|
||||
private bool _readFromHistory0;
|
||||
private bool _hasHistory;
|
||||
|
||||
private bool _activeLogged;
|
||||
|
||||
@@ -49,6 +56,19 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private static float ParseBlend()
|
||||
{
|
||||
string value = Environment.GetEnvironmentVariable("RYUJINX_TAA_BLEND");
|
||||
|
||||
if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float blend) &&
|
||||
blend >= 0f && blend <= 0.99f)
|
||||
{
|
||||
return blend;
|
||||
}
|
||||
|
||||
return 0.90f;
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
_pipeline.Initialize();
|
||||
@@ -57,8 +77,10 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
|
||||
ResourceLayout layout = new ResourceLayoutBuilder()
|
||||
.Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2)
|
||||
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1)
|
||||
.Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build();
|
||||
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) // current color
|
||||
.Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 3) // history (read)
|
||||
.Add(ResourceStages.Compute, ResourceType.Image, 0, true) // present output
|
||||
.Add(ResourceStages.Compute, ResourceType.Image, 1, true).Build(); // history (write)
|
||||
|
||||
_sampler = _renderer.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear));
|
||||
|
||||
@@ -101,6 +123,10 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
_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;
|
||||
|
||||
// Fresh history: the first frame has nothing to blend against.
|
||||
_readFromHistory0 = true;
|
||||
_hasHistory = false;
|
||||
}
|
||||
|
||||
public TextureView Run(TextureView view, CommandBufferScoped cbs, int width, int height)
|
||||
@@ -110,20 +136,26 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
if (!_activeLogged)
|
||||
{
|
||||
_activeLogged = true;
|
||||
Logger.Info?.Print(LogClass.Gpu, "TAA: active");
|
||||
Logger.Info?.Print(LogClass.Gpu, $"TAA: active (accumulation, history blend {HistoryBlend:0.00}).");
|
||||
}
|
||||
|
||||
// Ping-pong: read last frame's result, write this frame's result to the other target.
|
||||
TextureView historyRead = _readFromHistory0 ? _history0 : _history1;
|
||||
TextureView historyWrite = _readFromHistory0 ? _history1 : _history0;
|
||||
|
||||
_pipeline.SetCommandBuffer(cbs);
|
||||
_pipeline.SetProgram(_program);
|
||||
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _sampler);
|
||||
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, historyRead, _sampler);
|
||||
|
||||
ReadOnlySpan<float> dimensionsBuffer = [view.Width, view.Height];
|
||||
int rangeSize = dimensionsBuffer.Length * sizeof(float);
|
||||
ReadOnlySpan<float> paramsBuffer = [view.Width, view.Height, HistoryBlend, _hasHistory ? 1f : 0f];
|
||||
int rangeSize = paramsBuffer.Length * sizeof(float);
|
||||
using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize);
|
||||
buffer.Holder.SetDataUnchecked(buffer.Offset, dimensionsBuffer);
|
||||
buffer.Holder.SetDataUnchecked(buffer.Offset, paramsBuffer);
|
||||
|
||||
_pipeline.SetUniformBuffers([new BufferAssignment(2, buffer.Range)]);
|
||||
_pipeline.SetImage(ShaderStage.Compute, 0, _output.GetView(FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)));
|
||||
_pipeline.SetImage(1, historyWrite.GetImageView());
|
||||
|
||||
int dispatchX = BitUtils.DivRoundUp(view.Width, 16);
|
||||
int dispatchY = BitUtils.DivRoundUp(view.Height, 16);
|
||||
@@ -132,6 +164,10 @@ namespace Ryujinx.Graphics.Vulkan.Effects
|
||||
|
||||
_pipeline.Finish();
|
||||
|
||||
// Next frame reads the target we just wrote.
|
||||
_readFromHistory0 = !_readFromHistory0;
|
||||
_hasHistory = true;
|
||||
|
||||
return _output;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user