aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/engines
diff options
context:
space:
mode:
authorDuck912 <350528965+Duck912@users.noreply.github.com>2026-11-01 21:43:28 -0500
committerDuck912 <350528965+Duck912@users.noreply.github.com>2026-11-01 21:54:49 -0500
commit92d49d8a0c3e60894765bc2c332fdc936c87fc45 (patch)
treeaf4be3c6ba6223db85ac7e2750d2fe46984bf503 /src/video_core/engines
parentd125cfb41c0b78b0f4880fac20fab65c913d97e3 (diff)
video_core: temporal upscale stand-in for Fortune's Weave's DLSS
The one-frame blit left 3D edges aliased and shaking: the game jitters its camera every frame for DLSS to accumulate. Fire Emblem: Fortune's Weave's first DLSS pass (also rejected) has the motion vectors (cbuf0+0x290, R16G16) and the frame's jitter (+0x1b0/+0x1b4, in input pixels). Kepler compute keeps them, and the stand-in for the last network pass now draws temporal_upscale.frag: input samples placed where the jitter put them and weighted by distance, blended with the previous result reprojected along the motion vectors (Catmull-Rom) and clipped to the 3x3 neighbourhood in YCoCg. The history is copied from the result right after drawing it, so post-processing the game later applies to its output does not feed back. EDEN_NX2_TEMPORAL_UPSCALE=0 restores the blit. Assisted-by: Claude Opus 5.5 (debugging, implementation and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Diffstat (limited to 'src/video_core/engines')
-rw-r--r--src/video_core/engines/kepler_compute.cpp47
-rw-r--r--src/video_core/engines/kepler_compute.h8
2 files changed, 54 insertions, 1 deletions
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index 233304d273..b8a6567907 100644
--- a/src/video_core/engines/kepler_compute.cpp
+++ b/src/video_core/engines/kepler_compute.cpp
@@ -6,6 +6,7 @@
#include <algorithm>
#include <atomic>
+#include <bit>
#include <bitset>
#include <cstdlib>
#include <fstream>
@@ -557,6 +558,7 @@ bool KeplerCompute::DispatchNativeCompute(const NativeQmdScheduler::Descriptor&
// The console runs this on tensor cores; stand in with a plain upscale of its input.
return UpscaleInsteadOfNativeProgram();
}
+ RememberDlssMotionVectors();
return false;
}
return true;
@@ -588,6 +590,30 @@ 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.
+void KeplerCompute::RememberDlssMotionVectors() {
+ const auto& cbuf = launch_description.const_buffer_config[0];
+ if (cbuf.size.Value() < 0x294) return;
+ const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low;
+ const auto image = [&](u32 offset) -> std::optional<Texture::TICEntry> {
+ const u32 tic_index = memory_manager.Read<u32>(cbuf_address + offset) & 0xfffff;
+ if (tic_index > regs.tic.limit) return std::nullopt;
+ return GetTICEntry(tic_index);
+ };
+ const auto color = image(0x278), motion = image(0x290);
+ if (!color || !motion || color->format != Texture::TextureFormat::B10G11R11 ||
+ motion->format != Texture::TextureFormat::R16G16 || motion->Address() == 0 ||
+ motion->Width() != color->Width() || motion->Height() != color->Height() ||
+ !motion->IsBlockLinear())
+ return;
+ dlss_motion_vectors = *motion;
+ dlss_motion_color = color->Address();
+ dlss_jitter = {std::bit_cast<f32>(memory_manager.Read<u32>(cbuf_address + 0x1b0)),
+ std::bit_cast<f32>(memory_manager.Read<u32>(cbuf_address + 0x1b4))};
+}
+
// Title 4's upscaler reads the tonemapped frame through the texture handle at cbuf0+0x90 and
// writes the output resolution image through the surface handle at cbuf0+0xa0 (from its
// TLD/SUST operands). Fire Emblem: Fortune's Weave's DLSS network ends with a pass that reads the
@@ -609,10 +635,12 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
output->IsBlockLinear();
};
auto input = image(0x90), output = image(0xa0);
+ bool fortunes_weave = false;
if (!usable(input, output)) {
input = image(0x328);
output = image(0x348);
if (!usable(input, output)) return false;
+ fortunes_weave = true;
}
using RTFormat = Tegra::RenderTargetFormat;
RTFormat format{};
@@ -709,7 +737,24 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
source.format = static_cast<RTFormat>(std::strtoul(value, nullptr, 16));
}
}
- const bool done = rasterizer->AccelerateSurfaceCopy(source, surface(*output), config);
+ // Fortune's Weave: a temporal upscale along its motion vectors that undoes this frame's camera
+ // jitter (both from the frame's first DLSS pass) and accumulates the frames as DLSS would.
+ // EDEN_NX2_TEMPORAL_UPSCALE=0 blits instead.
+ const auto upscale_temporally = [&] {
+ static const bool enabled = [] {
+ const char* value = std::getenv("EDEN_NX2_TEMPORAL_UPSCALE");
+ return value == nullptr || std::string_view{value} != "0";
+ }();
+ if (!fortunes_weave || !enabled || !dlss_motion_vectors ||
+ dlss_motion_color != input->Address())
+ 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});
+ };
+ const bool done = upscale_temporally() ||
+ rasterizer->AccelerateSurfaceCopy(source, surface(*output), config);
if (static u32 checked{}; checked++ == sample_at) {
const u64 size = u64{output->Width()} * output->Height() * 4;
if (const auto device = memory_manager.GpuToCpuAddress(output->Address())) {
diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h
index b525636dde..b5aee3706a 100644
--- a/src/video_core/engines/kepler_compute.h
+++ b/src/video_core/engines/kepler_compute.h
@@ -266,7 +266,15 @@ private:
bool DispatchNativeCompute(const NativeQmdScheduler::Descriptor& descriptor);
void ProcessNativeReportSemaphore(u32 argument);
void DescribeNativeResources(const NativeQmdScheduler::Descriptor& qmd);
+ 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.
+ std::optional<Texture::TICEntry> dlss_motion_vectors;
+ GPUVAddr dlss_motion_color{};
+ std::array<f32, 2> dlss_jitter{};
bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call);
NativeQmdScheduler native_qmd;
std::array<u32, 0xc00>* native_data_ram{};