diff options
| author | rdenno <rhysdenno@gmail.com> | 2026-11-02 09:26:01 -0500 |
|---|---|---|
| committer | rdenno <rhysdenno@gmail.com> | 2026-11-02 09:26:01 -0500 |
| commit | 2786bdf580e41bf0ba6e2a6452db73c4f5b97ed2 (patch) | |
| tree | 2c59cf5d66b869af52f338f3dea6e679e590f7bb /src/video_core | |
| parent | 7ac108e2571d7cb39585c58994524131dd9d8c22 (diff) | |
| parent | c0cb3325e22756edb65c87d33ef298349ade0ad3 (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')
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); |
