aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-11-03 01:20:35 +0100
committercirakg <cirakg@cirakg.xyz>2026-11-03 01:20:35 +0100
commit70934ece3d2253c7579cf8d71b13e3a0547070e2 (patch)
treebeffcc29083a897914c62d5992482093ddfad29a /src
parentfdeecea52df593d6d2a1ce6e1977e3aab1bb9135 (diff)
parentebec180bf1125b4f29b068922d1220c8af9b63b0 (diff)
Merge upstream Djihads80/Eglantine nx2 into ldn/room-handshake
Diffstat (limited to 'src')
-rw-r--r--src/core/file_sys/nup_writer.cpp7
-rw-r--r--src/core/hle/service/nx2_gsv.h105
-rw-r--r--src/qt_common/config/shared_translation.cpp12
-rw-r--r--src/qt_common/config/shared_translation.h6
-rw-r--r--src/shader_recompiler/frontend/ampere/lower_to_maxwell.h53
-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/host1x/codec_types.h8
-rw-r--r--src/video_core/host1x/codecs/vp9.cpp43
-rw-r--r--src/video_core/host1x/nx2_video_decoder.cpp78
-rw-r--r--src/video_core/host1x/nx2_video_decoder.h3
-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
29 files changed, 1817 insertions, 220 deletions
diff --git a/src/core/file_sys/nup_writer.cpp b/src/core/file_sys/nup_writer.cpp
index 4161969f98..54bb4c77d3 100644
--- a/src/core/file_sys/nup_writer.cpp
+++ b/src/core/file_sys/nup_writer.cpp
@@ -638,10 +638,13 @@ private:
std::ranges::find(skip, lower) != skip.end()) {
continue;
}
+ // Keep the dump's own layout for anything that is not exefs/romfs, the
+ // same way the reference packer does: a root-level data/ folder stays
+ // data/, and other extras keep their name instead of moving under data/.
if (entry.is_directory(ec)) {
- CollectTree(index, entry.path(), "data/" + name);
+ CollectTree(index, entry.path(), name);
} else {
- AddFile(index, entry, "data/" + name);
+ AddFile(index, entry, name);
}
}
return plan.packages.back().title_id;
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index 259681a2a2..290d82746d 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -2347,7 +2347,7 @@ public:
{8, &INx2VideoSession::Flush, "Flush"},
{101, &INx2VideoSession::Configure, "Configure"},
{109, &INx2VideoSession::SubmitInput, "SubmitInputBuffer"},
- {115, &INx2VideoSession::Succeed, "Finalize"},
+ {115, &INx2VideoSession::Finalize, "Finalize"},
{200, &INx2VideoSession::Succeed, "Initialize"},
{201, &INx2VideoSession::Succeed, "InitializeWithFlag"},
{202, &INx2VideoSession::Succeed, "InitializeWithFlagAndValue"},
@@ -2419,6 +2419,10 @@ private:
if (Nx2FirstVideoCommand(GetServiceName(), ctx.GetCommand())) {
LOG_INFO(Service, "{} TIPC {} {} (first use): words {:08x}", GetServiceName(),
ctx.GetCommand(), what, fmt::join(std::span<const u32>{w, 12}, " "));
+ } else if (static const bool trace = std::getenv("EDEN_NX2_VDEC_TRACE") != nullptr; trace) {
+ // EDEN_NX2_VDEC_TRACE=1: every video decoder call at Info level.
+ LOG_INFO(Service, "{} TIPC {} {}: [2]={:#x} [3]={:#x} [4]={:#x} [5]={:#x}",
+ GetServiceName(), ctx.GetCommand(), what, w[2], w[3], w[4], w[5]);
} else {
LOG_DEBUG(Service, "{} TIPC {} {}: [2]={:#x} [3]={:#x} [4]={:#x} [5]={:#x}",
GetServiceName(), ctx.GetCommand(), what, w[2], w[3], w[4], w[5]);
@@ -2447,10 +2451,31 @@ private:
void Configure(HLERequestContext& ctx) {
const auto* w = ctx.CommandBuffer();
std::string codec;
+ std::span<const u8> name_buf;
if (!ctx.BufferDescriptorA().empty()) {
- const auto name = ctx.ReadBufferA(0);
- codec.assign(reinterpret_cast<const char*>(name.data()),
- strnlen(reinterpret_cast<const char*>(name.data()), name.size()));
+ name_buf = ctx.ReadBufferA(0);
+ codec.assign(reinterpret_cast<const char*>(name_buf.data()),
+ strnlen(reinterpret_cast<const char*>(name_buf.data()), name_buf.size()));
+ }
+ if (name_buf.empty() && !ctx.BufferDescriptorX().empty()) {
+ name_buf = ctx.ReadBufferX(0);
+ codec.assign(reinterpret_cast<const char*>(name_buf.data()),
+ strnlen(reinterpret_cast<const char*>(name_buf.data()), name_buf.size()));
+ }
+ if (!name_buf.empty()) {
+ if (codec.empty()) {
+ const std::string_view sv{reinterpret_cast<const char*>(name_buf.data()), name_buf.size()};
+ for (const auto& candidate : {"video/avc", "video/vp9", "video/vp8", "video/hevc"}) {
+ if (sv.find(candidate) != std::string_view::npos) {
+ codec = candidate;
+ break;
+ }
+ }
+ }
+ if (codec.empty()) {
+ LOG_INFO(Service, "{} configure: no codec name in {:02x}", GetServiceName(),
+ fmt::join(name_buf, " "));
+ }
}
Log(ctx, "configure");
LOG_INFO(Service, "{} configure codec \"{}\" {}x{} rate {}", GetServiceName(), codec, w[5],
@@ -2513,7 +2538,24 @@ private:
if (release_fence) {
m_release_fences[image] = *release_fence;
}
- value = m_sync_base + ++m_handed;
+ // After the end of a stream nothing fills an image again, so a release completes at
+ // once: its fence is the syncpoint's current value. Mario Kart World releases its
+ // images after a movie ends and waits for the returned fences; a fence for a frame
+ // that never comes hung the game on a black screen after its title movie.
+ static const bool release_now = [] {
+ const char* value = std::getenv("EDEN_NX2_VDEC_EOS_RELEASE_NOW");
+ return value != nullptr && value[0] != '0';
+ }();
+ if (release_now && release_fence && m_stream_ended && m_inputs.empty() &&
+ m_decoded.empty()) {
+ const u32 reached = static_cast<u32>(
+ system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(m_syncpoint));
+ m_sync_base = reached;
+ m_handed = 0;
+ value = reached;
+ } else {
+ value = m_sync_base + ++m_handed;
+ }
}
m_cv.notify_one();
return {~m_syncpoint, 0, value, 0};
@@ -2559,7 +2601,8 @@ private:
{
std::scoped_lock lk{m_mutex};
m_inputs.push_back(std::move(input));
- m_ready->room.store(m_inputs.size() < MaxQueuedInputs, std::memory_order_relaxed);
+ m_stream_ended = false;
+ m_ready->room.store(!end && m_inputs.size() < MaxQueuedInputs, std::memory_order_relaxed);
}
m_cv.notify_one();
const u64 id = ++m_submitted;
@@ -2574,12 +2617,41 @@ private:
std::scoped_lock lk{m_mutex};
m_inputs.clear();
m_flush = true;
+ m_stream_ended = false;
m_ready->room.store(true, std::memory_order_relaxed);
+ // Only Stop: Bananza flushes (8) after its movies end and treats every completed
+ // fence as a frame to acquire.
+ if (ctx.GetCommand() == 207) {
+ CompleteHandedFences();
+ }
}
m_cv.notify_one();
TipcReplyWords(ctx, {});
}
+ void Finalize(HLERequestContext& ctx) {
+ Log(ctx, "finalize");
+ {
+ std::scoped_lock lk{m_mutex};
+ CompleteHandedFences();
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ // Advances the decoder's syncpoint to the last value handed out (m_mutex held).
+ void CompleteHandedFences() {
+ if (m_syncpoint == NoSyncpoint) {
+ return;
+ }
+ auto& syncpoints = system.Host1x().GetSyncpointManager();
+ const u32 target = m_sync_base + m_handed;
+ u32 current = static_cast<u32>(syncpoints.GetGuestSyncpointValue(m_syncpoint));
+ while (static_cast<s32>(target - current) > 0) {
+ syncpoints.IncrementGuest(m_syncpoint);
+ ++current;
+ }
+ }
+
void GetOutputInfo(HLERequestContext& ctx) {
Log(ctx, "output info");
std::optional<Output> output;
@@ -2647,6 +2719,10 @@ private:
lk.lock();
// Even an image written during a flush counts: the client waits for its value.
m_outputs.push_back({image, width, height, frame.pts, frame.end});
+ if (frame.end) {
+ m_stream_ended = true;
+ m_ready->room.store(false, std::memory_order_relaxed);
+ }
system.Host1x().GetSyncpointManager().IncrementGuest(m_syncpoint);
++m_frames_out;
if (m_frames_out == 1 || (m_frames_out % 600) == 0 || frame.end) {
@@ -2658,8 +2734,16 @@ private:
}
Input input = std::move(m_inputs.front());
m_inputs.pop_front();
- m_ready->room.store(m_inputs.size() < MaxQueuedInputs, std::memory_order_relaxed);
- const std::string codec = m_codec;
+ if (!m_stream_ended) {
+ m_ready->room.store(m_inputs.size() < MaxQueuedInputs, std::memory_order_relaxed);
+ }
+ std::string codec = m_codec;
+ if (codec.empty() && Tegra::Host1x::IsH264Stream(input.data)) {
+ codec = "video/avc";
+ m_codec = codec;
+ LOG_INFO(Service, "{} empty codec, detected H.264 stream: using \"{}\"",
+ GetServiceName(), codec);
+ }
lk.unlock();
if (!decoder || decoder->Name() != codec) {
decoder = std::make_unique<Tegra::Host1x::Nx2VideoDecoder>(codec);
@@ -2754,6 +2838,10 @@ private:
memory.WriteBlock(address + layout.chroma_offset, bytes.data() + layout.chroma_offset,
layout.pitch * ((height + 1) / 2));
}
+ // The game samples the image on the GPU: drop the texture cache's copy of the old frame.
+ if (const auto device = Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address)) {
+ system.GPU().InvalidateRegion(*device, layout.size);
+ }
return {width, height};
}
@@ -2809,6 +2897,7 @@ private:
std::deque<u32> m_free; ///< Output images, in the order the client acquires them
std::map<u32, Fence> m_release_fences;
std::deque<Output> m_outputs; ///< Finished, for 213
+ bool m_stream_ended{}; ///< End-of-stream frame handed out, nothing submitted since
bool m_flush{};
bool m_quit{};
std::thread m_worker;
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/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h
index 024ed27ea3..81bd4b9e74 100644
--- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h
+++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h
@@ -88,6 +88,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32
result.source_instruction_offsets.push_back(static_cast<u32>(result.instructions.size()));
u32 opcode = insn.Bits(0, 12);
if (insn.Bits(12, 4) == 15) continue; // @!PT: never executes (compiler padding)
+ u32 f16_dest = 255, f16_second = 255, f16_components = 0;
std::optional<u32> active_link;
u32 active_entry = 0;
if (main_exited) {
@@ -113,6 +114,19 @@ inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32
const u32 reg = insn.Bits(bit, 8);
if (reg >= (uses_uniform_bank ? high_uniform_base : 246U) && reg < 255) return fail(index, "native program uses lowering scratch registers");
}
+ // TLD/TLD4 .F16 (bit 79): the components come packed in pairs, (0, 1) in Rd and (2, 3) in
+ // the second destination. Lowered as the F32 form into R248/R249 and R246/R247, then packed.
+ if ((opcode == 0xb66 || opcode == 0xb63) && insn.Bits(79, 2) == 1) {
+ f16_dest = insn.Bits(16, 8);
+ f16_second = insn.Bits(64, 8);
+ f16_components = static_cast<u32>(std::popcount(insn.Bits(72, 4)));
+ if (f16_dest >= 246 || (f16_second != 255 && f16_second >= 246) || insn.Bits(61, 3) == 5 ||
+ (f16_second == 255 && f16_components > 2))
+ return fail(index, "native half-precision texture result registers");
+ insn.words[2] &= ~(1U << 15);
+ insn.words[0] = (insn.words[0] & ~0x00ff0000U) | (f16_dest == 255 ? 0x00ff0000U : (248U << 16));
+ if (f16_second != 255) insn.words[2] = (insn.words[2] & ~0xffU) | 246U;
+ }
// UR0-UR21 live in R216-R237 and UR22-UR27 in R240-R245 (R239 holds the uniform
// predicates), so even/odd pairs stay adjacent; UR28 and up below R216. Sites that
// still check reg < 16 rely on UR0-UR15 being R216-R231.
@@ -899,19 +913,22 @@ inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32
// (x, y[, layer]) and the four gradients sit in the same registers for both.
const u32 dimension = insn.Bits(61, 3);
const u32 high = insn.Bits(64, 8); // destination of the third and fourth channels
+ // Bit 90 is forwarded below (Maxwell's NODEP); Splatoon Raiders sets it. Bits 72-75 are
+ // the channel mask: the first two enabled channels go to Rd, Rd+1, the others to the
+ // second destination (RZ drops them; a third channel alone may be in an odd register).
const u32 mask = insn.Bits(72, 4), channels = static_cast<u32>(std::popcount(mask));
- // Bit 90 is forwarded below (Maxwell's NODEP); Splatoon Raiders sets it.
if ((insn.words[2] & ~(0xfffU | (1U << 26))) != 0x001e0000 || mask == 0 || insn.Bits(59, 2) ||
(dimension != 1 && dimension != 5) || binding >= 18 || offset > 0x1fff ||
- (channels > 1 && (dest & 1)) || dest >= 248 ||
- (channels > 2 ? high != 255 && ((high & 1) || high >= 248) : high != 255) ||
+ (channels > 1 && (dest & 1)) || dest >= 248 || (channels <= 2 && high != 255) ||
+ (high != 255 && ((channels > 3 && (high & 1)) || high >= 248)) ||
coordinates >= 248 || derivatives >= 247)
return fail(index, "native gradient texture mode");
if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
return fail(index, "native texture descriptor buffer conflict");
result.required_texture_buffer = binding;
- // Maxwell writes consecutive registers: when the second pair is elsewhere (Splatoon
- // Raiders: R12/R18, R18/R44), write them to scratch and move each pair into place.
+ // Maxwell writes the channels to consecutive registers: when the second destination
+ // doesn't follow Rd (Splatoon Raiders: R12/R18, R18/R44), write them to scratch and move
+ // each into place.
const bool consecutive = channels <= 2 || high == dest + 2;
const u64 target = consecutive ? dest : 248;
emit(0xde00000000000000ULL | predicate | target | (u64{coordinates} << 8) |
@@ -920,8 +937,8 @@ inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32
if (!consecutive) {
for (u32 i = 0; i < channels; ++i) {
if (i >= 2 && high == 255) break;
- emit(0x5c98000000000000ULL | predicate | (i < 2 ? dest + i : high + i - 2) |
- (u64{15} << 39) | (u64{248 + i} << 20));
+ const u32 to = i < 2 ? static_cast<u32>(dest) + i : high + (i - 2);
+ emit(0x5c98000000000000ULL | predicate | to | (u64{15} << 39) | (u64{248 + i} << 20));
}
}
break;
@@ -1573,12 +1590,13 @@ inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32
}
case 0x305: case 0xb05: case 0xd05: { // F2I NTZ, register/constant-buffer/uniform source
const u32 mode = insn.words[2] & ~(opcode == 0xd05 ? 0x08000000U : 0U);
- const bool half = opcode == 0x305 && mode == 0x0010f000;
+ // F16 source: bit 72 signs the result as in the other forms, bits 60-61 pick H0/H1.
+ const bool half = opcode == 0x305 && (mode & ~0x100U) == 0x0010f000;
const bool small = mode == 0x00212800;
const bool floor = (mode & ~0x10100U) == 0x00207000;
const bool nearest = (mode & ~0x10100U) == 0x00203000;
if ((!half && !small && !floor && !nearest && (mode & ~0x10100U) != 0x0020f000) ||
- (opcode != 0xb05 && (insn.words[1] & 0xffffff00U)) ||
+ (opcode != 0xb05 && (insn.words[1] & (half ? 0xefffff00U : 0xffffff00U))) ||
(opcode == 0xd05 && uniform_register(insn.Bits(32, 8)) == ~0U)) {
return fail(index, "native float-to-integer conversion mode");
}
@@ -1586,7 +1604,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32
u32 source = opcode == 0xb05 ? materialize(2, 0, 252, false, false)
: opcode == 0xd05 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8);
if (half) {
- extract_half({source, 2, false, false}, 0, 252);
+ extract_half({source, insn.Bits(60, 1) ? 3U : 2U, false, false}, 0, 252);
source = 252;
}
if (!is_signed) {
@@ -2870,6 +2888,21 @@ inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32
return fail(index, "unsupported native opcode");
}
if (!operand_error.empty()) return fail(index, operand_error);
+ if (f16_components != 0) {
+ // Each pair as F2FP.PACK_AB: the second value in the high half.
+ const u64 pred = u64{insn.Bits(12, 4)} << 16;
+ const auto pack = [&](u32 target, u32 low, u32 high) {
+ result.instructions.push_back(0x5ca8000000000000ULL | pred | 250 | (u64{1} << 8) |
+ (u64{2} << 10) | (u64{high} << 20));
+ result.instructions.push_back(0x5ca8000000000000ULL | pred | 251 | (u64{1} << 8) |
+ (u64{2} << 10) | (u64{low} << 20));
+ result.instructions.push_back(0x5c18000000000000ULL | pred | target | (u64{250} << 8) |
+ (u64{251} << 20) | (u64{16} << 39));
+ };
+ if (f16_dest != 255) pack(f16_dest, 248, f16_components > 1 ? 249U : 255U);
+ if (f16_second != 255 && f16_components > 2)
+ pack(f16_second, 246, f16_components > 3 ? 247U : 255U);
+ }
if (output_mask) {
emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{output_mask} << 20));
emit(0x38f0000000070000ULL | (u64{254} << 8) | (u64{output_mask} << 20));
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/host1x/codec_types.h b/src/video_core/host1x/codec_types.h
index b2f80c822a..da9b713b26 100644
--- a/src/video_core/host1x/codec_types.h
+++ b/src/video_core/host1x/codec_types.h
@@ -312,6 +312,10 @@ struct Vp9PictureInfo {
bool allow_high_precision_mv;
bool segment_enabled;
bool mode_ref_delta_enabled;
+
+ Segmentation segmentation;
+ std::array<u8, 7> seg_tree_probs;
+ std::array<u8, 3> seg_pred_probs;
};
struct Vp9FrameContainer {
@@ -377,6 +381,10 @@ struct PictureInfo {
.allow_high_precision_mv = allow_high_precision_mv != 0,
.segment_enabled = segmentation.enabled != 0,
.mode_ref_delta_enabled = loop_filter.mode_ref_delta_enabled != 0,
+
+ .segmentation = segmentation,
+ .seg_tree_probs = {},
+ .seg_pred_probs = {},
};
}
};
diff --git a/src/video_core/host1x/codecs/vp9.cpp b/src/video_core/host1x/codecs/vp9.cpp
index dcc6261e5c..dd91f7f1b7 100644
--- a/src/video_core/host1x/codecs/vp9.cpp
+++ b/src/video_core/host1x/codecs/vp9.cpp
@@ -331,19 +331,16 @@ void VP9::WriteMvProbabilityUpdate(VpxRangeEncoder& writer, u8 new_prob, u8 old_
}
void VP9::WriteSegmentation(VpxBitStreamWriter& writer) {
- bool enabled = current_picture_info.segmentation.enabled != 0;
+ bool enabled = current_frame_info.segmentation.enabled != 0;
writer.WriteBit(enabled);
if (!enabled) {
return;
}
- auto update_map = current_picture_info.segmentation.update_map != 0;
+ auto update_map = current_frame_info.segmentation.update_map != 0;
writer.WriteBit(update_map);
if (update_map) {
- EntropyProbs entropy_probs{};
- host1x.gmmu_manager.ReadBlock(regs.vp9_prob_tab_buffer_offset.Address(), &entropy_probs, sizeof(entropy_probs));
-
auto WriteProb = [&](u8 prob) {
bool coded = prob != 255;
writer.WriteBit(coded);
@@ -352,55 +349,55 @@ void VP9::WriteSegmentation(VpxBitStreamWriter& writer) {
}
};
- for (size_t i = 0; i < entropy_probs.mb_segment_tree_probs.size(); i++) {
- WriteProb(entropy_probs.mb_segment_tree_probs[i]);
+ for (size_t i = 0; i < current_frame_info.seg_tree_probs.size(); i++) {
+ WriteProb(current_frame_info.seg_tree_probs[i]);
}
- auto temporal_update = current_picture_info.segmentation.temporal_update != 0;
+ auto temporal_update = current_frame_info.segmentation.temporal_update != 0;
writer.WriteBit(temporal_update);
if (temporal_update) {
for (s32 i = 0; i < 3; i++) {
- WriteProb(entropy_probs.segment_pred_probs[i]);
+ WriteProb(current_frame_info.seg_pred_probs[i]);
}
}
}
- if (last_segmentation == current_picture_info.segmentation) {
+ if (last_segmentation == current_frame_info.segmentation) {
writer.WriteBit(false);
return;
}
- last_segmentation = current_picture_info.segmentation;
+ last_segmentation = current_frame_info.segmentation;
writer.WriteBit(true);
- writer.WriteBit(current_picture_info.segmentation.abs_delta != 0);
+ writer.WriteBit(current_frame_info.segmentation.abs_delta != 0);
constexpr s32 MAX_SEGMENTS = 8;
constexpr std::array SegmentationFeatureBits = {8, 6, 2, 0};
for (s32 i = 0; i < MAX_SEGMENTS; i++) {
- auto q_enabled = current_picture_info.segmentation.feature_enabled[i][0] != 0;
+ auto q_enabled = current_frame_info.segmentation.feature_enabled[i][0] != 0;
writer.WriteBit(q_enabled);
if (q_enabled) {
- writer.WriteS(current_picture_info.segmentation.feature_data[i][0],
+ writer.WriteS(current_frame_info.segmentation.feature_data[i][0],
SegmentationFeatureBits[0]);
}
- auto lf_enabled = current_picture_info.segmentation.feature_enabled[i][1] != 0;
+ auto lf_enabled = current_frame_info.segmentation.feature_enabled[i][1] != 0;
writer.WriteBit(lf_enabled);
if (lf_enabled) {
- writer.WriteS(current_picture_info.segmentation.feature_data[i][1],
+ writer.WriteS(current_frame_info.segmentation.feature_data[i][1],
SegmentationFeatureBits[1]);
}
- auto ref_enabled = current_picture_info.segmentation.feature_enabled[i][2] != 0;
+ auto ref_enabled = current_frame_info.segmentation.feature_enabled[i][2] != 0;
writer.WriteBit(ref_enabled);
if (ref_enabled) {
- writer.WriteU(current_picture_info.segmentation.feature_data[i][2],
+ writer.WriteU(current_frame_info.segmentation.feature_data[i][2],
SegmentationFeatureBits[2]);
}
- auto skip_enabled = current_picture_info.segmentation.feature_enabled[i][3] != 0;
+ auto skip_enabled = current_frame_info.segmentation.feature_enabled[i][3] != 0;
writer.WriteBit(skip_enabled);
}
}
@@ -411,6 +408,14 @@ Vp9PictureInfo VP9::GetVp9PictureInfo() {
InsertEntropy(regs.vp9_prob_tab_buffer_offset.Address(), vp9_info.entropy);
+ EntropyProbs entropy_probs{};
+ host1x.gmmu_manager.ReadBlock(regs.vp9_prob_tab_buffer_offset.Address(),
+ &entropy_probs, sizeof(entropy_probs));
+
+ vp9_info.seg_tree_probs = entropy_probs.mb_segment_tree_probs;
+ vp9_info.seg_pred_probs = entropy_probs.segment_pred_probs;
+
+
// surface_luma_offset[0:3] contains the address of the reference frame offsets in the following
// order: last, golden, altref, current.
for (size_t i = 0; i < 4; i++) {
diff --git a/src/video_core/host1x/nx2_video_decoder.cpp b/src/video_core/host1x/nx2_video_decoder.cpp
index 9b34bd84af..86869ad5e2 100644
--- a/src/video_core/host1x/nx2_video_decoder.cpp
+++ b/src/video_core/host1x/nx2_video_decoder.cpp
@@ -47,6 +47,45 @@ std::optional<NvdecCommon::VideoCodec> CodecFromMime(std::string_view mime) {
} // namespace
+bool IsH264Stream(std::span<const u8> data) {
+ if (data.size() >= 4) {
+ // Annex-B 3-byte start code: 00 00 01 <nal_header>
+ if (data[0] == 0 && data[1] == 0 && data[2] == 1) {
+ const u8 nal_type = data[3] & 0x1F;
+ if ((data[3] & 0x80) == 0 && nal_type >= 1 && nal_type <= 23) {
+ return true;
+ }
+ }
+ }
+ if (data.size() >= 5) {
+ // Annex-B 4-byte start code: 00 00 00 01 <nal_header>
+ if (data[0] == 0 && data[1] == 0 && data[2] == 0 && data[3] == 1) {
+ const u8 nal_type = data[4] & 0x1F;
+ if ((data[4] & 0x80) == 0 && nal_type >= 1 && nal_type <= 23) {
+ return true;
+ }
+ }
+ // avcC / MP4 length-prefixed NAL unit: [u32 be length] <nal_header>
+ const u32 nalu_len = (static_cast<u32>(data[0]) << 24) |
+ (static_cast<u32>(data[1]) << 16) |
+ (static_cast<u32>(data[2]) << 8) |
+ static_cast<u32>(data[3]);
+ if (nalu_len > 0 && nalu_len <= data.size() - 4) {
+ const u8 nal_type = data[4] & 0x1F;
+ if ((data[4] & 0x80) == 0 && nal_type >= 1 && nal_type <= 23) {
+ return true;
+ }
+ }
+ }
+ if (data.size() >= 7) {
+ // avcC configuration record: version=1, profile, compat, level, lengthSizeMinusOne | 0xFC, numSPS | 0xE0
+ if (data[0] == 1 && (data[4] & 0xFC) == 0xFC && (data[5] & 0xE0) == 0xE0 && (data[5] & 0x1F) > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
struct Nx2VideoDecoder::Impl {
std::string name;
std::optional<FFmpeg::Decoder> decoder;
@@ -123,6 +162,31 @@ void Nx2VideoDecoder::Decode(std::span<const u8> data, u64 pts, std::vector<Nx2P
}
impl->packet.assign(data.size() + AV_INPUT_BUFFER_PADDING_SIZE, 0);
std::memcpy(impl->packet.data(), data.data(), data.size());
+
+ // Convert MP4/avcC length-prefixed NALUs to Annex-B start codes for FFmpeg if needed.
+ if (impl->name.find("avc") != std::string::npos || impl->name.find("264") != std::string::npos) {
+ const bool is_annex_b = data.size() >= 4 &&
+ ((data[0] == 0 && data[1] == 0 && data[2] == 1) ||
+ (data.size() >= 5 && data[0] == 0 && data[1] == 0 && data[2] == 0 && data[3] == 1));
+ if (!is_annex_b && data.size() >= 5) {
+ size_t offset = 0;
+ while (offset + 4 < data.size()) {
+ const u32 nalu_len = (static_cast<u32>(impl->packet[offset]) << 24) |
+ (static_cast<u32>(impl->packet[offset + 1]) << 16) |
+ (static_cast<u32>(impl->packet[offset + 2]) << 8) |
+ static_cast<u32>(impl->packet[offset + 3]);
+ if (nalu_len == 0 || offset + 4 + nalu_len > data.size()) {
+ break;
+ }
+ impl->packet[offset] = 0;
+ impl->packet[offset + 1] = 0;
+ impl->packet[offset + 2] = 0;
+ impl->packet[offset + 3] = 1;
+ offset += 4 + nalu_len;
+ }
+ }
+ }
+
FFmpeg::Packet packet{std::span<const u8>{impl->packet.data(), data.size()}};
packet.GetPacket()->pts = static_cast<s64>(pts);
// Hidden VP9 frames never come out: keep the set from growing over a long movie.
@@ -162,6 +226,20 @@ void Nx2VideoDecoder::Render(const Nx2Picture& picture, Nx2PictureFormat format,
const AVPixelFormat target = format == Nx2PictureFormat::Nv12 ? AV_PIX_FMT_NV12
: format == Nx2PictureFormat::Rgba8 ? AV_PIX_FMT_RGBA
: AV_PIX_FMT_BGRA;
+ // Frames copied back from a host GPU decoder are already NV12. Copy them as they are:
+ // FFmpeg 6.1's swscale leaves the chroma plane of an NV12 to NV12 copy at zero, which shows
+ // the movies green.
+ if (target == AV_PIX_FMT_NV12 && f->format == AV_PIX_FMT_NV12 &&
+ static_cast<u32>(f->width) == width && static_cast<u32>(f->height) == height) {
+ for (u32 y = 0; y < height; ++y) {
+ std::memcpy(out.data() + y * pitch, f->data[0] + y * f->linesize[0], width);
+ }
+ for (u32 y = 0; y < (height + 1) / 2; ++y) {
+ std::memcpy(out.data() + chroma_offset + y * pitch, f->data[1] + y * f->linesize[1],
+ width);
+ }
+ return;
+ }
impl->scaler = sws_getCachedContext(impl->scaler, f->width, f->height,
static_cast<AVPixelFormat>(f->format),
static_cast<int>(width), static_cast<int>(height), target,
diff --git a/src/video_core/host1x/nx2_video_decoder.h b/src/video_core/host1x/nx2_video_decoder.h
index c979a2c801..347b0e6525 100644
--- a/src/video_core/host1x/nx2_video_decoder.h
+++ b/src/video_core/host1x/nx2_video_decoder.h
@@ -32,6 +32,9 @@ enum class Nx2PictureFormat : u32 {
Bgra8,
};
+/// Checks if stream data begins with an H.264 Annex-B start code or avcC/MP4 length-prefixed NALUs.
+bool IsH264Stream(std::span<const u8> data);
+
/// The decoder behind the Switch 2 video decoder service (nn::codec::video): FFmpeg decodes the
/// elementary stream the game demuxed, in software or on the host GPU (NVDEC emulation setting).
/// Used from one thread at a time.
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;