mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-04-04 02:18:28 -04:00
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:
@@ -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
|
||||
)
|
||||
|
||||
163
src/video_core/host_shaders/taa.frag
Normal file
163
src/video_core/host_shaders/taa.frag
Normal 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);
|
||||
}
|
||||
46
src/video_core/host_shaders/taa.vert
Normal file
46
src/video_core/host_shaders/taa.vert
Normal 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)
|
||||
}
|
||||
164
src/video_core/host_shaders/vulkan_taa.frag
Normal file
164
src/video_core/host_shaders/vulkan_taa.frag
Normal 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);
|
||||
}
|
||||
47
src/video_core/host_shaders/vulkan_taa.vert
Normal file
47
src/video_core/host_shaders/vulkan_taa.vert
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user