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