diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index b41369260..f8216d08a 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -215,12 +215,14 @@
- @string/anti_aliasing_none
- @string/anti_aliasing_fxaa
- @string/anti_aliasing_smaa
+ - @string/anti_aliasing_taa
- 0
- 1
- 2
+ - 3
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 13a49eb1b..518184aa2 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -641,6 +641,7 @@
None
FXAA
SMAA
+ TAA
Auto
diff --git a/src/citron/configuration/shared_translation.h b/src/citron/configuration/shared_translation.h
index 6664e209a..16487dab0 100644
--- a/src/citron/configuration/shared_translation.h
+++ b/src/citron/configuration/shared_translation.h
@@ -28,6 +28,7 @@ static const std::map anti_aliasing_texts_map =
{Settings::AntiAliasing::None, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "None"))},
{Settings::AntiAliasing::Fxaa, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FXAA"))},
{Settings::AntiAliasing::Smaa, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "SMAA"))},
+ {Settings::AntiAliasing::Taa, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "TAA"))},
};
static const std::map scaling_filter_texts_map = {
diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h
index 623b6913b..a655fe07d 100644
--- a/src/common/settings_enums.h
+++ b/src/common/settings_enums.h
@@ -149,7 +149,7 @@ ENUM(ResolutionSetup, Res1_2X, Res3_4X, Res1X, Res3_2X, Res2X, Res3X, Res4X, Res
ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, ScaleForce, Fsr, Fsr2, MaxEnum);
-ENUM(AntiAliasing, None, Fxaa, Smaa, MaxEnum);
+ENUM(AntiAliasing, None, Fxaa, Smaa, Taa, MaxEnum);
ENUM(FSR2QualityMode, Quality, Balanced, Performance, UltraPerformance);
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index d6e48960c..daeae9f2f 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -134,6 +134,8 @@ add_library(video_core STATIC
renderer_opengl/present/present_uniforms.h
renderer_opengl/present/smaa.cpp
renderer_opengl/present/smaa.h
+ renderer_opengl/present/taa.cpp
+ renderer_opengl/present/taa.h
renderer_opengl/present/util.h
renderer_opengl/present/window_adapt_pass.cpp
renderer_opengl/present/window_adapt_pass.h
@@ -191,6 +193,8 @@ add_library(video_core STATIC
renderer_vulkan/present/fxaa.cpp
renderer_vulkan/present/fxaa.h
renderer_vulkan/present/layer.cpp
+ renderer_vulkan/present/taa.cpp
+ renderer_vulkan/present/taa.h
renderer_vulkan/present/layer.h
renderer_vulkan/present/present_push_constants.h
renderer_vulkan/present/smaa.cpp
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index 93bed95ba..44f8b7f13 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -52,6 +52,8 @@ set(SHADER_FILES
smaa_blending_weight_calculation.frag
smaa_neighborhood_blending.vert
smaa_neighborhood_blending.frag
+ taa.frag
+ taa.vert
vulkan_blit_depth_stencil.frag
vulkan_color_clear.frag
vulkan_color_clear.vert
@@ -67,6 +69,8 @@ set(SHADER_FILES
vulkan_present_scaleforce_fp16.frag
vulkan_present_scaleforce_fp32.frag
vulkan_quad_indexed.comp
+ vulkan_taa.frag
+ vulkan_taa.vert
vulkan_turbo_mode.comp
vulkan_uint8.comp
)
diff --git a/src/video_core/host_shaders/taa.frag b/src/video_core/host_shaders/taa.frag
new file mode 100644
index 000000000..536276a4f
--- /dev/null
+++ b/src/video_core/host_shaders/taa.frag
@@ -0,0 +1,163 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#version 460
+
+#ifdef VULKAN
+
+#define BINDING_COLOR_TEXTURE 1
+#define BINDING_PREVIOUS_TEXTURE 2
+#define BINDING_MOTION_TEXTURE 3
+#define BINDING_DEPTH_TEXTURE 4
+
+#else // ^^^ Vulkan ^^^ // vvv OpenGL vvv
+
+#define BINDING_COLOR_TEXTURE 0
+#define BINDING_PREVIOUS_TEXTURE 1
+#define BINDING_MOTION_TEXTURE 2
+#define BINDING_DEPTH_TEXTURE 3
+
+#endif
+
+layout (location = 0) in vec4 posPos;
+
+layout (location = 0) out vec4 frag_color;
+
+// Textures
+layout (binding = BINDING_COLOR_TEXTURE) uniform sampler2D current_texture;
+layout (binding = BINDING_PREVIOUS_TEXTURE) uniform sampler2D previous_texture;
+layout (binding = BINDING_MOTION_TEXTURE) uniform sampler2D motion_texture;
+layout (binding = BINDING_DEPTH_TEXTURE) uniform sampler2D depth_texture;
+
+// TAA parameters
+layout (binding = 5) uniform TaaParams {
+ vec2 jitter_offset;
+ float frame_count;
+ float blend_factor;
+ vec2 inv_resolution;
+ float motion_scale;
+};
+
+// TAA configuration
+const float TAA_CLAMP_FACTOR = 0.9; // More aggressive clamping to reduce ghosting
+const float TAA_SHARPENING = 0.15; // Reduced sharpening to prevent artifacts
+const float TAA_REJECTION_SAMPLES = 8.0;
+
+// Halton sequence for jittering (2,3)
+const vec2 HALTON_SEQUENCE[8] = vec2[8](
+ vec2(0.0, 0.0),
+ vec2(0.5, 0.333333),
+ vec2(0.25, 0.666667),
+ vec2(0.75, 0.111111),
+ vec2(0.125, 0.444444),
+ vec2(0.625, 0.777778),
+ vec2(0.375, 0.222222),
+ vec2(0.875, 0.555556)
+);
+
+// Get Halton jitter for frame
+vec2 GetHaltonJitter(float frame_index) {
+ int index = int(mod(frame_index, TAA_REJECTION_SAMPLES));
+ return HALTON_SEQUENCE[index] - 0.5;
+}
+
+// Clamp color to neighborhood to prevent ghosting
+vec3 ClampToNeighborhood(vec3 current, vec3 history) {
+ vec2 texel_size = inv_resolution;
+ vec3 color_min = current;
+ vec3 color_max = current;
+
+ // Sample 3x3 neighborhood around current pixel
+ for (int x = -1; x <= 1; x++) {
+ for (int y = -1; y <= 1; y++) {
+ vec2 offset = vec2(float(x), float(y)) * texel_size;
+ vec3 neighbor = texture(current_texture, posPos.xy + offset).rgb;
+ color_min = min(color_min, neighbor);
+ color_max = max(color_max, neighbor);
+ }
+ }
+
+ // Clamp history to neighborhood with some tolerance
+ vec3 clamped = clamp(history, color_min, color_max);
+ return mix(history, clamped, TAA_CLAMP_FACTOR);
+}
+
+// Motion vector based history rejection
+bool IsValidMotion(vec2 motion_vector) {
+ // Reject if motion is too large (likely disocclusion) or too small (likely invalid)
+ float motion_length = length(motion_vector);
+ return motion_length > 0.001 && motion_length < 0.05; // Valid motion range
+}
+
+// Edge detection for sharpening
+float GetEdgeLuminance(vec2 uv) {
+ vec2 texel_size = inv_resolution;
+ float luma = dot(texture(current_texture, uv).rgb, vec3(0.299, 0.587, 0.114));
+
+ float luma_l = dot(texture(current_texture, uv + vec2(-texel_size.x, 0.0)).rgb, vec3(0.299, 0.587, 0.114));
+ float luma_r = dot(texture(current_texture, uv + vec2(texel_size.x, 0.0)).rgb, vec3(0.299, 0.587, 0.114));
+ float luma_u = dot(texture(current_texture, uv + vec2(0.0, -texel_size.y)).rgb, vec3(0.299, 0.587, 0.114));
+ float luma_d = dot(texture(current_texture, uv + vec2(0.0, texel_size.y)).rgb, vec3(0.299, 0.587, 0.114));
+
+ float edge_h = abs(luma_l - luma_r);
+ float edge_v = abs(luma_u - luma_d);
+
+ return max(edge_h, edge_v);
+}
+
+void main() {
+ vec2 current_uv = posPos.xy; // Jittered UV for current frame
+ vec2 previous_uv = posPos.zw; // Non-jittered UV for history
+
+ // Sample current frame with jitter
+ vec3 current_color = texture(current_texture, current_uv).rgb;
+
+ // Get motion vector (use non-jittered UV for consistency)
+ vec2 motion_vector = texture(motion_texture, previous_uv).xy * motion_scale;
+
+ // Calculate history UV using motion vector (start from non-jittered position)
+ vec2 history_uv = previous_uv - motion_vector;
+
+ // Sample previous frame at history position
+ vec3 history_color = texture(previous_texture, history_uv).rgb;
+
+ // Motion vector validation
+ bool valid_motion = IsValidMotion(motion_vector);
+
+ // Edge detection for adaptive blending
+ float edge_strength = GetEdgeLuminance(current_uv);
+ float adaptive_blend = mix(blend_factor, 0.8, edge_strength);
+
+ // Clamp history to neighborhood to prevent ghosting
+ vec3 clamped_history = ClampToNeighborhood(current_color, history_color);
+
+ // Temporal blending with improved ghosting prevention
+ vec3 taa_result;
+ if (valid_motion && frame_count > 0.0) {
+ // Use more aggressive blending to reduce ghosting
+ float final_blend = max(adaptive_blend, 0.3); // Minimum 30% current frame
+ taa_result = mix(clamped_history, current_color, final_blend);
+ } else {
+ // Fallback to current frame if motion is invalid or first frame
+ taa_result = current_color;
+ }
+
+ // Optional sharpening to counteract TAA blur
+ if (TAA_SHARPENING > 0.0) {
+ vec2 texel_size = inv_resolution;
+ vec3 sharpened = current_color * (1.0 + 4.0 * TAA_SHARPENING) -
+ TAA_SHARPENING * (
+ texture(current_texture, current_uv + vec2(texel_size.x, 0.0)).rgb +
+ texture(current_texture, current_uv - vec2(texel_size.x, 0.0)).rgb +
+ texture(current_texture, current_uv + vec2(0.0, texel_size.y)).rgb +
+ texture(current_texture, current_uv - vec2(0.0, texel_size.y)).rgb
+ );
+
+ taa_result = mix(taa_result, sharpened, 0.3);
+ }
+
+ // Preserve alpha from current frame
+ float alpha = texture(current_texture, current_uv).a;
+
+ frag_color = vec4(taa_result, alpha);
+}
diff --git a/src/video_core/host_shaders/taa.vert b/src/video_core/host_shaders/taa.vert
new file mode 100644
index 000000000..06c4be39e
--- /dev/null
+++ b/src/video_core/host_shaders/taa.vert
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#version 460
+
+out gl_PerVertex {
+ vec4 gl_Position;
+};
+
+const vec2 vertices[3] =
+ vec2[3](vec2(-1,-1), vec2(3,-1), vec2(-1, 3));
+
+layout (location = 0) out vec4 posPos;
+
+#ifdef VULKAN
+
+#define BINDING_COLOR_TEXTURE 0
+#define VERTEX_ID gl_VertexIndex
+
+#else // ^^^ Vulkan ^^^ // vvv OpenGL vvv
+
+#define BINDING_COLOR_TEXTURE 0
+#define VERTEX_ID gl_VertexID
+
+#endif
+
+layout (binding = BINDING_COLOR_TEXTURE) uniform sampler2D input_texture;
+
+// TAA jitter offset (passed as uniform)
+layout (binding = 1) uniform TaaParams {
+ vec2 jitter_offset;
+ float frame_count;
+ float blend_factor;
+};
+
+void main() {
+ vec2 vertex = vertices[VERTEX_ID];
+ gl_Position = vec4(vertex, 0.0, 1.0);
+ vec2 vert_tex_coord = (vertex + 1.0) / 2.0;
+
+ // Apply jitter for temporal sampling (already scaled in C++)
+ vec2 jittered_tex_coord = vert_tex_coord + jitter_offset;
+
+ posPos.xy = jittered_tex_coord;
+ posPos.zw = vert_tex_coord; // Previous frame position (no jitter)
+}
diff --git a/src/video_core/host_shaders/vulkan_taa.frag b/src/video_core/host_shaders/vulkan_taa.frag
new file mode 100644
index 000000000..db7b46ca1
--- /dev/null
+++ b/src/video_core/host_shaders/vulkan_taa.frag
@@ -0,0 +1,164 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#version 460
+
+#ifdef VULKAN
+
+#define BINDING_COLOR_TEXTURE 1
+#define BINDING_PREVIOUS_TEXTURE 2
+#define BINDING_MOTION_TEXTURE 3
+#define BINDING_DEPTH_TEXTURE 4
+
+#else // ^^^ Vulkan ^^^ // vvv OpenGL vvv
+
+#define BINDING_COLOR_TEXTURE 0
+#define BINDING_PREVIOUS_TEXTURE 1
+#define BINDING_MOTION_TEXTURE 2
+#define BINDING_DEPTH_TEXTURE 3
+
+#endif
+
+layout (location = 0) in vec4 posPos;
+
+layout (location = 0) out vec4 frag_color;
+
+// Textures
+layout (binding = BINDING_COLOR_TEXTURE) uniform sampler2D current_texture;
+layout (binding = BINDING_PREVIOUS_TEXTURE) uniform sampler2D previous_texture;
+layout (binding = BINDING_MOTION_TEXTURE) uniform sampler2D motion_texture;
+layout (binding = BINDING_DEPTH_TEXTURE) uniform sampler2D depth_texture;
+
+// TAA parameters
+layout (binding = 5) uniform TaaParams {
+ vec2 jitter_offset;
+ float frame_count;
+ float blend_factor;
+ vec2 inv_resolution;
+ float motion_scale;
+ float padding[3]; // Padding to 32-byte alignment
+};
+
+// TAA configuration
+const float TAA_CLAMP_FACTOR = 0.9; // More aggressive clamping to reduce ghosting
+const float TAA_SHARPENING = 0.15; // Reduced sharpening to prevent artifacts
+const float TAA_REJECTION_SAMPLES = 8.0;
+
+// Halton sequence for jittering (2,3)
+const vec2 HALTON_SEQUENCE[8] = vec2[8](
+ vec2(0.0, 0.0),
+ vec2(0.5, 0.333333),
+ vec2(0.25, 0.666667),
+ vec2(0.75, 0.111111),
+ vec2(0.125, 0.444444),
+ vec2(0.625, 0.777778),
+ vec2(0.375, 0.222222),
+ vec2(0.875, 0.555556)
+);
+
+// Get Halton jitter for frame
+vec2 GetHaltonJitter(float frame_index) {
+ int index = int(mod(frame_index, TAA_REJECTION_SAMPLES));
+ return HALTON_SEQUENCE[index] - 0.5;
+}
+
+// Clamp color to neighborhood to prevent ghosting
+vec3 ClampToNeighborhood(vec3 current, vec3 history) {
+ vec2 texel_size = inv_resolution;
+ vec3 color_min = current;
+ vec3 color_max = current;
+
+ // Sample 3x3 neighborhood around current pixel
+ for (int x = -1; x <= 1; x++) {
+ for (int y = -1; y <= 1; y++) {
+ vec2 offset = vec2(float(x), float(y)) * texel_size;
+ vec3 neighbor = texture(current_texture, posPos.xy + offset).rgb;
+ color_min = min(color_min, neighbor);
+ color_max = max(color_max, neighbor);
+ }
+ }
+
+ // Clamp history to neighborhood with some tolerance
+ vec3 clamped = clamp(history, color_min, color_max);
+ return mix(history, clamped, TAA_CLAMP_FACTOR);
+}
+
+// Motion vector based history rejection
+bool IsValidMotion(vec2 motion_vector) {
+ // Reject if motion is too large (likely disocclusion) or too small (likely invalid)
+ float motion_length = length(motion_vector);
+ return motion_length > 0.001 && motion_length < 0.05; // Valid motion range
+}
+
+// Edge detection for sharpening
+float GetEdgeLuminance(vec2 uv) {
+ vec2 texel_size = inv_resolution;
+ float luma = dot(texture(current_texture, uv).rgb, vec3(0.299, 0.587, 0.114));
+
+ float luma_l = dot(texture(current_texture, uv + vec2(-texel_size.x, 0.0)).rgb, vec3(0.299, 0.587, 0.114));
+ float luma_r = dot(texture(current_texture, uv + vec2(texel_size.x, 0.0)).rgb, vec3(0.299, 0.587, 0.114));
+ float luma_u = dot(texture(current_texture, uv + vec2(0.0, -texel_size.y)).rgb, vec3(0.299, 0.587, 0.114));
+ float luma_d = dot(texture(current_texture, uv + vec2(0.0, texel_size.y)).rgb, vec3(0.299, 0.587, 0.114));
+
+ float edge_h = abs(luma_l - luma_r);
+ float edge_v = abs(luma_u - luma_d);
+
+ return max(edge_h, edge_v);
+}
+
+void main() {
+ vec2 current_uv = posPos.xy; // Jittered UV for current frame
+ vec2 previous_uv = posPos.zw; // Non-jittered UV for history
+
+ // Sample current frame with jitter
+ vec3 current_color = texture(current_texture, current_uv).rgb;
+
+ // Get motion vector (use non-jittered UV for consistency)
+ vec2 motion_vector = texture(motion_texture, previous_uv).xy * motion_scale;
+
+ // Calculate history UV using motion vector (start from non-jittered position)
+ vec2 history_uv = previous_uv - motion_vector;
+
+ // Sample previous frame at history position
+ vec3 history_color = texture(previous_texture, history_uv).rgb;
+
+ // Motion vector validation
+ bool valid_motion = IsValidMotion(motion_vector);
+
+ // Edge detection for adaptive blending
+ float edge_strength = GetEdgeLuminance(current_uv);
+ float adaptive_blend = mix(blend_factor, 0.8, edge_strength);
+
+ // Clamp history to neighborhood to prevent ghosting
+ vec3 clamped_history = ClampToNeighborhood(current_color, history_color);
+
+ // Temporal blending with improved ghosting prevention
+ vec3 taa_result;
+ if (valid_motion && frame_count > 0.0) {
+ // Use more aggressive blending to reduce ghosting
+ float final_blend = max(adaptive_blend, 0.3); // Minimum 30% current frame
+ taa_result = mix(clamped_history, current_color, final_blend);
+ } else {
+ // Fallback to current frame if motion is invalid or first frame
+ taa_result = current_color;
+ }
+
+ // Optional sharpening to counteract TAA blur
+ if (TAA_SHARPENING > 0.0) {
+ vec2 texel_size = inv_resolution;
+ vec3 sharpened = current_color * (1.0 + 4.0 * TAA_SHARPENING) -
+ TAA_SHARPENING * (
+ texture(current_texture, current_uv + vec2(texel_size.x, 0.0)).rgb +
+ texture(current_texture, current_uv - vec2(texel_size.x, 0.0)).rgb +
+ texture(current_texture, current_uv + vec2(0.0, texel_size.y)).rgb +
+ texture(current_texture, current_uv - vec2(0.0, texel_size.y)).rgb
+ );
+
+ taa_result = mix(taa_result, sharpened, 0.3);
+ }
+
+ // Preserve alpha from current frame
+ float alpha = texture(current_texture, current_uv).a;
+
+ frag_color = vec4(taa_result, alpha);
+}
diff --git a/src/video_core/host_shaders/vulkan_taa.vert b/src/video_core/host_shaders/vulkan_taa.vert
new file mode 100644
index 000000000..e0ceb9a55
--- /dev/null
+++ b/src/video_core/host_shaders/vulkan_taa.vert
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#version 460
+
+#ifdef VULKAN
+
+#define BINDING_COLOR_TEXTURE 0
+#define VERTEX_ID gl_VertexIndex
+
+#else // ^^^ Vulkan ^^^ // vvv OpenGL vvv
+
+#define BINDING_COLOR_TEXTURE 0
+#define VERTEX_ID gl_VertexID
+
+#endif
+
+out gl_PerVertex {
+ vec4 gl_Position;
+};
+
+const vec2 vertices[3] =
+ vec2[3](vec2(-1,-1), vec2(3,-1), vec2(-1, 3));
+
+layout (location = 0) out vec4 posPos;
+
+// TAA jitter offset (passed as uniform)
+layout (binding = 5) uniform TaaParams {
+ vec2 jitter_offset;
+ float frame_count;
+ float blend_factor;
+ vec2 inv_resolution;
+ float motion_scale;
+ float padding[3]; // Padding to 32-byte alignment
+};
+
+void main() {
+ vec2 vertex = vertices[VERTEX_ID];
+ gl_Position = vec4(vertex, 0.0, 1.0);
+ vec2 vert_tex_coord = (vertex + 1.0) / 2.0;
+
+ // Apply jitter for temporal sampling (already scaled in C++)
+ vec2 jittered_tex_coord = vert_tex_coord + jitter_offset;
+
+ posPos.xy = jittered_tex_coord;
+ posPos.zw = vert_tex_coord; // Previous frame position (no jitter)
+}
diff --git a/src/video_core/renderer_opengl/present/layer.cpp b/src/video_core/renderer_opengl/present/layer.cpp
index 0bdec4cb5..d9efdb219 100644
--- a/src/video_core/renderer_opengl/present/layer.cpp
+++ b/src/video_core/renderer_opengl/present/layer.cpp
@@ -12,6 +12,7 @@
#include "video_core/renderer_opengl/present/layer.h"
#include "video_core/renderer_opengl/present/present_uniforms.h"
#include "video_core/renderer_opengl/present/smaa.h"
+#include "video_core/renderer_opengl/present/taa.h"
#include "video_core/surface.h"
#include "video_core/textures/decoders.h"
@@ -59,10 +60,16 @@ GLuint Layer::ConfigureDraw(std::array& out_matrix,
texture = fxaa->Draw(program_manager, info.display_texture);
break;
case Settings::AntiAliasing::Smaa:
- default:
CreateSMAA();
texture = smaa->Draw(program_manager, info.display_texture);
break;
+ case Settings::AntiAliasing::Taa:
+ CreateTAA();
+ texture = taa->Draw(program_manager, info.display_texture,
+ GL_NONE, GL_NONE, GL_NONE, 0); // TODO: Add proper motion vectors
+ break;
+ default:
+ break;
}
}
@@ -215,6 +222,7 @@ void Layer::ConfigureFramebufferTexture(const Tegra::FramebufferConfig& framebuf
void Layer::CreateFXAA() {
smaa.reset();
+ taa.reset();
if (!fxaa) {
fxaa = std::make_unique(
Settings::values.resolution_info.ScaleUp(framebuffer_texture.width),
@@ -224,6 +232,7 @@ void Layer::CreateFXAA() {
void Layer::CreateSMAA() {
fxaa.reset();
+ taa.reset();
if (!smaa) {
smaa = std::make_unique(
Settings::values.resolution_info.ScaleUp(framebuffer_texture.width),
@@ -231,4 +240,14 @@ void Layer::CreateSMAA() {
}
}
+void Layer::CreateTAA() {
+ fxaa.reset();
+ smaa.reset();
+ auto scaled_width = Settings::values.resolution_info.ScaleUp(framebuffer_texture.width);
+ auto scaled_height = Settings::values.resolution_info.ScaleUp(framebuffer_texture.height);
+ if (!taa || taa->NeedsRecreation(scaled_width, scaled_height)) {
+ taa = std::make_unique(scaled_width, scaled_height);
+ }
+}
+
} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/present/layer.h b/src/video_core/renderer_opengl/present/layer.h
index 0cfd6df99..5294d3c0f 100644
--- a/src/video_core/renderer_opengl/present/layer.h
+++ b/src/video_core/renderer_opengl/present/layer.h
@@ -33,6 +33,7 @@ class FXAA;
class ProgramManager;
class RasterizerOpenGL;
class SMAA;
+class TAA;
/// Structure used for storing information about the textures for the Switch screen
struct TextureInfo {
@@ -66,6 +67,7 @@ private:
void CreateFXAA();
void CreateSMAA();
+ void CreateTAA();
private:
RasterizerOpenGL& rasterizer;
@@ -82,6 +84,7 @@ private:
std::unique_ptr fsr2;
std::unique_ptr fxaa;
std::unique_ptr smaa;
+ std::unique_ptr taa;
};
} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/present/taa.cpp b/src/video_core/renderer_opengl/present/taa.cpp
new file mode 100644
index 000000000..be35d230f
--- /dev/null
+++ b/src/video_core/renderer_opengl/present/taa.cpp
@@ -0,0 +1,162 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Shader headers generated by CMake
+#include "video_core/host_shaders/taa_frag.h"
+#include "video_core/host_shaders/taa_vert.h"
+#include "video_core/renderer_opengl/gl_shader_manager.h"
+#include "video_core/renderer_opengl/gl_shader_util.h"
+#include "video_core/renderer_opengl/present/taa.h"
+#include "video_core/renderer_opengl/present/util.h"
+#include "common/logging/log.h"
+
+namespace OpenGL {
+
+TAA::TAA(u32 render_width, u32 render_height) : width(render_width), height(render_height), current_frame(0) {
+ // Validate dimensions
+ if (width == 0 || height == 0) {
+ LOG_ERROR(Render_OpenGL, "TAA: Invalid dimensions {}x{}", width, height);
+ return;
+ }
+
+ vert_shader = CreateProgram(HostShaders::TAA_VERT, GL_VERTEX_SHADER);
+ frag_shader = CreateProgram(HostShaders::TAA_FRAG, GL_FRAGMENT_SHADER);
+
+ sampler = CreateBilinearSampler();
+
+ // Create uniform buffer
+ uniform_buffer.Create();
+ glNamedBufferStorage(uniform_buffer.handle, sizeof(TaaParams), nullptr,
+ GL_DYNAMIC_STORAGE_BIT | GL_MAP_WRITE_BIT);
+
+ // Initialize TAA parameters
+ params.frame_count = 0.0f;
+ params.blend_factor = 0.25f; // Increased blend factor to reduce ghosting
+ params.inv_resolution[0] = 1.0f / static_cast(width);
+ params.inv_resolution[1] = 1.0f / static_cast(height);
+ params.motion_scale = 1.0f;
+ params.jitter_offset[0] = 0.0f;
+ params.jitter_offset[1] = 0.0f;
+
+ CreateFramebuffers();
+}
+
+TAA::~TAA() = default;
+
+void TAA::CreateFramebuffers() {
+ framebuffer.Create();
+
+ // Current frame texture (RGBA16F for HDR support)
+ current_texture.Create(GL_TEXTURE_2D);
+ glTextureStorage2D(current_texture.handle, 1, GL_RGBA16F, width, height);
+ glNamedFramebufferTexture(framebuffer.handle, GL_COLOR_ATTACHMENT0, current_texture.handle, 0);
+
+ // Previous frame texture
+ previous_texture.Create(GL_TEXTURE_2D);
+ glTextureStorage2D(previous_texture.handle, 1, GL_RGBA16F, width, height);
+
+ // Motion vector texture (RG16F for 2D motion vectors) - initialize with zeros
+ motion_texture.Create(GL_TEXTURE_2D);
+ glTextureStorage2D(motion_texture.handle, 1, GL_RG16F, width, height);
+ // Clear motion texture to zero motion vectors
+ glClearTexImage(motion_texture.handle, 0, GL_RG, GL_FLOAT, nullptr);
+
+ // Depth texture (R32F for depth buffer) - initialize with far depth
+ depth_texture.Create(GL_TEXTURE_2D);
+ glTextureStorage2D(depth_texture.handle, 1, GL_R32F, width, height);
+ // Clear depth texture to far depth (1.0)
+ const float far_depth = 1.0f;
+ glClearTexImage(depth_texture.handle, 0, GL_RED, GL_FLOAT, &far_depth);
+
+ // Check framebuffer completeness
+ GLenum status = glCheckNamedFramebufferStatus(framebuffer.handle, GL_FRAMEBUFFER);
+ if (status != GL_FRAMEBUFFER_COMPLETE) {
+ LOG_ERROR(Render_OpenGL, "TAA framebuffer incomplete: 0x{:04X}", status);
+ }
+}
+
+void TAA::UpdateJitter(u32 frame_count) {
+ // Halton sequence (2,3) for low-discrepancy sampling
+ constexpr float halton_2[8] = {0.0f, 0.5f, 0.25f, 0.75f, 0.125f, 0.625f, 0.375f, 0.875f};
+ constexpr float halton_3[8] = {0.0f, 0.333333f, 0.666667f, 0.111111f, 0.444444f, 0.777778f, 0.222222f, 0.555556f};
+
+ // Ensure safe array access
+ const size_t index = static_cast(frame_count) % 8;
+ // Reduce jitter intensity to minimize visible jittering
+ const float jitter_scale = 0.5f; // Reduce jitter by 50%
+ params.jitter_offset[0] = (halton_2[index] - 0.5f) * jitter_scale * params.inv_resolution[0];
+ params.jitter_offset[1] = (halton_3[index] - 0.5f) * jitter_scale * params.inv_resolution[1];
+}
+
+GLuint TAA::Draw(ProgramManager& program_manager, GLuint input_texture, GLuint prev_texture,
+ GLuint motion_text, GLuint depth_text, u32 frame_count) {
+ // Validate input texture
+ if (input_texture == 0) {
+ LOG_ERROR(Render_OpenGL, "TAA: Invalid input texture");
+ return input_texture;
+ }
+
+ // Update parameters
+ params.frame_count = static_cast(frame_count);
+ UpdateJitter(frame_count);
+
+ // Update uniform buffer
+ void* buffer_data = glMapNamedBuffer(uniform_buffer.handle, GL_WRITE_ONLY);
+ if (buffer_data) {
+ memcpy(buffer_data, ¶ms, sizeof(TaaParams));
+ glUnmapNamedBuffer(uniform_buffer.handle);
+ }
+
+ // Bind TAA program
+ program_manager.BindPresentPrograms(vert_shader.handle, frag_shader.handle);
+
+ // Bind framebuffer
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer.handle);
+
+ // Bind textures - use fallback textures if motion/depth are not available
+ glBindTextureUnit(0, input_texture); // Current frame
+ glBindTextureUnit(1, previous_texture.handle); // Previous frame (from TAA)
+
+ // Use motion texture if provided, otherwise use a dummy texture
+ if (motion_text != 0 && motion_text != GL_NONE) {
+ glBindTextureUnit(2, motion_text);
+ } else {
+ glBindTextureUnit(2, motion_texture.handle); // Use our dummy motion texture
+ }
+
+ // Use depth texture if provided, otherwise use a dummy texture
+ if (depth_text != 0 && depth_text != GL_NONE) {
+ glBindTextureUnit(3, depth_text);
+ } else {
+ glBindTextureUnit(3, depth_texture.handle); // Use our dummy depth texture
+ }
+
+ // Bind samplers
+ glBindSampler(0, sampler.handle);
+ glBindSampler(1, sampler.handle);
+ glBindSampler(2, sampler.handle);
+ glBindSampler(3, sampler.handle);
+
+ // Bind uniform buffer
+ glBindBufferBase(GL_UNIFORM_BUFFER, 5, uniform_buffer.handle);
+
+ // Draw full-screen triangle
+ glFrontFace(GL_CCW);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ glFrontFace(GL_CW);
+
+ // Copy current frame to previous frame for next iteration
+ glCopyImageSubData(current_texture.handle, GL_TEXTURE_2D, 0, 0, 0, 0,
+ previous_texture.handle, GL_TEXTURE_2D, 0, 0, 0, 0,
+ width, height, 1);
+
+ return current_texture.handle;
+}
+
+void TAA::SwapBuffers() {
+ // Swap current and previous textures
+ std::swap(current_texture, previous_texture);
+ current_frame++;
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/present/taa.h b/src/video_core/renderer_opengl/present/taa.h
new file mode 100644
index 000000000..ba62fccc0
--- /dev/null
+++ b/src/video_core/renderer_opengl/present/taa.h
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "video_core/renderer_opengl/gl_resource_manager.h"
+
+namespace OpenGL {
+
+class ProgramManager;
+
+class TAA {
+public:
+ explicit TAA(u32 render_width, u32 render_height);
+ ~TAA();
+
+ GLuint Draw(ProgramManager& program_manager, GLuint input_texture, GLuint previous_texture,
+ GLuint motion_texture, GLuint depth_texture, u32 frame_count);
+
+ void SwapBuffers();
+
+ bool NeedsRecreation(u32 render_width, u32 render_height) const {
+ return this->width != render_width || this->height != render_height;
+ }
+
+private:
+ void CreateFramebuffers();
+ void UpdateJitter(u32 frame_count);
+
+ OGLProgram vert_shader;
+ OGLProgram frag_shader;
+ OGLSampler sampler;
+
+ // Current and previous frame buffers
+ OGLFramebuffer framebuffer;
+ OGLTexture current_texture;
+ OGLTexture previous_texture;
+ OGLTexture motion_texture;
+ OGLTexture depth_texture;
+
+ // Uniform buffer for TAA parameters
+ OGLBuffer uniform_buffer;
+
+ u32 width;
+ u32 height;
+ u32 current_frame;
+
+ // TAA parameters
+ struct TaaParams {
+ alignas(8) float jitter_offset[2];
+ alignas(4) float frame_count;
+ alignas(4) float blend_factor;
+ alignas(8) float inv_resolution[2];
+ alignas(4) float motion_scale;
+ alignas(4) float padding[3]; // Padding to 32-byte alignment
+ };
+
+ TaaParams params;
+};
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp
index 24480ef2b..941bb80f6 100644
--- a/src/video_core/renderer_vulkan/present/layer.cpp
+++ b/src/video_core/renderer_vulkan/present/layer.cpp
@@ -13,6 +13,7 @@
#include "video_core/renderer_vulkan/present/layer.h"
#include "video_core/renderer_vulkan/present/present_push_constants.h"
#include "video_core/renderer_vulkan/present/smaa.h"
+#include "video_core/renderer_vulkan/present/taa.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/renderer_vulkan/vk_blit_screen.h"
#include "video_core/textures/decoders.h"
@@ -205,6 +206,9 @@ void Layer::SetAntiAliasPass() {
case Settings::AntiAliasing::Smaa:
anti_alias = std::make_unique(device, memory_allocator, image_count, render_area);
break;
+ case Settings::AntiAliasing::Taa:
+ anti_alias = std::make_unique(device, memory_allocator, image_count, render_area);
+ break;
default:
anti_alias = std::make_unique();
break;
diff --git a/src/video_core/renderer_vulkan/present/taa.cpp b/src/video_core/renderer_vulkan/present/taa.cpp
new file mode 100644
index 000000000..2ac97ca51
--- /dev/null
+++ b/src/video_core/renderer_vulkan/present/taa.cpp
@@ -0,0 +1,362 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/common_types.h"
+
+#include "video_core/host_shaders/vulkan_taa_frag_spv.h"
+#include "video_core/host_shaders/vulkan_taa_vert_spv.h"
+#include "video_core/renderer_vulkan/present/taa.h"
+#include "video_core/renderer_vulkan/present/util.h"
+#include "video_core/renderer_vulkan/vk_scheduler.h"
+#include "video_core/renderer_vulkan/vk_shader_util.h"
+#include "video_core/vulkan_common/vulkan_device.h"
+#include "common/logging/log.h"
+
+namespace Vulkan {
+
+TAA::TAA(const Device& device, MemoryAllocator& allocator, size_t image_count, VkExtent2D extent)
+ : m_device(device), m_allocator(allocator), m_extent(extent),
+ m_image_count(static_cast(image_count)) {
+
+ // Validate dimensions
+ if (extent.width == 0 || extent.height == 0) {
+ LOG_ERROR(Render_Vulkan, "TAA: Invalid dimensions {}x{}", extent.width, extent.height);
+ return;
+ }
+
+ // Initialize TAA parameters
+ m_params.frame_count = 0.0f;
+ m_params.blend_factor = 0.25f; // Increased blend factor to reduce ghosting
+ m_params.inv_resolution[0] = 1.0f / static_cast(extent.width);
+ m_params.inv_resolution[1] = 1.0f / static_cast(extent.height);
+ m_params.motion_scale = 1.0f;
+ m_params.jitter_offset[0] = 0.0f;
+ m_params.jitter_offset[1] = 0.0f;
+
+ CreateImages();
+ CreateRenderPasses();
+ CreateSampler();
+ CreateShaders();
+ CreateDescriptorPool();
+ CreateDescriptorSetLayouts();
+ CreateDescriptorSets();
+ CreatePipelineLayouts();
+ CreatePipelines();
+}
+
+TAA::~TAA() = default;
+
+void TAA::CreateImages() {
+ for (u32 i = 0; i < m_image_count; i++) {
+ Image& image = m_dynamic_images.emplace_back();
+
+ // Current frame texture (RGBA16F for HDR support)
+ image.image = CreateWrappedImage(m_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT);
+ image.image_view =
+ CreateWrappedImageView(m_device, image.image, VK_FORMAT_R16G16B16A16_SFLOAT);
+
+ // Previous frame texture
+ image.previous_image = CreateWrappedImage(m_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT);
+ image.previous_image_view =
+ CreateWrappedImageView(m_device, image.previous_image, VK_FORMAT_R16G16B16A16_SFLOAT);
+
+ // Motion vector texture (RG16F for 2D motion vectors)
+ image.motion_image = CreateWrappedImage(m_allocator, m_extent, VK_FORMAT_R16G16_SFLOAT);
+ image.motion_image_view =
+ CreateWrappedImageView(m_device, image.motion_image, VK_FORMAT_R16G16_SFLOAT);
+
+ // Depth texture (R32F for depth buffer)
+ image.depth_image = CreateWrappedImage(m_allocator, m_extent, VK_FORMAT_R32_SFLOAT);
+ image.depth_image_view =
+ CreateWrappedImageView(m_device, image.depth_image, VK_FORMAT_R32_SFLOAT);
+ }
+
+ // Create uniform buffer - using VMA allocator like other Vulkan implementations
+ m_uniform_buffer = CreateWrappedBuffer(m_allocator, sizeof(TaaParams), MemoryUsage::Upload);
+}
+
+void TAA::CreateRenderPasses() {
+ m_renderpass = CreateWrappedRenderPass(m_device, VK_FORMAT_R16G16B16A16_SFLOAT);
+
+ for (auto& image : m_dynamic_images) {
+ image.framebuffer =
+ CreateWrappedFramebuffer(m_device, m_renderpass, image.image_view, m_extent);
+ }
+}
+
+void TAA::CreateSampler() {
+ const VkSamplerCreateInfo sampler_info{
+ .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .magFilter = VK_FILTER_LINEAR,
+ .minFilter = VK_FILTER_LINEAR,
+ .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
+ .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .mipLodBias = 0.0f,
+ .anisotropyEnable = VK_FALSE,
+ .maxAnisotropy = 0.0f,
+ .compareEnable = VK_FALSE,
+ .compareOp = VK_COMPARE_OP_NEVER,
+ .minLod = 0.0f,
+ .maxLod = 0.0f,
+ .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
+ .unnormalizedCoordinates = VK_FALSE,
+ };
+
+ m_sampler = m_device.GetLogical().CreateSampler(sampler_info);
+}
+
+void TAA::CreateShaders() {
+ m_vertex_shader = CreateWrappedShaderModule(m_device, VULKAN_TAA_VERT_SPV);
+ m_fragment_shader = CreateWrappedShaderModule(m_device, VULKAN_TAA_FRAG_SPV);
+}
+
+void TAA::CreateDescriptorPool() {
+ const std::array pool_sizes{{
+ {
+ .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = m_image_count * 5, // 5 textures per image
+ },
+ {
+ .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+ .descriptorCount = m_image_count,
+ },
+ }};
+
+ const VkDescriptorPoolCreateInfo pool_info{
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .maxSets = m_image_count,
+ .poolSizeCount = static_cast(pool_sizes.size()),
+ .pPoolSizes = pool_sizes.data(),
+ };
+
+ m_descriptor_pool = m_device.GetLogical().CreateDescriptorPool(pool_info);
+}
+
+void TAA::CreateDescriptorSetLayouts() {
+ const std::array layout_bindings{{
+ {
+ .binding = 0,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
+ .pImmutableSamplers = nullptr,
+ },
+ {
+ .binding = 1,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
+ .pImmutableSamplers = nullptr,
+ },
+ {
+ .binding = 2,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
+ .pImmutableSamplers = nullptr,
+ },
+ {
+ .binding = 3,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
+ .pImmutableSamplers = nullptr,
+ },
+ {
+ .binding = 4,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
+ .pImmutableSamplers = nullptr,
+ },
+ {
+ .binding = 5,
+ .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
+ .pImmutableSamplers = nullptr,
+ },
+ }};
+
+ const VkDescriptorSetLayoutCreateInfo layout_info{
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .bindingCount = static_cast(layout_bindings.size()),
+ .pBindings = layout_bindings.data(),
+ };
+
+ m_descriptor_set_layout = m_device.GetLogical().CreateDescriptorSetLayout(layout_info);
+}
+
+void TAA::CreateDescriptorSets() {
+ VkDescriptorSetLayout layout = *m_descriptor_set_layout;
+ for (auto& image : m_dynamic_images) {
+ image.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, {layout});
+ }
+}
+
+void TAA::CreatePipelineLayouts() {
+ m_pipeline_layout = CreateWrappedPipelineLayout(m_device, m_descriptor_set_layout);
+}
+
+void TAA::CreatePipelines() {
+ m_pipeline = CreateWrappedPipeline(m_device, m_renderpass, m_pipeline_layout,
+ std::tie(m_vertex_shader, m_fragment_shader));
+}
+
+void TAA::UpdateDescriptorSets(VkImageView image_view, size_t image_index) {
+ auto& image = m_dynamic_images[image_index];
+
+ // Update uniform buffer
+ std::span mapped_span = m_uniform_buffer.Mapped();
+ if (!mapped_span.empty()) {
+ memcpy(mapped_span.data(), &m_params, sizeof(TaaParams));
+ m_uniform_buffer.Flush();
+ }
+
+ // Update all TAA descriptor sets
+ std::vector image_infos;
+ std::vector updates;
+ image_infos.reserve(6);
+
+ // Binding 0: Dummy texture (not used by shader)
+ updates.push_back(
+ CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, image.descriptor_sets[0], 0));
+
+ // Binding 1: Current frame texture (input)
+ updates.push_back(
+ CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, image.descriptor_sets[0], 1));
+
+ // Binding 2: Previous frame texture
+ updates.push_back(
+ CreateWriteDescriptorSet(image_infos, *m_sampler, *image.previous_image_view, image.descriptor_sets[0], 2));
+
+ // Binding 3: Motion vector texture
+ updates.push_back(
+ CreateWriteDescriptorSet(image_infos, *m_sampler, *image.motion_image_view, image.descriptor_sets[0], 3));
+
+ // Binding 4: Depth texture
+ updates.push_back(
+ CreateWriteDescriptorSet(image_infos, *m_sampler, *image.depth_image_view, image.descriptor_sets[0], 4));
+
+ // Binding 5: Uniform buffer
+ const VkDescriptorBufferInfo buffer_info{
+ .buffer = *m_uniform_buffer,
+ .offset = 0,
+ .range = sizeof(TaaParams),
+ };
+
+ updates.push_back(VkWriteDescriptorSet{
+ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+ .pNext = nullptr,
+ .dstSet = image.descriptor_sets[0],
+ .dstBinding = 5,
+ .dstArrayElement = 0,
+ .descriptorCount = 1,
+ .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+ .pImageInfo = nullptr,
+ .pBufferInfo = &buffer_info,
+ .pTexelBufferView = nullptr,
+ });
+
+ m_device.GetLogical().UpdateDescriptorSets(updates, {});
+}
+
+void TAA::UpdateJitter(u32 frame_count) {
+ // Halton sequence (2,3) for low-discrepancy sampling
+ constexpr float halton_2[8] = {0.0f, 0.5f, 0.25f, 0.75f, 0.125f, 0.625f, 0.375f, 0.875f};
+ constexpr float halton_3[8] = {0.0f, 0.333333f, 0.666667f, 0.111111f, 0.444444f, 0.777778f, 0.222222f, 0.555556f};
+
+ // Ensure safe array access
+ const size_t index = static_cast(frame_count) % 8;
+ // Reduce jitter intensity to minimize visible jittering
+ const float jitter_scale = 0.5f; // Reduce jitter by 50%
+ m_params.jitter_offset[0] = (halton_2[index] - 0.5f) * jitter_scale * m_params.inv_resolution[0];
+ m_params.jitter_offset[1] = (halton_3[index] - 0.5f) * jitter_scale * m_params.inv_resolution[1];
+}
+
+void TAA::UploadImages(Scheduler& scheduler) {
+ if (m_images_ready) {
+ return;
+ }
+ m_images_ready = true;
+}
+
+void TAA::Draw(Scheduler& scheduler, size_t image_index, VkImage* inout_image,
+ VkImageView* inout_image_view) {
+ UpdateJitter(m_current_frame);
+ m_params.frame_count = static_cast(m_current_frame);
+
+ UpdateDescriptorSets(*inout_image_view, image_index);
+ UploadImages(scheduler);
+
+ auto& image = m_dynamic_images[image_index];
+
+ const VkRenderPassBeginInfo renderpass_begin_info{
+ .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
+ .pNext = nullptr,
+ .renderPass = *m_renderpass,
+ .framebuffer = *image.framebuffer,
+ .renderArea =
+ {
+ .offset = {0, 0},
+ .extent = m_extent,
+ },
+ .clearValueCount = 0,
+ .pClearValues = nullptr,
+ };
+
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([this, &image](vk::CommandBuffer cmdbuf) {
+ BeginRenderPass(cmdbuf, *m_renderpass, *image.framebuffer, m_extent);
+ cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, *m_pipeline);
+ cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, *m_pipeline_layout, 0,
+ image.descriptor_sets, {});
+ const VkViewport viewport{
+ .x = 0.0f,
+ .y = 0.0f,
+ .width = static_cast(m_extent.width),
+ .height = static_cast(m_extent.height),
+ .minDepth = 0.0f,
+ .maxDepth = 1.0f,
+ };
+ cmdbuf.SetViewport(0, {viewport});
+ const VkRect2D scissor{
+ .offset = {0, 0},
+ .extent = m_extent,
+ };
+ cmdbuf.SetScissor(0, {scissor});
+ cmdbuf.Draw(3, 1, 0, 0);
+ });
+
+ scheduler.RequestOutsideRenderPassOperationContext();
+
+ // Copy current frame to previous frame for next iteration
+ scheduler.Record([this, &image](vk::CommandBuffer cmdbuf) {
+ const VkImageCopy copy_region{
+ .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
+ .srcOffset = {0, 0, 0},
+ .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
+ .dstOffset = {0, 0, 0},
+ .extent = {m_extent.width, m_extent.height, 1},
+ };
+
+ cmdbuf.CopyImage(*image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ *image.previous_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ copy_region);
+ });
+
+ *inout_image = *image.image;
+ *inout_image_view = *image.image_view;
+
+ m_current_frame++;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/present/taa.h b/src/video_core/renderer_vulkan/present/taa.h
new file mode 100644
index 000000000..c0d161f16
--- /dev/null
+++ b/src/video_core/renderer_vulkan/present/taa.h
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "video_core/renderer_vulkan/present/anti_alias_pass.h"
+#include "video_core/vulkan_common/vulkan_memory_allocator.h"
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+namespace Vulkan {
+
+class Device;
+class Scheduler;
+class StagingBufferPool;
+
+class TAA final : public AntiAliasPass {
+public:
+ explicit TAA(const Device& device, MemoryAllocator& allocator, size_t image_count,
+ VkExtent2D extent);
+ ~TAA() override;
+
+ void Draw(Scheduler& scheduler, size_t image_index, VkImage* inout_image,
+ VkImageView* inout_image_view) override;
+
+private:
+ void CreateImages();
+ void CreateRenderPasses();
+ void CreateSampler();
+ void CreateShaders();
+ void CreateDescriptorPool();
+ void CreateDescriptorSetLayouts();
+ void CreateDescriptorSets();
+ void CreatePipelineLayouts();
+ void CreatePipelines();
+ void UpdateDescriptorSets(VkImageView image_view, size_t image_index);
+ void UploadImages(Scheduler& scheduler);
+ void UpdateJitter(u32 frame_count);
+
+ const Device& m_device;
+ MemoryAllocator& m_allocator;
+ const VkExtent2D m_extent;
+ const u32 m_image_count;
+
+ vk::ShaderModule m_vertex_shader{};
+ vk::ShaderModule m_fragment_shader{};
+ vk::DescriptorPool m_descriptor_pool{};
+ vk::DescriptorSetLayout m_descriptor_set_layout{};
+ vk::PipelineLayout m_pipeline_layout{};
+ vk::Pipeline m_pipeline{};
+ vk::RenderPass m_renderpass{};
+ vk::Buffer m_uniform_buffer{};
+
+ struct Image {
+ vk::DescriptorSets descriptor_sets{};
+ vk::Framebuffer framebuffer{};
+ vk::Image image{};
+ vk::ImageView image_view{};
+ // TAA specific textures
+ vk::Image previous_image{};
+ vk::ImageView previous_image_view{};
+ vk::Image motion_image{};
+ vk::ImageView motion_image_view{};
+ vk::Image depth_image{};
+ vk::ImageView depth_image_view{};
+ };
+ std::vector m_dynamic_images{};
+ bool m_images_ready{};
+
+ vk::Sampler m_sampler{};
+
+ // TAA parameters
+ struct TaaParams {
+ alignas(8) float jitter_offset[2];
+ alignas(4) float frame_count;
+ alignas(4) float blend_factor;
+ alignas(8) float inv_resolution[2];
+ alignas(4) float motion_scale;
+ alignas(4) float padding[3]; // Padding to 32-byte alignment
+ };
+
+ TaaParams m_params{};
+ u32 m_current_frame = 0;
+};
+
+} // namespace Vulkan