Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 76 additions & 33 deletions src/video_core/engines/kepler_compute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <atomic>
#include <bit>
#include <bitset>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <optional>
Expand Down Expand Up @@ -642,7 +643,7 @@ VideoCore::GameDlssReason KeplerCompute::GameDlssBlocker() const {

bool KeplerCompute::EmulatesNativeTensorPrograms() const {
if (dlss_chain.active) {
return dlss_chain.failure == VideoCore::GameDlssReason::None &&
return dlss_network_translated && dlss_chain.failure == VideoCore::GameDlssReason::None &&
GameDlssBlocker() == VideoCore::GameDlssReason::None;
}
// Donkey Kong Bananza's programs are emulated: rejected, they write nothing and the screen is
Expand Down Expand Up @@ -807,8 +808,9 @@ bool KeplerCompute::FinishDlssEvaluation() {
return UpscaleInsteadOfNativeProgram();
}

// Fortune's Weave's last DLSS network pass: it reads the input colour (the first pass's) through
// cbuf0+0x328 and writes the upscaled image through +0x348.
// The last DLSS network pass reads the input colour (the first pass's) through cbuf0+0x328 and
// writes the output through +0x348: larger in Fortune's Weave, the same size in Fast Fusion's menus
// (DLAA).
bool KeplerCompute::IsDlssOutputPass() {
const auto& cbuf = launch_description.const_buffer_config[0];
if (cbuf.size.Value() < 0x34c) {
Expand All @@ -824,7 +826,8 @@ bool KeplerCompute::IsDlssOutputPass() {
};
const auto input = image(0x328), output = image(0x348);
return input && output && input->Address() == dlss_motion_color && output->Address() != 0 &&
input->format == output->format && output->Width() > input->Width();
output->Address() != input->Address() && input->format == output->format &&
output->Width() >= input->Width() && output->Height() >= input->Height();
}

// Logs the images a rejected program's handles (constant bank 0, bytes 0x80-0x11c) point to, so a
Expand Down Expand Up @@ -853,10 +856,14 @@ 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, 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.
// The first pass of a DLSS evaluation reads its inputs through cbuf0, with this frame's camera
// jitter (x, y; input pixels, Halton bases 2 and 3 less 0.5) at +0x1b0. Fire Emblem: Fortune's
// Weave's (rejected, before the network) has the input colour (B10G11R11) at +0x278, its motion
// vectors (R16G16 float, UV offsets) at +0x290 and its depth (Z32, reversed) at +0x298. Fast
// Fusion's (its menus draw the vehicles with DLAA) has the colour (R16G16B16A16 float) at +0x278,
// the motion vectors (R16G16 float) at +0x284 with their scale to input pixels at +0x198 (half the
// input's size: they are in NDC), the depth (D24S8) at +0x288 and whether it is reversed at
// +0x218. The motion vectors and depth are the colour's size.
bool KeplerCompute::RememberDlssMotionVectors() {
const auto& cbuf = launch_description.const_buffer_config[0];
if (cbuf.size.Value() < 0x294) return false;
Expand All @@ -866,24 +873,52 @@ bool KeplerCompute::RememberDlssMotionVectors() {
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())
const auto value = [&](u32 offset) {
return std::bit_cast<f32>(memory_manager.Read<u32>(cbuf_address + offset));
};
const auto color = image(0x278);
if (!color) return false;
const auto fits = [&](const std::optional<Texture::TICEntry>& tic) {
return tic && tic->Address() != 0 && tic->Width() == color->Width() &&
tic->Height() == color->Height() && tic->IsBlockLinear();
};
using Texture::TextureFormat;
std::optional<Texture::TICEntry> motion, depth;
std::array<f32, 2> motion_scale{1.0f, 1.0f};
bool depth_reversed = true;
if (color->format == TextureFormat::B10G11R11) {
motion = image(0x290);
depth = cbuf.size.Value() >= 0x29c ? image(0x298) : std::nullopt;
if (!fits(depth) || depth->format != TextureFormat::Z32) {
depth.reset();
}
} else if (color->format == TextureFormat::R16G16B16A16) {
motion = image(0x284);
depth = image(0x288);
if (!fits(depth) || (depth->format != TextureFormat::S8Z24 &&
depth->format != TextureFormat::Z24S8 &&
depth->format != TextureFormat::X8Z24 &&
depth->format != TextureFormat::Z32)) {
return false;
}
motion_scale = {value(0x198) / static_cast<f32>(color->Width()),
value(0x19c) / static_cast<f32>(color->Height())};
if (!std::isfinite(motion_scale[0]) || !std::isfinite(motion_scale[1]) ||
motion_scale[0] == 0.0f || motion_scale[1] == 0.0f) {
return false;
}
depth_reversed = memory_manager.Read<u32>(cbuf_address + 0x218) != 0;
} else {
return false;
}
if (!fits(motion) || motion->format != TextureFormat::R16G16) return false;
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))};
dlss_motion_scale = motion_scale;
dlss_depth = depth;
dlss_depth_reversed = depth_reversed;
dlss_network_translated = color->format == TextureFormat::B10G11R11;
dlss_jitter = {value(0x1b0), value(0x1b4)};
return true;
}

Expand All @@ -894,7 +929,8 @@ bool KeplerCompute::RememberDlssMotionVectors() {
// the input colour through cbuf0+0x328 and writes the DLSS output through cbuf0+0x348 (B10G11R11;
// 720p to 1440p docked). Blit one to the other with bilinear filtering when both are plain 2D
// images of the same format and the output is larger: title 4's slots first, then Fortune's
// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair.
// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair. A DLSS
// evaluation's output may also be the input's size (Fast Fusion's menus).
bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
const auto& cbuf = launch_description.const_buffer_config[0];
const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low;
Expand All @@ -910,13 +946,19 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
to->IsBlockLinear();
};
std::optional<Texture::TICEntry> input = image(0x90), output = image(0xa0);
bool fortunes_weave = false;
bool dlss_evaluation = false;
if (!upscale_pair(input, output)) {
input = image(0x328);
output = image(0x348);
fortunes_weave = upscale_pair(input, output);
}
if (!upscale_pair(input, output)) {
dlss_evaluation =
upscale_pair(input, output) ||
(input && output && dlss_motion_vectors && input->Address() == dlss_motion_color &&
input->format == output->format && output->Address() != 0 &&
output->Address() != input->Address() && output->Width() == input->Width() &&
output->Height() == input->Height() && input->IsBlockLinear() &&
output->IsBlockLinear());
}
if (!dlss_evaluation && !upscale_pair(input, output)) {
input.reset();
output.reset();
for (u32 first = 0x80; first < 0x120 && !input; first += 4) {
Expand Down Expand Up @@ -1054,22 +1096,23 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
source.format = static_cast<RTFormat>(std::strtoul(value, nullptr, 16));
}
}
// 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.
// A DLSS evaluation: 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 ||
if (!dlss_evaluation || !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_depth ? &*dlss_depth : nullptr,
dlss_jitter, {1.0f, 1.0f});
dlss_depth_reversed, dlss_jitter,
dlss_motion_scale);
};
const bool done = upscale_temporally() ||
rasterizer->AccelerateSurfaceCopy(source, surface(*output), config);
Expand Down
12 changes: 9 additions & 3 deletions src/video_core/engines/kepler_compute.h
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,19 @@ class KeplerCompute final : public EngineInterface {
VideoCore::GameDlssStatus status{};
} dlss_chain;

/// 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.
/// Motion vectors and depth of the frame'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, what turns the motion vectors into UV offsets, and whether the
/// depth is reversed.
std::optional<Texture::TICEntry> dlss_motion_vectors;
std::optional<Texture::TICEntry> dlss_depth;
GPUVAddr dlss_motion_color{};
std::array<f32, 2> dlss_jitter{};
std::array<f32, 2> dlss_motion_scale{1.0f, 1.0f};
bool dlss_depth_reversed{true};
/// Whether the evaluation's network is translated when it may draw: Fortune's Weave's. Fast
/// Fusion's took the host driver 13 s to compile, freezing its menu.
bool dlss_network_translated{true};
bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call);
NativeQmdScheduler native_qmd;
std::array<u32, 0xc00>* native_data_ram{};
Expand Down
2 changes: 2 additions & 0 deletions src/video_core/host_shaders/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile_edge_direction.frag
${CMAKE_CURRENT_SOURCE_DIR}/fsr3_optical_flow.comp
${CMAKE_CURRENT_SOURCE_DIR}/fsr3_frame_interpolation.comp
${CMAKE_CURRENT_SOURCE_DIR}/fsr3_upscaler_alpha.comp
${CMAKE_CURRENT_SOURCE_DIR}/fsr3_upscaler_reverse_depth.comp
${CMAKE_CURRENT_SOURCE_DIR}/dlss_network_probe.comp
)

Expand Down
87 changes: 87 additions & 0 deletions src/video_core/host_shaders/fsr3_upscaler_alpha.comp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

#version 460 core
#extension GL_EXT_samplerless_texture_functions : require

// DLSS can upscale the colour's alpha (Fast Fusion blends its menu vehicles over the menu with it);
// FSR 3.1 writes 1. Carry the input's alpha into FSR's output: sampled where the output pixel lies
// in the unjittered frame, then blended with the previous result along FSR's dilated motion vectors
// and kept within the alpha around the sample, so edges settle on their coverage instead of
// following the camera jitter.

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(binding = 0) uniform texture2D input_color;
layout(binding = 1) uniform texture2D dilated_motion_vectors; // UV offsets to the previous frame
layout(binding = 2) uniform texture2D history;
layout(binding = 3, r16f) uniform writeonly image2D next_history;
layout(binding = 4, rgba16f) uniform image2D upscaled_output;

// The start of cbFSR3Upscaler (ffx_fsr3upscaler_callbacks_glsl.h).
layout(binding = 5, std140) uniform Constants {
ivec2 render_size;
ivec2 previous_render_size;
ivec2 upscale_size;
ivec2 previous_upscale_size;
ivec2 max_render_size;
ivec2 max_upscale_size;
vec4 device_to_view_depth;
vec2 jitter; // input texel k shows the frame at k + 0.5 - jitter
vec2 previous_jitter;
vec2 motion_vector_scale;
vec2 downscale_factor;
vec2 motion_vector_jitter_cancellation;
float tan_half_fov;
float jitter_phase_count;
float delta_time;
float delta_pre_exposure;
float view_space_to_meters;
float frame_index; // 0 when the history starts over
};

layout(binding = 1001) uniform sampler linear_clamp;

// The share of this frame's alpha in the result, as a temporal anti-aliasing resolve takes it.
const float CURRENT_WEIGHT = 0.1;

// Alpha as blending takes it: 0 to 1, and 0 for what the game left undefined (NaN), which would
// otherwise stay in the history.
float Coverage(float alpha) {
return isnan(alpha) ? 0.0 : clamp(alpha, 0.0, 1.0);
}

void main() {
const ivec2 position = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(position, upscale_size))) {
return;
}
const vec2 uv = (vec2(position) + 0.5) / vec2(upscale_size);
const vec2 sample_uv = uv + jitter / vec2(render_size);
const ivec2 center = ivec2(sample_uv * vec2(render_size));
float lowest = 1.0;
float highest = 0.0;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
const ivec2 texel = clamp(center + ivec2(x, y), ivec2(0), render_size - 1);
const float alpha = Coverage(texelFetch(input_color, texel, 0).a);
lowest = min(lowest, alpha);
highest = max(highest, alpha);
}
}
const float current = clamp(
Coverage(textureLod(sampler2D(input_color, linear_clamp), sample_uv, 0.0).a), lowest,
highest);

float alpha = current;
const ivec2 motion_texel = clamp(ivec2(uv * vec2(render_size)), ivec2(0), render_size - 1);
const vec2 previous = uv + texelFetch(dilated_motion_vectors, motion_texel, 0).xy;
if (frame_index > 0.0 && all(greaterThanEqual(previous, vec2(0.0))) &&
all(lessThanEqual(previous, vec2(1.0)))) {
const float remembered =
Coverage(textureLod(sampler2D(history, linear_clamp), previous, 0.0).r);
alpha = mix(clamp(remembered, lowest, highest), current, CURRENT_WEIGHT);
}
imageStore(next_history, position, vec4(alpha));
imageStore(upscaled_output, position, vec4(imageLoad(upscaled_output, position).rgb, alpha));
}
21 changes: 21 additions & 0 deletions src/video_core/host_shaders/fsr3_upscaler_reverse_depth.comp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

#version 460 core
#extension GL_EXT_samplerless_texture_functions : require

// FSR 3.1's passes are built for reversed depth (FFX_FSR3UPSCALER_OPTION_INVERTED_DEPTH): reverse
// a game's standard depth (0 near, 1 far) for them.

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(binding = 0) uniform texture2D depth;
layout(binding = 1, r32f) uniform writeonly image2D reversed_depth;

void main() {
const ivec2 position = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(position, imageSize(reversed_depth)))) {
return;
}
imageStore(reversed_depth, position, vec4(1.0 - texelFetch(depth, position, 0).r));
}
6 changes: 3 additions & 3 deletions src/video_core/rasterizer_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ class RasterizerInterface {

/// 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), with the frame's depth when known. False when unsupported; the caller then falls
/// back to AccelerateSurfaceCopy.
/// frame), with the frame's depth (reversed or not) 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,
const Tegra::Texture::TICEntry* depth, std::array<f32, 2> jitter,
const Tegra::Texture::TICEntry* depth, bool depth_reversed, std::array<f32, 2> jitter,
std::array<f32, 2> motion_scale) {
return false;
}
Expand Down
3 changes: 3 additions & 0 deletions src/video_core/renderer_vulkan/blit_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,9 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView
cmdbuf.Draw(3, 1, 0, 0);
cmdbuf.EndRenderPass();
});
// Like BlitImpl: the game's draws that follow set their dynamic state again (Fast Fusion's
// menu composited nothing after an FSR 3.1 output was drawn here).
scheduler.InvalidateState();
}

void BlitImageHelper::TemporalUpscale(const Framebuffer* dst_framebuffer,
Expand Down
Loading