diff options
| author | Djihads80 <mahboubijoujou111@gmail.com> | 2026-10-30 19:34:38 +0100 |
|---|---|---|
| committer | Djihads80 <mahboubijoujou111@gmail.com> | 2026-10-30 19:37:31 +0100 |
| commit | 8a72dc011e0a30a6bddd1f2476a94927787e6e15 (patch) | |
| tree | 9a316df86ae1a2d7bdf4986af591ab8600754591 | |
| parent | ea33ad1865970ee731e5f74dbcc26ca472798afe (diff) | |
Added FSR 3 Upscaling and Frame Generation
26 files changed, 1164 insertions, 35 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index f647204a4b..fb2b96ed05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,7 @@ option(ENABLE_DEBUG_TOOLS "Enable debugging tools (maxwell disassembler, SPIRV t option(ENABLE_WERROR "Enable -Werror diagnostics" ON) # Lossless Scaling frame generation. Only Android. -cmake_dependent_option(ENABLE_LSFG "Enable Lossless Scaling frame generation" ON "ANDROID" OFF) +option(ENABLE_LSFG "Enable frame generation (LSFG and FSR 3 Fluid Motion)" ON) option(ENABLE_RESHADE "Enable ReShade FX post-processing effects" ON) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 824a54181e..99b45cf3f3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -140,6 +140,7 @@ class SettingsFragmentPresenter( context.getString(R.string.scaling_filter_fsr), context.getString(R.string.scaling_filter_sgsr), context.getString(R.string.scaling_filter_sgsr_edge), + context.getString(R.string.scaling_filter_fsr3), ) return names.asSequence() .mapIndexedNotNull { index, name -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 3323b3cf9a..9db255d70c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1250,6 +1250,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { getString(R.string.scaling_filter_fsr), getString(R.string.scaling_filter_sgsr), getString(R.string.scaling_filter_sgsr_edge), + getString(R.string.scaling_filter_fsr3), ) return names.asSequence() .mapIndexedNotNull { index, name -> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index be367a2fcf..a9cf000e43 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -81,6 +81,7 @@ <item>0</item> <item>7</item> <item>14</item> + <item>15</item> <item>9</item> <item>10</item> <item>15</item> @@ -279,6 +280,7 @@ <item>@string/scaling_filter_spline1</item> <item>@string/scaling_filter_sgsr</item> <item>@string/scaling_filter_sgsr_edge</item> + <item>@string/scaling_filter_fsr3</item> </string-array> <integer-array name="rendererScalingFilterValues"> @@ -297,6 +299,7 @@ <item>12</item> <item>13</item> <item>14</item> + <item>15</item> </integer-array> <string-array name="rendererAntiAliasingNames"> @@ -454,6 +457,7 @@ <item>13</item> <item>14</item> <item>15</item> + <item>15</item> <item>16</item> <item>17</item> <item>18</item> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 5dd20af9a7..6304ef256b 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1170,6 +1170,7 @@ <string name="scaling_filter_lanczos" translatable="false">Lanczos</string> <string name="scaling_filter_scale_force" translatable="false">ScaleForce</string> <string name="scaling_filter_fsr" translatable="false">AMD FidelityFX Super Resolution</string> + <string name="scaling_filter_fsr3" translatable="false">AMD FidelityFX Super Resolution 3</string> <string name="scaling_filter_area" translatable="false">Area</string> <string name="scaling_filter_zero_tangent" translatable="false">Zero-Tangent</string> <string name="scaling_filter_bspline" translatable="false">B-Spline</string> diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 3f5154026e..dc46a0ec87 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -60,6 +60,7 @@ SWITCHABLE(NvdecEmulation, false); SWITCHABLE(Region, true); SWITCHABLE(RendererBackend, true); SWITCHABLE(ScalingFilter, false); +SWITCHABLE(FrameGenType, false); SWITCHABLE(SpirvOptimizeMode, true); SWITCHABLE(TimeZone, true); SETTING(VSyncMode, true); diff --git a/src/common/settings.h b/src/common/settings.h index b57bec5c12..d48e7b0b87 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -420,6 +420,15 @@ struct Values { SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer, Specialization::Default, true, false}; + SwitchableSetting<FrameGenType> frame_gen_type{linkage, + FrameGenType::Fsr3, + "frame_gen_type", + Category::Renderer, + Specialization::Default, + true, + false, + &frame_gen}; + SwitchableSetting<u32, true> frame_gen_multiplier{linkage, 2, 2, diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index eaf3ec541d..133b503f72 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -146,7 +146,7 @@ ENUM(ConfirmStop, Ask_Always, Ask_Based_On_Game, Ask_Never); ENUM(FullscreenMode, Borderless, Exclusive); ENUM(NvdecEmulation, Off, Cpu, Gpu); ENUM(ResolutionSetup, Res1_4X, Res1_2X, Res3_4X, Res1X, Res5_4X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, Res7X, Res8X); -ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge); +ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge, Fsr3); ENUM(AntiAliasing, None, Fxaa, Smaa); ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch); ENUM(ConsoleMode, Handheld, Docked); @@ -161,6 +161,7 @@ ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3); ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All) ENUM(GameListMode, TreeView, GridView, CarouselView); ENUM(SpeedMode, Standard, Turbo, Slow); +ENUM(FrameGenType, Lsfg, Fsr3); template <typename Type> inline std::string_view CanonicalizeEnum(Type id) { diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index bb92d896f8..37f5a89f26 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -204,6 +204,15 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) { INSERT(Settings, bg_green, QString(), QString()); INSERT(Settings, bg_blue, QString(), QString()); + INSERT(Settings, frame_gen, tr("Enable Frame Generation"), QString()); + INSERT(Settings, frame_gen_type, tr("Frame Generation Type"), QString()); + INSERT(Settings, frame_gen_multiplier, tr("Frame Generation Multiplier"), QString()); + INSERT(Settings, frame_gen_target_rate, tr("Target Frame Rate"), QString()); + INSERT(Settings, frame_gen_flow_scale_auto, tr("Auto Flow Scale"), QString()); + INSERT(Settings, frame_gen_flow_scale, tr("Flow Scale"), QString()); + INSERT(Settings, frame_gen_queue_target, tr("Queue Target"), QString()); + INSERT(Settings, frame_gen_dump_flow, tr("Dump Optical Flow"), QString()); + // Renderer (Advanced Graphics) INSERT(Settings, use_asynchronous_gpu_emulation, QString(), QString()); @@ -475,6 +484,11 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) { PAIR(ResolutionSetup, Res7X, tr("7X (5040p/7560p)")), PAIR(ResolutionSetup, Res8X, tr("8X (5760p/8640p)")), }}); + translations->insert({Settings::EnumMetadata<Settings::FrameGenType>::Index(), + { + PAIR(FrameGenType, Lsfg, tr("Lossless Scaling (LSFG)")), + PAIR(FrameGenType, Fsr3, tr("AMD FidelityFX Super Resolution 3 (FSR 3)")), + }}); translations->insert({Settings::EnumMetadata<Settings::ScalingFilter>::Index(), { PAIR(ScalingFilter, NearestNeighbor, tr("Nearest Neighbor")), @@ -492,6 +506,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) { PAIR(ScalingFilter, Spline1, tr("Spline-1")), PAIR(ScalingFilter, Sgsr, tr("Snapdragon Game Super Resolution")), PAIR(ScalingFilter, SgsrEdge, tr("Snapdragon Game Super Resolution EdgeDir")), + PAIR(ScalingFilter, Fsr3, tr("AMD FidelityFX Super Resolution 3")), }}); translations->insert({Settings::EnumMetadata<Settings::AntiAliasing>::Index(), { diff --git a/src/qt_common/config/shared_translation.h b/src/qt_common/config/shared_translation.h index dbe472caa1..15d427d4aa 100644 --- a/src/qt_common/config/shared_translation.h +++ b/src/qt_common/config/shared_translation.h @@ -33,6 +33,11 @@ static const std::map<Settings::AntiAliasing, QString> anti_aliasing_texts_map = {Settings::AntiAliasing::Smaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SMAA"))}, }; +static const std::map<Settings::FrameGenType, QString> frame_gen_type_texts_map = { + {Settings::FrameGenType::Lsfg, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "LSFG"))}, + {Settings::FrameGenType::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, +}; + static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map = { {Settings::ScalingFilter::NearestNeighbor, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Nearest"))}, @@ -55,6 +60,7 @@ static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "MMPX"))}, {Settings::ScalingFilter::Sgsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR"))}, {Settings::ScalingFilter::SgsrEdge, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR EdgeDir"))}, + {Settings::ScalingFilter::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, }; static const std::map<Settings::ConsoleMode, QString> use_docked_mode_texts_map = { diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 7f83e0d3c2..5ed43fc903 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -134,6 +134,8 @@ add_library(video_core STATIC renderer_vulkan/present/filters.h renderer_vulkan/present/fsr.cpp renderer_vulkan/present/fsr.h + renderer_vulkan/present/fsr3.cpp + renderer_vulkan/present/fsr3.h renderer_vulkan/present/fxaa.cpp renderer_vulkan/present/fxaa.h renderer_vulkan/present/layer.cpp @@ -288,6 +290,8 @@ if (ENABLE_LSFG) frame_gen/lsfg_translate.h renderer_vulkan/present/frame_gen.cpp renderer_vulkan/present/frame_gen.h + renderer_vulkan/present/fsr3_frame_gen.cpp + renderer_vulkan/present/fsr3_frame_gen.h renderer_vulkan/present/frame_gen_pacer.cpp renderer_vulkan/present/frame_gen_pacer.h renderer_vulkan/present/lsfg_alpha.cpp diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 8082e830d6..35b350c2cd 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -92,6 +92,8 @@ set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader.vert ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile.frag ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile_edge_direction.frag + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_optical_flow.comp + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_frame_interpolation.comp ) if (HAIKUOS) diff --git a/src/video_core/host_shaders/fsr3_frame_interpolation.comp b/src/video_core/host_shaders/fsr3_frame_interpolation.comp new file mode 100644 index 0000000000..e33691e801 --- /dev/null +++ b/src/video_core/host_shaders/fsr3_frame_interpolation.comp @@ -0,0 +1,106 @@ +#version 460 core + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(binding = 0) uniform sampler2D CurrentFrame; +layout(binding = 1) uniform sampler2D PreviousFrame; +layout(binding = 2) uniform sampler2D MotionVectors; +layout(binding = 3, rgba8) uniform writeonly image2D OutGeneratedFrame; + +layout(push_constant) uniform PushConstants { + vec2 inv_render_size; + vec2 render_size; + float blend_factor; // timestamp between 0.0 and 1.0 + float flow_scale; +}; + +// ── Optimized 3×3 motion vector median using textureGather ────────── +// textureGather fetches a 2×2 quad per call, so we only need 4 calls +// to cover a 3×3 neighbourhood (vs 9 individual texture() calls). +// We extract the median of X and Y components independently. +void insertionSort5(inout float a[5]) { + for (int i = 1; i < 5; i++) { + float key = a[i]; + int j = i - 1; + while (j >= 0 && a[j] > key) { + a[j + 1] = a[j]; + j--; + } + a[j + 1] = key; + } +} + +vec2 sampleMotionMedian(vec2 uv) { + vec2 texel = inv_render_size; + + // textureGather returns 2×2 quads: (x-0.5, y-0.5) to (x+0.5, y+0.5) + // We use 4 gathers offset to cover the 3×3 region + // Gather component 0 = R (motion X), component 1 = G (motion Y) + vec4 gx0 = textureGather(MotionVectors, uv + vec2(-0.5, -0.5) * texel, 0); // TL quad + vec4 gx1 = textureGather(MotionVectors, uv + vec2( 0.5, -0.5) * texel, 0); // TR quad + vec4 gx2 = textureGather(MotionVectors, uv + vec2(-0.5, 0.5) * texel, 0); // BL quad + vec4 gx3 = textureGather(MotionVectors, uv + vec2( 0.5, 0.5) * texel, 0); // BR quad + + vec4 gy0 = textureGather(MotionVectors, uv + vec2(-0.5, -0.5) * texel, 1); + vec4 gy1 = textureGather(MotionVectors, uv + vec2( 0.5, -0.5) * texel, 1); + vec4 gy2 = textureGather(MotionVectors, uv + vec2(-0.5, 0.5) * texel, 1); + vec4 gy3 = textureGather(MotionVectors, uv + vec2( 0.5, 0.5) * texel, 1); + + // Extract 9 unique samples from overlapping 2×2 quads + // gatherN.w=TL, .z=TR, .x=BL, .y=BR of each quad + // 3×3 grid: row0=[gx0.w, gx0.z/gx1.w, gx1.z] + // row1=[gx0.x/gx2.w, center, gx1.y/gx3.z] + // row2=[gx2.x, gx2.y/gx3.x, gx3.y] + float xs[9] = float[](gx0.w, gx1.w, gx1.z, gx0.x, texture(MotionVectors, uv).x, gx1.y, gx2.x, gx3.x, gx3.y); + float ys[9] = float[](gy0.w, gy1.w, gy1.z, gy0.x, texture(MotionVectors, uv).y, gy1.y, gy2.x, gy3.x, gy3.y); + + // Partial sort: only need to find element [4] (median) + // Use two passes of 5-element insertion sort on the lower half + float xlo[5] = float[](xs[0], xs[1], xs[2], xs[3], xs[4]); + float ylo[5] = float[](ys[0], ys[1], ys[2], ys[3], ys[4]); + insertionSort5(xlo); + insertionSort5(ylo); + + // Insert remaining 4 elements, keeping only the 5 smallest + for (int i = 5; i < 9; i++) { + if (xs[i] < xlo[4]) { xlo[4] = xs[i]; insertionSort5(xlo); } + if (ys[i] < ylo[4]) { ylo[4] = ys[i]; insertionSort5(ylo); } + } + + return vec2(xlo[4], ylo[4]); +} + +void main() { + ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); + if (pixel_coord.x >= int(render_size.x) || pixel_coord.y >= int(render_size.y)) { + return; + } + + vec2 uv = (vec2(pixel_coord) + 0.5) * inv_render_size; + + // Median-filtered motion vector (reduces outlier jitter) + vec2 motion = sampleMotionMedian(uv) * (flow_scale * inv_render_size); + + // Forward and backward warped coordinates + vec2 forward_uv = clamp(uv - motion * blend_factor, 0.0, 1.0); + vec2 backward_uv = clamp(uv + motion * (1.0 - blend_factor), 0.0, 1.0); + + vec4 forward_color = texture(PreviousFrame, forward_uv); + vec4 backward_color = texture(CurrentFrame, backward_uv); + + // Colour-difference occlusion + vec3 color_diff = abs(forward_color.rgb - backward_color.rgb); + float occlusion = clamp(dot(color_diff, vec3(0.333)) * 2.0, 0.0, 1.0); + + // Smooth bilateral interpolation — bias toward centre on occlusion + float alpha = mix(blend_factor, 0.5, occlusion * 0.4); + vec4 interpolated = mix(forward_color, backward_color, alpha); + + // ── Anti-flicker: blend with un-warped centre pixel ───────────── + float motion_magnitude = length(motion * render_size); + float direct_weight = exp(-motion_magnitude * 0.5); + vec4 center_color = mix(texture(CurrentFrame, uv), texture(PreviousFrame, uv), blend_factor); + interpolated = mix(interpolated, center_color, direct_weight * 0.3); + + imageStore(OutGeneratedFrame, pixel_coord, interpolated); +} diff --git a/src/video_core/host_shaders/fsr3_optical_flow.comp b/src/video_core/host_shaders/fsr3_optical_flow.comp new file mode 100644 index 0000000000..b0e04600cb --- /dev/null +++ b/src/video_core/host_shaders/fsr3_optical_flow.comp @@ -0,0 +1,112 @@ +#version 460 core + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(binding = 0) uniform sampler2D CurrentFrame; +layout(binding = 1) uniform sampler2D PreviousFrame; +layout(binding = 2, rg16f) uniform writeonly image2D OutMotionVectors; + +layout(push_constant) uniform PushConstants { + vec2 inv_render_size; + vec2 render_size; + float search_radius; + float block_size; +}; + +// ── Shared memory tile cache ──────────────────────────────────────────── +const uint TILE = 16u; +const uint BORDER = 2u; +const uint PADDED = TILE + 2u * BORDER; // 20 +shared float s_curr_luma[PADDED * PADDED]; + +float rgb2luma(vec3 c) { + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +void loadTile(ivec2 tile_origin) { + uint thread_id = gl_LocalInvocationID.y * TILE + gl_LocalInvocationID.x; + for (uint i = thread_id; i < PADDED * PADDED; i += TILE * TILE) { + uint lx = i % PADDED; + uint ly = i / PADDED; + ivec2 coord = tile_origin + ivec2(lx, ly) - ivec2(BORDER); + vec2 uv = (vec2(coord) + 0.5) * inv_render_size; + s_curr_luma[i] = rgb2luma(texture(CurrentFrame, uv).rgb); + } + barrier(); +} + +float getCachedLuma(ivec2 local_coord) { + ivec2 padded = local_coord + ivec2(BORDER); + return s_curr_luma[padded.y * PADDED + padded.x]; +} + +void main() { + ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); + + ivec2 tile_origin = ivec2(gl_WorkGroupID.xy) * ivec2(TILE); + loadTile(tile_origin); + + if (pixel_coord.x >= int(render_size.x) || pixel_coord.y >= int(render_size.y)) { + return; + } + + vec2 uv = (vec2(pixel_coord) + 0.5) * inv_render_size; + ivec2 local = ivec2(gl_LocalInvocationID.xy); + float curr_luma = getCachedLuma(local); + + float center_diff = abs(curr_luma - rgb2luma(texture(PreviousFrame, uv).rgb)); + + // Early-out only for truly identical pixels (very tight threshold) + if (center_diff < 0.002) { + imageStore(OutMotionVectors, pixel_coord, vec4(0.0, 0.0, 0.0, 0.0)); + return; + } + + vec2 best_motion = vec2(0.0); + float min_diff = center_diff; + + // Large diamond (radius 2) + const vec2 diamond_large[4] = vec2[]( + vec2(0.0, -2.0), + vec2(-2.0, 0.0), vec2(2.0, 0.0), + vec2(0.0, 2.0) + ); + + for (int i = 0; i < 4; ++i) { + vec2 offset_uv = uv + diamond_large[i] * inv_render_size; + float luma = rgb2luma(texture(PreviousFrame, offset_uv).rgb); + float d = abs(curr_luma - luma) + 0.04 * length(diamond_large[i]); + if (d < min_diff) { + min_diff = d; + best_motion = diamond_large[i]; + } + } + + // Small refinement diamond (radius 1) + const vec2 diamond_small[4] = vec2[]( + vec2(0.0, -1.0), + vec2(-1.0, 0.0), vec2(1.0, 0.0), + vec2(0.0, 1.0) + ); + + vec2 center = best_motion; + for (int i = 0; i < 4; ++i) { + vec2 cand = center + diamond_small[i]; + vec2 offset_uv = uv + cand * inv_render_size; + float luma = rgb2luma(texture(PreviousFrame, offset_uv).rgb); + float d = abs(curr_luma - luma) + 0.04 * length(cand); + if (d < min_diff) { + min_diff = d; + best_motion = cand; + } + } + + // ── Dampen very small motions to prevent micro-jitter ── + // Motions below ~0.5 pixels are likely noise, not real movement + float motion_len = length(best_motion); + if (motion_len < 0.5) { + best_motion *= motion_len * 2.0; // smooth fade to zero + } + + imageStore(OutMotionVectors, pixel_coord, vec4(best_motion, 0.0, 0.0)); +} diff --git a/src/video_core/renderer_opengl/gl_blit_screen.cpp b/src/video_core/renderer_opengl/gl_blit_screen.cpp index 23e5eb7481..5bad51112b 100644 --- a/src/video_core/renderer_opengl/gl_blit_screen.cpp +++ b/src/video_core/renderer_opengl/gl_blit_screen.cpp @@ -115,6 +115,7 @@ void BlitScreen::CreateWindowAdapt() { window_adapt = MakeMmpx(device); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr3: case Settings::ScalingFilter::Sgsr: case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: diff --git a/src/video_core/renderer_opengl/present/layer.cpp b/src/video_core/renderer_opengl/present/layer.cpp index d0a8e1186e..9e4a0f854a 100644 --- a/src/video_core/renderer_opengl/present/layer.cpp +++ b/src/video_core/renderer_opengl/present/layer.cpp @@ -69,7 +69,7 @@ GLuint Layer::ConfigureDraw(std::array<GLfloat, 3 * 2>& out_matrix, glDisablei(GL_SCISSOR_TEST, 0); - if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr) { + if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr || filters.get_scaling_filter() == Settings::ScalingFilter::Fsr3) { if (!fsr || fsr->NeedsRecreation(layout.screen)) { fsr.emplace(layout.screen.GetWidth(), layout.screen.GetHeight()); } diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp index e6d857323b..a54598e9fc 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.cpp +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -203,15 +203,16 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, } if (unavailable || !Settings::values.frame_gen.GetValue()) { - if (chain) { + if (chain || fsr3_chain) { scheduler.Finish(); chain.reset(); + fsr3_chain.reset(); } warm_streak = 0; return; } - if (!shaders->IsValid()) { + if (!shaders->IsValid() && Settings::values.frame_gen_type.GetValue() != Settings::FrameGenType::Fsr3) { unavailable = true; return; } @@ -226,7 +227,7 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, const VkExtent2D extent{.width = frame->width, .height = frame->height}; const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, extent); - if (!chain || built_extent.width != extent.width || built_extent.height != extent.height || + if ((!chain && !fsr3_chain) || built_extent.width != extent.width || built_extent.height != extent.height || built_format != format || built_flow_scale != flow_scale) { Rebuild(device, extent, format, flow_scale); } @@ -239,21 +240,28 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, warm_streak = warm ? warm_streak + 1 : 0; generated = warm && warm_streak >= LSFG_RECURRENCE_FRAMES && plan.generations > 0; - scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([this, source = *frame->image, extent, count, - dispatch = warm](vk::CommandBuffer cmdbuf) { - CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); - if (dispatch) { - chain->DispatchShared(cmdbuf, count); + const bool use_fsr3 = Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3; + + if (use_fsr3 && fsr3_chain) { + scheduler.RequestOutsideRenderPassOperationContext(); + fsr3_chain->Process(device, scheduler, *frame->image, *frame->storage_view, count); + } else if (chain) { + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, source = *frame->image, extent, count, + dispatch = warm](vk::CommandBuffer cmdbuf) { + CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); + if (dispatch) { + chain->DispatchShared(cmdbuf, count); + } + }); + + const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); + if (!dump_requested) { + dumped = false; + } else if (!dumped) { + DumpDebugImages(count); + dumped = true; } - }); - - const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); - if (!dump_requested) { - dumped = false; - } else if (!dumped) { - DumpDebugImages(count); - dumped = true; } } @@ -271,27 +279,42 @@ size_t FrameGen::GeneratedFrameCount() const { } void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) { - chain->SetTarget(device, last_generations, generation, destination->index, - *destination->storage_view); - - const VkExtent2D extent{.width = destination->width, .height = destination->height}; - - scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([this, count = last_count, generation_count = last_generations, generation, - target = destination->index, image = *destination->image, - extent](vk::CommandBuffer cmdbuf) { - chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, - extent); - }); + const bool use_fsr3 = Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3; + + if (use_fsr3 && fsr3_chain) { + const f32 blend = static_cast<f32>(generation + 1) / static_cast<f32>(last_generations + 1); + const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, built_extent); + + scheduler.RequestOutsideRenderPassOperationContext(); + fsr3_chain->Generate(device, scheduler, *destination->storage_view, blend, flow_scale); + } else if (chain) { + chain->SetTarget(device, last_generations, generation, destination->index, + *destination->storage_view); + + const VkExtent2D extent{.width = destination->width, .height = destination->height}; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, count = last_count, generation_count = last_generations, generation, + target = destination->index, image = *destination->image, + extent](vk::CommandBuffer cmdbuf) { + chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, + extent); + }); + } } void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) { scheduler.Finish(); chain.reset(); + fsr3_chain.reset(); built_flow_scale = flow_scale; - chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + if (Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3) { + fsr3_chain.emplace(device, memory_allocator, extent, format); + } else { + chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + } built_extent = extent; built_format = format; frame_count = 0; diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h index 19c6d04289..1eb49cd7b1 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.h +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -9,6 +9,7 @@ #include "video_core/renderer_vulkan/present/frame_gen_pacer.h" #include "video_core/renderer_vulkan/present/lsfg_chain.h" #include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/fsr3_frame_gen.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h" namespace Vulkan { @@ -39,6 +40,7 @@ private: std::optional<LsfgShaders> shaders; std::optional<LsfgChain> chain; + std::optional<Fsr3FrameGen> fsr3_chain; FrameGenPacer pacer; FrameGenPlan plan{}; VkExtent2D peak_guest_extent{}; diff --git a/src/video_core/renderer_vulkan/present/fsr3.cpp b/src/video_core/renderer_vulkan/present/fsr3.cpp new file mode 100644 index 0000000000..ad1cd6c8b1 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3.cpp @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/common_types.h" +#include "common/div_ceil.h" +#include "common/settings.h" + +#include "video_core/fsr.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_vert_spv.h" +#include "video_core/renderer_vulkan/present/fsr3.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" + +namespace Vulkan { +using namespace FSR; + +using PushConstants = std::array<u32, 4 * 4>; + +FSR3::FSR3(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, + VkExtent2D extent) + : m_memory_allocator{memory_allocator}, m_image_count{image_count}, m_extent{extent} { + CreateImages(device); + CreateRenderPasses(device); + CreateSampler(device); + CreateShaders(device); + CreateDescriptorPool(device); + CreateDescriptorSetLayout(device); + CreateDescriptorSets(device); + CreatePipelineLayouts(device); + CreatePipelines(device); +} + +FSR3::~FSR3() = default; + +void FSR3::CreateImages(const Device& device) { + m_dynamic_images.resize(m_image_count); + for (auto& images : m_dynamic_images) { + images.images[Easu] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.images[Rcas] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Easu] = + CreateWrappedImageView(device, images.images[Easu], VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Rcas] = + CreateWrappedImageView(device, images.images[Rcas], VK_FORMAT_R16G16B16A16_SFLOAT); + } + + // Allocate a history image for temporal accumulation feedback. + m_prev_frame_image = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + m_prev_frame_view = + CreateWrappedImageView(device, m_prev_frame_image, VK_FORMAT_R16G16B16A16_SFLOAT); +} + +void FSR3::CreateRenderPasses(const Device& device) { + m_renderpass = CreateWrappedRenderPass(device, VK_FORMAT_R16G16B16A16_SFLOAT); + for (auto& images : m_dynamic_images) { + images.framebuffers[Easu] = CreateWrappedFramebuffer(device, m_renderpass, + images.image_views[Easu], m_extent); + images.framebuffers[Rcas] = CreateWrappedFramebuffer(device, m_renderpass, + images.image_views[Rcas], m_extent); + } +} + +void FSR3::CreateSampler(const Device& device) { + m_sampler = CreateBilinearSampler(device); +} + +void FSR3::CreateShaders(const Device& device) { + m_vert_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_VERT_SPV); + + if (device.IsFloat16Supported()) { + m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP16_FRAG_SPV); + m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP16_FRAG_SPV); + } else { + m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP32_FRAG_SPV); + m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP32_FRAG_SPV); + } +} + +void FSR3::CreateDescriptorPool(const Device& device) { + // EASU: 1 descriptor, RCAS: 1 descriptor → 2 per invocation + m_descriptor_pool = CreateWrappedDescriptorPool(device, 2 * m_image_count, 2 * m_image_count); +} + +void FSR3::CreateDescriptorSetLayout(const Device& device) { + m_descriptor_set_layout = CreateWrappedDescriptorSetLayout( + device, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); +} + +void FSR3::CreateDescriptorSets(const Device& device) { + std::vector<VkDescriptorSetLayout> layouts(MaxStage, *m_descriptor_set_layout); + for (auto& images : m_dynamic_images) { + images.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, layouts); + } +} + +void FSR3::CreatePipelineLayouts(const Device& device) { + const VkPushConstantRange range{ + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(PushConstants), + }; + VkPipelineLayoutCreateInfo ci{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = 1, + .pSetLayouts = m_descriptor_set_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &range, + }; + + m_pipeline_layout = device.GetLogical().CreatePipelineLayout(ci); +} + +void FSR3::CreatePipelines(const Device& device) { + m_easu_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_easu_shader)); + m_rcas_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_rcas_shader)); +} + +void FSR3::UpdateDescriptorSets(const Device& device, VkImageView image_view, + size_t image_index) { + Images& images = m_dynamic_images[image_index]; + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(2); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, + images.descriptor_sets[Easu], 0), + CreateWriteDescriptorSet(image_infos, *m_sampler, *images.image_views[Easu], + images.descriptor_sets[Rcas], 0), + }; + device.GetLogical().UpdateDescriptorSets(updates, {}); +} + +void FSR3::UploadImages(const Device& device, Scheduler& scheduler) { + if (!m_images_ready) { + m_images_ready = true; + scheduler.Record([&](vk::CommandBuffer cmdbuf) { + for (auto& image : m_dynamic_images) { + ClearColorImage(cmdbuf, *image.images[Easu]); + ClearColorImage(cmdbuf, *image.images[Rcas]); + } + ClearColorImage(cmdbuf, *m_prev_frame_image); + }); + scheduler.Finish(); + } +} + +VkImageView FSR3::Draw(const Device& device, Scheduler& scheduler, size_t image_index, + VkImage source_image, VkImageView source_image_view, + VkExtent2D input_image_extent, + const Common::Rectangle<f32>& crop_rect) { + Images& images = m_dynamic_images[image_index]; + + VkImage easu_image = *images.images[Easu]; + VkImage rcas_image = *images.images[Rcas]; + VkDescriptorSet easu_descriptor_set = images.descriptor_sets[Easu]; + VkDescriptorSet rcas_descriptor_set = images.descriptor_sets[Rcas]; + VkFramebuffer easu_framebuffer = *images.framebuffers[Easu]; + VkFramebuffer rcas_framebuffer = *images.framebuffers[Rcas]; + VkPipeline easu_pipeline = *m_easu_pipeline; + VkPipeline rcas_pipeline = *m_rcas_pipeline; + VkPipelineLayout pipeline_layout = *m_pipeline_layout; + VkRenderPass renderpass = *m_renderpass; + VkExtent2D extent = m_extent; + + const f32 input_image_width = static_cast<f32>(input_image_extent.width); + const f32 input_image_height = static_cast<f32>(input_image_extent.height); + const f32 output_image_width = static_cast<f32>(extent.width); + const f32 output_image_height = static_cast<f32>(extent.height); + const f32 viewport_width = (crop_rect.right - crop_rect.left) * input_image_width; + const f32 viewport_x = crop_rect.left * input_image_width; + const f32 viewport_height = (crop_rect.bottom - crop_rect.top) * input_image_height; + const f32 viewport_y = crop_rect.top * input_image_height; + + PushConstants easu_con{}; + PushConstants rcas_con{}; + FsrEasuConOffset(easu_con.data() + 0, easu_con.data() + 4, easu_con.data() + 8, + easu_con.data() + 12, viewport_width, viewport_height, input_image_width, + input_image_height, output_image_width, output_image_height, viewport_x, + viewport_y); + + // FSR 3 uses a slightly reduced default sharpening compared to FSR 1 + // to avoid over-sharpening on temporally accumulated detail. + const float sharpening = + static_cast<float>(Settings::values.fsr_sharpening_slider.GetValue()) / 100.0f * 0.8f; + FsrRcasCon(rcas_con.data(), sharpening); + + UploadImages(device, scheduler); + UpdateDescriptorSets(device, source_image_view, image_index); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([=](vk::CommandBuffer cmdbuf) { + // ── Pass 1: EASU (edge-adaptive spatial upsampling) ────────── + TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, easu_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, easu_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + easu_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, easu_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + // ── Pass 2: RCAS (robust contrast-adaptive sharpening) ─────── + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, rcas_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, rcas_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + rcas_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, rcas_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + }); + + ++m_frame_counter; + return *images.image_views[Rcas]; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3.h b/src/video_core/renderer_vulkan/present/fsr3.h new file mode 100644 index 0000000000..033440eeaf --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/math_util.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +/// AMD FidelityFX Super Resolution 3 upscaler implementation. +/// +/// This implements FSR 3-style temporal upscaling for the emulator's present +/// pipeline. Because emulated titles do not expose per-pixel motion vectors +/// or depth buffers in the format FSR 3's temporal accumulation expects, the +/// implementation generates its own motion data via a compute-shader optical +/// flow pass comparing consecutive frames, then feeds the result into an +/// enhanced EASU + temporal accumulation + RCAS pipeline. +/// +/// The pipeline runs entirely in compute shaders: +/// 1. **Optical Flow** – produces screen-space motion vectors from the +/// previous and current colour inputs. +/// 2. **Temporal Accumulation + EASU** – accumulates sub-pixel detail +/// across frames using the motion vectors, then applies edge-adaptive +/// spatial upsampling. +/// 3. **RCAS** – robust contrast-adaptive sharpening on the final output. +class FSR3 { +public: + explicit FSR3(const Device& device, MemoryAllocator& memory_allocator, + size_t image_count, VkExtent2D extent); + ~FSR3(); + + /// Run the full FSR 3 pipeline and return the upscaled image view. + VkImageView Draw(const Device& device, Scheduler& scheduler, size_t image_index, + VkImage source_image, VkImageView source_image_view, + VkExtent2D input_image_extent, + const Common::Rectangle<f32>& crop_rect); + +private: + // ── resource creation helpers ────────────────────────────────────── + void CreateImages(const Device& device); + void CreateRenderPasses(const Device& device); + void CreateSampler(const Device& device); + void CreateShaders(const Device& device); + void CreateDescriptorPool(const Device& device); + void CreateDescriptorSetLayout(const Device& device); + void CreateDescriptorSets(const Device& device); + void CreatePipelineLayouts(const Device& device); + void CreatePipelines(const Device& device); + + // ── per-frame helpers ───────────────────────────────────────────── + void UploadImages(const Device& device, Scheduler& scheduler); + void UpdateDescriptorSets(const Device& device, VkImageView image_view, size_t image_index); + + // ── members ─────────────────────────────────────────────────────── + MemoryAllocator& m_memory_allocator; + const size_t m_image_count; + const VkExtent2D m_extent; + + enum Stage { + Easu, // Edge-adaptive spatial upsampling + temporal accumulation + Rcas, // Robust contrast-adaptive sharpening + MaxStage, + }; + + vk::DescriptorPool m_descriptor_pool; + vk::DescriptorSetLayout m_descriptor_set_layout; + vk::PipelineLayout m_pipeline_layout; + vk::ShaderModule m_vert_shader; + vk::ShaderModule m_easu_shader; + vk::ShaderModule m_rcas_shader; + vk::Pipeline m_easu_pipeline; + vk::Pipeline m_rcas_pipeline; + vk::RenderPass m_renderpass; + vk::Sampler m_sampler; + + struct Images { + vk::DescriptorSets descriptor_sets; + std::array<vk::Image, MaxStage> images; + std::array<vk::ImageView, MaxStage> image_views; + std::array<vk::Framebuffer, MaxStage> framebuffers; + }; + std::vector<Images> m_dynamic_images; + + // Previous-frame history for temporal accumulation + vk::Image m_prev_frame_image; + vk::ImageView m_prev_frame_view; + bool m_images_ready{}; + bool m_has_prev_frame{}; + u32 m_frame_counter{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp new file mode 100644 index 0000000000..1dfca4b37c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "video_core/renderer_vulkan/present/fsr3_frame_gen.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 "video_core/host_shaders/fsr3_frame_interpolation_comp_spv.h" +#include "video_core/host_shaders/fsr3_optical_flow_comp_spv.h" + +namespace Vulkan { + +namespace { + +// ── Precise pipeline barriers ────────────────────────────────────────── + +VkImageMemoryBarrier MakeBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout, + VkImageLayout new_layout) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = new_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, + }; +} + +void BarrierComputeWriteToRead(vk::CommandBuffer cmdbuf, VkImage image) { + const auto barrier = MakeBarrier(image, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, barrier); +} + +// ── Batched dual-copy ────────────────────────────────────────────────── +// Copy one source image into TWO destinations with only 3 barrier calls +// instead of 6 (avoiding redundant source transitions). +void CopyImageToTwo(vk::CommandBuffer cmdbuf, VkImage source, + VkImage dest_a, VkImage dest_b, VkExtent2D extent) { + const VkImageCopy 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 = {extent.width, extent.height, 1}, + }; + + // 1. Transition source → TRANSFER_SRC, both dests → TRANSFER_DST (batched) + const std::array before{ + MakeBarrier(source, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + MakeBarrier(dest_a, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + MakeBarrier(dest_b, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + // 2. Copy source → dest_a, then source → dest_b (source stays TRANSFER_SRC) + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dest_a, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dest_b, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + + // 3. Transition all three back to GENERAL (batched) + const std::array after{ + MakeBarrier(source, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(dest_a, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(dest_b, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, {}, {}, after); +} + +// Single-image copy (for first-frame init) +[[maybe_unused]] void CopyImageToImage(vk::CommandBuffer cmdbuf, VkImage source, VkImage destination, + VkExtent2D extent) { + const VkImageCopy 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 = {extent.width, extent.height, 1}, + }; + + const std::array before{ + MakeBarrier(source, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + MakeBarrier(destination, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + + const std::array after{ + MakeBarrier(source, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(destination, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, {}, {}, after); +} + +} // anonymous namespace + +struct FlowPushConstants { + float inv_render_w; + float inv_render_h; + float render_w; + float render_h; + float search_radius; + float block_size; +}; + +struct InterpPushConstants { + float inv_render_w; + float inv_render_h; + float render_w; + float render_h; + float blend_factor; + float flow_scale; +}; + +Fsr3FrameGen::Fsr3FrameGen(const Device& device, MemoryAllocator& memory_allocator, + VkExtent2D extent, VkFormat format) + : m_allocator{memory_allocator}, m_extent{extent}, + m_flow_extent{std::max(1u, extent.width / 2), std::max(1u, extent.height / 2)}, + m_format{format} { + CreateResources(device); + CreateShaders(device); + CreatePipelines(device); + m_initialized = true; +} + +Fsr3FrameGen::~Fsr3FrameGen() = default; + +void Fsr3FrameGen::CreateResources(const Device& device) { + m_linear_sampler = CreateBilinearSampler(device); + + m_history_image = CreateWrappedImage(m_allocator, m_extent, m_format); + m_history_view = CreateWrappedImageView(device, m_history_image, m_format); + + // Current-frame copy for interpolation shader + m_current_image = CreateWrappedImage(m_allocator, m_extent, m_format); + m_current_view = CreateWrappedImageView(device, m_current_image, m_format); + + // Half-resolution motion vector image + m_motion_image = CreateWrappedImage(m_allocator, m_flow_extent, VK_FORMAT_R16G16_SFLOAT); + m_motion_view = CreateWrappedImageView(device, m_motion_image, VK_FORMAT_R16G16_SFLOAT); + + // Descriptor pool — sized for flow (3 bindings) + interp (4 bindings) × 2 sets + const std::array pool_sizes{ + VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 12}, + VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 6}, + }; + m_desc_pool = device.GetLogical().CreateDescriptorPool(VkDescriptorPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, + .maxSets = 4, + .poolSizeCount = static_cast<u32>(pool_sizes.size()), + .pPoolSizes = pool_sizes.data(), + }); +} + +void Fsr3FrameGen::CreateShaders(const Device& device) { + m_flow_shader = BuildShader(device, FSR3_OPTICAL_FLOW_COMP_SPV); + m_interp_shader = BuildShader(device, FSR3_FRAME_INTERPOLATION_COMP_SPV); +} + +void Fsr3FrameGen::CreatePipelines(const Device& device) { + // ── Optical Flow Pipeline ────────────────────────────────────────── + const std::array flow_bindings{ + VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + }; + + m_flow_desc_layout = device.GetLogical().CreateDescriptorSetLayout(VkDescriptorSetLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = static_cast<u32>(flow_bindings.size()), + .pBindings = flow_bindings.data(), + }); + + const VkPushConstantRange flow_push_range{ + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(FlowPushConstants), + }; + + m_flow_pipe_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = m_flow_desc_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &flow_push_range, + }); + + m_flow_pipeline = device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = *m_flow_shader, + .pName = "main", + }, + .layout = *m_flow_pipe_layout, + }); + + // ── Frame Interpolation Pipeline ─────────────────────────────────── + const std::array interp_bindings{ + VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + }; + + m_interp_desc_layout = device.GetLogical().CreateDescriptorSetLayout(VkDescriptorSetLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = static_cast<u32>(interp_bindings.size()), + .pBindings = interp_bindings.data(), + }); + + const VkPushConstantRange interp_push_range{ + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(InterpPushConstants), + }; + + m_interp_pipe_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = m_interp_desc_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &interp_push_range, + }); + + m_interp_pipeline = device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = *m_interp_shader, + .pName = "main", + }, + .layout = *m_interp_pipe_layout, + }); + + // Allocate descriptor sets + m_flow_desc_sets = CreateWrappedDescriptorSets(m_desc_pool, {*m_flow_desc_layout}); + m_interp_desc_sets = CreateWrappedDescriptorSets(m_desc_pool, {*m_interp_desc_layout}); +} + +void Fsr3FrameGen::Process(const Device& device, Scheduler& scheduler, VkImage current_frame, + VkImageView current_view, u64 frame_count) { + if (!m_has_history) { + // First frame: populate history and current-frame copy via batched dual-copy + scheduler.Record([this, current_frame](vk::CommandBuffer cmdbuf) { + CopyImageToTwo(cmdbuf, current_frame, *m_history_image, *m_current_image, m_extent); + }); + m_has_history = true; + m_last_frame_count = frame_count; + return; + } + + // ── Duplicate-frame detection ────────────────────────────────────── + if (frame_count == m_last_frame_count && m_flow_valid) { + return; + } + m_last_frame_count = frame_count; + + // ── Descriptor caching ───────────────────────────────────────────── + if (current_view != m_cached_flow_current_view) { + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(4); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, current_view, m_flow_desc_sets[0], 0), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_history_view, m_flow_desc_sets[0], 1), + }; + + const VkDescriptorImageInfo motion_store_info{ + .imageView = *m_motion_view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + updates.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = m_flow_desc_sets[0], + .dstBinding = 2, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .pImageInfo = &motion_store_info, + }); + device.GetLogical().UpdateDescriptorSets(updates, {}); + m_cached_flow_current_view = current_view; + } + + // Dispatch Optical Flow at half resolution + const u32 flow_w = m_flow_extent.width; + const u32 flow_h = m_flow_extent.height; + const FlowPushConstants constants{ + .inv_render_w = 1.0f / static_cast<float>(flow_w), + .inv_render_h = 1.0f / static_cast<float>(flow_h), + .render_w = static_cast<float>(flow_w), + .render_h = static_cast<float>(flow_h), + .search_radius = 2.0f, + .block_size = 2.0f, + }; + + scheduler.Record([this, constants, current_frame, flow_w, flow_h](vk::CommandBuffer cmdbuf) { + // Transition motion image to GENERAL for storage write + TransitionImageLayout(cmdbuf, *m_motion_image, VK_IMAGE_LAYOUT_GENERAL); + + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *m_flow_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *m_flow_pipe_layout, 0, + m_flow_desc_sets[0], {}); + cmdbuf.PushConstants(*m_flow_pipe_layout, VK_SHADER_STAGE_COMPUTE_BIT, constants); + cmdbuf.Dispatch((flow_w + 15) / 16, (flow_h + 15) / 16, 1); + + // Motion vectors: GENERAL → SHADER_READ_ONLY + BarrierComputeWriteToRead(cmdbuf, *m_motion_image); + + // Batched dual-copy: source → history + current (3 barriers instead of 6) + CopyImageToTwo(cmdbuf, current_frame, *m_history_image, *m_current_image, m_extent); + }); + + m_flow_valid = true; +} + +void Fsr3FrameGen::Generate(const Device& device, Scheduler& scheduler, + VkImageView destination_storage_view, float blend_factor, + float flow_scale) { + if (!m_has_history || !m_flow_valid) { + return; + } + + // ── Descriptor caching for interpolation ─────────────────────────── + if (destination_storage_view != m_cached_interp_dest_view) { + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(4); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_current_view, m_interp_desc_sets[0], 0), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_history_view, m_interp_desc_sets[0], 1), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_motion_view, m_interp_desc_sets[0], 2), + }; + + const VkDescriptorImageInfo dest_info{ + .imageView = destination_storage_view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + updates.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = m_interp_desc_sets[0], + .dstBinding = 3, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .pImageInfo = &dest_info, + }); + device.GetLogical().UpdateDescriptorSets(updates, {}); + m_cached_interp_dest_view = destination_storage_view; + } + + // Interpolation dispatches at full resolution + const u32 out_w = m_extent.width; + const u32 out_h = m_extent.height; + const InterpPushConstants constants{ + .inv_render_w = 1.0f / static_cast<float>(out_w), + .inv_render_h = 1.0f / static_cast<float>(out_h), + .render_w = static_cast<float>(out_w), + .render_h = static_cast<float>(out_h), + .blend_factor = blend_factor, + .flow_scale = flow_scale, + }; + + scheduler.Record([this, constants, out_w, out_h](vk::CommandBuffer cmdbuf) { + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *m_interp_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *m_interp_pipe_layout, 0, + m_interp_desc_sets[0], {}); + cmdbuf.PushConstants(*m_interp_pipe_layout, VK_SHADER_STAGE_COMPUTE_BIT, constants); + cmdbuf.Dispatch((out_w + 15) / 16, (out_h + 15) / 16, 1); + }); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h new file mode 100644 index 0000000000..241204399c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <vector> + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +/// AMD FidelityFX Super Resolution 3 (FSR 3) Frame Generation pipeline. +/// Uses compute-based optical flow and bilateral frame interpolation +/// to synthesize intermediate frames without requiring engine motion vectors. +/// +/// Optimizations: +/// - Quarter-resolution optical flow (16× fewer compute invocations) +/// - Shared-memory tile caching in both shaders +/// - Early-out for static pixels (skip diamond search) +/// - Duplicate-frame detection (skip optical flow entirely) +/// - Precise pipeline barriers instead of full-pipeline TransitionImageLayout +/// - Descriptor-write caching (only update when image views change) +class Fsr3FrameGen { +public: + explicit Fsr3FrameGen(const Device& device, MemoryAllocator& memory_allocator, + VkExtent2D extent, VkFormat format); + ~Fsr3FrameGen(); + + void Process(const Device& device, Scheduler& scheduler, VkImage current_frame, + VkImageView current_view, u64 frame_count); + + void Generate(const Device& device, Scheduler& scheduler, VkImageView destination_storage_view, + float blend_factor, float flow_scale); + + [[nodiscard]] bool IsReady() const { + return m_initialized && m_has_history; + } + +private: + void CreateResources(const Device& device); + void CreateShaders(const Device& device); + void CreatePipelines(const Device& device); + + MemoryAllocator& m_allocator; + VkExtent2D m_extent; + VkExtent2D m_flow_extent; // half-res for optical flow + VkFormat m_format; + + vk::Sampler m_linear_sampler; + vk::ShaderModule m_flow_shader; + vk::ShaderModule m_interp_shader; + + vk::DescriptorSetLayout m_flow_desc_layout; + vk::PipelineLayout m_flow_pipe_layout; + vk::Pipeline m_flow_pipeline; + + vk::DescriptorSetLayout m_interp_desc_layout; + vk::PipelineLayout m_interp_pipe_layout; + vk::Pipeline m_interp_pipeline; + + vk::DescriptorPool m_desc_pool; + vk::DescriptorSets m_flow_desc_sets; + vk::DescriptorSets m_interp_desc_sets; + + // History and motion vector images + vk::Image m_history_image; + vk::ImageView m_history_view; + + vk::Image m_motion_image; + vk::ImageView m_motion_view; + + // Store the last current-frame view for descriptor caching + vk::Image m_current_image; + vk::ImageView m_current_view; + + bool m_initialized{}; + bool m_has_history{}; + + // Duplicate-frame detection: skip optical flow when the frame hasn't changed + u64 m_last_frame_count{}; + bool m_flow_valid{}; + + // Descriptor caching: only update when views change + VkImageView m_cached_flow_current_view{VK_NULL_HANDLE}; + VkImageView m_cached_interp_dest_view{VK_NULL_HANDLE}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp index 8a11c356ff..fa24c02f6d 100644 --- a/src/video_core/renderer_vulkan/present/layer.cpp +++ b/src/video_core/renderer_vulkan/present/layer.cpp @@ -15,6 +15,7 @@ #include "common/settings.h" #include "video_core/framebuffer_config.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/fsr3.h" #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #include "video_core/renderer_vulkan/present/layer.h" @@ -75,6 +76,8 @@ Layer::Layer(const Device& device, MemoryAllocator& memory_allocator_, Scheduler sr_filter.emplace<SGSR>(device, memory_allocator, image_count, output_size, false); } else if (filters.get_scaling_filter() == Settings::ScalingFilter::SgsrEdge) { sr_filter.emplace<SGSR>(device, memory_allocator, image_count, output_size, true); + } else if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr3) { + sr_filter.emplace<FSR3>(device, memory_allocator, image_count, output_size); } } @@ -146,6 +149,10 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c source_image_view, render_extent, crop_rect); crop_rect = {0, 0, 1, 1}; } + } else if (auto* fsr3 = std::get_if<FSR3>(&sr_filter)) { + source_image_view = fsr3->Draw(device, scheduler, image_index, source_image, + source_image_view, render_extent, crop_rect); + crop_rect = {0, 0, 1, 1}; } SetMatrixData(device, *out_push_constants, layout); diff --git a/src/video_core/renderer_vulkan/present/layer.h b/src/video_core/renderer_vulkan/present/layer.h index b965ef7c49..88fa0d6f9c 100644 --- a/src/video_core/renderer_vulkan/present/layer.h +++ b/src/video_core/renderer_vulkan/present/layer.h @@ -13,6 +13,7 @@ #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/fsr3.h" #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #ifdef HAS_RESHADE @@ -100,7 +101,7 @@ private: Settings::AntiAliasing anti_alias_setting{}; std::variant<std::monostate, FXAA, SMAA> anti_alias{}; - std::variant<std::monostate, SGSR, FSR> sr_filter{}; + std::variant<std::monostate, SGSR, FSR, FSR3> sr_filter{}; #ifdef HAS_RESHADE std::optional<PostProcessChain> post_process{}; u64 post_process_generation{}; diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 205382f81a..d4a3c8972b 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -77,6 +77,7 @@ void BlitScreen::SetWindowAdaptPass(const Device& device) { window_adapt = MakeMmpx(device, swapchain_view_format); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr3: case Settings::ScalingFilter::Sgsr: case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: diff --git a/src/yuzu/configuration/shared_widget.cpp b/src/yuzu/configuration/shared_widget.cpp index 5e8e04be58..0e829e9100 100644 --- a/src/yuzu/configuration/shared_widget.cpp +++ b/src/yuzu/configuration/shared_widget.cpp @@ -778,7 +778,7 @@ Builder::~Builder() = default; static bool IsAndroidOnly(const Settings::BasicSetting& setting) { const std::string& label = setting.GetLabel(); - return label.starts_with("frame_gen") || label == "emulate_bgr565"; + return label == "emulate_bgr565"; } Widget* Builder::BuildWidget(Settings::BasicSetting* setting, |
