feat: add Temporal Anti-Aliasing (TAA) support for OpenGL and Vulkan

- Add TAA option to AntiAliasing enum in settings
- Implement TAA shaders for both OpenGL (GLSL) and Vulkan (SPIR-V)
- Add OpenGL TAA class with framebuffer management and temporal blending
- Add Vulkan TAA class following existing AntiAliasPass architecture
- Integrate TAA into OpenGL and Vulkan rendering pipelines
- Add UI translations and Android string resources for TAA option
- Implement Halton sequence jittering for temporal sampling
- Add motion vector validation and neighborhood clamping to reduce ghosting
- Configure aggressive temporal blending to minimize visual artifacts
- Add proper descriptor set management for Vulkan TAA implementation

The TAA implementation provides high-quality anti-aliasing by combining
information from multiple frames with per-pixel jittering, resulting
in smoother edges and reduced aliasing artifacts while maintaining
good performance and temporal stability.

Fixes: Black screen issues with proper descriptor set bindings
Fixes: Ghosting artifacts with improved temporal blending parameters
Fixes: Jitter visibility with reduced jitter intensity (50% scaling)
Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2025-09-21 10:34:19 +10:00
parent 42bc6b7b6c
commit 19cf31b215
17 changed files with 1130 additions and 2 deletions

View File

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

View File

@@ -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);
}

View File

@@ -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)
}

View File

@@ -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);
}

View File

@@ -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)
}