aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authornowhere02 <140357165+nowhere02@users.noreply.github.com>2026-11-02 20:59:00 -0300
committerGitHub <noreply@github.com>2026-11-02 20:59:00 -0300
commit3c499c582cfa27585b3acd4593ede3e9c9f3b5e1 (patch)
tree2f19414a5e89a067ed9c3f600217ae408e8918fd /src
parentaae778edce69f18513e952d3d267518b0f7d3f72 (diff)
parent9acd36eb9a291e0631346f5b17d69d0e8eb26cc5 (diff)
Merge pull request #346 from Duck912/fe-fsr31
FSR 3.1 implementation for DLSS games
Diffstat (limited to 'src')
-rw-r--r--src/qt_common/config/shared_translation.cpp12
-rw-r--r--src/qt_common/config/shared_translation.h6
-rw-r--r--src/video_core/CMakeLists.txt2
-rw-r--r--src/video_core/engines/kepler_compute.cpp18
-rw-r--r--src/video_core/engines/kepler_compute.h7
-rw-r--r--src/video_core/host_shaders/CMakeLists.txt66
-rw-r--r--src/video_core/host_shaders/temporal_sharpen.frag49
-rw-r--r--src/video_core/host_shaders/temporal_upscale.frag85
-rw-r--r--src/video_core/rasterizer_interface.h9
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp175
-rw-r--r--src/video_core/renderer_vulkan/blit_image.h25
-rw-r--r--src/video_core/renderer_vulkan/vk_descriptor_pool.cpp7
-rw-r--r--src/video_core/renderer_vulkan/vk_descriptor_pool.h2
-rw-r--r--src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp693
-rw-r--r--src/video_core/renderer_vulkan/vk_fsr3_upscaler.h119
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp6
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.h1
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp353
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h34
-rw-r--r--src/video_core/texture_cache/texture_cache.h63
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h3
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h5
22 files changed, 1559 insertions, 181 deletions
diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp
index 37f5a89f26..c5907e1b65 100644
--- a/src/qt_common/config/shared_translation.cpp
+++ b/src/qt_common/config/shared_translation.cpp
@@ -145,8 +145,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
"Higher resolutions require more VRAM and bandwidth.\n"
"Options lower than 1X can cause artifacts."));
INSERT(Settings, scaling_filter, tr("Window Adapting Filter:"), QString());
- INSERT(Settings, fsr_sharpening_slider, tr("FSR Sharpness:"),
- tr("Determines how sharpened the image will look using FSR's or SGSR's dynamic contrast."));
+ INSERT(Settings, fsr_sharpening_slider, tr("RCAS Sharpness:"),
+ tr("Strength of RCAS (AMD's contrast-adaptive sharpening) on the upscaler used in place "
+ "of a game's DLSS (AMD FSR 3.1); 0% turns it off.\nAlso sets the sharpening "
+ "of the AMD FSR 1 and SGSR window adapting filters."));
INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"),
tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA "
"can produce a more stable picture in lower resolutions."));
@@ -487,7 +489,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
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)")),
+ PAIR(FrameGenType, Fsr3, tr("Optical Flow Interpolation (built-in)")),
}});
translations->insert({Settings::EnumMetadata<Settings::ScalingFilter>::Index(),
{
@@ -497,7 +499,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
PAIR(ScalingFilter, Gaussian, tr("Gaussian")),
PAIR(ScalingFilter, Lanczos, tr("Lanczos")),
PAIR(ScalingFilter, ScaleForce, tr("ScaleForce")),
- PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX Super Resolution")),
+ PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX Super Resolution 1 (FSR 1)")),
PAIR(ScalingFilter, Area, tr("Area")),
PAIR(ScalingFilter, Mmpx, tr("MMPX")),
PAIR(ScalingFilter, ZeroTangent, tr("Zero-Tangent")),
@@ -506,7 +508,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")),
+ PAIR(ScalingFilter, Fsr3, tr("AMD FidelityFX Super Resolution 1, softer sharpening")),
}});
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 15d427d4aa..7f29071acc 100644
--- a/src/qt_common/config/shared_translation.h
+++ b/src/qt_common/config/shared_translation.h
@@ -35,7 +35,7 @@ static const std::map<Settings::AntiAliasing, QString> anti_aliasing_texts_map =
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"))},
+ {Settings::FrameGenType::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Optical Flow"))},
};
static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map = {
@@ -55,12 +55,12 @@ static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map
{Settings::ScalingFilter::Lanczos, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Lanczos"))},
{Settings::ScalingFilter::ScaleForce,
QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "ScaleForce"))},
- {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR"))},
+ {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 1"))},
{Settings::ScalingFilter::Area, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Area"))},
{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"))},
+ {Settings::ScalingFilter::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 1 Soft"))},
};
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 b3ef88c5f1..d0e84c324c 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -169,6 +169,8 @@ add_library(video_core STATIC
renderer_vulkan/vk_command_pool.h
renderer_vulkan/vk_compute_pass.cpp
renderer_vulkan/vk_compute_pass.h
+ renderer_vulkan/vk_fsr3_upscaler.cpp
+ renderer_vulkan/vk_fsr3_upscaler.h
renderer_vulkan/vk_compute_pipeline.cpp
renderer_vulkan/vk_compute_pipeline.h
renderer_vulkan/vk_descriptor_buffer.cpp
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index 925b1b2ca1..be6a1d5eac 100644
--- a/src/video_core/engines/kepler_compute.cpp
+++ b/src/video_core/engines/kepler_compute.cpp
@@ -591,8 +591,9 @@ void KeplerCompute::DescribeNativeResources(const NativeQmdScheduler::Descriptor
std::atomic<u32> g_nx2_trace_draws_after_upscale{};
// Fire Emblem: Fortune's Weave's first DLSS pass (rejected, before the network) has the input colour
-// at cbuf0+0x278, its motion vectors (R16G16 float, the input's size) at +0x290 and this frame's
-// camera jitter (x, y; input pixels, Halton bases 2 and 3 less 0.5) at +0x1b0.
+// at cbuf0+0x278, its motion vectors (R16G16 float, the input's size) at +0x290, its depth (Z32, the
+// input's size) at +0x298 and this frame's camera jitter (x, y; input pixels, Halton bases 2 and 3
+// less 0.5) at +0x1b0.
void KeplerCompute::RememberDlssMotionVectors() {
const auto& cbuf = launch_description.const_buffer_config[0];
if (cbuf.size.Value() < 0x294) return;
@@ -610,6 +611,14 @@ void KeplerCompute::RememberDlssMotionVectors() {
return;
dlss_motion_vectors = *motion;
dlss_motion_color = color->Address();
+ const auto depth = cbuf.size.Value() >= 0x29c ? image(0x298) : std::nullopt;
+ if (depth && depth->format == Texture::TextureFormat::Z32 && depth->Address() != 0 &&
+ depth->Width() == color->Width() && depth->Height() == color->Height() &&
+ depth->IsBlockLinear()) {
+ dlss_depth = *depth;
+ } else {
+ dlss_depth.reset();
+ }
dlss_jitter = {std::bit_cast<f32>(memory_manager.Read<u32>(cbuf_address + 0x1b0)),
std::bit_cast<f32>(memory_manager.Read<u32>(cbuf_address + 0x1b4))};
}
@@ -794,8 +803,9 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
return false;
Fermi2D::Surface motion = surface(*dlss_motion_vectors);
motion.format = RTFormat::R16G16_FLOAT;
- return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion, dlss_jitter,
- {1.0f, 1.0f});
+ return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion,
+ dlss_depth ? &*dlss_depth : nullptr,
+ dlss_jitter, {1.0f, 1.0f});
};
const bool done = upscale_temporally() ||
rasterizer->AccelerateSurfaceCopy(source, surface(*output), config);
diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h
index b5aee3706a..6a06390294 100644
--- a/src/video_core/engines/kepler_compute.h
+++ b/src/video_core/engines/kepler_compute.h
@@ -269,10 +269,11 @@ private:
void RememberDlssMotionVectors();
bool UpscaleInsteadOfNativeProgram();
- /// Motion vectors of Fire Emblem: Fortune's Weave's DLSS evaluation, from its first pass (run
- /// earlier in the frame than the network pass the stand-in replaces), with the input colour
- /// image and camera jitter they go with.
+ /// Motion vectors and depth of Fire Emblem: Fortune's Weave's DLSS evaluation, from its first
+ /// pass (run earlier in the frame than the network pass the stand-in replaces), with the input
+ /// colour image and camera jitter they go with.
std::optional<Texture::TICEntry> dlss_motion_vectors;
+ std::optional<Texture::TICEntry> dlss_depth;
GPUVAddr dlss_motion_color{};
std::array<f32, 2> dlss_jitter{};
bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call);
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index 3dc26ec3ba..7fbf9a80be 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -73,6 +73,7 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/smaa_blending_weight_calculation.frag
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.vert
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.frag
+ ${CMAKE_CURRENT_SOURCE_DIR}/temporal_sharpen.frag
${CMAKE_CURRENT_SOURCE_DIR}/temporal_upscale.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_blit_depth_stencil.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.frag
@@ -212,6 +213,71 @@ foreach(VARIANT IN ITEMS ${SHADER_TYPE_VARIANTS})
set(SHADER_HEADERS ${SHADER_HEADERS} ${VARIANT_HEADER_FILE})
endforeach()
+# AMD FidelityFX Super Resolution 3.1 upscaler (externals/FidelityFX-SDK, unmodified): the SDK's
+# Vulkan pass files, compiled with one set of options (render-size motion vectors, reversed depth,
+# HDR colour, reference Lanczos), plus the sharpening and the no-wave-operation SPD variants.
+set(FSR3_GPU_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-SDK/include/FidelityFX/gpu)
+set(FSR3_PASS_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-SDK/shaders/fsr3upscaler)
+file(GLOB FSR3_GPU_FILES ${FSR3_GPU_DIR}/*.h ${FSR3_GPU_DIR}/fsr1/*.h ${FSR3_GPU_DIR}/spd/*.h
+ ${FSR3_GPU_DIR}/fsr3upscaler/*.h)
+set(FSR3_FLAGS
+ -DFFX_GPU=1
+ -DFFX_GLSL=1
+ -DFFX_FSR3UPSCALER_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF=0
+ -DFFX_FSR3UPSCALER_OPTION_ACCUMULATE_SAMPLERS_USE_DATA_HALF=0
+ -DFFX_FSR3UPSCALER_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF=1
+ -DFFX_FSR3UPSCALER_OPTION_POSTPROCESSLOCKSTATUS_SAMPLERS_USE_DATA_HALF=0
+ -DFFX_FSR3UPSCALER_OPTION_UPSAMPLE_USE_LANCZOS_TYPE=2
+ -DFFX_FSR3UPSCALER_OPTION_REPROJECT_USE_LANCZOS_TYPE=0
+ -DFFX_FSR3UPSCALER_OPTION_HDR_COLOR_INPUT=1
+ -DFFX_FSR3UPSCALER_OPTION_LOW_RESOLUTION_MOTION_VECTORS=1
+ -DFFX_FSR3UPSCALER_OPTION_JITTERED_MOTION_VECTORS=0
+ -DFFX_FSR3UPSCALER_OPTION_INVERTED_DEPTH=1
+)
+set(FSR3_VARIANTS
+ "prepare_inputs|prepare_inputs|none"
+ "luma_pyramid|luma_pyramid|none"
+ "luma_pyramid_nowave|luma_pyramid|-DFFX_SPD_NO_WAVE_OPERATIONS=1"
+ "shading_change_pyramid|shading_change_pyramid|none"
+ "shading_change_pyramid_nowave|shading_change_pyramid|-DFFX_SPD_NO_WAVE_OPERATIONS=1"
+ "shading_change|shading_change|none"
+ "prepare_reactivity|prepare_reactivity|none"
+ "luma_instability|luma_instability|none"
+ "accumulate|accumulate|none"
+ "accumulate_sharpen|accumulate|-DFFX_FSR3UPSCALER_OPTION_APPLY_SHARPENING=1"
+ "rcas|rcas|none"
+)
+
+foreach(VARIANT IN ITEMS ${FSR3_VARIANTS})
+ string(REPLACE "|" ";" VARIANT_PARTS "${VARIANT}")
+ list(GET VARIANT_PARTS 0 FSR3_NAME)
+ list(GET VARIANT_PARTS 1 FSR3_PASS)
+ list(GET VARIANT_PARTS 2 FSR3_DEFINE)
+ if (FSR3_DEFINE STREQUAL "none")
+ set(FSR3_DEFINE "")
+ endif()
+ if (NOT FSR3_DEFINE MATCHES "APPLY_SHARPENING")
+ list(APPEND FSR3_DEFINE -DFFX_FSR3UPSCALER_OPTION_APPLY_SHARPENING=0)
+ endif()
+ set(FSR3_SOURCE ${FSR3_PASS_DIR}/ffx_fsr3upscaler_${FSR3_PASS}_pass.glsl)
+ string(TOUPPER fsr3_upscaler_${FSR3_NAME}_comp_spv FSR3_VARIABLE_NAME)
+ set(FSR3_HEADER_FILE ${SHADER_DIR}/fsr3_upscaler_${FSR3_NAME}_comp_spv.h)
+ add_custom_command(
+ OUTPUT
+ ${FSR3_HEADER_FILE}
+ COMMAND
+ ${GLSLANGVALIDATOR} -V ${QUIET_FLAG} -S comp -I"${FSR3_GPU_DIR}"
+ -I"${FSR3_GPU_DIR}/fsr3upscaler" ${FSR3_FLAGS} ${FSR3_DEFINE}
+ --variable-name ${FSR3_VARIABLE_NAME} -o ${FSR3_HEADER_FILE} ${FSR3_SOURCE}
+ --target-env ${SPIR_V_VERSION}
+ MAIN_DEPENDENCY
+ ${FSR3_SOURCE}
+ DEPENDS
+ ${FSR3_GPU_FILES}
+ )
+ set(SHADER_HEADERS ${SHADER_HEADERS} ${FSR3_HEADER_FILE})
+endforeach()
+
foreach(FILEPATH IN ITEMS ${FIDELITYFX_FILES})
get_filename_component(FILENAME ${FILEPATH} NAME)
string(REPLACE "." "_" HEADER_NAME ${FILENAME})
diff --git a/src/video_core/host_shaders/temporal_sharpen.frag b/src/video_core/host_shaders/temporal_sharpen.frag
new file mode 100644
index 0000000000..45a13013cb
--- /dev/null
+++ b/src/video_core/host_shaders/temporal_sharpen.frag
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// RCAS (FidelityFX FSR 1's contrast-adaptive sharpening) on the DLSS stand-in's result, before the
+// game post-processes it. The input is linear HDR; RCAS's limits assume 0 to 1, so it sharpens in
+// the stand-in's range-compressed space.
+
+#version 460 core
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_ARB_shading_language_420pack : enable
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_EXT_shader_explicit_arithmetic_types : require
+
+layout(binding = 0) uniform sampler2D InputTexture;
+
+layout(push_constant) uniform PushConstants {
+ // The vertex stage's texture scale and offset come first.
+ layout(offset = 16) uvec4 Const0; // FsrRcasCon
+};
+
+layout(location = 0) in vec2 texcoord;
+layout(location = 0) out vec4 color;
+
+#define A_GPU 1
+#define A_GLSL 1
+#include "ffx_a.h"
+
+vec3 Compress(vec3 c) {
+ return c / (1.0 + max(max(c.r, c.g), c.b));
+}
+
+vec3 Expand(vec3 c) {
+ c = clamp(c, vec3(0.0), vec3(1.0 - 1.0 / 65504.0));
+ return c / (1.0 - max(max(c.r, c.g), c.b));
+}
+
+#define FSR_RCAS_F 1
+AF4 FsrRcasLoadF(ASU2 p) {
+ return AF4(Compress(texelFetch(InputTexture, p, 0).rgb), 1.0);
+}
+void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {}
+
+#include "ffx_fsr1.h"
+
+void main() {
+ AF3 c;
+ FsrRcasF(c.r, c.g, c.b, AU2(gl_FragCoord.xy), Const0);
+ color = vec4(Expand(c), 1.0);
+}
diff --git a/src/video_core/host_shaders/temporal_upscale.frag b/src/video_core/host_shaders/temporal_upscale.frag
index 0e76df2b43..ee31e035b1 100644
--- a/src/video_core/host_shaders/temporal_upscale.frag
+++ b/src/video_core/host_shaders/temporal_upscale.frag
@@ -11,12 +11,17 @@
layout(binding = 0) uniform sampler2D input_color;
layout(binding = 1) uniform sampler2D motion_vectors;
layout(binding = 2) uniform sampler2D history;
+layout(binding = 3, rgba16f) uniform writeonly image2D next_history;
layout(push_constant) uniform PushConstants {
// The vertex stage's texture scale and offset come first.
layout(offset = 16) vec2 jitter; // this frame's sample offset, in input pixels
vec2 motion_scale; // motion vector -> UV offset to the previous frame
float current_weight; // largest weight of this frame; 1 ignores the history
+ float kernel_scale; // accumulation kernel distance scale
+ float clip_width; // neighbourhood clip of still pixels, in deviations
+ float clip_motion; // clip of pixels moving a pixel or more per frame
+ float kernel_motion; // kernel distance scale of moving pixels
};
layout(location = 0) in vec2 texcoord; // 0 to 1 across the output
@@ -83,50 +88,92 @@ void main() {
const vec2 target = texcoord * vec2(input_size);
const ivec2 nearest = ivec2(floor(target + jitter));
+ // The longest motion around the pixel, so that an edge follows the object in front.
+ vec2 motion = vec2(0.0);
+ float longest = -1.0;
+ for (int y = -1; y <= 1; ++y) {
+ for (int x = -1; x <= 1; ++x) {
+ // The motion vectors can be at another scale than the colour (resolution scaling).
+ const ivec2 input_texel = clamp(nearest + ivec2(x, y), ivec2(0), input_size - 1);
+ const ivec2 texel = clamp(ivec2((vec2(input_texel) + 0.5) * vec2(motion_size) /
+ vec2(input_size)),
+ ivec2(0), motion_size - 1);
+ const vec2 candidate = texelFetch(motion_vectors, texel, 0).rg;
+ const float length_squared = dot(candidate, candidate);
+ if (length_squared > longest) {
+ longest = length_squared;
+ motion = candidate;
+ }
+ }
+ }
+ const vec2 previous = texcoord + motion * motion_scale;
+ // Still pixels: a narrow kernel (detail from many jitter positions accumulates)
+ // and a looser clip (keeps detail only some positions sample, e.g. thin bars). Moving pixels
+ // (a quarter pixel or more per frame): the wide kernel and tight clip, as their history is unreliable.
+ // The history's alpha remembers recent motion, decaying over a few frames: background that a
+ // moving object has just uncovered has no motion of its own but a history of that object.
+ const vec2 history_position = previous * vec2(textureSize(history, 0));
+ const float moved = smoothstep(0.02, 0.25, length(motion * motion_scale * vec2(textureSize(history, 0))));
+ const float moving = max(moved, texture(history, history_position).a);
+ const float kernel = mix(kernel_scale, kernel_motion, moving);
+ const float clip = mix(clip_width, clip_motion, moving);
+
vec3 filtered = vec3(0.0);
float total_weight = 0.0;
float nearest_weight = 0.0;
vec3 sum = vec3(0.0);
vec3 squares = vec3(0.0);
- vec2 motion = vec2(0.0);
- float longest = -1.0;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
const ivec2 texel = clamp(nearest + ivec2(x, y), ivec2(0), input_size - 1);
const vec3 sample_color = RgbToYCoCg(Compress(texelFetch(input_color, texel, 0).rgb));
// A Blackman-Harris-like window over the sample's distance to the output pixel.
- const vec2 distance = vec2(texel) + 0.5 - jitter - target;
+ const vec2 distance = (vec2(texel) + 0.5 - jitter - target) * kernel;
const float weight = exp(-2.29 * dot(distance, distance));
filtered += sample_color * weight;
total_weight += weight;
nearest_weight = max(nearest_weight, weight);
sum += sample_color;
squares += sample_color * sample_color;
- // The longest motion around the pixel, so that an edge follows the object in front.
- const vec2 candidate =
- texelFetch(motion_vectors, clamp(texel, ivec2(0), motion_size - 1), 0).rg;
- const float length_squared = dot(candidate, candidate);
- if (length_squared > longest) {
- longest = length_squared;
- motion = candidate;
- }
}
}
- const vec3 current = filtered / total_weight;
+ const vec3 current = filtered / max(total_weight, 1e-6);
const vec3 mean = sum / 9.0;
const vec3 deviation = sqrt(max(squares / 9.0 - mean * mean, vec3(0.0)));
- const vec3 low = mean - deviation;
- const vec3 high = mean + deviation;
+ const vec3 low = mean - clip * deviation;
+ const vec3 high = mean + clip * deviation;
- const vec2 previous = texcoord + motion * motion_scale;
// A frame whose samples land close to this pixel counts more; one whose samples land between
// pixels mostly keeps the history (from other jitter positions) there.
float weight = current_weight * nearest_weight;
+ vec3 result = current;
+ float remembered = max(moved, 0.85 * moving);
if (current_weight >= 1.0 || any(lessThan(previous, vec2(0.0))) ||
any(greaterThan(previous, vec2(1.0)))) {
- weight = 1.0;
+ // No usable history: the wide kernel, as a narrow one would show the input's pixels.
+ vec3 wide = vec3(0.0);
+ float wide_total = 0.0;
+ for (int y = -1; y <= 1; ++y) {
+ for (int x = -1; x <= 1; ++x) {
+ const ivec2 texel = clamp(nearest + ivec2(x, y), ivec2(0), input_size - 1);
+ const vec2 distance = vec2(texel) + 0.5 - jitter - target;
+ const float wide_weight = exp(-2.29 * dot(distance, distance));
+ wide += RgbToYCoCg(Compress(texelFetch(input_color, texel, 0).rgb)) * wide_weight;
+ wide_total += wide_weight;
+ }
+ }
+ result = wide / wide_total;
+ remembered = 1.0;
+ } else {
+ const vec3 unclipped = RgbToYCoCg(Compress(SampleHistory(history_position)));
+ const vec3 past = ClipToBox(unclipped, low, high);
+ result = mix(past, current, weight);
+ // A history far outside the neighbourhood means the content changed without
+ // motion vectors (some animations have none): treat the pixel as moving for a while.
+ const vec3 extent = 0.5 * (high - low) + 1.0 / 65504.0;
+ const vec3 units = abs(unclipped - 0.5 * (low + high)) / extent;
+ remembered = max(remembered, smoothstep(1.25, 2.5, max(units.x, max(units.y, units.z))));
}
- const vec2 history_position = previous * vec2(textureSize(history, 0));
- const vec3 past = ClipToBox(RgbToYCoCg(Compress(SampleHistory(history_position))), low, high);
- color = vec4(Expand(YCoCgToRgb(mix(past, current, weight))), 1.0);
+ color = vec4(Expand(YCoCgToRgb(result)), 1.0);
+ imageStore(next_history, ivec2(gl_FragCoord.xy), vec4(color.rgb, remembered));
}
diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h
index e9eaa2b3c2..363bb2d875 100644
--- a/src/video_core/rasterizer_interface.h
+++ b/src/video_core/rasterizer_interface.h
@@ -28,6 +28,9 @@ class AccelerateDMAInterface;
namespace Control {
struct ChannelState;
}
+namespace Texture {
+struct TICEntry;
+}
} // namespace Tegra
namespace VideoCore {
@@ -179,10 +182,12 @@ public:
/// Stand-in for a DLSS evaluation: temporal upscale of src into dst along motion_vectors
/// (jitter in src pixels; motion_scale turns a motion vector into a UV offset to the previous
- /// frame). False when unsupported; the caller then falls back to AccelerateSurfaceCopy.
+ /// frame), with the frame's depth when known. False when unsupported; the caller then falls
+ /// back to AccelerateSurfaceCopy.
[[nodiscard]] virtual bool AccelerateTemporalUpscale(
const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst,
- const Tegra::Engines::Fermi2D::Surface& motion_vectors, std::array<f32, 2> jitter,
+ const Tegra::Engines::Fermi2D::Surface& motion_vectors,
+ const Tegra::Texture::TICEntry* depth, std::array<f32, 2> jitter,
std::array<f32, 2> motion_scale) {
return false;
}
diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp
index 506daef11e..0628da1d0f 100644
--- a/src/video_core/renderer_vulkan/blit_image.cpp
+++ b/src/video_core/renderer_vulkan/blit_image.cpp
@@ -7,6 +7,7 @@
#include <algorithm>
#include "video_core/renderer_vulkan/vk_texture_cache.h"
+#include "video_core/fsr.h"
#include "common/div_ceil.h"
#include "common/settings.h"
@@ -34,6 +35,7 @@
#include "video_core/host_shaders/convert_non_msaa_to_msaa_depth_stencil_frag_spv.h"
#include "video_core/host_shaders/convert_s8d24_to_abgr8_frag_spv.h"
#include "video_core/host_shaders/full_screen_triangle_vert_spv.h"
+#include "video_core/host_shaders/temporal_sharpen_frag_spv.h"
#include "video_core/host_shaders/temporal_upscale_frag_spv.h"
#include "video_core/host_shaders/vulkan_blit_depth_stencil_frag_spv.h"
#include "video_core/host_shaders/vulkan_color_clear_frag_spv.h"
@@ -99,9 +101,19 @@ struct TemporalUpscalePushConstants {
std::array<float, 2> jitter;
std::array<float, 2> motion_scale;
float current_weight;
+ float kernel_scale;
+ float clip_width;
+ float clip_motion;
+ float kernel_motion;
float padding;
};
+// The vertex stage's PushConstants first, then temporal_sharpen.frag's FsrRcasCon.
+struct TemporalSharpenPushConstants {
+ PushConstants vertex;
+ std::array<u32, 4> rcas;
+};
+
struct MSAACopyPushConstants {
std::array<s32, 2> dst_offset;
std::array<s32, 2> src_offset;
@@ -120,10 +132,27 @@ constexpr std::array TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS{
TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<0>,
TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<1>,
};
-constexpr std::array THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS{
+// temporal_upscale.frag: input colour, motion vectors and history, then the next history (storage).
+constexpr std::array TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS{
TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<0>,
TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<1>,
TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<2>,
+ VkDescriptorSetLayoutBinding{
+ .binding = 3,
+ .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
+ .pImmutableSamplers = nullptr,
+ },
+};
+constexpr DescriptorBankInfo TEMPORAL_UPSCALE_DESCRIPTOR_BANK_INFO{
+ .uniform_buffers = 0,
+ .storage_buffers = 0,
+ .texture_buffers = 0,
+ .image_buffers = 0,
+ .textures = 3,
+ .images = 1,
+ .score = 2,
};
constexpr VkDescriptorSetLayoutCreateInfo ONE_TEXTURE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
@@ -149,12 +178,12 @@ constexpr VkDescriptorSetLayoutCreateInfo TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_CRE
.bindingCount = static_cast<u32>(TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()),
.pBindings = TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(),
};
-constexpr VkDescriptorSetLayoutCreateInfo THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{
+constexpr VkDescriptorSetLayoutCreateInfo TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
- .bindingCount = static_cast<u32>(THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()),
- .pBindings = THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(),
+ .bindingCount = static_cast<u32>(TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()),
+ .pBindings = TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(),
};
template <VkShaderStageFlags stageFlags, size_t size>
inline constexpr VkPushConstantRange PUSH_CONSTANT_RANGE{
@@ -359,11 +388,12 @@ constexpr std::array<VkPipelineShaderStageCreateInfo, 2> MakeStages(
}
void UpdateOneTextureDescriptorSet(const Device& device, VkDescriptorSet descriptor_set,
- VkSampler sampler, VkImageView image_view) {
+ VkSampler sampler, VkImageView image_view,
+ VkImageLayout image_layout = VK_IMAGE_LAYOUT_GENERAL) {
const VkDescriptorImageInfo image_info{
.sampler = sampler,
.imageView = image_view,
- .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .imageLayout = image_layout,
};
const VkWriteDescriptorSet write_descriptor_set{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
@@ -380,13 +410,14 @@ void UpdateOneTextureDescriptorSet(const Device& device, VkDescriptorSet descrip
device.GetLogical().UpdateDescriptorSets(write_descriptor_set, nullptr);
}
-void UpdateThreeTexturesDescriptorSet(const Device& device, VkDescriptorSet descriptor_set,
- VkSampler sampler, std::array<VkImageView, 3> image_views) {
- std::array<VkDescriptorImageInfo, 3> image_infos{};
- std::array<VkWriteDescriptorSet, 3> write_descriptor_sets{};
- for (u32 binding = 0; binding < 3; ++binding) {
+void UpdateTemporalUpscaleDescriptorSet(const Device& device, VkDescriptorSet descriptor_set,
+ VkSampler sampler, std::array<VkImageView, 4> image_views) {
+ std::array<VkDescriptorImageInfo, 4> image_infos{};
+ std::array<VkWriteDescriptorSet, 4> write_descriptor_sets{};
+ for (u32 binding = 0; binding < 4; ++binding) {
+ const bool storage = binding == 3;
image_infos[binding] = VkDescriptorImageInfo{
- .sampler = sampler,
+ .sampler = storage ? VK_NULL_HANDLE : sampler,
.imageView = image_views[binding],
.imageLayout = VK_IMAGE_LAYOUT_GENERAL,
};
@@ -397,7 +428,8 @@ void UpdateThreeTexturesDescriptorSet(const Device& device, VkDescriptorSet desc
.dstBinding = binding,
.dstArrayElement = 0,
.descriptorCount = 1,
- .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorType = storage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
+ : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.pImageInfo = &image_infos[binding],
.pBufferInfo = nullptr,
.pTexelBufferView = nullptr,
@@ -639,14 +671,14 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
ONE_TEXTURE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)),
two_textures_set_layout(device.GetLogical().CreateDescriptorSetLayout(
TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)),
- three_textures_set_layout(device.GetLogical().CreateDescriptorSetLayout(
- THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)),
+ temporal_upscale_set_layout(device.GetLogical().CreateDescriptorSetLayout(
+ TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)),
one_texture_descriptor_allocator{
descriptor_pool.Allocator(device_, scheduler_, *one_texture_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<1>)},
two_textures_descriptor_allocator{
descriptor_pool.Allocator(device_, scheduler_, *two_textures_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<2>)},
- three_textures_descriptor_allocator{
- descriptor_pool.Allocator(device_, scheduler_, *three_textures_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<3>)},
+ temporal_upscale_descriptor_allocator{descriptor_pool.Allocator(
+ device_, scheduler_, *temporal_upscale_set_layout, TEMPORAL_UPSCALE_DESCRIPTOR_BANK_INFO)},
one_texture_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
one_texture_set_layout.address(),
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))),
@@ -656,9 +688,14 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))),
temporal_upscale_pipeline_layout(
device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
- three_textures_set_layout.address(),
+ temporal_upscale_set_layout.address(),
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
sizeof(TemporalUpscalePushConstants)>))),
+ temporal_sharpen_pipeline_layout(
+ device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
+ one_texture_set_layout.address(),
+ PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
+ sizeof(TemporalSharpenPushConstants)>))),
clear_color_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
nullptr, PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(float) * 4>))),
msaa_copy_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
@@ -671,6 +708,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
full_screen_vert(BuildShader(device, FULL_SCREEN_TRIANGLE_VERT_SPV)),
blit_color_to_color_frag(BuildShader(device, BLIT_COLOR_FLOAT_FRAG_SPV)),
temporal_upscale_frag(BuildShader(device, TEMPORAL_UPSCALE_FRAG_SPV)),
+ temporal_sharpen_frag(BuildShader(device, TEMPORAL_SHARPEN_FRAG_SPV)),
blit_color_srgb_to_linear_frag(BuildShader(device, BLIT_COLOR_SRGB_TO_LINEAR_FRAG_SPV)),
blit_color_msaa_frag(BuildShader(device, BLIT_COLOR_MSAA_FRAG_SPV)),
blit_depth_stencil_frag(device.IsExtShaderStencilExportSupported()
@@ -752,7 +790,9 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView
TransitionImageLayout(cmdbuf, src_image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
BeginRenderPass(cmdbuf, dst_framebuffer);
const VkDescriptorSet descriptor_set = one_texture_descriptor_allocator.Commit();
- UpdateOneTextureDescriptorSet(device, descriptor_set, src_sampler, src_image_view);
+ // The source was just transitioned: describe it in that layout.
+ UpdateOneTextureDescriptorSet(device, descriptor_set, src_sampler, src_image_view,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set,
nullptr);
@@ -764,20 +804,26 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView
void BlitImageHelper::TemporalUpscale(const Framebuffer* dst_framebuffer,
const ImageView& input_color, const ImageView& motion_vectors,
- VkImageView history, const Region2D& dst_region,
+ VkImageView history, VkImageView next_history,
+ const Region2D& dst_region,
std::array<f32, 2> jitter, std::array<f32, 2> motion_scale,
- f32 current_weight) {
+ f32 current_weight, f32 kernel_scale, f32 clip_width,
+ f32 clip_motion, f32 kernel_motion) {
const VkPipelineLayout layout = *temporal_upscale_pipeline_layout;
const VkPipeline pipeline = FindOrEmplaceTemporalUpscalePipeline(dst_framebuffer->RenderPass());
const VkSampler sampler = *linear_sampler;
- const std::array<VkImageView, 3> views{input_color.Handle(Shader::TextureType::Color2D),
+ const std::array<VkImageView, 4> views{input_color.Handle(Shader::TextureType::Color2D),
motion_vectors.Handle(Shader::TextureType::Color2D),
- history};
+ history, next_history};
const TemporalUpscalePushConstants push_constants{
.vertex = {.tex_scale = {1.0f, 1.0f}, .tex_offset = {0.0f, 0.0f}},
.jitter = jitter,
.motion_scale = motion_scale,
.current_weight = current_weight,
+ .kernel_scale = kernel_scale,
+ .clip_width = clip_width,
+ .clip_motion = clip_motion,
+ .kernel_motion = kernel_motion,
.padding = 0.0f,
};
RecordShaderReadBarrier(scheduler, input_color);
@@ -785,8 +831,34 @@ void BlitImageHelper::TemporalUpscale(const Framebuffer* dst_framebuffer,
scheduler.RequestRenderpass(dst_framebuffer);
scheduler.Record([this, dst_region, pipeline, layout, sampler, views,
push_constants](vk::CommandBuffer cmdbuf) {
- const VkDescriptorSet descriptor_set = three_textures_descriptor_allocator.Commit();
- UpdateThreeTexturesDescriptorSet(device, descriptor_set, sampler, views);
+ const VkDescriptorSet descriptor_set = temporal_upscale_descriptor_allocator.Commit();
+ UpdateTemporalUpscaleDescriptorSet(device, descriptor_set, sampler, views);
+ cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
+ cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set,
+ nullptr);
+ BindBlitState(cmdbuf, dst_region);
+ cmdbuf.PushConstants(layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
+ push_constants);
+ cmdbuf.Draw(3, 1, 0, 0);
+ });
+ scheduler.InvalidateState();
+}
+
+void BlitImageHelper::TemporalSharpen(const Framebuffer* dst_framebuffer, VkImageView source,
+ const Region2D& dst_region, f32 sharpness) {
+ const VkPipelineLayout layout = *temporal_sharpen_pipeline_layout;
+ const VkPipeline pipeline = FindOrEmplaceTemporalSharpenPipeline(dst_framebuffer->RenderPass());
+ const VkSampler sampler = *nearest_sampler;
+ TemporalSharpenPushConstants push_constants{
+ .vertex = {.tex_scale = {1.0f, 1.0f}, .tex_offset = {0.0f, 0.0f}},
+ .rcas = {},
+ };
+ FSR::FsrRcasCon(push_constants.rcas.data(), sharpness);
+ scheduler.RequestRenderpass(dst_framebuffer);
+ scheduler.Record([this, dst_region, pipeline, layout, sampler, source,
+ push_constants](vk::CommandBuffer cmdbuf) {
+ const VkDescriptorSet descriptor_set = one_texture_descriptor_allocator.Commit();
+ UpdateOneTextureDescriptorSet(device, descriptor_set, sampler, source);
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set,
nullptr);
@@ -1437,6 +1509,59 @@ VkPipeline BlitImageHelper::FindOrEmplaceTemporalUpscalePipeline(VkRenderPass re
return *temporal_upscale_pipelines.back();
}
+VkPipeline BlitImageHelper::FindOrEmplaceTemporalSharpenPipeline(VkRenderPass renderpass) {
+ const auto it = std::ranges::find(temporal_sharpen_keys, renderpass);
+ if (it != temporal_sharpen_keys.end()) {
+ return *temporal_sharpen_pipelines[std::distance(temporal_sharpen_keys.begin(), it)];
+ }
+ temporal_sharpen_keys.push_back(renderpass);
+ const std::array stages = MakeStages(*full_screen_vert, *temporal_sharpen_frag);
+ const VkPipelineColorBlendAttachmentState blend_attachment{
+ .blendEnable = VK_FALSE,
+ .srcColorBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .dstColorBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .colorBlendOp = VK_BLEND_OP_ADD,
+ .srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .alphaBlendOp = VK_BLEND_OP_ADD,
+ .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
+ VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT,
+ };
+ const VkPipelineColorBlendStateCreateInfo color_blend_create_info{
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .logicOpEnable = VK_FALSE,
+ .logicOp = VK_LOGIC_OP_CLEAR,
+ .attachmentCount = 1,
+ .pAttachments = &blend_attachment,
+ .blendConstants = {0.0f, 0.0f, 0.0f, 0.0f},
+ };
+ const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device);
+ temporal_sharpen_pipelines.push_back(device.GetLogical().CreateGraphicsPipeline({
+ .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .stageCount = static_cast<u32>(stages.size()),
+ .pStages = stages.data(),
+ .pVertexInputState = &PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
+ .pInputAssemblyState = &input_assembly_ci,
+ .pTessellationState = nullptr,
+ .pViewportState = &PIPELINE_VIEWPORT_STATE_CREATE_INFO,
+ .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
+ .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
+ .pDepthStencilState = nullptr,
+ .pColorBlendState = &color_blend_create_info,
+ .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO,
+ .layout = *temporal_sharpen_pipeline_layout,
+ .renderPass = renderpass,
+ .subpass = 0,
+ .basePipelineHandle = VK_NULL_HANDLE,
+ .basePipelineIndex = 0,
+ }, device.StaticPipelineCache()));
+ return *temporal_sharpen_pipelines.back();
+}
+
VkPipeline BlitImageHelper::FindOrEmplaceDepthStencilPipeline(const BlitImagePipelineKey& key) {
const auto it = std::ranges::find(blit_depth_stencil_keys, key);
if (it != blit_depth_stencil_keys.end()) {
diff --git a/src/video_core/renderer_vulkan/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h
index 4741f9fe45..a1c802848b 100644
--- a/src/video_core/renderer_vulkan/blit_image.h
+++ b/src/video_core/renderer_vulkan/blit_image.h
@@ -83,11 +83,20 @@ public:
const Region2D& src_region, const Extent3D& src_size);
/// Temporal upscale (temporal_upscale.frag) into dst_framebuffer: the jittered input colour,
- /// accumulated with the history image reprojected along the motion vectors.
+ /// accumulated with the history image reprojected along the motion vectors. The result is also
+ /// stored to next_history (a storage image of the output's size).
void TemporalUpscale(const Framebuffer* dst_framebuffer, const ImageView& input_color,
const ImageView& motion_vectors, VkImageView history,
- const Region2D& dst_region, std::array<f32, 2> jitter,
- std::array<f32, 2> motion_scale, f32 current_weight);
+ VkImageView next_history, const Region2D& dst_region,
+ std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale, f32 current_weight,
+ f32 kernel_scale, f32 clip_width, f32 clip_motion,
+ f32 kernel_motion);
+
+ /// RCAS (temporal_sharpen.frag) of source into dst_framebuffer; sharpness in stops as
+ /// FsrRcasCon takes it (0 is the strongest).
+ void TemporalSharpen(const Framebuffer* dst_framebuffer, VkImageView source,
+ const Region2D& dst_region, f32 sharpness);
void BlitColorMSAA(const Framebuffer* dst_framebuffer, const ImageView& src_image_view,
const Region2D& dst_region, const Region2D& src_region);
@@ -179,6 +188,8 @@ private:
[[nodiscard]] VkPipeline FindOrEmplaceTemporalUpscalePipeline(VkRenderPass renderpass);
+ [[nodiscard]] VkPipeline FindOrEmplaceTemporalSharpenPipeline(VkRenderPass renderpass);
+
[[nodiscard]] VkPipeline FindOrEmplaceDepthStencilPipeline(const BlitImagePipelineKey& key);
[[nodiscard]] VkPipeline FindOrEmplaceClearColorPipeline(const BlitImagePipelineKey& key);
@@ -216,19 +227,21 @@ private:
vk::DescriptorSetLayout one_texture_set_layout;
vk::DescriptorSetLayout two_textures_set_layout;
- vk::DescriptorSetLayout three_textures_set_layout;
+ vk::DescriptorSetLayout temporal_upscale_set_layout;
DescriptorAllocator one_texture_descriptor_allocator;
DescriptorAllocator two_textures_descriptor_allocator;
- DescriptorAllocator three_textures_descriptor_allocator;
+ DescriptorAllocator temporal_upscale_descriptor_allocator;
vk::PipelineLayout one_texture_pipeline_layout;
vk::PipelineLayout two_textures_pipeline_layout;
vk::PipelineLayout temporal_upscale_pipeline_layout;
+ vk::PipelineLayout temporal_sharpen_pipeline_layout;
vk::PipelineLayout clear_color_pipeline_layout;
vk::PipelineLayout msaa_copy_pipeline_layout;
vk::PipelineLayout msaa_copy_depth_stencil_pipeline_layout;
vk::ShaderModule full_screen_vert;
vk::ShaderModule blit_color_to_color_frag;
vk::ShaderModule temporal_upscale_frag;
+ vk::ShaderModule temporal_sharpen_frag;
vk::ShaderModule blit_color_srgb_to_linear_frag;
vk::ShaderModule blit_color_msaa_frag;
vk::ShaderModule blit_depth_stencil_frag;
@@ -262,6 +275,8 @@ private:
std::vector<vk::Pipeline> blit_color_pipelines;
std::vector<VkRenderPass> temporal_upscale_keys;
std::vector<vk::Pipeline> temporal_upscale_pipelines;
+ std::vector<VkRenderPass> temporal_sharpen_keys;
+ std::vector<vk::Pipeline> temporal_sharpen_pipelines;
std::vector<BlitImagePipelineKey> blit_depth_stencil_keys;
std::vector<vk::Pipeline> blit_depth_stencil_pipelines;
std::vector<BlitImagePipelineKey> clear_color_keys;
diff --git a/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp b/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp
index b020d99b93..e19dabac46 100644
--- a/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp
+++ b/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp
@@ -31,7 +31,8 @@ struct DescriptorBank {
bool DescriptorBankInfo::IsSuperset(const DescriptorBankInfo& subset) const noexcept {
return uniform_buffers >= subset.uniform_buffers && storage_buffers >= subset.storage_buffers &&
texture_buffers >= subset.texture_buffers && image_buffers >= subset.image_buffers &&
- textures >= subset.textures && images >= subset.images;
+ textures >= subset.textures && images >= subset.images &&
+ sampled_images >= subset.sampled_images && samplers >= subset.samplers;
}
template <typename Descriptors>
@@ -59,7 +60,7 @@ static DescriptorBankInfo MakeBankInfo(std::span<const Shader::Info> infos) {
}
static void AllocatePool(const Device& device, DescriptorBank& bank) {
- std::array<VkDescriptorPoolSize, 6> pool_sizes;
+ std::array<VkDescriptorPoolSize, 8> pool_sizes;
size_t pool_cursor{};
const u32 sets_per_pool = device.GetSetsPerPool();
const auto add = [&](VkDescriptorType type, u32 count) {
@@ -77,6 +78,8 @@ static void AllocatePool(const Device& device, DescriptorBank& bank) {
add(VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, info.image_buffers);
add(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, info.textures);
add(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, info.images);
+ add(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, info.sampled_images);
+ add(VK_DESCRIPTOR_TYPE_SAMPLER, info.samplers);
bank.pools.push_back(device.GetLogical().CreateDescriptorPool({
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.pNext = nullptr,
diff --git a/src/video_core/renderer_vulkan/vk_descriptor_pool.h b/src/video_core/renderer_vulkan/vk_descriptor_pool.h
index 4efeb60373..8213649090 100644
--- a/src/video_core/renderer_vulkan/vk_descriptor_pool.h
+++ b/src/video_core/renderer_vulkan/vk_descriptor_pool.h
@@ -30,6 +30,8 @@ struct DescriptorBankInfo {
u32 image_buffers{}; ///< Number of image buffer descriptors
u32 textures{}; ///< Number of texture descriptors
u32 images{}; ///< Number of image descriptors
+ u32 sampled_images{}; ///< Number of sampled image descriptors (without a sampler)
+ u32 samplers{}; ///< Number of sampler descriptors
s32 score{}; ///< Number of descriptors in total
};
diff --git a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp
new file mode 100644
index 0000000000..99275800d1
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp
@@ -0,0 +1,693 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <cfloat>
+#include <cmath>
+#include <cstring>
+#include <numbers>
+#include <span>
+
+#include "common/alignment.h"
+#include "common/assert.h"
+#include "common/div_ceil.h"
+#include "common/logging.h"
+#include "video_core/fsr.h"
+#include "video_core/host_shaders/fsr3_upscaler_accumulate_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_accumulate_sharpen_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_luma_instability_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_luma_pyramid_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_luma_pyramid_nowave_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_prepare_inputs_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_prepare_reactivity_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_rcas_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_shading_change_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_shading_change_pyramid_comp_spv.h"
+#include "video_core/host_shaders/fsr3_upscaler_shading_change_pyramid_nowave_comp_spv.h"
+#include "video_core/renderer_vulkan/vk_compute_pass.h"
+#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
+#include "video_core/renderer_vulkan/vk_fsr3_upscaler.h"
+#include "video_core/renderer_vulkan/vk_scheduler.h"
+#include "video_core/renderer_vulkan/vk_staging_buffer_pool.h"
+#include "video_core/vulkan_common/vulkan_device.h"
+#include "video_core/vulkan_common/vulkan_memory_allocator.h"
+
+namespace Vulkan {
+namespace {
+
+// cbFSR3Upscaler in ffx_fsr3upscaler_callbacks_glsl.h (std140; the SDK's Fsr3UpscalerConstants).
+struct Constants {
+ std::array<s32, 2> render_size;
+ std::array<s32, 2> previous_render_size;
+ std::array<s32, 2> upscale_size;
+ std::array<s32, 2> previous_upscale_size;
+ std::array<s32, 2> max_render_size;
+ std::array<s32, 2> max_upscale_size;
+ std::array<f32, 4> device_to_view_depth;
+ std::array<f32, 2> jitter;
+ std::array<f32, 2> previous_jitter;
+ std::array<f32, 2> motion_vector_scale;
+ std::array<f32, 2> downscale_factor;
+ std::array<f32, 2> motion_vector_jitter_cancellation;
+ f32 tan_half_fov;
+ f32 jitter_phase_count;
+ f32 delta_time;
+ f32 delta_pre_exposure;
+ f32 view_space_to_meters;
+ f32 frame_index;
+ f32 velocity_factor;
+ f32 reactiveness_scale;
+ f32 shading_change_scale;
+ f32 accumulation_added_per_frame;
+ f32 min_disocclusion_accumulation;
+};
+static_assert(sizeof(Constants) == 148);
+
+// cbSPD and cbRCAS.
+struct SpdConstants {
+ u32 mips;
+ u32 num_work_groups;
+ std::array<u32, 2> work_group_offset;
+ std::array<u32, 2> render_size;
+};
+
+struct RcasConstants {
+ std::array<u32, 4> config;
+};
+
+// Descriptor kinds in binding order, as the pass files number them (FSR3UPSCALER_BIND_*). Every
+// pass also has the two samplers at bindings 1000 (point clamp) and 1001 (linear clamp).
+enum class Kind : u8 { Texture, Image, Uniform };
+constexpr Kind T = Kind::Texture;
+constexpr Kind I = Kind::Image;
+constexpr Kind U = Kind::Uniform;
+
+constexpr std::array PREPARE_INPUTS{T, T, T, I, I, I, I, I, U};
+constexpr std::array LUMA_PYRAMID{T, T, I, I, I, I, I, I, I, I, I, U, U};
+constexpr std::array SHADING_CHANGE_PYRAMID{T, T, T, T, I, I, I, I, I, I, I, U, U};
+constexpr std::array SHADING_CHANGE{T, I, U};
+constexpr std::array PREPARE_REACTIVITY{T, T, T, T, T, T, T, T, T, I, I, I, U};
+constexpr std::array LUMA_INSTABILITY{T, T, T, T, T, T, T, I, I, U};
+constexpr std::array ACCUMULATE{T, T, T, T, T, T, T, T, T, I, I, I, U};
+constexpr std::array RCAS{T, T, I, U, U};
+
+enum PassId : size_t {
+ PassPrepareInputs,
+ PassLumaPyramid,
+ PassShadingChangePyramid,
+ PassShadingChange,
+ PassPrepareReactivity,
+ PassLumaInstability,
+ PassAccumulate,
+ PassAccumulateSharpen,
+ PassRcas,
+ PassCount,
+};
+
+constexpr u32 SPD_STORED_MIPS = 6;
+
+/// One descriptor of a dispatch: an image view, or a uniform buffer range.
+struct Resource {
+ VkImageView view{};
+ VkBuffer buffer{};
+ VkDeviceSize offset{};
+ VkDeviceSize size{};
+};
+
+constexpr VkMemoryBarrier MemoryBarrier(VkAccessFlags src, VkAccessFlags dst) {
+ return VkMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = src,
+ .dstAccessMask = dst,
+ };
+}
+
+constexpr VkImageSubresourceRange ALL_LEVELS{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = VK_REMAINING_MIP_LEVELS,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+};
+
+// ffxFsr3UpscalerGetJitterPhaseCount.
+s32 JitterPhaseCount(u32 render_width, u32 display_width) {
+ const f32 ratio = static_cast<f32>(display_width) / static_cast<f32>(render_width);
+ return static_cast<s32>(8.0f * ratio * ratio);
+}
+
+// DLSS takes no camera, so FSR's depth-to-view-space conversion (which only scales its
+// disocclusion thresholds) uses a typical near plane and vertical field of view.
+constexpr f32 CAMERA_NEAR = 0.1f;
+constexpr f32 CAMERA_FOV = 60.0f * std::numbers::pi_v<f32> / 180.0f;
+
+} // Anonymous namespace
+
+class Fsr3Upscaler::Pass final : public ComputePass {
+public:
+ explicit Pass(const Device& device_, Scheduler& scheduler, DescriptorPool& descriptor_pool,
+ std::span<const Kind> kinds, std::span<const u32> code,
+ const std::array<VkSampler, 2>& samplers)
+ : Pass(device_, scheduler, descriptor_pool, MakeLayout(kinds, samplers), code) {}
+
+ void Dispatch(Scheduler& scheduler, ComputePassDescriptorQueue& queue,
+ std::span<const Resource> resources, u32 groups_x, u32 groups_y) {
+ queue.Acquire(scheduler, resources.size());
+ for (const Resource& resource : resources) {
+ if (resource.buffer) {
+ queue.AddBuffer(resource.buffer, resource.offset, resource.size);
+ } else {
+ queue.AddImage(resource.view);
+ }
+ }
+ const void* const descriptor_data{queue.UpdateData()};
+ scheduler.Record([this, descriptor_data, groups_x, groups_y](vk::CommandBuffer cmdbuf) {
+ const VkDescriptorSet set = descriptor_allocator.Commit();
+ device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data);
+ cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
+ cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
+ cmdbuf.Dispatch(groups_x, groups_y, 1);
+ // The next pass reads what this one wrote (everything stays in GENERAL).
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0,
+ MemoryBarrier(VK_ACCESS_SHADER_WRITE_BIT,
+ VK_ACCESS_SHADER_READ_BIT |
+ VK_ACCESS_SHADER_WRITE_BIT));
+ });
+ }
+
+private:
+ struct Layout {
+ std::vector<VkDescriptorSetLayoutBinding> bindings;
+ std::vector<VkDescriptorUpdateTemplateEntry> templates;
+ DescriptorBankInfo bank;
+ };
+
+ explicit Pass(const Device& device_, Scheduler& scheduler, DescriptorPool& descriptor_pool,
+ const Layout& pass_layout, std::span<const u32> code)
+ : ComputePass(device_, scheduler, descriptor_pool, pass_layout.bindings,
+ pass_layout.templates, pass_layout.bank, {}, code) {}
+
+ static Layout MakeLayout(std::span<const Kind> kinds,
+ const std::array<VkSampler, 2>& samplers) {
+ Layout result;
+ for (u32 binding = 0; binding < kinds.size(); ++binding) {
+ VkDescriptorType type{};
+ switch (kinds[binding]) {
+ case Kind::Texture:
+ type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
+ ++result.bank.sampled_images;
+ break;
+ case Kind::Image:
+ type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
+ ++result.bank.images;
+ break;
+ case Kind::Uniform:
+ type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+ ++result.bank.uniform_buffers;
+ break;
+ }
+ result.bindings.push_back({
+ .binding = binding,
+ .descriptorType = type,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
+ .pImmutableSamplers = nullptr,
+ });
+ result.templates.push_back({
+ .dstBinding = binding,
+ .dstArrayElement = 0,
+ .descriptorCount = 1,
+ .descriptorType = type,
+ .offset = binding * sizeof(DescriptorUpdateEntry),
+ .stride = sizeof(DescriptorUpdateEntry),
+ });
+ }
+ for (u32 index = 0; index < samplers.size(); ++index) {
+ result.bindings.push_back({
+ .binding = 1000 + index,
+ .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
+ .pImmutableSamplers = &samplers[index],
+ });
+ ++result.bank.samplers;
+ }
+ result.bank.score = static_cast<s32>(result.bindings.size());
+ return result;
+ }
+};
+
+Fsr3Upscaler::Fsr3Upscaler(const Device& device_, Scheduler& scheduler_,
+ MemoryAllocator& memory_allocator_,
+ StagingBufferPool& staging_buffer_pool_,
+ DescriptorPool& descriptor_pool,
+ ComputePassDescriptorQueue& descriptor_queue_)
+ : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_},
+ staging_buffer_pool{staging_buffer_pool_}, descriptor_queue{descriptor_queue_} {
+ const auto sampler = [&](VkFilter filter, VkSamplerMipmapMode mipmap) {
+ return device.GetLogical().CreateSampler(VkSamplerCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .magFilter = filter,
+ .minFilter = filter,
+ .mipmapMode = mipmap,
+ .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 = 1.0f,
+ .compareEnable = VK_FALSE,
+ .compareOp = VK_COMPARE_OP_NEVER,
+ .minLod = 0.0f,
+ .maxLod = VK_LOD_CLAMP_NONE,
+ .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
+ .unnormalizedCoordinates = VK_FALSE,
+ });
+ };
+ point_sampler = sampler(VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_NEAREST);
+ linear_sampler = sampler(VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_LINEAR);
+ const std::array<VkSampler, 2> samplers{*point_sampler, *linear_sampler};
+
+ // SPD reduces with quad shuffles where the device has them in compute shaders.
+ const bool quads = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_QUAD_BIT) &&
+ (device.GetSubgroupSupportedStages() & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
+ const auto pass = [&](std::span<const Kind> kinds, std::span<const u32> code) {
+ return std::make_unique<Pass>(device, scheduler, descriptor_pool, kinds, code, samplers);
+ };
+ passes.resize(PassCount);
+ passes[PassPrepareInputs] = pass(PREPARE_INPUTS, FSR3_UPSCALER_PREPARE_INPUTS_COMP_SPV);
+ passes[PassLumaPyramid] =
+ pass(LUMA_PYRAMID, quads ? std::span<const u32>(FSR3_UPSCALER_LUMA_PYRAMID_COMP_SPV)
+ : std::span<const u32>(FSR3_UPSCALER_LUMA_PYRAMID_NOWAVE_COMP_SPV));
+ passes[PassShadingChangePyramid] = pass(
+ SHADING_CHANGE_PYRAMID,
+ quads ? std::span<const u32>(FSR3_UPSCALER_SHADING_CHANGE_PYRAMID_COMP_SPV)
+ : std::span<const u32>(FSR3_UPSCALER_SHADING_CHANGE_PYRAMID_NOWAVE_COMP_SPV));
+ passes[PassShadingChange] = pass(SHADING_CHANGE, FSR3_UPSCALER_SHADING_CHANGE_COMP_SPV);
+ passes[PassPrepareReactivity] =
+ pass(PREPARE_REACTIVITY, FSR3_UPSCALER_PREPARE_REACTIVITY_COMP_SPV);
+ passes[PassLumaInstability] = pass(LUMA_INSTABILITY, FSR3_UPSCALER_LUMA_INSTABILITY_COMP_SPV);
+ passes[PassAccumulate] = pass(ACCUMULATE, FSR3_UPSCALER_ACCUMULATE_COMP_SPV);
+ passes[PassAccumulateSharpen] = pass(ACCUMULATE, FSR3_UPSCALER_ACCUMULATE_SHARPEN_COMP_SPV);
+ passes[PassRcas] = pass(RCAS, FSR3_UPSCALER_RCAS_COMP_SPV);
+}
+
+Fsr3Upscaler::~Fsr3Upscaler() = default;
+
+bool Fsr3Upscaler::IsSupported(const Device& device) {
+ if (!device.IsStorageImageExtendedFormatsSupported()) {
+ return false;
+ }
+ constexpr VkFormatFeatureFlags storage =
+ VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
+ VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
+ for (const VkFormat format :
+ {VK_FORMAT_R8_UNORM, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R32_SFLOAT,
+ VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT}) {
+ if (!device.IsFormatSupported(format, storage, FormatType::Optimal)) {
+ return false;
+ }
+ }
+ return device.IsFormatSupported(VK_FORMAT_R32_UINT, VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT,
+ FormatType::Optimal) &&
+ device.IsFormatSupported(VK_FORMAT_R32G32B32A32_SFLOAT,
+ VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT, FormatType::Optimal) &&
+ device.IsFormatSupported(VK_FORMAT_R16_SNORM,
+ VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
+ VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT,
+ FormatType::Optimal);
+}
+
+VkImage Fsr3Upscaler::OutputImage() const noexcept {
+ return *output.image;
+}
+
+VkImageView Fsr3Upscaler::OutputView() const noexcept {
+ return *output.view;
+}
+
+Fsr3Upscaler::Texture Fsr3Upscaler::CreateTexture(VkFormat format, u32 width, u32 height,
+ u32 levels) {
+ Texture texture;
+ texture.image = memory_allocator.CreateImage(VkImageCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = format,
+ .extent = {(std::max)(width, 1U), (std::max)(height, 1U), 1},
+ .mipLevels = levels,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_OPTIMAL,
+ .usage = static_cast<VkImageUsageFlags>(
+ VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
+ (format == VK_FORMAT_R16_SNORM ? 0 : VK_IMAGE_USAGE_STORAGE_BIT)),
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .queueFamilyIndexCount = 0,
+ .pQueueFamilyIndices = nullptr,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ });
+ const auto view = [&](u32 base_level, u32 level_count) {
+ return device.GetLogical().CreateImageView(VkImageViewCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .image = *texture.image,
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = format,
+ .components{},
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = base_level,
+ .levelCount = level_count,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ },
+ });
+ };
+ texture.view = view(0, levels);
+ if (levels > 1) {
+ for (u32 level = 0; level < levels; ++level) {
+ texture.levels.push_back(view(level, 1));
+ }
+ }
+ return texture;
+}
+
+void Fsr3Upscaler::CreateResources(VkExtent2D render_size, VkExtent2D upscale_size) {
+ const u32 rw = render_size.width;
+ const u32 rh = render_size.height;
+ const u32 uw = upscale_size.width;
+ const u32 uh = upscale_size.height;
+ for (size_t i = 0; i < 2; ++i) {
+ accumulation[i] = CreateTexture(VK_FORMAT_R8_UNORM, rw, rh);
+ luma[i] = CreateTexture(VK_FORMAT_R16_SFLOAT, rw, rh);
+ // The pass files declare the luma history as rgba8 (the SDK allocates RGBA16F, which a
+ // storage format qualifier does not match); allocate what the shaders store.
+ luma_history[i] = CreateTexture(VK_FORMAT_R8G8B8A8_UNORM, rw, rh);
+ internal_upscaled[i] = CreateTexture(VK_FORMAT_R16G16B16A16_SFLOAT, uw, uh);
+ }
+ farthest_depth = CreateTexture(VK_FORMAT_R16_SFLOAT, rw, rh);
+ shading_change = CreateTexture(VK_FORMAT_R8_UNORM, rw / 2, rh / 2);
+ new_locks = CreateTexture(VK_FORMAT_R8_UNORM, uw, uh);
+ const u32 spd_levels =
+ static_cast<u32>(std::floor(std::log2(static_cast<f32>((std::max)(rw / 2, rh / 2))))) + 1;
+ spd_mips = CreateTexture(VK_FORMAT_R16G16_SFLOAT, rw / 2, rh / 2,
+ (std::max)(spd_levels, SPD_STORED_MIPS));
+ farthest_depth_mip1 = CreateTexture(VK_FORMAT_R16_SFLOAT, rw / 2, rh / 2);
+ dilated_motion_vectors = CreateTexture(VK_FORMAT_R16G16_SFLOAT, rw, rh);
+ dilated_depth = CreateTexture(VK_FORMAT_R32_SFLOAT, rw, rh);
+ previous_nearest_depth = CreateTexture(VK_FORMAT_R32_UINT, rw, rh);
+ dilated_reactive_masks = CreateTexture(VK_FORMAT_R8G8B8A8_UNORM, rw, rh);
+ frame_info = CreateTexture(VK_FORMAT_R32G32B32A32_SFLOAT, 1, 1);
+ spd_atomic = CreateTexture(VK_FORMAT_R32_UINT, 1, 1);
+ lanczos_lut = CreateTexture(VK_FORMAT_R16_SNORM, 128, 1);
+ default_reactive = CreateTexture(VK_FORMAT_R8_UNORM, 1, 1);
+ output = CreateTexture(VK_FORMAT_R16G16B16A16_SFLOAT, uw, uh);
+ render_extent = render_size;
+ upscale_extent = upscale_size;
+ first_execution = true;
+ frame_parity = 0;
+ LOG_INFO(Render_Vulkan, "FSR 3.1 upscaler: {}x{} -> {}x{}", rw, rh, uw, uh);
+}
+
+void Fsr3Upscaler::Upscale(const Inputs& in) {
+ if (!output.image || in.render_size.width != render_extent.width ||
+ in.render_size.height != render_extent.height ||
+ in.upscale_size.width != upscale_extent.width ||
+ in.upscale_size.height != upscale_extent.height) {
+ if (output.image) {
+ scheduler.Finish(); // the old images may still be in use
+ }
+ CreateResources(in.render_size, in.upscale_size);
+ }
+ const bool first = first_execution;
+ const bool reset = in.reset || first;
+ first_execution = false;
+
+ const s32 rw = static_cast<s32>(render_extent.width);
+ const s32 rh = static_cast<s32>(render_extent.height);
+ const s32 uw = static_cast<s32>(upscale_extent.width);
+ const s32 uh = static_cast<s32>(upscale_extent.height);
+
+ // Constants, as fsr3upscalerDispatch computes them.
+ Constants constants{};
+ constants.render_size = {rw, rh};
+ constants.previous_render_size = first ? constants.render_size : previous_render_size;
+ constants.upscale_size = {uw, uh};
+ constants.previous_upscale_size = first ? constants.upscale_size : previous_upscale_size;
+ constants.max_render_size = constants.render_size;
+ constants.max_upscale_size = constants.upscale_size;
+ {
+ // Reversed infinite depth (setupDeviceDepthToViewSpaceDepthParams with near = FLT_MAX).
+ const f32 near_plane = CAMERA_NEAR;
+ const f32 fov = CAMERA_FOV;
+ const f32 aspect = static_cast<f32>(rw) / static_cast<f32>(rh);
+ const f32 cot_half_fov = std::cos(0.5f * fov) / std::sin(0.5f * fov);
+ constants.device_to_view_depth = {-1.0f * (0.0f + FLT_EPSILON), near_plane,
+ 1.0f / (cot_half_fov / aspect), 1.0f / cot_half_fov};
+ constants.tan_half_fov = std::tan(std::atan(std::tan(fov * 0.5f) * aspect));
+ }
+ constants.jitter = in.jitter;
+ constants.previous_jitter = first ? in.jitter : previous_jitter;
+ constants.motion_vector_scale = {in.motion_vector_scale[0] / static_cast<f32>(rw),
+ in.motion_vector_scale[1] / static_cast<f32>(rh)};
+ constants.downscale_factor = {static_cast<f32>(rw) / static_cast<f32>(uw),
+ static_cast<f32>(rh) / static_cast<f32>(uh)};
+ constants.motion_vector_jitter_cancellation = {0.0f, 0.0f};
+ const s32 phase_count = JitterPhaseCount(render_extent.width, upscale_extent.width);
+ if (reset || jitter_phase_count == 0.0f) {
+ jitter_phase_count = static_cast<f32>(phase_count);
+ } else if (static_cast<f32>(phase_count) > jitter_phase_count) {
+ jitter_phase_count += 1.0f;
+ } else if (static_cast<f32>(phase_count) < jitter_phase_count) {
+ jitter_phase_count -= 1.0f;
+ }
+ constants.jitter_phase_count = jitter_phase_count;
+ constants.delta_time = std::clamp(in.frame_time_ms / 1000.0f, 0.0f, 1.0f);
+ constants.delta_pre_exposure = 1.0f;
+ constants.view_space_to_meters = 1.0f;
+ frame_index = reset ? 0.0f : frame_index + 1.0f;
+ constants.frame_index = frame_index;
+ // The SDK's defaults (FFX_FSR3UPSCALER_CONFIGURE_UPSCALE_KEY_*).
+ constants.velocity_factor = 1.0f;
+ constants.reactiveness_scale = 1.0f;
+ constants.shading_change_scale = 1.0f;
+ constants.accumulation_added_per_frame = 1.0f / 3.0f;
+ constants.min_disocclusion_accumulation = -1.0f / 3.0f;
+ previous_render_size = constants.render_size;
+ previous_upscale_size = constants.upscale_size;
+ previous_jitter = in.jitter;
+
+ // ffxSpdSetup over the whole render area.
+ const u32 spd_groups_x = (render_extent.width - 1) / 64 + 1;
+ const u32 spd_groups_y = (render_extent.height - 1) / 64 + 1;
+ const SpdConstants spd{
+ .mips = static_cast<u32>((std::min)(
+ std::floor(std::log2(static_cast<f32>(
+ (std::max)(render_extent.width, render_extent.height)))),
+ 12.0f)),
+ .num_work_groups = spd_groups_x * spd_groups_y,
+ .work_group_offset = {0, 0},
+ .render_size = {render_extent.width, render_extent.height},
+ };
+ RcasConstants rcas{};
+ FSR::FsrRcasCon(rcas.config.data(), 2.0f - 2.0f * std::clamp(in.sharpness, 0.0f, 1.0f));
+
+ const VkDeviceSize alignment = (std::max<VkDeviceSize>)(device.GetUniformBufferAlignment(), 16);
+ const VkDeviceSize slot = Common::AlignUp(VkDeviceSize{sizeof(Constants)}, alignment);
+ const VkDeviceSize spd_slot = Common::AlignUp(VkDeviceSize{sizeof(SpdConstants)}, alignment);
+ const VkDeviceSize rcas_slot = Common::AlignUp(VkDeviceSize{sizeof(RcasConstants)}, alignment);
+ const StagingBufferRef uniforms =
+ staging_buffer_pool.Request(slot + spd_slot + rcas_slot, MemoryUsage::Upload);
+ std::memcpy(uniforms.mapped_span.data(), &constants, sizeof(constants));
+ std::memcpy(uniforms.mapped_span.data() + slot, &spd, sizeof(spd));
+ std::memcpy(uniforms.mapped_span.data() + slot + spd_slot, &rcas, sizeof(rcas));
+ const Resource cb_fsr{.buffer = uniforms.buffer, .offset = uniforms.offset, .size = slot};
+ const Resource cb_spd{
+ .buffer = uniforms.buffer, .offset = uniforms.offset + slot, .size = spd_slot};
+ const Resource cb_rcas{
+ .buffer = uniforms.buffer, .offset = uniforms.offset + slot + spd_slot, .size = rcas_slot};
+
+ // Clears: everything when the images are new, the history on a reset, and the per-frame
+ // scratch (previous nearest depth, the SPD counter and mips) always.
+ const size_t p = frame_parity;
+ const size_t q = p ^ 1;
+ std::vector<VkImage> clear_zero{*previous_nearest_depth.image, *spd_atomic.image,
+ *spd_mips.image};
+ std::vector<VkImage> clear_frame_info;
+ if (reset) {
+ clear_zero.push_back(*accumulation[p].image);
+ clear_frame_info.push_back(*frame_info.image);
+ }
+ std::vector<VkImage> initialize;
+ if (first) {
+ for (const auto* textures : {&accumulation, &luma, &luma_history, &internal_upscaled}) {
+ for (const Texture& texture : *textures) {
+ initialize.push_back(*texture.image);
+ }
+ }
+ for (const Texture* texture :
+ {&farthest_depth, &shading_change, &new_locks, &spd_mips, &farthest_depth_mip1,
+ &dilated_motion_vectors, &dilated_depth, &previous_nearest_depth,
+ &dilated_reactive_masks, &frame_info, &spd_atomic, &lanczos_lut, &default_reactive,
+ &output}) {
+ initialize.push_back(*texture->image);
+ }
+ clear_zero = initialize;
+ std::erase(clear_zero, *lanczos_lut.image);
+ std::erase(clear_zero, *frame_info.image);
+ clear_frame_info = {*frame_info.image};
+ }
+ std::optional<StagingBufferRef> lut_upload;
+ if (first) {
+ // FSR's Lanczos-2 table (fsr3upscalerCreate).
+ std::array<s16, 128> weights{};
+ for (size_t i = 0; i < weights.size(); ++i) {
+ const f32 x = 2.0f * static_cast<f32>(i) / static_cast<f32>(weights.size() - 1);
+ const f32 pi_x = std::numbers::pi_v<f32> * x;
+ const f32 y = std::abs(x) < FLT_EPSILON
+ ? 1.0f
+ : (std::sin(pi_x) / pi_x) * (std::sin(0.5f * pi_x) / (0.5f * pi_x));
+ weights[i] = static_cast<s16>(std::round(y * 32767.0f));
+ }
+ lut_upload = staging_buffer_pool.Request(sizeof(weights), MemoryUsage::Upload);
+ std::memcpy(lut_upload->mapped_span.data(), weights.data(), sizeof(weights));
+ }
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([initialize, clear_zero, clear_frame_info, lut = *lanczos_lut.image,
+ lut_buffer = lut_upload ? lut_upload->buffer : VkBuffer{},
+ lut_offset = lut_upload ? lut_upload->offset : 0](vk::CommandBuffer cmdbuf) {
+ // The inputs come from the game's draws and dispatches; our images from the last frame.
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ 0,
+ MemoryBarrier(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
+ VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
+ VK_ACCESS_SHADER_WRITE_BIT |
+ VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT |
+ VK_ACCESS_TRANSFER_WRITE_BIT));
+ if (!initialize.empty()) {
+ std::vector<VkImageMemoryBarrier> barriers;
+ for (const VkImage image : initialize) {
+ barriers.push_back(VkImageMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = 0,
+ .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = image,
+ .subresourceRange = ALL_LEVELS,
+ });
+ }
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
+ 0, {}, {}, barriers);
+ }
+ if (lut_buffer) {
+ const VkBufferImageCopy copy{
+ .bufferOffset = lut_offset,
+ .bufferRowLength = 0,
+ .bufferImageHeight = 0,
+ .imageSubresource{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .mipLevel = 0,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ },
+ .imageOffset{0, 0, 0},
+ .imageExtent{128, 1, 1},
+ };
+ cmdbuf.CopyBufferToImage(lut_buffer, lut, VK_IMAGE_LAYOUT_GENERAL, copy);
+ }
+ for (const VkImage image : clear_zero) {
+ cmdbuf.ClearColorImage(image, VK_IMAGE_LAYOUT_GENERAL, VkClearColorValue{}, ALL_LEVELS);
+ }
+ for (const VkImage image : clear_frame_info) {
+ VkClearColorValue value{};
+ value.float32[0] = -1.0f;
+ value.float32[1] = 1.0f;
+ cmdbuf.ClearColorImage(image, VK_IMAGE_LAYOUT_GENERAL, value, ALL_LEVELS);
+ }
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ 0,
+ MemoryBarrier(VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT));
+ });
+
+ const auto tex = [](const Texture& texture) { return Resource{.view = *texture.view}; };
+ const auto mip = [&](u32 level) {
+ return Resource{.view = *spd_mips.levels[(std::min)(level, static_cast<u32>(
+ spd_mips.levels.size() - 1))]};
+ };
+ const Resource color{.view = in.color};
+ const Resource depth{.view = in.depth};
+ const Resource motion{.view = in.motion_vectors};
+ const Resource exposure = tex(frame_info); // FSR's own exposure (auto exposure)
+ const Resource current_luma = tex(luma[p]);
+ const auto groups = [](s32 size, u32 tile) {
+ return Common::DivCeil(static_cast<u32>(size), tile);
+ };
+
+ passes[PassPrepareInputs]->Dispatch(
+ scheduler, descriptor_queue,
+ std::array{motion, depth, color, tex(dilated_motion_vectors), tex(dilated_depth),
+ tex(previous_nearest_depth), tex(farthest_depth), current_luma, cb_fsr},
+ groups(rw, 8), groups(rh, 8));
+ passes[PassLumaPyramid]->Dispatch(
+ scheduler, descriptor_queue,
+ std::array{current_luma, tex(farthest_depth), tex(spd_atomic), tex(frame_info), mip(0),
+ mip(1), mip(2), mip(3), mip(4), mip(5), tex(farthest_depth_mip1), cb_fsr,
+ cb_spd},
+ spd_groups_x, spd_groups_y);
+ passes[PassShadingChangePyramid]->Dispatch(
+ scheduler, descriptor_queue,
+ std::array{current_luma, tex(luma[q]), tex(dilated_motion_vectors), exposure,
+ tex(spd_atomic), mip(0), mip(1), mip(2), mip(3), mip(4), mip(5), cb_fsr, cb_spd},
+ spd_groups_x, spd_groups_y);
+ passes[PassShadingChange]->Dispatch(
+ scheduler, descriptor_queue, std::array{tex(spd_mips), tex(shading_change), cb_fsr},
+ groups(rw / 2, 8), groups(rh / 2, 8));
+ passes[PassPrepareReactivity]->Dispatch(
+ scheduler, descriptor_queue,
+ std::array{tex(previous_nearest_depth), tex(dilated_motion_vectors), tex(dilated_depth),
+ tex(default_reactive), tex(default_reactive), tex(accumulation[p]),
+ tex(shading_change), current_luma, exposure, tex(dilated_reactive_masks),
+ tex(new_locks), tex(accumulation[q]), cb_fsr},
+ groups(rw, 8), groups(rh, 8));
+ passes[PassLumaInstability]->Dispatch(
+ scheduler, descriptor_queue,
+ std::array{exposure, tex(dilated_reactive_masks), tex(dilated_motion_vectors),
+ tex(frame_info), tex(luma_history[p]), tex(farthest_depth_mip1), current_luma,
+ tex(luma_history[q]), tex(farthest_depth), cb_fsr},
+ groups(rw, 8), groups(rh, 8));
+ passes[in.sharpen ? PassAccumulateSharpen : PassAccumulate]->Dispatch(
+ scheduler, descriptor_queue,
+ std::array{exposure, tex(dilated_reactive_masks), tex(dilated_motion_vectors),
+ tex(internal_upscaled[p]), tex(lanczos_lut), tex(farthest_depth_mip1),
+ current_luma, tex(farthest_depth), color, tex(internal_upscaled[q]),
+ tex(output), tex(new_locks), cb_fsr},
+ groups(uw, 8), groups(uh, 8));
+ if (in.sharpen) {
+ passes[PassRcas]->Dispatch(
+ scheduler, descriptor_queue,
+ std::array{exposure, tex(internal_upscaled[q]), tex(output), cb_fsr, cb_rcas},
+ groups(uw, 16), groups(uh, 16));
+ }
+ scheduler.Record([](vk::CommandBuffer cmdbuf) {
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
+ MemoryBarrier(VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT));
+ });
+ frame_parity = q;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h
new file mode 100644
index 0000000000..e62b5fdeff
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h
@@ -0,0 +1,119 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <memory>
+#include <vector>
+
+#include "common/common_types.h"
+#include "video_core/renderer_vulkan/vk_update_descriptor.h"
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+namespace Vulkan {
+
+class DescriptorPool;
+class Device;
+class MemoryAllocator;
+class Scheduler;
+class StagingBufferPool;
+
+/// AMD FidelityFX Super Resolution 3.1 (the FSR 3.1.4 upscaler from FidelityFX SDK v1.1.4, shaders
+/// in externals/FidelityFX-SDK) in place of a game's DLSS evaluation. The host side follows the
+/// SDK's ffx_fsr3upscaler.cpp: the same passes, internal images and constants, recorded through the
+/// scheduler. Inputs are taken as reversed depth, render-size motion vectors and HDR colour, and
+/// the exposure is computed by FSR itself.
+class Fsr3Upscaler {
+public:
+ struct Inputs {
+ VkImageView color; ///< Jittered linear colour at the render size.
+ VkImageView depth; ///< Device depth (reversed) at the render size.
+ VkImageView motion_vectors; ///< Motion to the previous frame at the render size.
+ VkExtent2D render_size;
+ VkExtent2D upscale_size;
+ /// This frame's sample offset in render pixels: input texel k sampled k + 0.5 - jitter.
+ std::array<f32, 2> jitter;
+ /// Turns a motion vector into render pixels (the render size for UV offsets).
+ std::array<f32, 2> motion_vector_scale;
+ f32 frame_time_ms;
+ bool reset;
+ bool sharpen;
+ f32 sharpness; ///< 0 to 1 as FSR takes it: 2 stops of RCAS at 0, none at 1.
+ };
+
+ explicit Fsr3Upscaler(const Device& device, Scheduler& scheduler,
+ MemoryAllocator& memory_allocator, StagingBufferPool& staging_buffer_pool,
+ DescriptorPool& descriptor_pool,
+ ComputePassDescriptorQueue& descriptor_queue);
+ ~Fsr3Upscaler();
+
+ /// Whether the device has what the passes need (formats, shader features).
+ [[nodiscard]] static bool IsSupported(const Device& device);
+
+ /// Records one evaluation into Output(): RGBA16F at the upscale size, GENERAL layout, ready to
+ /// be sampled by a fragment shader.
+ void Upscale(const Inputs& inputs);
+
+ [[nodiscard]] VkImage OutputImage() const noexcept;
+ [[nodiscard]] VkImageView OutputView() const noexcept;
+
+ /// Nearest-neighbour clamp sampler (normalized coordinates), for reading Output().
+ [[nodiscard]] VkSampler PointSampler() const noexcept {
+ return *point_sampler;
+ }
+
+private:
+ class Pass;
+
+ struct Texture {
+ vk::Image image;
+ vk::ImageView view; ///< Every level.
+ std::vector<vk::ImageView> levels; ///< One view per level, for images with mips.
+ };
+
+ void CreateResources(VkExtent2D render_size, VkExtent2D upscale_size);
+ Texture CreateTexture(VkFormat format, u32 width, u32 height, u32 levels = 1);
+
+ const Device& device;
+ Scheduler& scheduler;
+ MemoryAllocator& memory_allocator;
+ StagingBufferPool& staging_buffer_pool;
+ ComputePassDescriptorQueue& descriptor_queue;
+
+ vk::Sampler point_sampler;
+ vk::Sampler linear_sampler;
+ std::vector<std::unique_ptr<Pass>> passes;
+
+ // Ping-pong pairs, indexed by frame parity like the SDK's resource tables.
+ std::array<Texture, 2> accumulation;
+ std::array<Texture, 2> luma;
+ std::array<Texture, 2> luma_history;
+ std::array<Texture, 2> internal_upscaled;
+ Texture farthest_depth; ///< Also the luma instability (the SDK aliases them).
+ Texture shading_change;
+ Texture new_locks;
+ Texture spd_mips;
+ Texture farthest_depth_mip1;
+ Texture dilated_motion_vectors;
+ Texture dilated_depth;
+ Texture previous_nearest_depth;
+ Texture dilated_reactive_masks;
+ Texture frame_info;
+ Texture spd_atomic;
+ Texture lanczos_lut;
+ Texture default_reactive;
+ Texture output;
+
+ VkExtent2D render_extent{};
+ VkExtent2D upscale_extent{};
+ std::array<s32, 2> previous_render_size{};
+ std::array<s32, 2> previous_upscale_size{};
+ std::array<f32, 2> previous_jitter{};
+ f32 jitter_phase_count{};
+ f32 frame_index{};
+ u32 frame_parity{};
+ bool first_execution{true};
+};
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index 3898c571f5..0e5124dba6 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -1305,10 +1305,10 @@ bool RasterizerVulkan::AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surf
bool RasterizerVulkan::AccelerateTemporalUpscale(
const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst,
- const Tegra::Engines::Fermi2D::Surface& motion_vectors, std::array<f32, 2> jitter,
- std::array<f32, 2> motion_scale) {
+ const Tegra::Engines::Fermi2D::Surface& motion_vectors, const Tegra::Texture::TICEntry* depth,
+ std::array<f32, 2> jitter, std::array<f32, 2> motion_scale) {
std::scoped_lock lock{texture_cache.mutex};
- return texture_cache.TemporalUpscale(dst, src, motion_vectors, jitter, motion_scale);
+ return texture_cache.TemporalUpscale(dst, src, motion_vectors, depth, jitter, motion_scale);
}
Tegra::Engines::AccelerateDMAInterface& RasterizerVulkan::AccessAccelerateDMA() {
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h
index dbb6ebdca8..7fa536f53f 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.h
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.h
@@ -133,6 +133,7 @@ public:
bool AccelerateTemporalUpscale(const Tegra::Engines::Fermi2D::Surface& src,
const Tegra::Engines::Fermi2D::Surface& dst,
const Tegra::Engines::Fermi2D::Surface& motion_vectors,
+ const Tegra::Texture::TICEntry* depth,
std::array<f32, 2> jitter,
std::array<f32, 2> motion_scale) override;
Tegra::Engines::AccelerateDMAInterface& AccessAccelerateDMA() override;
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 928146027d..e3f427dead 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -1203,11 +1203,13 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched
StagingBufferPool& staging_buffer_pool_,
BlitImageHelper& blit_image_helper_,
RenderPassCache& render_pass_cache_,
- DescriptorPool& descriptor_pool,
- ComputePassDescriptorQueue& compute_pass_descriptor_queue)
+ DescriptorPool& descriptor_pool_,
+ ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
: device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_},
staging_buffer_pool{staging_buffer_pool_}, blit_image_helper{blit_image_helper_},
- render_pass_cache{render_pass_cache_}, resolution{Settings::values.resolution_info} {
+ render_pass_cache{render_pass_cache_}, descriptor_pool{descriptor_pool_},
+ compute_pass_descriptor_queue{compute_pass_descriptor_queue_},
+ resolution{Settings::values.resolution_info} {
// Before anything below reads astc_recompression (passes, view formats).
UseBc3ForSwitch2OnSmallCards(device);
// Huge ASTC arrays always decode on the GPU (IsHugeAstcArray), whatever the decode setting.
@@ -1535,49 +1537,99 @@ void TextureCacheRuntime::ReinterpretImage(Image& dst, Image& src,
});
}
-bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst,
- ImageView& input_color, ImageView& motion_vectors,
+bool TextureCacheRuntime::WantsTemporalUpscaleDepth() const {
+ // FSR 3.1 replaces DLSS; the temporal stand-in only runs when FSR cannot (no depth, a device
+ // without the formats, a failed start). EDEN_NX2_DLSS_REPLACEMENT=standin forces
+ // the stand-in for A/B runs.
+ static const bool force_standin = [] {
+ const char* const value = std::getenv("EDEN_NX2_DLSS_REPLACEMENT");
+ return value && std::string_view{value} == "standin";
+ }();
+ return !force_standin && !fsr3_unsupported;
+}
+
+bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageView& input_color,
+ ImageView& motion_vectors, ImageView& depth,
+ std::array<u32, 2> render_size,
std::array<f32, 2> jitter,
std::array<f32, 2> motion_scale) {
- if (dst.Samples() != VK_SAMPLE_COUNT_1_BIT || ImageAspectMask(dst.format) != VK_IMAGE_ASPECT_COLOR_BIT) {
+ if (!WantsTemporalUpscaleDepth()) {
return false;
}
- const VkFormat format =
- MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, dst.format).format;
- const VkExtent2D extent{dst.size.width, dst.size.height};
- const VkImage output = dst.ImageHandle();
- bool reset = false;
- if (!temporal_history.image || temporal_history.format != format ||
- temporal_history.extent.width != extent.width ||
- temporal_history.extent.height != extent.height) {
- if (temporal_history.image) {
- scheduler.Finish(); // the old history may still be sampled
+ if (!fsr3_upscaler) {
+ if (!Fsr3Upscaler::IsSupported(device)) {
+ LOG_WARNING(Render_Vulkan, "FSR 3.1 needs formats or features this device lacks; "
+ "using the temporal stand-in");
+ fsr3_unsupported = true;
+ return false;
}
- temporal_history.image = memory_allocator.CreateImage(VkImageCreateInfo{
- .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
- .pNext = nullptr,
- .flags = 0,
- .imageType = VK_IMAGE_TYPE_2D,
- .format = format,
- .extent = {extent.width, extent.height, 1},
- .mipLevels = 1,
- .arrayLayers = 1,
- .samples = VK_SAMPLE_COUNT_1_BIT,
- .tiling = VK_IMAGE_TILING_OPTIMAL,
- .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
- .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
- .queueFamilyIndexCount = 0,
- .pQueueFamilyIndices = nullptr,
- .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
- });
- temporal_history.view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{
- .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ try {
+ fsr3_upscaler = std::make_unique<Fsr3Upscaler>(device, scheduler, memory_allocator,
+ staging_buffer_pool, descriptor_pool,
+ compute_pass_descriptor_queue);
+ } catch (const vk::Exception& exception) {
+ LOG_ERROR(Render_Vulkan, "FSR 3.1 failed to start ({}); using the temporal stand-in",
+ exception.what());
+ fsr3_unsupported = true;
+ return false;
+ }
+ }
+ const VkExtent2D extent = dst_framebuffer->RenderArea();
+ // A gap in the evaluations (a menu, a movie, a load) starts the history again.
+ const auto now = std::chrono::steady_clock::now();
+ const f32 frame_time_ms =
+ fsr3_last_evaluation
+ ? std::chrono::duration<f32, std::milli>(now - *fsr3_last_evaluation).count()
+ : 1000.0f / 30.0f;
+ const bool reset = !fsr3_last_evaluation || frame_time_ms > 250.0f;
+ fsr3_last_evaluation = now;
+ // FSR's own RCAS, set by the sharpness slider: FSR takes 0-1 for 2 to 0 stops, and the slider
+ // stores stops x 100, so this is the percentage the slider shows; 0% (stored 200) is off.
+ // EDEN_NX2_TEMPORAL_SHARPEN=<stops>|off overrides the slider.
+ static const char* const sharpen_override = std::getenv("EDEN_NX2_TEMPORAL_SHARPEN");
+ const int slider = Settings::values.fsr_sharpening_slider.GetValue();
+ bool sharpen = slider < 200;
+ f32 stops = static_cast<f32>(slider) / 100.0f;
+ if (sharpen_override) {
+ sharpen = std::string_view{sharpen_override} != "off";
+ stops = static_cast<f32>(std::strtod(sharpen_override, nullptr));
+ }
+ fsr3_upscaler->Upscale({
+ .color = input_color.Handle(Shader::TextureType::Color2D),
+ .depth = depth.DepthView(),
+ .motion_vectors = motion_vectors.Handle(Shader::TextureType::Color2D),
+ .render_size = {render_size[0], render_size[1]},
+ .upscale_size = extent,
+ .jitter = jitter,
+ .motion_vector_scale = {motion_scale[0] * static_cast<f32>(render_size[0]),
+ motion_scale[1] * static_cast<f32>(render_size[1])},
+ .frame_time_ms = frame_time_ms,
+ .reset = reset,
+ .sharpen = sharpen,
+ .sharpness = 1.0f - stops / 2.0f,
+ });
+ // Draw the result into the DLSS output, which keeps its own format; the blit leaves FSR's
+ // output image in SHADER_READ_ONLY_OPTIMAL, so return it to GENERAL for the next frame.
+ const Region2D region{
+ .start = {0, 0},
+ .end = {static_cast<s32>(extent.width), static_cast<s32>(extent.height)},
+ };
+ const VkImage output = fsr3_upscaler->OutputImage();
+ blit_image_helper.BlitColor(dst_framebuffer, fsr3_upscaler->OutputView(), output,
+ fsr3_upscaler->PointSampler(), region, region,
+ {extent.width, extent.height, 1});
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([output](vk::CommandBuffer cmdbuf) {
+ const VkImageMemoryBarrier barrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
- .flags = 0,
- .image = *temporal_history.image,
- .viewType = VK_IMAGE_VIEW_TYPE_2D,
- .format = format,
- .components{},
+ .srcAccessMask = VK_ACCESS_SHADER_READ_BIT,
+ .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = output,
.subresourceRange{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
@@ -1585,7 +1637,82 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie
.baseArrayLayer = 0,
.layerCount = 1,
},
- });
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, barrier);
+ });
+ static bool logged{};
+ if (!logged) {
+ logged = true;
+ LOG_INFO(Render_Vulkan, "DLSS replaced by FSR 3.1: {}x{} -> {}x{}", render_size[0],
+ render_size[1], extent.width, extent.height);
+ }
+ return true;
+}
+
+bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst,
+ ImageView& input_color, ImageView& motion_vectors,
+ ImageView* depth, std::array<u32, 2> render_size,
+ std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale) {
+ if (dst.Samples() != VK_SAMPLE_COUNT_1_BIT || ImageAspectMask(dst.format) != VK_IMAGE_ASPECT_COLOR_BIT) {
+ return false;
+ }
+ if (depth && UpscaleWithFsr3(dst_framebuffer, input_color, motion_vectors, *depth, render_size,
+ jitter, motion_scale)) {
+ return true;
+ }
+ const VkFormat format =
+ MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, dst.format).format;
+ // The framebuffer's area is the output's size on the host (scaled with resolution scaling).
+ const VkExtent2D extent = dst_framebuffer->RenderArea();
+ // The history is kept at half-float precision: in the output's format (B10G11R11 here, 5-6
+ // mantissa bits) a frame's small share of the blend rounded away, so the history never
+ // converged on the detail the jittered frames add.
+ constexpr VkFormat history_format = VK_FORMAT_R16G16B16A16_SFLOAT;
+ bool reset = false;
+ if (!temporal_history.images[0] || temporal_history.format != format ||
+ temporal_history.extent.width != extent.width ||
+ temporal_history.extent.height != extent.height) {
+ if (temporal_history.images[0]) {
+ scheduler.Finish(); // the old history may still be in use
+ }
+ for (size_t i = 0; i < temporal_history.images.size(); ++i) {
+ temporal_history.images[i] = memory_allocator.CreateImage(VkImageCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = history_format,
+ .extent = {extent.width, extent.height, 1},
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_OPTIMAL,
+ .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .queueFamilyIndexCount = 0,
+ .pQueueFamilyIndices = nullptr,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ });
+ temporal_history.views[i] = device.GetLogical().CreateImageView(VkImageViewCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .image = *temporal_history.images[i],
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = history_format,
+ .components{},
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ },
+ });
+ }
+ temporal_history.current = 0;
temporal_history.format = format;
temporal_history.extent = extent;
reset = true;
@@ -1593,24 +1720,15 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie
static_cast<u32>(format));
}
- // The history holds this pass's previous result, copied right after drawing it: the game may
- // draw into its DLSS output afterwards (post-processing), which must not feed back. The history
- // stays in the GENERAL layout, which the blit helper's descriptors use.
- const VkImage history = *temporal_history.image;
- const VkImageSubresourceLayers output_layers{
- .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
- .mipLevel = static_cast<u32>(dst.range.base.level),
- .baseArrayLayer = static_cast<u32>(dst.range.base.layer),
- .layerCount = 1,
- };
- static constexpr VkImageSubresourceLayers history_layers{
- .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
- .mipLevel = 0,
- .baseArrayLayer = 0,
- .layerCount = 1,
- };
+ // The shader reads the previous result from one history image and stores this one to the
+ // other: the game may draw into its DLSS output afterwards (post-processing), which must not
+ // feed back. Both stay in the GENERAL layout, which the blit helper's descriptors use.
+ const size_t read_index = temporal_history.current;
+ const size_t write_index = read_index ^ 1;
+ const VkImage history = *temporal_history.images[read_index];
+ const VkImage next_history = *temporal_history.images[write_index];
const auto barrier = [](VkImage image, VkAccessFlags src_access, VkAccessFlags dst_access,
- VkImageLayout old_layout, const VkImageSubresourceLayers& layers) {
+ VkImageLayout old_layout) {
return VkImageMemoryBarrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
@@ -1623,20 +1741,24 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie
.image = image,
.subresourceRange{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
- .baseMipLevel = layers.mipLevel,
+ .baseMipLevel = 0,
.levelCount = 1,
- .baseArrayLayer = layers.baseArrayLayer,
+ .baseArrayLayer = 0,
.layerCount = 1,
},
};
};
if (reset) {
- // A new history image has no contents yet; the first frame does not read it.
+ // New history images have no contents yet; the first frame does not use what it reads.
scheduler.RequestOutsideRenderPassOperationContext();
- scheduler.Record([history, barrier](vk::CommandBuffer cmdbuf) {
- cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
- 0, barrier(history, 0, VK_ACCESS_SHADER_READ_BIT,
- VK_IMAGE_LAYOUT_UNDEFINED, history_layers));
+ scheduler.Record([history, next_history, barrier](vk::CommandBuffer cmdbuf) {
+ constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
+ const std::array barriers{
+ barrier(history, 0, access, VK_IMAGE_LAYOUT_UNDEFINED),
+ barrier(next_history, 0, access, VK_IMAGE_LAYOUT_UNDEFINED),
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
+ VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, {}, {}, barriers);
});
}
const Region2D dst_region{
@@ -1644,42 +1766,79 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie
.end = {static_cast<s32>(extent.width), static_cast<s32>(extent.height)},
};
// Each frame weighs at most 0.1 against the history (scaled by how close its samples land to
- // the pixel; the neighbourhood clip limits what older samples contribute).
- constexpr f32 current_weight = 0.1f;
+ // the pixel; the neighbourhood clip limits what older samples contribute). Still pixels use a
+ // sample kernel a third as wide as for moving ones (the jittered frames fill in the detail) and
+ // a clip of 1.5 deviations instead of 1.
+ // EDEN_NX2_TEMPORAL_WEIGHT / _KERNEL / _CLIP / _CLIP_MOTION / _KERNEL_MOTION
+ // override them (all 1 = the previous stand-in's filter).
+ const auto tuned = [](const char* name, f32 fallback) {
+ const char* value = std::getenv(name);
+ return value ? static_cast<f32>(std::strtod(value, nullptr)) : fallback;
+ };
+ static const f32 current_weight = tuned("EDEN_NX2_TEMPORAL_WEIGHT", 0.1f);
+ static const f32 kernel_scale = tuned("EDEN_NX2_TEMPORAL_KERNEL", 3.0f);
+ static const f32 clip_width = tuned("EDEN_NX2_TEMPORAL_CLIP", 1.5f);
+ static const f32 clip_motion = tuned("EDEN_NX2_TEMPORAL_CLIP_MOTION", 1.0f);
+ static const f32 kernel_motion = tuned("EDEN_NX2_TEMPORAL_KERNEL_MOTION", 1.0f);
blit_image_helper.TemporalUpscale(dst_framebuffer, input_color, motion_vectors,
- *temporal_history.view, dst_region, jitter, motion_scale,
- reset ? 1.0f : current_weight);
+ *temporal_history.views[read_index],
+ *temporal_history.views[write_index], dst_region, jitter,
+ motion_scale, reset ? 1.0f : current_weight, kernel_scale,
+ clip_width, clip_motion, kernel_motion);
+ // RCAS on the output, set by the FSR sharpness slider (stops as FSR takes them, 0% = stored 200
+ // turns it off). It reads the unsharpened result just stored, which stays the history.
+ // EDEN_NX2_TEMPORAL_SHARPEN=<stops>|off overrides the slider.
+ static const char* const sharpen_override = std::getenv("EDEN_NX2_TEMPORAL_SHARPEN");
+ const int slider = Settings::values.fsr_sharpening_slider.GetValue();
+ bool sharpen = slider < 200;
+ f32 sharpness = static_cast<f32>(slider) / 100.0f;
+ if (sharpen_override) {
+ sharpen = std::string_view{sharpen_override} != "off";
+ sharpness = static_cast<f32>(std::strtod(sharpen_override, nullptr));
+ }
+ const VkImage output = dst.ImageHandle();
+ const u32 output_level = static_cast<u32>(dst.range.base.level);
+ const u32 output_layer = static_cast<u32>(dst.range.base.layer);
scheduler.RequestOutsideRenderPassOperationContext();
- scheduler.Record([output, history, extent, output_layers, barrier](vk::CommandBuffer cmdbuf) {
- constexpr VkAccessFlags written = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- const std::array before{
- barrier(output, written, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL,
- output_layers),
- barrier(history, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
- VK_IMAGE_LAYOUT_GENERAL, history_layers),
- };
- cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
- VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
- VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before);
- const VkImageCopy copy{
- .srcSubresource = output_layers,
- .srcOffset = {0, 0, 0},
- .dstSubresource = history_layers,
- .dstOffset = {0, 0, 0},
- .extent = {extent.width, extent.height, 1},
- };
- cmdbuf.CopyImage(output, VK_IMAGE_LAYOUT_GENERAL, history, VK_IMAGE_LAYOUT_GENERAL, copy);
- const std::array after{
- barrier(output, VK_ACCESS_TRANSFER_READ_BIT,
- VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
- VK_ACCESS_TRANSFER_READ_BIT,
- VK_IMAGE_LAYOUT_GENERAL, output_layers),
- barrier(history, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
- VK_IMAGE_LAYOUT_GENERAL, history_layers),
+ scheduler.Record([history, next_history, barrier, output, output_level, output_layer,
+ sharpen](vk::CommandBuffer cmdbuf) {
+ // The next frame reads what was stored now and stores over what was read now; the
+ // sharpening pass reads what was stored now and draws over the output again.
+ const std::array barriers{
+ barrier(next_history, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
+ VK_IMAGE_LAYOUT_GENERAL),
+ barrier(history, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT,
+ VK_IMAGE_LAYOUT_GENERAL),
+ VkImageMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = output,
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = output_level,
+ .levelCount = 1,
+ .baseArrayLayer = output_layer,
+ .layerCount = 1,
+ },
+ },
};
- cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0,
- {}, {}, after);
+ constexpr VkPipelineStageFlags stages =
+ VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+ cmdbuf.PipelineBarrier(stages, stages, 0, {}, {},
+ vk::Span<VkImageMemoryBarrier>(barriers.data(),
+ sharpen ? barriers.size() : 2));
});
+ if (sharpen) {
+ blit_image_helper.TemporalSharpen(dst_framebuffer, *temporal_history.views[write_index],
+ dst_region, sharpness);
+ }
+ temporal_history.current = write_index;
return true;
}
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index 564b2cbf9e..e7dbb97628 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -6,13 +6,17 @@
#pragma once
+#include <chrono>
#include <deque>
+#include <memory>
+#include <optional>
#include <span>
#include "video_core/texture_cache/texture_cache_base.h"
#include "shader_recompiler/shader_info.h"
#include "video_core/renderer_vulkan/vk_compute_pass.h"
+#include "video_core/renderer_vulkan/vk_fsr3_upscaler.h"
#include "video_core/renderer_vulkan/vk_render_pass_cache.h"
#include "video_core/renderer_vulkan/vk_staging_buffer_pool.h"
#include "video_core/texture_cache/image_view_base.h"
@@ -99,12 +103,17 @@ public:
Tegra::Engines::Fermi2D::Filter filter,
Tegra::Engines::Fermi2D::Operation operation);
- /// Stands in for a DLSS evaluation: temporal upscale of input_color into dst, accumulated with
- /// a copy of dst's previous contents (the last output). False when dst cannot take it.
+ /// Stands in for a DLSS evaluation: temporal upscale of input_color (render_size on the host)
+ /// into dst, with FSR 3.1 when given the depth, else with the temporal stand-in. False when dst
+ /// cannot take it.
bool TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& input_color,
- ImageView& motion_vectors, std::array<f32, 2> jitter,
+ ImageView& motion_vectors, ImageView* depth,
+ std::array<u32, 2> render_size, std::array<f32, 2> jitter,
std::array<f32, 2> motion_scale);
+ /// True when TemporalUpscale would use the depth (FSR 3.1 not forced off and not failed).
+ [[nodiscard]] bool WantsTemporalUpscaleDepth() const;
+
void CopyImage(Image& dst, Image& src, std::span<const VideoCommon::ImageCopy> copies);
void CopyImageMSAA(Image& dst, Image& src, std::span<const VideoCommon::ImageCopy> copies);
@@ -187,11 +196,24 @@ public:
StagingBufferPool& staging_buffer_pool;
BlitImageHelper& blit_image_helper;
RenderPassCache& render_pass_cache;
+ DescriptorPool& descriptor_pool;
+ ComputePassDescriptorQueue& compute_pass_descriptor_queue;
+
+ /// FSR 3.1 in place of a DLSS evaluation (TemporalUpscale), created on first use.
+ bool UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageView& input_color,
+ ImageView& motion_vectors, ImageView& depth,
+ std::array<u32, 2> render_size, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale);
+ std::unique_ptr<Fsr3Upscaler> fsr3_upscaler;
+ bool fsr3_unsupported{};
+ std::optional<std::chrono::steady_clock::time_point> fsr3_last_evaluation;
- /// TemporalUpscale's history: its previous result, copied right after drawing it.
+ /// TemporalUpscale's history: its previous result (images[current]) and the image the next
+ /// result is stored to.
struct TemporalHistory {
- vk::Image image;
- vk::ImageView view;
+ std::array<vk::Image, 2> images;
+ std::array<vk::ImageView, 2> views;
+ size_t current{};
VkFormat format{};
VkExtent2D extent{};
} temporal_history;
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 4929c2df44..abb22b9bfb 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -1098,6 +1098,7 @@ template <class P>
bool TextureCache<P>::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& dst,
const Tegra::Engines::Fermi2D::Surface& src,
const Tegra::Engines::Fermi2D::Surface& motion_vectors,
+ const Tegra::Texture::TICEntry* depth,
std::array<f32, 2> jitter, std::array<f32, 2> motion_scale) {
if constexpr (FRAMEBUFFER_BLITS) {
return false;
@@ -1123,6 +1124,12 @@ bool TextureCache<P>::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds
has_deleted_images = false;
const ImageId motion_id =
FindOrInsertImage(motion_info, motion_vectors.Address(), RelaxedOptions{});
+ // The depth only feeds FSR 3.1 (looked up only when FSR can run); without it the stand-in
+ // runs.
+ ImageId depth_id{};
+ if (depth && motion_id && runtime.WantsTemporalUpscaleDepth()) {
+ depth_id = FindOrInsertImage(ImageInfo(*depth), depth->Address(), RelaxedOptions{});
+ }
if (has_deleted_images || !motion_id) {
return false; // the colour and output ids may be stale; the caller blits instead
}
@@ -1131,12 +1138,47 @@ bool TextureCache<P>::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds
PrepareImage(src_id, false, false);
PrepareImage(motion_id, false, false);
PrepareImage(dst_id, true, false);
+ if (depth_id) {
+ PrepareImage(depth_id, false, false);
+ if (slot_images[depth_id].info.num_samples != 1 ||
+ slot_images[depth_id].info.format != PixelFormat::D32_FLOAT) {
+ depth_id = {};
+ }
+ }
for (const ImageId id : {src_id, motion_id, dst_id}) {
- if (True(slot_images[id].flags & ImageFlagBits::Rescaled) ||
- slot_images[id].info.num_samples != 1) {
+ if (slot_images[id].info.num_samples != 1) {
return false;
}
}
+ // With resolution scaling, bring the images that can follow to the same scale, as BlitImage
+ // does; the stand-in reads each image at its own size, so any left behind still work.
+ const auto rescaled = [&](ImageId id) {
+ return True(slot_images[id].flags & ImageFlagBits::Rescaled);
+ };
+ if (rescaled(src_id) || rescaled(motion_id) || rescaled(dst_id) ||
+ (depth_id && rescaled(depth_id))) {
+ for (const ImageId id : {src_id, motion_id, dst_id, depth_id}) {
+ if (id && !rescaled(id) && ImageCanRescale(slot_images[id])) {
+ ScaleUp(slot_images[id]);
+ }
+ }
+ }
+ // FSR 3.1 reads colour, motion and depth texel for texel: all at the colour's scale.
+ if (depth_id && (rescaled(depth_id) != rescaled(src_id) ||
+ rescaled(motion_id) != rescaled(src_id))) {
+ depth_id = {};
+ }
+ const Extent3D src_size = slot_images[src_id].info.size;
+ const std::array<u32, 2> render_size =
+ rescaled(src_id)
+ ? std::array{Settings::values.resolution_info.ScaleUp(src_size.width),
+ Settings::values.resolution_info.ScaleUp(src_size.height)}
+ : std::array{src_size.width, src_size.height};
+ // The jitter is in the input's guest pixels.
+ if (rescaled(src_id)) {
+ const f32 factor = Settings::values.resolution_info.up_factor;
+ jitter = {jitter[0] * factor, jitter[1] * factor};
+ }
const auto view = [&](ImageId id, const Tegra::Engines::Fermi2D::Surface& surface,
PixelFormat format) -> std::optional<std::pair<FramebufferId, ImageViewId>> {
const std::optional base = slot_images[id].TryFindBase(surface.Address());
@@ -1152,12 +1194,21 @@ bool TextureCache<P>::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds
if (!src_view || !motion_view || !dst_view) {
return false;
}
+ ImageViewId depth_view{};
+ if (depth_id) {
+ if (const std::optional base = slot_images[depth_id].TryFindBase(depth->Address())) {
+ const SubresourceRange range{.base = *base, .extent = {1, 1}};
+ depth_view = FindOrEmplaceImageView(
+ depth_id, ImageViewInfo(ImageViewType::e2D, PixelFormat::D32_FLOAT, range));
+ }
+ }
// After the views: creating them may reallocate the framebuffer slots.
Framebuffer* const dst_framebuffer = &slot_framebuffers[dst_view->first];
- return runtime.TemporalUpscale(dst_framebuffer, slot_image_views[dst_view->second],
- slot_image_views[src_view->second],
- slot_image_views[motion_view->second], jitter,
- motion_scale);
+ return runtime.TemporalUpscale(
+ dst_framebuffer, slot_image_views[dst_view->second], slot_image_views[src_view->second],
+ slot_image_views[motion_view->second],
+ depth_view ? &slot_image_views[depth_view] : nullptr, render_size, jitter,
+ motion_scale);
}
}
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index 09deb8da02..5847cae7ba 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -288,7 +288,8 @@ public:
bool TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& dst,
const Tegra::Engines::Fermi2D::Surface& src,
const Tegra::Engines::Fermi2D::Surface& motion_vectors,
- std::array<f32, 2> jitter, std::array<f32, 2> motion_scale);
+ const Tegra::Texture::TICEntry* depth, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale);
/// Try to find a cached image view in the given CPU address
[[nodiscard]] std::pair<ImageView*, bool> TryFindFramebufferImageView(
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index 5f75bcc18e..edc091318c 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -561,6 +561,11 @@ FN_MAX_LIMIT_LIST
return features.features.shaderStorageImageReadWithoutFormat;
}
+ /// Returns true if storage images of the extended formats (e.g. R16F, RG16F, R8) are supported.
+ bool IsStorageImageExtendedFormatsSupported() const {
+ return features.features.shaderStorageImageExtendedFormats;
+ }
+
/// Returns true if shader int64 is supported.
bool IsShaderInt64Supported() const {
return features.features.shaderInt64;