aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/texture_cache/texture_cache.h
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/texture_cache/texture_cache.h
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/texture_cache/texture_cache.h')
-rw-r--r--src/video_core/texture_cache/texture_cache.h67
1 files changed, 67 insertions, 0 deletions
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 33ca051482..32775b8719 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -957,6 +957,73 @@ bool TextureCache<P>::BlitImage(const Tegra::Engines::Fermi2D::Surface& dst,
}
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,
+ std::array<f32, 2> jitter, std::array<f32, 2> motion_scale) {
+ if constexpr (FRAMEBUFFER_BLITS) {
+ return false;
+ } else {
+ const Tegra::Engines::Fermi2D::Config copy{
+ .operation = Tegra::Engines::Fermi2D::Operation::SrcCopy,
+ .filter = Tegra::Engines::Fermi2D::Filter::Bilinear,
+ .must_accelerate = true,
+ .dst_x0 = 0,
+ .dst_y0 = 0,
+ .dst_x1 = static_cast<s32>(dst.width),
+ .dst_y1 = static_cast<s32>(dst.height),
+ .src_x0 = 0,
+ .src_y0 = 0,
+ .src_x1 = static_cast<s32>(src.width),
+ .src_y1 = static_cast<s32>(src.height),
+ };
+ const auto result = GetBlitImages(dst, src, copy);
+ if (!result) {
+ return false;
+ }
+ const ImageInfo motion_info(motion_vectors);
+ has_deleted_images = false;
+ const ImageId motion_id =
+ FindOrInsertImage(motion_info, motion_vectors.Address(), RelaxedOptions{});
+ if (has_deleted_images || !motion_id) {
+ return false; // the colour and output ids may be stale; the caller blits instead
+ }
+ const ImageId dst_id = result->dst_id;
+ const ImageId src_id = result->src_id;
+ PrepareImage(src_id, false, false);
+ PrepareImage(motion_id, false, false);
+ PrepareImage(dst_id, true, false);
+ 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) {
+ return false;
+ }
+ }
+ 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());
+ if (!base) {
+ return std::nullopt;
+ }
+ const SubresourceRange range{.base = *base, .extent = {1, 1}};
+ return RenderTargetFromImage(id, ImageViewInfo(ImageViewType::e2D, format, range));
+ };
+ const auto src_view = view(src_id, src, result->src_format);
+ const auto motion_view = view(motion_id, motion_vectors, motion_info.format);
+ const auto dst_view = view(dst_id, dst, result->dst_format);
+ if (!src_view || !motion_view || !dst_view) {
+ return false;
+ }
+ // 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);
+ }
+}
+
+template <class P>
std::pair<typename P::ImageView*, bool> TextureCache<P>::TryFindFramebufferImageView(
const Tegra::FramebufferConfig& config, DAddr cpu_addr) {
// TODO: Properly implement this