From edabc3e61486d59d67f33dfd992154927197450f Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:11:02 -0400 Subject: memory_manager: copy big pages without host memory page by page GPU reads and writes of a continuous big page copied through GetPointer() without checking it; the small-page path checks. A big page whose host memory is not there yet (a Switch 2 region backed as it is written, with its backing pending) made that memcpy fault: Mario Kart World crashed in WriteBlockImpl (access violation) on its title screen in two runs of a build on the 10-31 nx2. Such a page now takes the per-page copy, which reports and skips missing pages, as non-continuous big pages already did. Three 10-minute runs after the change, no crash. --- src/video_core/memory_manager.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'src/video_core/memory_manager.cpp') diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index 9111ba10d2..a7d8aec382 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -431,11 +431,14 @@ void MemoryManager::ReadBlockImpl(GPUVAddr gpu_src_addr, void* dest_buffer, std: if (!unsafe) { rasterizer->FlushRegion(dev_addr_base, copy_amount, which); } - if (!IsBigPageContinuous(page_index)) [[unlikely]] { - memory.ReadBlockUnsafe(dev_addr_base, dest_buffer, copy_amount); - } else { - u8* physical = memory.GetPointer(dev_addr_base); + // A continuous big page may still have no host memory (a Switch 2 region backed as it is + // written, while its backing is pending): the per-page copy handles that. + u8* const physical = IsBigPageContinuous(page_index) ? memory.GetPointer(dev_addr_base) + : nullptr; + if (physical != nullptr) [[likely]] { std::memcpy(dest_buffer, physical, copy_amount); + } else { + memory.ReadBlockUnsafe(dev_addr_base, dest_buffer, copy_amount); } dest_buffer = static_cast(dest_buffer) + copy_amount; }; @@ -485,11 +488,12 @@ void MemoryManager::WriteBlockImpl(GPUVAddr gpu_dest_addr, const void* src_buffe if (!unsafe) { rasterizer->InvalidateRegion(dev_addr_base, copy_amount, which); } - if (!IsBigPageContinuous(page_index)) [[unlikely]] { - memory.WriteBlockUnsafe(dev_addr_base, src_buffer, copy_amount); - } else { - u8* physical = memory.GetPointer(dev_addr_base); + u8* const physical = IsBigPageContinuous(page_index) ? memory.GetPointer(dev_addr_base) + : nullptr; + if (physical != nullptr) [[likely]] { std::memcpy(physical, src_buffer, copy_amount); + } else { + memory.WriteBlockUnsafe(dev_addr_base, src_buffer, copy_amount); } src_buffer = static_cast(src_buffer) + copy_amount; }; -- cgit v1.2.3 From 40902b09c7b86de7cf12f26839241f26925d04b5 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:03:37 -0500 Subject: nx2: leave unwritten sparse blocks without memory when they get zeros Title 1 clears about 1 GB of its sparse regions at boot through the GPU memory manager. Every block the clear touched got 4 MiB of guest memory, which the clear itself then committed on the host, and counted as written, so the texture cache gave the images over it device memory too. A block nobody wrote reads zero already: when the part of a write that lands on it is all zero, leave it as it is. (cherry picked from commit 0d7ff63fd1a373cecff6ba91022c9027e801e53f) --- src/core/hle/service/nx2_gsv.h | 47 +++++++++++++++++++++++++++++++++++++-- src/video_core/memory_manager.cpp | 4 ++-- src/video_core/nx2.h | 18 +++++++++++---- 3 files changed, 61 insertions(+), 8 deletions(-) (limited to 'src/video_core/memory_manager.cpp') diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index e037b820d6..9d0fd597b2 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -340,7 +340,9 @@ public: }); if (LazySparseRegions()) { Tegra::Nx2SetSparseBacking( - [this](u64 gpu_address, u64 size) { BackSparseWrite(gpu_address, size); }); + [this](u64 gpu_address, u64 size, const u8* data) { + BackSparseWrite(gpu_address, size, data); + }); } } @@ -735,6 +737,23 @@ private: TipcReplyWords(ctx, {static_cast(count), static_cast(u64{count} >> 32)}); } + static bool IsAllZero(const u8* data, size_t size) { + size_t i = 0; + for (; i + sizeof(u64) <= size; i += sizeof(u64)) { + u64 word; + std::memcpy(&word, data + i, sizeof(word)); + if (word != 0) { + return false; + } + } + for (; i < size; ++i) { + if (data[i] != 0) { + return false; + } + } + return true; + } + // Returns the pages of destroyed regions the GPU thread is done with to the kernel. void FreeReleasedRegions() { auto* const process = system.ApplicationProcess(); @@ -842,7 +861,7 @@ private: // usually on the GPU thread): the blocks the write touches get memory, cleared, behind the // region's device pages. The GPU mapping stays as it is, so the images over the region keep // their storage and only see the write. - void BackSparseWrite(u64 gpu_address, u64 size) { + void BackSparseWrite(u64 gpu_address, u64 size, const u8* data) { struct Pending { u64 region; u64 block; @@ -865,6 +884,7 @@ private: } }; bool waits_for_other = false; + u64 zero_bytes = 0; { std::unique_lock lk{m_region_mutex}; for_regions([&](u64 start, SparseRegion& region, u64 first, u64 last) { @@ -875,6 +895,18 @@ private: region.blocks[block] = BlockWritten; Tegra::Nx2SparseBlockWritten(start, block); } else if (region.blocks[block] == BlockEmpty) { + // Zeros over a block nobody wrote change nothing: it reads zero without + // memory. Title 1 clears ~1 GB of them at boot. + const u64 block_begin = start + block * SparseBlockSize; + const u64 write_begin = (std::max)(gpu_address, block_begin); + const u64 write_end = (std::min)( + {end, block_begin + SparseBlockSize, start + region.size}); + if (data != nullptr && + IsAllZero(data + (write_begin - gpu_address), write_end - write_begin) && + !Tegra::Nx2SparseBlockIsWritten(start, block)) { + zero_bytes += write_end - write_begin; + continue; + } region.blocks[block] = BlockPending; todo.push_back({start, block}); } else if (region.blocks[block] == BlockPending) { @@ -883,6 +915,16 @@ private: } }); } + if (zero_bytes != 0) { + std::scoped_lock lk{m_region_mutex}; + const u64 before = m_sparse_zero_bytes; + m_sparse_zero_bytes += zero_bytes; + if (before == 0 || (before >> 28) != (m_sparse_zero_bytes >> 28)) { + LOG_INFO(Service, "nx2:gpu {} MiB of zeros over unwritten sparse blocks left out " + "(they read zero without memory)", + m_sparse_zero_bytes >> 20); + } + } // Allocating and mapping happens outside the lock (see AllocateGpuOnlyBacking). for (const auto& [start, block] : todo) { u64 block_size = SparseBlockSize; @@ -993,6 +1035,7 @@ private: std::map m_sparse_regions; ///< By GPU address u64 m_sparse_blocks{}; ///< Sparse blocks backed so far u64 m_sparse_backed_bytes{}; ///< Sparse region memory given out so far + u64 m_sparse_zero_bytes{}; ///< Zeros written over unwritten sparse blocks, left out u64 m_sparse_released_bytes{}; ///< Blocks given back once the game mapped tiles there // nn::gpu::sdk::SparseMapRequest (216), filled from NvRm's nvgpu_as_remap_op. diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index a7d8aec382..ddfd2529fd 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -302,7 +302,7 @@ void MemoryManager::Write(GPUVAddr addr, T data) { return; } // A Switch 2 sparse block nobody wrote yet gets its memory now. - Tegra::Nx2BackSparseWrite(addr, sizeof(T)); + Tegra::Nx2BackSparseWrite(addr, sizeof(T), &data); if (auto page_pointer{GetPointer(addr)}; page_pointer) { std::memcpy(page_pointer, &data, sizeof(T)); return; @@ -469,7 +469,7 @@ void MemoryManager::ReadBlockUnsafe(GPUVAddr gpu_src_addr, void* dest_buffer, co void MemoryManager::WriteBlockImpl(GPUVAddr gpu_dest_addr, const void* src_buffer, std::size_t size, [[maybe_unused]] VideoCommon::CacheType which, bool unsafe) { // Give Switch 2 sparse pages the write lands on their memory first (otherwise it is dropped). - Tegra::Nx2BackSparseWrite(gpu_dest_addr, size); + Tegra::Nx2BackSparseWrite(gpu_dest_addr, size, src_buffer); auto just_advance = [&]([[maybe_unused]] std::size_t page_index, [[maybe_unused]] std::size_t offset, std::size_t copy_amount) { src_buffer = static_cast(src_buffer) + copy_amount; }; diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 1fb49e15a6..db8c92adfd 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -97,8 +97,9 @@ inline void Nx2ClearSemaphoreRegions() { // Switch 2 sparse regions (nx2:gpu 214, word 7 bit 0x200: title 1's terrain texture arrays and // layer pools, 170 MB-5.8 GB each, of which the game fills a little) get memory per block when the // GPU first writes there, not in full. nx2:gpu installs the handler and the span its regions cover; -// the GPU memory manager calls Nx2BackSparseWrite before each write. -using Nx2SparseBackingHandler = std::function; +// the GPU memory manager calls Nx2BackSparseWrite before each write, with the bytes it writes +// (blocks nobody wrote that only get zeros need no memory: they read zero already). +using Nx2SparseBackingHandler = std::function; inline std::recursive_mutex g_nx2_sparse_backing_mutex; inline Nx2SparseBackingHandler g_nx2_sparse_backing; inline std::atomic g_nx2_sparse_low{~u64{0}}; @@ -122,14 +123,14 @@ inline void Nx2AddSparseSpan(u64 gpu_address, u64 size) { } } -inline void Nx2BackSparseWrite(u64 gpu_address, u64 size) { +inline void Nx2BackSparseWrite(u64 gpu_address, u64 size, const void* data) { if (gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] { return; } std::scoped_lock lk{g_nx2_sparse_backing_mutex}; if (g_nx2_sparse_backing) { - g_nx2_sparse_backing(gpu_address, size); + g_nx2_sparse_backing(gpu_address, size, static_cast(data)); } } @@ -174,6 +175,15 @@ inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { } } +/// True unless the block of the region at region_address is known to hold nothing (no write from +/// the CPU or the GPU yet). +inline bool Nx2SparseBlockIsWritten(u64 region_address, u64 block) { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + const auto it = g_nx2_sparse_state.find(region_address); + return it == g_nx2_sparse_state.end() || block >= it->second.written.size() || + it->second.written[block]; +} + /// The game maps its own memory over [offset, +size) of the region at region_address (mapped), or /// unmaps it there. inline void Nx2SparseTilesMapped(u64 region_address, u64 offset, u64 size, bool mapped) { -- cgit v1.2.3