aboutsummaryrefslogtreecommitdiff
path: root/src/video_core
diff options
context:
space:
mode:
authorrdenno <rhysdenno@gmail.com>2026-11-02 09:26:01 -0500
committerrdenno <rhysdenno@gmail.com>2026-11-02 09:26:01 -0500
commit2786bdf580e41bf0ba6e2a6452db73c4f5b97ed2 (patch)
tree2c59cf5d66b869af52f338f3dea6e679e590f7bb /src/video_core
parent7ac108e2571d7cb39585c58994524131dd9d8c22 (diff)
parentc0cb3325e22756edb65c87d33ef298349ade0ad3 (diff)
Merge branch 'nx2-experimental' into fix/hw-shader-lowering
nx2-experimental lowers partial-mask TXD as well (Splatoon Raiders). Keep its RZ second destination and this branch's odd register for a lone third channel; take its wider F2I mode check with the .H1 source. Assisted-by: Claude Opus 5.5 (conflict resolution)
Diffstat (limited to 'src/video_core')
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h43
-rw-r--r--src/video_core/engines/kepler_compute.cpp61
-rw-r--r--src/video_core/engines/kepler_compute.h8
-rw-r--r--src/video_core/engines/maxwell_3d.cpp15
-rw-r--r--src/video_core/engines/maxwell_3d.h12
-rw-r--r--src/video_core/host_shaders/CMakeLists.txt2
-rw-r--r--src/video_core/host_shaders/temporal_upscale.frag132
-rw-r--r--src/video_core/host_shaders/vulkan_vertex_repack.comp26
-rw-r--r--src/video_core/rasterizer_interface.h11
-rw-r--r--src/video_core/renderer_opengl/present/layer.cpp5
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp147
-rw-r--r--src/video_core/renderer_vulkan/blit_image.h15
-rw-r--r--src/video_core/renderer_vulkan/fixed_pipeline_state.cpp13
-rw-r--r--src/video_core/renderer_vulkan/present/layer.cpp2
-rw-r--r--src/video_core/renderer_vulkan/vertex_repack.h99
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.cpp51
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.h8
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.cpp64
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.h14
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp25
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.h5
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp150
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h15
-rw-r--r--src/video_core/renderer_vulkan/vk_update_descriptor.cpp8
-rw-r--r--src/video_core/renderer_vulkan/vk_update_descriptor.h3
-rw-r--r--src/video_core/surface.cpp2
-rw-r--r--src/video_core/texture_cache/texture_cache.h83
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h7
28 files changed, 1005 insertions, 21 deletions
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index a030b563d4..53bbae7610 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -390,6 +390,10 @@ template <class P>
void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr,
u32 size) {
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
+ if (!device_addr) {
+ channel_state->uniform_buffers[stage][index] = NULL_BINDING;
+ return;
+ }
const Binding binding{
.device_addr = *device_addr,
.size = size,
@@ -400,7 +404,11 @@ void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr
template <class P>
void BufferCache<P>::DisableGraphicsUniformBuffer(size_t stage, u32 index) {
- channel_state->uniform_buffers[stage][index] = NULL_BINDING;
+ auto& current = channel_state->uniform_buffers[stage][index];
+ if (current.device_addr == 0 && current.size == 0 && !current.buffer_id) [[likely]] {
+ return;
+ }
+ current = NULL_BINDING;
}
template <class P>
@@ -1005,6 +1013,26 @@ void BufferCache<P>::BindHostGraphicsUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 binding_index, bool needs_bind) {
++channel_state->uniform_cache_shots[0];
+ // Bank 24 (root table 0, NVN2's push constants) is read from bank 17, which such games do not
+ // bind: stream the table's current words as that buffer.
+ if (index == 17 && maxwell3d->root_tables.loaded && channel_state->uniform_buffers[stage][index].size == 0) {
+ const auto& words = maxwell3d->root_tables.tables[0];
+ const u32 size = (*channel_state->uniform_buffer_sizes)[stage][index];
+ const u32 copied = (std::min)(size, static_cast<u32>(sizeof(words)));
+ if constexpr (IS_OPENGL) {
+ std::array<u8, sizeof(words)> data{};
+ std::memcpy(data.data(), words.data(), copied);
+ runtime.BindFastUniformBuffer(stage, binding_index, copied);
+ runtime.PushFastUniformBuffer(stage, binding_index, std::span<const u8>(data.data(), copied));
+ } else {
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ std::memset(span.data(), 0, span.size());
+ std::memcpy(span.data(), words.data(), copied);
+ }
+ channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
+ channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
+ return;
+ }
const Binding& binding = channel_state->uniform_buffers[stage][index];
const DAddr device_addr = binding.device_addr;
const u32 size = (std::min)(binding.size, (*channel_state->uniform_buffer_sizes)[stage][index]);
@@ -1505,6 +1533,9 @@ void BufferCache<P>::UpdateDrawIndirect() {
template <class P>
void BufferCache<P>::UpdateUniformBuffers(size_t stage) {
+ if (channel_state->enabled_uniform_buffer_masks[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_uniform_buffer_masks[stage], [&](u32 index) {
Binding& binding = channel_state->uniform_buffers[stage][index];
if (binding.buffer_id) {
@@ -1522,6 +1553,9 @@ void BufferCache<P>::UpdateUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::UpdateStorageBuffers(size_t stage) {
+ if (channel_state->enabled_storage_buffers[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_storage_buffers[stage], [&](u32 index) {
// Resolve buffer
Binding& binding = channel_state->storage_buffers[stage][index];
@@ -1534,6 +1568,9 @@ void BufferCache<P>::UpdateStorageBuffers(size_t stage) {
template <class P>
void BufferCache<P>::UpdateTextureBuffers(size_t stage) {
+ if (channel_state->enabled_texture_buffers[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_texture_buffers[stage], [&](u32 index) {
Binding& binding = channel_state->texture_buffers[stage][index];
binding.buffer_id = FindBuffer(binding.device_addr, binding.size, false);
@@ -1628,10 +1665,10 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3
template <class P>
BufferId BufferCache<P>::FindBuffer(DAddr device_addr, u32 size, bool sparse_compatible) {
- if (device_addr == 0) {
+ const u64 page = device_addr >> CACHING_PAGEBITS;
+ if (device_addr == 0 || page >= page_table.size()) {
return NULL_BUFFER_ID;
}
- const u64 page = device_addr >> CACHING_PAGEBITS;
const BufferId buffer_id = page_table[page];
if (buffer_id) {
Buffer& buffer = slot_buffers[buffer_id];
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index 0377250fc1..4732167369 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,12 +590,38 @@ 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); Kirby Air Riders' reads a 2048x1152 image at cbuf0+0x80 and writes a
-// 2560x1440 one at +0x90. 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, else the first two
-// handles in cbuf0+0x80..0x11c that form such a pair.
+// TLD/SUST operands). Kirby Air Riders' reads a 2048x1152 image at cbuf0+0x80 and writes a
+// 2560x1440 one at +0x90; Fire Emblem: Fortune's Weave's DLSS network ends with a pass that reads
+// 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.
bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
const auto& cbuf = launch_description.const_buffer_config[0];
const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low;
@@ -609,6 +637,12 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
to->IsBlockLinear();
};
std::optional<Texture::TICEntry> input = image(0x90), output = image(0xa0);
+ bool fortunes_weave = false;
+ if (!upscale_pair(input, output)) {
+ input = image(0x328);
+ output = image(0x348);
+ fortunes_weave = upscale_pair(input, output);
+ }
if (!upscale_pair(input, output)) {
input.reset();
output.reset();
@@ -723,7 +757,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{};
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index b344514bc6..9731f54584 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -164,6 +164,8 @@ bool Maxwell3D::IsMethodExecutable(u32 method) {
case MAXWELL3D_REG_INDEX(vertex_array_instance_first):
case MAXWELL3D_REG_INDEX(vertex_array_instance_subsequent):
case MAXWELL3D_REG_INDEX(draw_texture.src_y0):
+ case RootTables::SelectMethod:
+ case RootTables::LoadMethod:
case MAXWELL3D_REG_INDEX(wait_for_idle):
case MAXWELL3D_REG_INDEX(shadow_ram_control):
case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr):
@@ -352,6 +354,17 @@ void Maxwell3D::ProcessMethodCall(u32 method, u32 argument, u32 nonshadow_argume
LOG_DEBUG(HW_GPU, "Native GPU surface clear: value={:#x}", argument);
}
switch (method) {
+ case RootTables::SelectMethod:
+ root_tables.selected = argument & 7;
+ root_tables.offset = (argument >> 8) & 0xff;
+ return;
+ case RootTables::LoadMethod:
+ if (root_tables.offset < 256 && root_tables.offset % 4 == 0) {
+ root_tables.tables[root_tables.selected][root_tables.offset / 4] = argument;
+ root_tables.loaded = true;
+ }
+ root_tables.offset += 4;
+ return;
case MAXWELL3D_REG_INDEX(wait_for_idle):
return rasterizer->WaitForIdle();
case MAXWELL3D_REG_INDEX(shadow_ram_control):
@@ -829,6 +842,8 @@ bool FrontHandledOutsideDrawManager(u32 method) {
return true;
}
switch (method) {
+ case Maxwell3D::RootTables::SelectMethod:
+ case Maxwell3D::RootTables::LoadMethod:
case MAXWELL3D_REG_INDEX(wait_for_idle):
case MAXWELL3D_REG_INDEX(shadow_ram_control):
case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr):
diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h
index 246274c897..af0e37978e 100644
--- a/src/video_core/engines/maxwell_3d.h
+++ b/src/video_core/engines/maxwell_3d.h
@@ -3179,6 +3179,18 @@ public:
std::array<u32, 0xc00>& GetNativeDataRam() { return native_data_ram; }
std::span<u32> GetNativeMacroPositions() { return native_macro_context ? std::span<u32>{native_macro_context->positions} : std::span<u32>{macro_positions}; }
+ /// Root tables: eight 256-byte tables written inline (method 0x504 selects a table, bits 0-2,
+ /// and a byte offset, bits 8-15; each 0x508 writes the next word) that shaders read as constant
+ /// banks 24-31. NVN2 keeps push constants in bank 24, which the Ampere lowering reads as bank 17.
+ struct RootTables {
+ static constexpr u32 SelectMethod = 0x504 / 4;
+ static constexpr u32 LoadMethod = 0x508 / 4;
+ std::array<std::array<u32, 64>, 8> tables{};
+ u32 selected{};
+ u32 offset{};
+ bool loaded{};
+ } root_tables;
+
/// Write the value to the register identified by method.
void CallMethod(Core::System& system, u32 method, u32 method_argument, bool is_last_call) override;
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index 35b350c2cd..9ff7563ff7 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -14,6 +14,7 @@ set(GLSL_INCLUDES
)
set(SHADER_FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/vulkan_vertex_repack.comp
${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/bc_encoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag
@@ -71,6 +72,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_upscale.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_blit_depth_stencil.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.vert
diff --git a/src/video_core/host_shaders/temporal_upscale.frag b/src/video_core/host_shaders/temporal_upscale.frag
new file mode 100644
index 0000000000..0e76df2b43
--- /dev/null
+++ b/src/video_core/host_shaders/temporal_upscale.frag
@@ -0,0 +1,132 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Stands in for a game's DLSS evaluation (the tensor-core network cannot run here): a temporal
+// upscale. Each frame's input samples are placed where the camera jitter put them and weighted by
+// their distance to the output pixel; they accumulate with the previous output, reprojected along
+// the game's motion vectors (bicubic) and clipped to the current 3x3 neighbourhood.
+
+#version 450
+
+layout(binding = 0) uniform sampler2D input_color;
+layout(binding = 1) uniform sampler2D motion_vectors;
+layout(binding = 2) uniform sampler2D 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
+};
+
+layout(location = 0) in vec2 texcoord; // 0 to 1 across the output
+layout(location = 0) out vec4 color;
+
+// The blit helper's samplers take unnormalized (texel) coordinates.
+
+// Blending in a range-compressed space keeps bright pixels from dominating the average.
+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));
+}
+
+vec3 RgbToYCoCg(vec3 c) {
+ return vec3(dot(c, vec3(0.25, 0.5, 0.25)), dot(c, vec3(0.5, 0.0, -0.5)),
+ dot(c, vec3(-0.25, 0.5, -0.25)));
+}
+
+vec3 YCoCgToRgb(vec3 c) {
+ return vec3(c.x + c.y - c.z, c.x + c.z, c.x - c.y - c.z);
+}
+
+// Moves value towards the box centre until it lies inside the box.
+vec3 ClipToBox(vec3 value, vec3 low, vec3 high) {
+ const vec3 center = 0.5 * (low + high);
+ const vec3 extent = 0.5 * (high - low) + 1.0 / 65504.0;
+ const vec3 offset = value - center;
+ const vec3 units = abs(offset / extent);
+ const float largest = max(units.x, max(units.y, units.z));
+ return largest > 1.0 ? center + offset / largest : value;
+}
+
+// Catmull-Rom filtered history from five bilinear fetches: repeated bilinear reprojection would
+// blur the accumulated image a little more every frame.
+vec3 SampleHistory(vec2 position) {
+ const vec2 texel1 = floor(position - 0.5) + 0.5;
+ const vec2 f = position - texel1;
+ const vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
+ const vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
+ const vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));
+ const vec2 w3 = f * f * (-0.5 + 0.5 * f);
+ const vec2 w12 = w1 + w2;
+ const vec2 texel0 = texel1 - 1.0;
+ const vec2 texel3 = texel1 + 2.0;
+ const vec2 texel12 = texel1 + w2 / w12;
+ vec3 sum = texture(history, vec2(texel12.x, texel0.y)).rgb * (w12.x * w0.y);
+ sum += texture(history, vec2(texel0.x, texel12.y)).rgb * (w0.x * w12.y);
+ sum += texture(history, texel12).rgb * (w12.x * w12.y);
+ sum += texture(history, vec2(texel3.x, texel12.y)).rgb * (w3.x * w12.y);
+ sum += texture(history, vec2(texel12.x, texel3.y)).rgb * (w12.x * w3.y);
+ const float weight = w12.x * w0.y + w0.x * w12.y + w12.x * w12.y + w3.x * w12.y + w12.x * w3.y;
+ return max(sum / weight, vec3(0.0));
+}
+
+void main() {
+ const ivec2 input_size = textureSize(input_color, 0);
+ const ivec2 motion_size = textureSize(motion_vectors, 0);
+ // The output pixel's centre in input pixels. Input texel k sampled the scene at k + 0.5 - jitter
+ // (the sign measured against consecutive frames).
+ const vec2 target = texcoord * vec2(input_size);
+ const ivec2 nearest = ivec2(floor(target + jitter));
+
+ 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 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 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 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;
+ if (current_weight >= 1.0 || any(lessThan(previous, vec2(0.0))) ||
+ any(greaterThan(previous, vec2(1.0)))) {
+ weight = 1.0;
+ }
+ 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);
+}
diff --git a/src/video_core/host_shaders/vulkan_vertex_repack.comp b/src/video_core/host_shaders/vulkan_vertex_repack.comp
new file mode 100644
index 0000000000..de9c649d93
--- /dev/null
+++ b/src/video_core/host_shaders/vulkan_vertex_repack.comp
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#version 450
+layout(local_size_x = 128) in;
+layout(std430, set = 0, binding = 0) readonly buffer Source { uint source_words[]; };
+layout(std430, set = 0, binding = 1) writeonly buffer Destination { uint dest_words[]; };
+layout(push_constant) uniform Parameters { uint source_bias; uint source_size; uint total_words; };
+
+// Diagnostic conversion of the captured 69-byte layout: insert three bytes before offset 65.
+void main() {
+ uint word = gl_GlobalInvocationID.x;
+ if (word >= total_words) return;
+ uint vertex = word / 18;
+ uint offset = (word % 18) * 4;
+ uint value = 0;
+ for (uint i = 0; i < 4; ++i) {
+ uint dest_byte = offset + i;
+ if (dest_byte >= 65 && dest_byte < 68) continue;
+ uint src_byte = vertex * 69 + dest_byte - (dest_byte >= 68 ? 3 : 0);
+ if (src_byte >= source_size) continue;
+ uint address = source_bias + src_byte;
+ uint byte_value = (source_words[address / 4] >> ((address % 4) * 8)) & 255;
+ value |= byte_value << (i * 8);
+ }
+ dest_words[word] = value;
+}
diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h
index b527c0e2a9..8486d44ac9 100644
--- a/src/video_core/rasterizer_interface.h
+++ b/src/video_core/rasterizer_interface.h
@@ -6,6 +6,7 @@
#pragma once
+#include <array>
#include <functional>
#include <optional>
#include <span>
@@ -162,6 +163,16 @@ public:
return false;
}
+ /// 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.
+ [[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,
+ std::array<f32, 2> motion_scale) {
+ return false;
+ }
+
[[nodiscard]] virtual Tegra::Engines::AccelerateDMAInterface& AccessAccelerateDMA() = 0;
virtual void AccelerateInlineToMemory(GPUVAddr address, size_t copy_size,
diff --git a/src/video_core/renderer_opengl/present/layer.cpp b/src/video_core/renderer_opengl/present/layer.cpp
index 9e4a0f854a..2656892f4a 100644
--- a/src/video_core/renderer_opengl/present/layer.cpp
+++ b/src/video_core/renderer_opengl/present/layer.cpp
@@ -183,6 +183,11 @@ void Layer::ConfigureFramebufferTexture(const Tegra::FramebufferConfig& framebuf
framebuffer_texture.gl_format = GL_RGBA;
framebuffer_texture.gl_type = GL_UNSIGNED_INT_8_8_8_8_REV;
break;
+ case Service::android::PixelFormat::Rgba1010102:
+ internal_format = GL_RGB10_A2;
+ framebuffer_texture.gl_format = GL_RGBA;
+ framebuffer_texture.gl_type = GL_UNSIGNED_INT_2_10_10_10_REV;
+ break;
case Service::android::PixelFormat::Rgb565:
internal_format = GL_RGB565;
framebuffer_texture.gl_format = GL_RGB;
diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp
index ddd52e9426..edd151b563 100644
--- a/src/video_core/renderer_vulkan/blit_image.cpp
+++ b/src/video_core/renderer_vulkan/blit_image.cpp
@@ -33,6 +33,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_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"
#include "video_core/host_shaders/vulkan_color_clear_vert_spv.h"
@@ -91,6 +92,15 @@ struct PushConstants {
std::array<float, 2> tex_offset;
};
+// The vertex stage's PushConstants first, then those of temporal_upscale.frag.
+struct TemporalUpscalePushConstants {
+ PushConstants vertex;
+ std::array<float, 2> jitter;
+ std::array<float, 2> motion_scale;
+ float current_weight;
+ float padding;
+};
+
struct MSAACopyPushConstants {
std::array<s32, 2> dst_offset;
std::array<s32, 2> src_offset;
@@ -109,6 +119,11 @@ 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{
+ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<0>,
+ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<1>,
+ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<2>,
+};
constexpr VkDescriptorSetLayoutCreateInfo ONE_TEXTURE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.pNext = nullptr,
@@ -133,6 +148,13 @@ 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{
+ .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(),
+};
template <VkShaderStageFlags stageFlags, size_t size>
inline constexpr VkPushConstantRange PUSH_CONSTANT_RANGE{
.stageFlags = stageFlags,
@@ -357,6 +379,32 @@ 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) {
+ image_infos[binding] = VkDescriptorImageInfo{
+ .sampler = sampler,
+ .imageView = image_views[binding],
+ .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
+ };
+ write_descriptor_sets[binding] = VkWriteDescriptorSet{
+ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+ .pNext = nullptr,
+ .dstSet = descriptor_set,
+ .dstBinding = binding,
+ .dstArrayElement = 0,
+ .descriptorCount = 1,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .pImageInfo = &image_infos[binding],
+ .pBufferInfo = nullptr,
+ .pTexelBufferView = nullptr,
+ };
+ }
+ device.GetLogical().UpdateDescriptorSets(write_descriptor_sets, nullptr);
+}
+
void UpdateTwoTexturesDescriptorSet(const Device& device, VkDescriptorSet descriptor_set,
VkSampler sampler, VkImageView image_view_0,
VkImageView image_view_1) {
@@ -590,10 +638,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)),
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>)},
one_texture_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
one_texture_set_layout.address(),
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))),
@@ -601,6 +653,11 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
two_textures_set_layout.address(),
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))),
+ temporal_upscale_pipeline_layout(
+ device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
+ three_textures_set_layout.address(),
+ PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
+ sizeof(TemporalUpscalePushConstants)>))),
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(
@@ -612,6 +669,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(MSAACopyPushConstants)>))),
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)),
blit_color_msaa_frag(BuildShader(device, BLIT_COLOR_MSAA_FRAG_SPV)),
blit_depth_stencil_frag(device.IsExtShaderStencilExportSupported()
? BuildShader(device, VULKAN_BLIT_DEPTH_STENCIL_FRAG_SPV)
@@ -702,6 +760,42 @@ 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,
+ std::array<f32, 2> jitter, std::array<f32, 2> motion_scale,
+ f32 current_weight) {
+ 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),
+ motion_vectors.Handle(Shader::TextureType::Color2D),
+ 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,
+ .padding = 0.0f,
+ };
+ RecordShaderReadBarrier(scheduler, input_color);
+ RecordShaderReadBarrier(scheduler, motion_vectors);
+ 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);
+ 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::BlitImpl(const Framebuffer* dst_framebuffer,
const ImageView& src_image_view, const Region2D& dst_region,
const Region2D& src_region, VkPipeline pipeline, VkSampler sampler,
@@ -1285,6 +1379,59 @@ VkPipeline BlitImageHelper::FindOrEmplaceColorPipeline(const BlitImagePipelineKe
return *blit_color_pipelines.back();
}
+VkPipeline BlitImageHelper::FindOrEmplaceTemporalUpscalePipeline(VkRenderPass renderpass) {
+ const auto it = std::ranges::find(temporal_upscale_keys, renderpass);
+ if (it != temporal_upscale_keys.end()) {
+ return *temporal_upscale_pipelines[std::distance(temporal_upscale_keys.begin(), it)];
+ }
+ temporal_upscale_keys.push_back(renderpass);
+ const std::array stages = MakeStages(*full_screen_vert, *temporal_upscale_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_upscale_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_upscale_pipeline_layout,
+ .renderPass = renderpass,
+ .subpass = 0,
+ .basePipelineHandle = VK_NULL_HANDLE,
+ .basePipelineIndex = 0,
+ }, device.StaticPipelineCache()));
+ return *temporal_upscale_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 62e6743a23..303beeea5c 100644
--- a/src/video_core/renderer_vulkan/blit_image.h
+++ b/src/video_core/renderer_vulkan/blit_image.h
@@ -82,6 +82,13 @@ public:
VkImage src_image, VkSampler src_sampler, const Region2D& dst_region,
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.
+ 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);
+
void BlitColorMSAA(const Framebuffer* dst_framebuffer, const ImageView& src_image_view,
const Region2D& dst_region, const Region2D& src_region);
@@ -170,6 +177,8 @@ private:
[[nodiscard]] VkPipeline FindOrEmplaceColorPipeline(const BlitImagePipelineKey& key);
+ [[nodiscard]] VkPipeline FindOrEmplaceTemporalUpscalePipeline(VkRenderPass renderpass);
+
[[nodiscard]] VkPipeline FindOrEmplaceDepthStencilPipeline(const BlitImagePipelineKey& key);
[[nodiscard]] VkPipeline FindOrEmplaceClearColorPipeline(const BlitImagePipelineKey& key);
@@ -207,15 +216,19 @@ private:
vk::DescriptorSetLayout one_texture_set_layout;
vk::DescriptorSetLayout two_textures_set_layout;
+ vk::DescriptorSetLayout three_textures_set_layout;
DescriptorAllocator one_texture_descriptor_allocator;
DescriptorAllocator two_textures_descriptor_allocator;
+ DescriptorAllocator three_textures_descriptor_allocator;
vk::PipelineLayout one_texture_pipeline_layout;
vk::PipelineLayout two_textures_pipeline_layout;
+ vk::PipelineLayout temporal_upscale_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 blit_color_msaa_frag;
vk::ShaderModule blit_depth_stencil_frag;
vk::ShaderModule blit_depth_frag;
@@ -246,6 +259,8 @@ private:
std::vector<BlitImagePipelineKey> blit_color_keys;
std::vector<vk::Pipeline> blit_color_pipelines;
+ std::vector<VkRenderPass> temporal_upscale_keys;
+ std::vector<vk::Pipeline> temporal_upscale_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/fixed_pipeline_state.cpp b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
index 9dac98c830..d1f78c0034 100644
--- a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
+++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
@@ -14,6 +14,7 @@
#include "common/settings.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/renderer_vulkan/fixed_pipeline_state.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/renderer_vulkan/vk_state_tracker.h"
namespace Vulkan {
@@ -268,6 +269,18 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
if (xfb_enabled) {
RefreshXfbState(xfb_state, regs);
}
+ if (!dynamic_vertex_input && ExperimentalStaticVertexRepackEnabled()) {
+ const u32 repack_mask = VertexRepackMask(regs);
+ // Refresh from guest values even when only the stride changed: offsets must not accumulate.
+ for (size_t index = 0; index < attributes.size(); ++index) {
+ const auto& input = regs.vertex_attrib_format[index];
+ attributes[index].offset.Assign(RepackedVertexOffset(repack_mask, input.buffer, input.offset));
+ }
+ for (u32 binding = 0; binding < vertex_strides.size(); ++binding) {
+ vertex_strides[binding] = static_cast<u16>(RepackedVertexStride(
+ repack_mask, binding, regs.vertex_streams[binding].stride));
+ }
+ }
}
void FixedPipelineState::BlendingAttachment::Refresh(const Maxwell& regs, size_t index) {
diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp
index fa24c02f6d..5652090bd7 100644
--- a/src/video_core/renderer_vulkan/present/layer.cpp
+++ b/src/video_core/renderer_vulkan/present/layer.cpp
@@ -52,6 +52,8 @@ VkFormat GetFormat(const Tegra::FramebufferConfig& framebuffer) {
return VK_FORMAT_R5G6B5_UNORM_PACK16;
case Service::android::PixelFormat::Bgra8888:
return VK_FORMAT_B8G8R8A8_UNORM;
+ case Service::android::PixelFormat::Rgba1010102:
+ return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
default:
UNIMPLEMENTED_MSG("Unknown framebuffer pixel format: {}",
static_cast<u32>(framebuffer.pixel_format));
diff --git a/src/video_core/renderer_vulkan/vertex_repack.h b/src/video_core/renderer_vulkan/vertex_repack.h
new file mode 100644
index 0000000000..b0849c25c7
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vertex_repack.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <cstdlib>
+#include <cstring>
+#include <limits>
+#include <optional>
+#include "video_core/engines/maxwell_3d.h"
+
+namespace Vulkan {
+
+constexpr bool VertexRepackInputSupported(bool dynamic_input, bool experimental_static) {
+ return dynamic_input || experimental_static;
+}
+
+inline bool ExperimentalStaticVertexRepackEnabled() {
+ static const bool enabled = [] {
+ const char* value = std::getenv("EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK");
+ return value && std::strcmp(value, "1") == 0;
+ }();
+ return enabled;
+}
+
+struct VertexRepackParameters {
+ u32 vertices;
+ u32 output_size;
+ u32 source_base;
+ u32 source_bias;
+ u32 source_range;
+ u32 dispatch_groups;
+};
+
+inline std::optional<VertexRepackParameters> MakeVertexRepackParameters(
+ u32 offset, u32 size, u64 alignment, u64 max_storage_range, u32 max_dispatch_groups) {
+ if (size == 0 || alignment == 0) {
+ return std::nullopt;
+ }
+ const u64 vertices = (u64{size} + 68) / 69;
+ const u64 output_size = vertices * 72;
+ const u64 base = u64{offset} / alignment * alignment;
+ const u64 bias = offset - base;
+ const u64 range = (bias + size + 3) / 4 * 4;
+ const u64 groups = (vertices * 18 + 127) / 128;
+ if (output_size > std::numeric_limits<u32>::max() ||
+ range > std::numeric_limits<u32>::max() || output_size > max_storage_range ||
+ range > max_storage_range || groups > max_dispatch_groups) {
+ return std::nullopt;
+ }
+ return VertexRepackParameters{static_cast<u32>(vertices), static_cast<u32>(output_size),
+ static_cast<u32>(base), static_cast<u32>(bias),
+ static_cast<u32>(range), static_cast<u32>(groups)};
+}
+
+inline bool VertexRepackEnabled() {
+ static const bool enabled = [] {
+ const char* value = std::getenv("EGLANTINE_TEST_VERTEX_REPACK");
+ return !value || std::strcmp(value, "0") != 0;
+ }();
+ return enabled;
+}
+
+inline u32 VertexRepackMask(const Tegra::Engines::Maxwell3D::Regs& regs) {
+ using Attribute = Tegra::Engines::Maxwell3D::Regs::VertexAttribute;
+ if (!VertexRepackEnabled()) {
+ return 0;
+ }
+ u32 candidates = 0;
+ u32 excluded = 0;
+ for (const auto& attribute : regs.vertex_attrib_format) {
+ const u32 binding = attribute.buffer;
+ if (attribute.constant || regs.vertex_streams[binding].stride != 69) {
+ continue;
+ }
+ if (attribute.size == Attribute::Size::Invalid) {
+ excluded |= 1U << binding;
+ continue;
+ }
+ const u32 offset = attribute.offset;
+ const u32 size = attribute.SizeInBytes();
+ if (offset == 65 && size == 4) {
+ candidates |= 1U << binding;
+ }
+ if (offset + size > 69 || (offset < 65 && offset + size > 65)) {
+ excluded |= 1U << binding;
+ }
+ }
+ return candidates & ~excluded;
+}
+
+inline u32 RepackedVertexOffset(u32 mask, u32 binding, u32 offset) {
+ return offset + ((mask & (1U << binding)) && offset >= 65 ? 3U : 0U);
+}
+
+inline u32 RepackedVertexStride(u32 mask, u32 binding, u32 stride) {
+ return mask & (1U << binding) ? 72U : stride;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
index 493de2cc20..fe0ab3a28e 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
@@ -7,11 +7,13 @@
#include <algorithm>
#include <array>
#include <cstring>
+#include <cstdlib>
#include <span>
#include <vector>
#include "video_core/buffer_cache/buffer_cache_base.h"
#include "video_core/renderer_vulkan/vk_buffer_cache.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/renderer_vulkan/maxwell_to_vk.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
@@ -359,6 +361,14 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
compute_pass_descriptor_queue),
multi_range_buffers(device_) {
const VkDriverIdKHR driver_id = device.GetDriverID();
+ const bool enable_vertex_repack = VertexRepackEnabled();
+ LOG_INFO(Render_Vulkan, "69-byte vertex layout repacking {} ({})",
+ enable_vertex_repack ? "enabled" : "disabled",
+ "portable Vulkan layout fallback");
+ if (enable_vertex_repack) {
+ vertex_repack_pass = std::make_unique<VertexRepackPass>(
+ device, scheduler, descriptor_pool, staging_pool, compute_pass_descriptor_queue);
+ }
limit_dynamic_storage_buffers = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY ||
driver_id == VK_DRIVER_ID_ARM_PROPRIETARY;
if (limit_dynamic_storage_buffers) {
@@ -374,6 +384,19 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
scheduler_, staging_pool_);
}
+u32 BufferCacheRuntime::SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs,
+ bool dynamic_input) {
+ const u32 old_mask = vertex_repack_mask;
+ const bool input_supported = VertexRepackInputSupported(
+ dynamic_input, ExperimentalStaticVertexRepackEnabled());
+ vertex_repack_mask = vertex_repack_pass && input_supported ? VertexRepackMask(regs) : 0;
+ if (vertex_repack_mask != old_mask) {
+ LOG_DEBUG(Render_Vulkan, "Vertex repack selection: mask={:#x}, dynamic_input={}",
+ vertex_repack_mask, dynamic_input);
+ }
+ return old_mask | vertex_repack_mask;
+}
+
StagingBufferRef BufferCacheRuntime::UploadStagingBuffer(size_t size) {
return staging_pool.Request(size, MemoryUsage::Upload);
}
@@ -629,6 +652,16 @@ void BufferCacheRuntime::BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset
if (index >= device.GetMaxVertexInputBindings()) {
return;
}
+ if (buffer != VK_NULL_HANDLE && size != 0 && IsVertexRepacked(index)) {
+ const auto repacked = vertex_repack_pass->Assemble(buffer, offset, size);
+ if (!repacked) {
+ return;
+ }
+ buffer = repacked->first;
+ offset = static_cast<u32>(repacked->second);
+ size = static_cast<u32>((u64{size} + 68) / 69 * 72);
+ stride = 72;
+ }
if (device.IsExtExtendedDynamicStateSupported()) {
scheduler.Record([index, buffer, offset, size, stride](vk::CommandBuffer cmdbuf) {
const VkDeviceSize vk_offset = buffer != VK_NULL_HANDLE ? offset : 0;
@@ -661,6 +694,24 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bi
}
}
buffer_handles[i] = handle;
+ const u32 binding = bindings.min_index + i;
+ if (handle != VK_NULL_HANDLE && IsVertexRepacked(binding) && bindings.strides[i] == 69 &&
+ bindings.sizes[i] > 0) {
+ const auto repacked = vertex_repack_pass->Assemble(
+ handle, static_cast<u32>(bindings.offsets[i]), static_cast<u32>(bindings.sizes[i]));
+ if (!repacked) {
+ continue;
+ }
+ buffer_handles[i] = repacked->first;
+ bindings.offsets[i] = repacked->second;
+ bindings.sizes[i] = (bindings.sizes[i] + 68) / 69 * 72;
+ bindings.strides[i] = 72;
+ static bool reported = false;
+ if (!reported) {
+ LOG_WARNING(Render_Vulkan, "Experimental GPU vertex repacking active: 69 -> 72 bytes");
+ reported = true;
+ }
+ }
}
const u32 device_max = device.GetMaxVertexInputBindings();
const u32 min_binding = (std::min)(bindings.min_index, device_max);
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index fff1f8164d..ff854445b5 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -110,6 +110,12 @@ public:
void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept;
+ u32 SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs, bool dynamic_input);
+
+ bool IsVertexRepacked(u32 binding) const {
+ return (vertex_repack_mask & (1U << binding)) != 0;
+ }
+
u64 CurrentTick();
bool IsFree(u64 tick);
@@ -259,6 +265,8 @@ private:
vk::Buffer null_buffer;
std::unique_ptr<Uint8Pass> uint8_pass;
+ std::unique_ptr<VertexRepackPass> vertex_repack_pass;
+ u32 vertex_repack_mask{};
QuadIndexedPass quad_index_pass;
MultiRangeBufferCache multi_range_buffers;
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
index 8cd8abb599..cb8dcde42f 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
@@ -26,8 +26,10 @@
#include "video_core/host_shaders/resolve_conditional_render_comp_spv.h"
#include "video_core/host_shaders/vulkan_quad_indexed_comp_spv.h"
#include "video_core/host_shaders/vulkan_uint8_comp_spv.h"
+#include "video_core/host_shaders/vulkan_vertex_repack_comp_spv.h"
#include "video_core/host_shaders/block_linear_unswizzle_3d_bcn_comp_spv.h"
#include "video_core/renderer_vulkan/vk_compute_pass.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/surface.h"
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
@@ -324,6 +326,62 @@ ComputePass::ComputePass(const Device& device_, Scheduler& scheduler, Descriptor
ComputePass::~ComputePass() = default;
+VertexRepackPass::VertexRepackPass(const Device& device_, Scheduler& scheduler_,
+ DescriptorPool& descriptor_pool, StagingBufferPool& staging_pool_,
+ ComputePassDescriptorQueue& descriptor_queue_)
+ : ComputePass(device_, scheduler_, descriptor_pool, INPUT_OUTPUT_DESCRIPTOR_SET_BINDINGS,
+ INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE, INPUT_OUTPUT_BANK_INFO,
+ COMPUTE_PUSH_CONSTANT_RANGE<12>, VULKAN_VERTEX_REPACK_COMP_SPV),
+ scheduler{scheduler_}, staging_pool{staging_pool_}, descriptor_queue{descriptor_queue_} {}
+
+std::optional<std::pair<VkBuffer, VkDeviceSize>> VertexRepackPass::Assemble(VkBuffer source,
+ u32 offset, u32 size) {
+ const auto parameters = MakeVertexRepackParameters(
+ offset, size, device.GetStorageBufferAlignment(), device.GetMaxStorageBufferRange(),
+ device.GetMaxComputeWorkGroupCount()[0]);
+ if (!parameters) {
+ LOG_ERROR(Render_Vulkan, "Vertex repack exceeds device limits: offset={}, size={}", offset, size);
+ return std::nullopt;
+ }
+ const u32 num_vertices = parameters->vertices;
+ const u32 output_size = parameters->output_size;
+ const auto dest = staging_pool.Request(output_size, MemoryUsage::DeviceLocal);
+ const u32 base_offset = parameters->source_base;
+ const u32 bias = parameters->source_bias;
+ descriptor_queue.Acquire(scheduler, 2);
+ descriptor_queue.AddBuffer(source, base_offset, parameters->source_range);
+ descriptor_queue.AddBuffer(dest.buffer, dest.offset, output_size);
+ const void* data = descriptor_queue.UpdateData();
+ scheduler.RequestOutsideRenderPassOperationContext();
+ LOG_DEBUG(Render_Vulkan, "Vertex repack queued: source_size={}, output_size={}, groups={}",
+ size, output_size, parameters->dispatch_groups);
+ scheduler.Record([this, data, bias, size, num_vertices,
+ groups = parameters->dispatch_groups](vk::CommandBuffer cmdbuf) {
+ const VkMemoryBarrier read_barrier{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
+ .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, read_barrier);
+ const auto set = descriptor_allocator.Commit();
+ device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, data);
+ cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
+ cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
+ const std::array<u32, 3> constants{bias, size, num_vertices * 18};
+ cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(constants), constants.data());
+ cmdbuf.Dispatch(groups, 1, 1);
+ const VkMemoryBarrier write_barrier{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
+ .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, 0, write_barrier);
+ });
+ return std::pair<VkBuffer, VkDeviceSize>{dest.buffer, dest.offset};
+}
+
Uint8Pass::Uint8Pass(const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool,
StagingBufferPool& staging_buffer_pool_,
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
@@ -743,11 +801,11 @@ void ASTCDecoderPass::AssembleInto(const VideoCommon::ImageInfo& info, u32 skip,
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
vk::PIPELINE_STAGE_GRAPHICS_COMPUTE, 0, image_barrier);
});
- // Each decode ends with the GPU thread waiting for the host GPU to go idle. Measured here
- // (count and time per 5 s); EDEN_NX2_ASTC_FLUSH=1 submits without waiting instead.
+ // Each decode ends with a submit to the host GPU. Measured here
+ // (count and time per 5 s); EDEN_NX2_ASTC_FLUSH=0 waits for the host GPU to go idle instead.
static const bool flush_only = [] {
const char* value = std::getenv("EDEN_NX2_ASTC_FLUSH");
- return value != nullptr && value[0] == '1';
+ return value == nullptr || value[0] != '0';
}();
if (!target.wait) {
return;
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h
index 9f2e8e9f1c..27fffbbbb7 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pass.h
+++ b/src/video_core/renderer_vulkan/vk_compute_pass.h
@@ -56,6 +56,20 @@ private:
vk::ShaderModule module;
};
+class VertexRepackPass final : public ComputePass {
+public:
+ VertexRepackPass(const Device& device, Scheduler& scheduler, DescriptorPool& descriptor_pool,
+ StagingBufferPool& staging_pool, ComputePassDescriptorQueue& descriptor_queue);
+ /// Returns nothing when the range exceeds the device limits; the binding is then left as is.
+ std::optional<std::pair<VkBuffer, VkDeviceSize>> Assemble(VkBuffer source, u32 offset,
+ u32 size);
+
+private:
+ Scheduler& scheduler;
+ StagingBufferPool& staging_pool;
+ ComputePassDescriptorQueue& descriptor_queue;
+};
+
class Uint8Pass final : public ComputePass {
public:
explicit Uint8Pass(const Device& device_, Scheduler& scheduler_,
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index 2f638026c9..d3dbba6163 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -299,6 +299,18 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex};
// update engine as channel may be different.
pipeline->SetEngine(maxwell3d, gpu_memory);
+ const u32 repack_dirty = buffer_cache_runtime.SetVertexRepackLayout(
+ maxwell3d->regs, pipeline->HasDynamicVertexInput());
+ if (repack_dirty) {
+ auto& flags = maxwell3d->dirty.flags;
+ flags[VideoCommon::Dirty::VertexBuffers] = true;
+ flags[Dirty::VertexInput] = true;
+ for (u32 binding = 0; binding < 32; ++binding) {
+ if (repack_dirty & (1U << binding)) {
+ flags[VideoCommon::Dirty::VertexBuffer0 + binding] = true;
+ }
+ }
+ }
if (Profile::Scope scope{Profile::Zone::Configure}; !pipeline->Configure(is_indexed)) {
report_failure("configure failed");
return;
@@ -1263,6 +1275,14 @@ bool RasterizerVulkan::AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surf
return texture_cache.BlitImage(dst, src, copy_config);
}
+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) {
+ std::scoped_lock lock{texture_cache.mutex};
+ return texture_cache.TemporalUpscale(dst, src, motion_vectors, jitter, motion_scale);
+}
+
Tegra::Engines::AccelerateDMAInterface& RasterizerVulkan::AccessAccelerateDMA() {
return accelerate_dma;
}
@@ -2293,7 +2313,8 @@ void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs)
.location = index,
.binding = binding,
.format = MaxwellToVK::VertexFormat(device, attribute.type, attribute.size),
- .offset = attribute.offset,
+ .offset = attribute.offset +
+ (buffer_cache_runtime.IsVertexRepacked(binding) && attribute.offset >= 65 ? 3 : 0),
});
}
@@ -2304,7 +2325,7 @@ void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs)
.sType = VK_STRUCTURE_TYPE_VERTEX_INPUT_BINDING_DESCRIPTION_2_EXT,
.pNext = nullptr,
.binding = binding,
- .stride = input_binding.stride,
+ .stride = buffer_cache_runtime.IsVertexRepacked(binding) ? 72U : input_binding.stride.Value(),
.inputRate = is_instanced ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX,
.divisor = is_instanced ? input_binding.frequency : 1,
});
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h
index e8989dcdfc..fc282fbe8d 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.h
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.h
@@ -128,6 +128,11 @@ public:
bool AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surface& src,
const Tegra::Engines::Fermi2D::Surface& dst,
const Tegra::Engines::Fermi2D::Config& copy_config) override;
+ 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,
+ std::array<f32, 2> motion_scale) override;
Tegra::Engines::AccelerateDMAInterface& AccessAccelerateDMA() override;
void AccelerateInlineToMemory(GPUVAddr address, size_t copy_size,
std::span<const u8> memory) override;
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index cb0453c178..30981fe413 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -1535,6 +1535,154 @@ void TextureCacheRuntime::ReinterpretImage(Image& dst, Image& src,
});
}
+bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst,
+ ImageView& input_color, ImageView& motion_vectors,
+ 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;
+ }
+ 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
+ }
+ 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,
+ .pNext = nullptr,
+ .flags = 0,
+ .image = *temporal_history.image,
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = format,
+ .components{},
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ },
+ });
+ temporal_history.format = format;
+ temporal_history.extent = extent;
+ reset = true;
+ LOG_INFO(Render_Vulkan, "Temporal upscale into {}x{} (format {})", extent.width, extent.height,
+ 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,
+ };
+ const auto barrier = [](VkImage image, VkAccessFlags src_access, VkAccessFlags dst_access,
+ VkImageLayout old_layout, const VkImageSubresourceLayers& layers) {
+ return VkImageMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = src_access,
+ .dstAccessMask = dst_access,
+ .oldLayout = old_layout,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = image,
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = layers.mipLevel,
+ .levelCount = 1,
+ .baseArrayLayer = layers.baseArrayLayer,
+ .layerCount = 1,
+ },
+ };
+ };
+ if (reset) {
+ // A new history image has no contents yet; the first frame does not read it.
+ 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));
+ });
+ }
+ const Region2D dst_region{
+ .start = {0, 0},
+ .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;
+ blit_image_helper.TemporalUpscale(dst_framebuffer, input_color, motion_vectors,
+ *temporal_history.view, dst_region, jitter, motion_scale,
+ reset ? 1.0f : current_weight);
+ 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),
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0,
+ {}, {}, after);
+ });
+ return true;
+}
+
void TextureCacheRuntime::BlitImage(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& src,
const Region2D& dst_region, const Region2D& src_region,
Tegra::Engines::Fermi2D::Filter filter,
@@ -4232,7 +4380,7 @@ void TextureCacheRuntime::TranscodeAstcToBcn(
.aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT,
.initialized = false,
.level_view = [&pending](s32 level) { return *pending.views[level]; },
- .wait = false,
+ .wait = true,
},
map, swizzles);
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index d5f8756486..564b2cbf9e 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -99,6 +99,12 @@ 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.
+ bool TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& input_color,
+ ImageView& motion_vectors, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale);
+
void CopyImage(Image& dst, Image& src, std::span<const VideoCommon::ImageCopy> copies);
void CopyImageMSAA(Image& dst, Image& src, std::span<const VideoCommon::ImageCopy> copies);
@@ -181,6 +187,15 @@ public:
StagingBufferPool& staging_buffer_pool;
BlitImageHelper& blit_image_helper;
RenderPassCache& render_pass_cache;
+
+ /// TemporalUpscale's history: its previous result, copied right after drawing it.
+ struct TemporalHistory {
+ vk::Image image;
+ vk::ImageView view;
+ VkFormat format{};
+ VkExtent2D extent{};
+ } temporal_history;
+
std::optional<ASTCDecoderPass> astc_decoder_pass;
std::optional<BcEncoderPass> bc_encoder_pass;
diff --git a/src/video_core/renderer_vulkan/vk_update_descriptor.cpp b/src/video_core/renderer_vulkan/vk_update_descriptor.cpp
index 742a8d9969..29f3ee71a1 100644
--- a/src/video_core/renderer_vulkan/vk_update_descriptor.cpp
+++ b/src/video_core/renderer_vulkan/vk_update_descriptor.cpp
@@ -20,10 +20,10 @@ UpdateDescriptorQueue::UpdateDescriptorQueue(const Device& device_, size_t frame
bool supports_descriptor_buffer_)
: device{device_}, frame_payload_size{frame_payload_size_},
supports_descriptor_buffer{supports_descriptor_buffer_},
- payload(frame_payload_size_ * FRAMES_IN_FLIGHT)
+ payload(std::make_unique_for_overwrite<DescriptorUpdateEntry[]>(frame_payload_size_ * FRAMES_IN_FLIGHT))
{
- payload_start = payload.data();
- payload_cursor = payload.data();
+ payload_start = payload.get();
+ payload_cursor = payload.get();
}
UpdateDescriptorQueue::~UpdateDescriptorQueue() = default;
@@ -32,7 +32,7 @@ void UpdateDescriptorQueue::TickFrame() {
if (++frame_index >= FRAMES_IN_FLIGHT) {
frame_index = 0;
}
- payload_start = payload.data() + frame_index * frame_payload_size;
+ payload_start = payload.get() + frame_index * frame_payload_size;
payload_cursor = payload_start;
}
diff --git a/src/video_core/renderer_vulkan/vk_update_descriptor.h b/src/video_core/renderer_vulkan/vk_update_descriptor.h
index 9f30e65dbd..4d393376b4 100644
--- a/src/video_core/renderer_vulkan/vk_update_descriptor.h
+++ b/src/video_core/renderer_vulkan/vk_update_descriptor.h
@@ -6,6 +6,7 @@
#pragma once
+#include <memory>
#include <variant>
#include <vector>
#include "video_core/vulkan_common/vulkan_wrapper.h"
@@ -122,7 +123,7 @@ private:
DescriptorUpdateEntry* payload_cursor = nullptr;
DescriptorUpdateEntry* payload_start = nullptr;
const DescriptorUpdateEntry* upload_start = nullptr;
- std::vector<DescriptorUpdateEntry> payload;
+ std::unique_ptr<DescriptorUpdateEntry[]> payload;
};
// TODO: should these be separate classes instead?
diff --git a/src/video_core/surface.cpp b/src/video_core/surface.cpp
index 66b9577cb5..ef4859ea3e 100644
--- a/src/video_core/surface.cpp
+++ b/src/video_core/surface.cpp
@@ -214,6 +214,8 @@ PixelFormat PixelFormatFromGPUPixelFormat(Service::android::PixelFormat format)
return PixelFormat::R5G6B5_UNORM;
case Service::android::PixelFormat::Bgra8888:
return PixelFormat::B8G8R8A8_UNORM;
+ case Service::android::PixelFormat::Rgba1010102:
+ return PixelFormat::A2B10G10R10_UNORM;
default:
UNIMPLEMENTED_MSG("Unimplemented format={}", format);
return PixelFormat::A8B8G8R8_UNORM;
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 45bc35dee6..3f3ae9cafd 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -125,6 +125,7 @@ template <class P>
void TextureCache<P>::RunGarbageCollector() {
bool high_priority_mode = false;
bool aggressive_mode = false;
+ bool evict_old_costly = false;
u64 ticks_to_destroy = 0;
size_t num_iterations = 0;
const auto Configure = [&](bool allow_aggressive) {
@@ -139,7 +140,8 @@ void TextureCache<P>::RunGarbageCollector() {
ticks_to_destroy = (std::max)(ticks_to_destroy, min_age);
num_iterations = aggressive_mode ? 40 : (high_priority_mode ? 20 : 10);
};
- const auto Cleanup = [this, &num_iterations, &high_priority_mode, &aggressive_mode](ImageId image_id) {
+ const auto Cleanup = [this, &num_iterations, &high_priority_mode, &aggressive_mode,
+ &evict_old_costly](ImageId image_id) {
if (num_iterations == 0) {
return true;
}
@@ -149,7 +151,8 @@ void TextureCache<P>::RunGarbageCollector() {
return false;
}
const bool must_download = IsDownloadable(image) && False(image.flags & ImageFlagBits::BadOverlap);
- if ((!aggressive_mode && True(image.flags & ImageFlagBits::CostlyLoad)) || (!high_priority_mode && must_download)) {
+ if ((!aggressive_mode && !evict_old_costly && True(image.flags & ImageFlagBits::CostlyLoad)) ||
+ (!high_priority_mode && must_download)) {
return false;
}
if (must_download) {
@@ -227,6 +230,15 @@ void TextureCache<P>::RunGarbageCollector() {
}
};
Configure(false);
+ static const u64 old_age = [] {
+ const char* value = std::getenv("EDEN_NX2_TC_OLD_AGE");
+ return value != nullptr ? std::strtoull(value, nullptr, 10) : 1800ULL;
+ }();
+ if (high_priority_mode && old_age != 0 && frame_tick > old_age) {
+ evict_old_costly = true;
+ lru_cache.ForEachItemBelow(frame_tick - old_age, Cleanup);
+ evict_old_costly = false;
+ }
Collect();
if (total_used_memory >= critical_memory) {
Configure(true);
@@ -1082,6 +1094,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
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index 24ebaf0689..09deb8da02 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -283,6 +283,13 @@ public:
const Tegra::Engines::Fermi2D::Surface& src,
const Tegra::Engines::Fermi2D::Config& copy);
+ /// Stand-in for a DLSS evaluation: temporal upscale of src into dst (see the runtime's
+ /// TemporalUpscale). False when the images cannot take it; the caller then blits.
+ 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);
+
/// Try to find a cached image view in the given CPU address
[[nodiscard]] std::pair<ImageView*, bool> TryFindFramebufferImageView(
const Tegra::FramebufferConfig& config, DAddr cpu_addr);