diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 9fd3ddf929..3690d9de07 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -70,6 +70,7 @@ #include "core/hle/service/nvdrv/nvdrv.h" #include "core/hle/service/nvdrv/nvdrv_interface.h" #include "core/hle/service/nx2_stub_interface.h" +#include "core/hle/service/nx2_movie_pacing.h" #include "core/hle/service/sm/sm.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/service.h" @@ -1448,6 +1449,7 @@ class INx2GpuSession final : public ServiceFramework { // shared with the vi2 session so presents can find an image's memory. Command meanings follow // the shared changes.patch (IGraphicsSupervisor), re-checked against title 4's SDK. struct Nx2NativeObjects { + MkwMovieActivity mkw_movie_activity; struct Memory { u64 size{}; u64 address{}; // CPU (== GPU) address once bound by 114/423 @@ -1851,6 +1853,11 @@ class INx2Vi2Session final : public ServiceFramework { if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) { return; } + const auto movie_now = std::chrono::steady_clock::now(); + if (m_objects->mkw_movie_activity.Active(movie_now)) { + m_next_vblank = movie_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; @@ -2511,6 +2518,9 @@ class INx2VideoSession final : public ServiceFramework { m_cv.notify_all(); m_worker.join(); } + if (m_movie_program_id == 0x0400C3F00006E000ULL && m_width >= 1920) { + m_objects->mkw_movie_activity.Reset(); + } // The ready timer runs on the HostTiming thread: clear its event under its lock first. { std::scoped_lock lk{m_ready->mutex}; @@ -2632,6 +2642,8 @@ class INx2VideoSession final : public ServiceFramework { m_width = w[5]; m_height = w[6]; m_clock_offset.reset(); + const auto* process = ctx.GetThread().GetOwnerProcess(); + m_movie_program_id = process ? process->GetProgramId() : 0; } if (!m_worker.joinable()) { m_worker = std::thread{[this] { DecodeLoop(); }}; @@ -2917,6 +2929,9 @@ class INx2VideoSession final : public ServiceFramework { m_decoded.clear(); m_last_decoded_pts = 0; m_dropped_in_a_row = 0; + if (MkwMovieActivity::IsLargeMovie(m_movie_program_id, m_width)) { + m_objects->mkw_movie_activity.Reset(); + } if (decoder) { lk.unlock(); decoder->Reset(); @@ -2942,6 +2957,10 @@ class INx2VideoSession final : public ServiceFramework { const auto [width, height] = WriteImage(decoder.get(), image, frame, bytes); lk.lock(); // Even an image written during a flush counts: the client waits for its value. + if (!m_flush && frame.picture && frame.picture->frame && + MkwMovieActivity::IsLargeMovie(m_movie_program_id, frame.picture->width)) { + m_objects->mkw_movie_activity.RecordFrame(std::chrono::steady_clock::now()); + } m_outputs.push_back({image, width, height, frame.pts, frame.end}); if (frame.end) { m_stream_ended = true; @@ -3119,6 +3138,7 @@ class INx2VideoSession final : public ServiceFramework { std::string m_codec; u32 m_width{}; u32 m_height{}; + u64 m_movie_program_id{}; u32 m_handed{}; ///< Output images handed out (registered or released) so far u64 m_frames_out{}; u64 m_frames_dropped{}; diff --git a/src/core/hle/service/nx2_movie_pacing.h b/src/core/hle/service/nx2_movie_pacing.h new file mode 100644 index 0000000000..7f9db677c1 --- /dev/null +++ b/src/core/hle/service/nx2_movie_pacing.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "common/common_types.h" + +namespace Service { + +class MkwMovieActivity { +public: + using Clock = std::chrono::steady_clock; + + static constexpr bool IsLargeMovie(u64 program_id, u32 width) { + return program_id == 0x0400C3F00006E000ULL && width >= 1920; + } + + void RecordFrame(Clock::time_point now) { + last_frame.store(Ticks(now), std::memory_order_relaxed); + } + + void Reset() { + last_frame.store(0, std::memory_order_relaxed); + } + + bool Active(Clock::time_point now) const { + const s64 last = last_frame.load(std::memory_order_relaxed); + const s64 elapsed = Ticks(now) - last; + return last != 0 && elapsed >= 0 && elapsed < 250'000'000; + } + +private: + static s64 Ticks(Clock::time_point now) { + return std::chrono::duration_cast(now.time_since_epoch()).count(); + } + + std::atomic last_frame{}; +}; + +} // namespace Service diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index dff8765036..d38658d6a9 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(tests core/internal_network/socket_proxy.cpp core/nup_writer.cpp core/nx2_debug_server.cpp + core/nx2_movie_pacing.cpp core/priority_queue.cpp core/internal_network/network.cpp video_core/frame_dump_schedule.cpp diff --git a/src/tests/core/nx2_movie_pacing.cpp b/src/tests/core/nx2_movie_pacing.cpp new file mode 100644 index 0000000000..dde4422e24 --- /dev/null +++ b/src/tests/core/nx2_movie_pacing.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "core/hle/service/nx2_movie_pacing.h" + +TEST_CASE("MKW movie pacing is limited to recent large frames in the current session", "[nx2]") { + using Service::MkwMovieActivity; + using namespace std::chrono_literals; + CHECK(MkwMovieActivity::IsLargeMovie(0x0400C3F00006E000ULL, 2560)); + CHECK_FALSE(MkwMovieActivity::IsLargeMovie(0x0400C3F00006E000ULL, 640)); + CHECK_FALSE(MkwMovieActivity::IsLargeMovie(0x0400123400000000ULL, 2560)); + + MkwMovieActivity activity; + MkwMovieActivity other_session; + const MkwMovieActivity::Clock::time_point start{1s}; + CHECK_FALSE(activity.Active(start)); + activity.RecordFrame(start); + CHECK(activity.Active(start)); + CHECK(activity.Active(start + 249ms)); + CHECK_FALSE(activity.Active(start + 250ms)); + CHECK_FALSE(activity.Active(start - 1ms)); + CHECK_FALSE(other_session.Active(start)); + activity.RecordFrame(start + 200ms); + CHECK(activity.Active(start + 300ms)); + activity.Reset(); + CHECK_FALSE(activity.Active(start + 300ms)); +} diff --git a/src/video_core/mkw_wave_workaround.h b/src/video_core/mkw_wave_workaround.h new file mode 100644 index 0000000000..5428383b02 --- /dev/null +++ b/src/video_core/mkw_wave_workaround.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +#include "shader_recompiler/frontend/ampere/program_override.h" + +namespace VideoCommon { + +// Workaround for a missing ground-height map in this exact shader. 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() || + (next_source != 0 && program.source_instruction_offsets[next_source - 1] > insert)) { + 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 ce8edaad61..e6ee2805b0 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -756,12 +756,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_program_key : 0; const auto& qmd{kepler_compute->launch_description}; const ComputePipelineCacheKey key{ .unique_hash = shader->unique_hash, @@ -832,7 +833,10 @@ 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 (title_id == 0x0400C3F00006E000ULL) { + 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/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index 66bbb93199..5fa04081ae 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -127,7 +127,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 f9697a8e9f..e6bd1d8e72 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -57,6 +57,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_debug_control.h" @@ -1256,7 +1257,8 @@ void RasterizerVulkan::DispatchCompute() { frame_dump.DrainIfFull(); } - 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; @@ -1273,6 +1275,20 @@ void RasterizerVulkan::DispatchCompute() { return; } const auto& qmd{kepler_compute->launch_description}; + const auto publish_collision_map = [&] { + if (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 (frame_dump.Capturing()) { const u64 shader_hash = pipeline->ShaderHash(); @@ -1348,6 +1364,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}; @@ -1364,6 +1381,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 bff787daef..27646c0044 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -37,6 +37,7 @@ #include "video_core/memory_manager.h" #include "video_core/nx2_debug_control.h" #include "video_core/nx2_diag.h" +#include "video_core/mkw_wave_workaround.h" #include "video_core/shader_environment.h" #include "video_core/texture_cache/format_lookup_table.h" #include "video_core/textures/texture.h" @@ -630,6 +631,7 @@ std::shared_ptr GenericEnvironment::LowerNativeCode(u32 program_base, lowered.error_offset, lowered.error, words[0] & 0xfff, words[0], words[1], words[2], words[3]); } + VideoCommon::ApplyMkwZeroDepthFallback(native, lowered); auto result = std::make_shared(); result->native_program_key = Shader::Ampere::NativeProgramKey(native); static const bool hash_for_indirect_reset = [] { diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 35e23e8e4c..f2e18baf82 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -497,6 +497,30 @@ 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 a65861d998..3f57fd1b36 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -235,6 +235,9 @@ class TextureCache : public VideoCommon::ChannelSetupCaches