From 9530cecfc4f8e44436503ee922921b7b72c512ff Mon Sep 17 00:00:00 2001 From: Codex draft Date: Wed, 4 Nov 2026 19:35:25 +0000 Subject: [PATCH 1/3] vulkan: restore wave displacement for empty Mario Kart World depth inputs Use the original wave amplitude when the fingerprinted native shader sees empty depth inputs. Keep the opt-in workaround in a separate Vulkan shader cache. Assisted-by: Codex (handoff analysis and draft implementation) --- src/video_core/mkw_wave_workaround.h | 49 +++++++++++++++++++ .../renderer_vulkan/vk_pipeline_cache.cpp | 6 ++- src/video_core/shader_environment.cpp | 5 ++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/video_core/mkw_wave_workaround.h diff --git a/src/video_core/mkw_wave_workaround.h b/src/video_core/mkw_wave_workaround.h new file mode 100644 index 0000000000..02f6dd9d92 --- /dev/null +++ b/src/video_core/mkw_wave_workaround.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +#include "shader_recompiler/frontend/ampere/program_override.h" + +namespace VideoCommon { + +// Experimental, opt-in workaround for a missing ground-height map. The original wave +// amplitudes and water exclusion mask still control the resulting displacement. +inline bool ApplyMkwZeroDepthFallback(std::span source, + Shader::Ampere::LoweredProgram& program) { + if (!program || source.size_bytes() != 0x3b0 || + Shader::Ampere::NativeProgramKey(source) != 0x5a4f3726590efb17ULL || + program.source_instruction_offsets.size() != source.size()) { + return false; + } + constexpr size_t next_source = 0x2d0 / sizeof(Shader::Ampere::Instruction); + const size_t insert = program.source_instruction_offsets[next_source]; + if (insert > program.instructions.size()) { + return false; + } + // At this point R8=(water-ground)*20, R9=water and R4=saturate(R8). + // Both R8 and R9 zero identifies the entirely empty pair, including neither + // real shallows (equal nonzero heights) nor dry ground (negative depth). + constexpr u64 always = u64{7} << 16; + constexpr std::array fallback{ + // LOP.OR R250, R8, R9 (PT destination; no live predicate is changed). + 0x5c40000000000000ULL | always | 250 | (u64{8} << 8) | (u64{9} << 20) | + (u64{1} << 41) | (u64{7} << 48), + // ISET.EQ.U32.AND with float-result mode: 1.0 if empty, otherwise 0. + 0x5b50000000000000ULL | always | 250 | (u64{250} << 8) | (u64{255} << 20) | + (u64{7} << 39) | (u64{1} << 44) | (u64{2} << 49), + // Empty input has R4=0; other input ORs with zero and stays unchanged. + 0x5c40000000000000ULL | always | 4 | (u64{4} << 8) | (u64{250} << 20) | + (u64{1} << 41) | (u64{7} << 48), + }; + program.instructions.insert(program.instructions.begin() + insert, fallback.begin(), + fallback.end()); + // This exact shader has no branches or calls; its predicated EXITs need no relocation. + for (size_t index = next_source; index < program.source_instruction_offsets.size(); ++index) { + program.source_instruction_offsets[index] += static_cast(fallback.size()); + } + return true; +} + +} // namespace VideoCommon diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 45bdfd3a03..9965cc903e 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -749,7 +749,11 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading } const auto shader_dir{Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir)}; - const auto base_dir{shader_dir / fmt::format("{:016x}", title_id)}; + auto base_dir{shader_dir / fmt::format("{:016x}", title_id)}; + if (const char* enabled = std::getenv("EDEN_MKW_ZERO_DEPTH_FALLBACK"); + enabled != nullptr && enabled[0] == '1') { + base_dir /= "mkw_zero_depth_v1"; + } if (!Common::FS::CreateDir(shader_dir) || !Common::FS::CreateDir(base_dir)) { LOG_ERROR(Common_Filesystem, "Failed to create pipeline cache directories"); return; diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index 9479e3dd7e..c0603b6740 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -34,6 +34,7 @@ #include "shader_recompiler/frontend/ampere/program_address_map.h" #include "video_core/engines/kepler_compute.h" #include "video_core/memory_manager.h" +#include "video_core/mkw_wave_workaround.h" #include "video_core/nx2_diag.h" #include "video_core/shader_environment.h" #include "video_core/texture_cache/format_lookup_table.h" @@ -524,6 +525,10 @@ std::shared_ptr GenericEnvironment::LowerNativeCode(u32 program_base, lowered.error_offset, lowered.error, words[0] & 0xfff, words[0], words[1], words[2], words[3]); } + if (const char* enabled = std::getenv("EDEN_MKW_ZERO_DEPTH_FALLBACK"); + enabled != nullptr && enabled[0] == '1') { + VideoCommon::ApplyMkwZeroDepthFallback(native, lowered); + } auto result = std::make_shared(); result->texture_bound = lowered.required_texture_buffer; result->source_size = native_header_size + lowered.source_size; From 13c88a9599d343cb958f3def6fc5d2e3320f1f36 Mon Sep 17 00:00:00 2001 From: Codex draft Date: Wed, 4 Nov 2026 19:36:17 +0000 Subject: [PATCH 2/3] vulkan: publish Mario Kart World collision heights to guest memory Write back the collision image after the fingerprinted compute pass using the existing pitch-aware texture transfer. Assisted-by: Codex (handoff analysis and draft implementation) --- .../renderer_vulkan/vk_pipeline_cache.cpp | 3 ++- .../renderer_vulkan/vk_pipeline_cache.h | 2 +- .../renderer_vulkan/vk_rasterizer.cpp | 24 ++++++++++++++++- src/video_core/shader_environment.cpp | 1 + src/video_core/shader_environment.h | 1 + src/video_core/texture_cache/texture_cache.h | 26 +++++++++++++++++++ .../texture_cache/texture_cache_base.h | 3 +++ 7 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 9965cc903e..c7e0828c2a 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -714,12 +714,13 @@ GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() { return CurrentGraphicsPipelineSlowPath(); } -ComputePipeline* PipelineCache::CurrentComputePipeline() { +ComputePipeline* PipelineCache::CurrentComputePipeline(u64& native_key) { const ShaderInfo* const shader{ComputeShader()}; if (!shader) { return nullptr; } + native_key = shader->lowered_native ? shader->lowered_native->native_key : 0; const auto& qmd{kepler_compute->launch_description}; const ComputePipelineCacheKey key{ .unique_hash = shader->unique_hash, diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index 05c9ddce65..51b2682046 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -124,7 +124,7 @@ class PipelineCache : public VideoCommon::ShaderCache { void TickFrame(); - [[nodiscard]] ComputePipeline* CurrentComputePipeline(); + [[nodiscard]] ComputePipeline* CurrentComputePipeline(u64& native_key); void LoadDiskResources(u64 title_id, std::stop_token stop_loading, const VideoCore::DiskResourceLoadCallback& callback); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index e9bd271b38..6832194e28 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -48,6 +48,7 @@ #include "video_core/renderer_vulkan/vk_update_descriptor.h" #include "video_core/shader_cache.h" #include "video_core/texture_cache/texture_cache_base.h" +#include "video_core/textures/texture.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/nx2_diag.h" @@ -1045,7 +1046,8 @@ void RasterizerVulkan::DispatchCompute() { FlushWork(); gpu_memory->FlushCaching(); - ComputePipeline* const pipeline{pipeline_cache.CurrentComputePipeline()}; + u64 native_key{}; + ComputePipeline* const pipeline{pipeline_cache.CurrentComputePipeline(native_key)}; if (!pipeline) { if (kepler_compute->IsNativeMode()) throw Shader::RuntimeError("Native compute pipeline unavailable"); return; @@ -1063,6 +1065,24 @@ void RasterizerVulkan::DispatchCompute() { } const auto& qmd{kepler_compute->launch_description}; + const auto publish_collision_map = [&] { + static const bool enabled = [] { + const char* value = std::getenv("EDEN_MKW_WATER_FEEDBACK"); + return value != nullptr && value[0] == '1'; + }(); + if (!enabled || native_key != 0x6e0ae200ee9aa220ULL || + (qmd.const_buffer_enable_mask & 1) == 0) { + return; + } + const auto& constants = qmd.const_buffer_config[0]; + if (constants.size < 0x3c) { + return; + } + // WaterSurfaceCollisionHeightMap exposes its output handle in CB0. + const u32 handle = gpu_memory->Read(constants.Address() + 0x38); + const u32 index = Tegra::Texture::TexturePair(handle, qmd.linked_tsc != 0).first; + texture_cache.PublishComputeCollisionImage(index); + }; auto indirect_address = kepler_compute->GetIndirectComputeAddress(); if (indirect_address) { // DispatchIndirect @@ -1079,6 +1099,7 @@ void RasterizerVulkan::DispatchCompute() { cmdbuf.DispatchIndirect(indirect_buffer, indirect_offset); }); SubmitNx2ListCopies(); + publish_collision_map(); return; } const std::array dim{qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z}; @@ -1102,6 +1123,7 @@ void RasterizerVulkan::DispatchCompute() { cmdbuf.Dispatch(dim[0], dim[1], dim[2]); }); SubmitNx2ListCopies(); + publish_collision_map(); // Log compute dispatch if (GPU::Logging::IsActive() && diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index c0603b6740..d5834a8bc5 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -530,6 +530,7 @@ std::shared_ptr GenericEnvironment::LowerNativeCode(u32 VideoCommon::ApplyMkwZeroDepthFallback(native, lowered); } auto result = std::make_shared(); + result->native_key = Shader::Ampere::NativeProgramKey(native); result->texture_bound = lowered.required_texture_buffer; result->source_size = native_header_size + lowered.source_size; auto& lowered_code = result->code; diff --git a/src/video_core/shader_environment.h b/src/video_core/shader_environment.h index 30d59955af..f6100c8453 100644 --- a/src/video_core/shader_environment.h +++ b/src/video_core/shader_environment.h @@ -32,6 +32,7 @@ namespace VideoCommon { /// A native (Switch 2) program after capture and lowering to Maxwell code. The shader cache keeps /// it so the environments built for the program's pipelines don't capture and lower it again. struct LoweredNativeCode { + u64 native_key{}; /// Shader header, lowered instructions with Maxwell scheduling words, then the native /// address map (what the environment's `code` used to be filled with). std::vector code; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index ea82ab02e7..482b102c1a 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -485,6 +485,32 @@ void TextureCache

::MarkModification(ImageId id) noexcept { MarkModification(slot_images[id]); } +template +bool TextureCache

::PublishComputeCollisionImage(u32 index) { + const auto view_id = VisitImageView(index, true); + if (!view_id || !slot_image_views[view_id].image_id) { + return false; + } + Image& image = slot_images[slot_image_views[view_id].image_id]; + const auto& info = image.info; + // The title writes a CPU-readable 256x256 pitch-linear SNORM height map. + if (info.type != ImageType::Linear || info.format != PixelFormat::R16G16B16A16_SNORM || + info.size.width != 256 || info.size.height != 256 || info.size.depth != 1 || + info.pitch != 2048 || info.resources.layers != 1 || info.resources.levels != 1 || + !IsDownloadable(image) || + !gpu_memory->IsFullyMappedRange(image.gpu_addr, image.guest_size_bytes)) { + return false; + } + const auto copies = FullDownloadCopies(info); + auto map = runtime.DownloadStagingBuffer(image.unswizzled_size_bytes); + image.DownloadMemory(map, copies); + runtime.Finish(); + SwizzleImage(*gpu_memory, image.gpu_addr, info, copies, map.mapped_span, swizzle_data_buffer); + image.flags &= ~ImageFlagBits::GpuModified; + return true; +} + + template void TextureCache

::FillImageViews(std::span views, bool compute, bool blacklist) { bool has_blacklisted = false; diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 5847cae7ba..85711272c7 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -228,6 +228,9 @@ class TextureCache : public VideoCommon::ChannelSetupCaches Date: Fri, 6 Nov 2026 20:16:32 -0700 Subject: [PATCH 3/3] nx2:vi2: don't pace presents while a large movie plays (Mario Kart World) PaceToVblank sleeps the presenting thread until the next vblank. While Mario Kart World plays its 2560x1440 VP9 cup intro movies, that sleep starves the game's movie player: it asked the decoder for ~34 frames per second, so the 24 s intro took ~34 s (~67 s with the game's 30 FPS mode) and its sound stuttered. Build 99, which has no present pacing, plays it in 24.9 s. With EDEN_MKW_MOVIE_UNPACED=1, the video decoder stamps the time of each output frame of a stream at least 1920 pixels wide, and PaceToVblank skips its wait while such a frame came out within the last 250 ms. Small clips, menus and gameplay keep normal pacing. Off by default. Tested on Batocera Linux, Radeon 890M (RADV), on top of #553. Assisted-by: Claude (diagnosis and patch) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019JiF1nWSHdriS5rDHzsENw --- src/core/hle/service/nx2_gsv.h | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 62cce1dbfc..89f8dbe426 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -3,6 +3,17 @@ #pragma once +#include +#include +#include +#include + +// Time of the last large movie frame the Switch 2 video decoder output (EDEN_MKW_MOVIE_UNPACED). +inline std::atomic& Nx2MkwMovieFrameClock() { + static std::atomic clock{0}; + return clock; +} + #include #include #include @@ -1815,6 +1826,22 @@ class INx2Vi2Session final : public ServiceFramework { if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) { return; } + // EDEN_MKW_MOVIE_UNPACED=1: no present pacing while a large (>= 1920 wide) movie plays. + // The pacing sleep starves Mario Kart World's movie player: its 2560x1440 cup intros ran + // at ~70% speed with stuttering sound (~35% with the game's 30 FPS mode). + static const bool mkw_movie_unpaced = [] { + const char* v = std::getenv("EDEN_MKW_MOVIE_UNPACED"); + return v != nullptr && v[0] == '1'; + }(); + if (mkw_movie_unpaced) { + const auto mkw_last = Nx2MkwMovieFrameClock().load(std::memory_order_relaxed); + const auto mkw_now = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + if (mkw_last != 0 && mkw_now - mkw_last < 250'000'000) { + m_next_vblank = std::chrono::steady_clock::now(); + return; + } + } static const u32 forced_interval = [] { const char* value = std::getenv("EDEN_NX2_SWAP_INTERVAL"); return value ? static_cast(std::strtoul(value, nullptr, 10)) : 0U; @@ -2721,6 +2748,10 @@ class INx2VideoSession final : public ServiceFramework { std::optional fence; if (const auto it = m_release_fences.find(image); it != m_release_fences.end()) { fence = it->second; + if (m_width >= 1920) { + Nx2MkwMovieFrameClock().store(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(), std::memory_order_relaxed); + } m_release_fences.erase(it); } lk.unlock();