diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-30 04:39:43 -0400 |
|---|---|---|
| committer | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-10-30 17:27:04 +0100 |
| commit | 6380b6687ded27892404c576becd78b7161ba68a (patch) | |
| tree | 8bea08c159da4d6b2b6640dcb0b6b3527589f5d7 /src | |
| parent | f9fc1e10fd7ed0a1c4664d48d2f095ce8826d87b (diff) | |
nx2: give sparse regions memory only where the GPU writes
nx2:gpu 214 regions with the sparse flag (word 7 bit 0x200) are Mario
Kart World's terrain texture arrays and layer pools, 170 MB to 5.8 GB,
mostly empty. Backed whole they ran the graphics pool dry (26 "could not
allocate" per session, later regions without memory), and backing a
block at a time made the texture cache rebuild the 1.45 GB arrays each
time a block was mapped.
A sparse region now gets a device (SMMU) range of its own when it is
created, mapped for the GPU at once, so its GPU mapping never changes.
Before a GPU-side write lands in it (WriteBlock, Write<T>), the 4 MiB
blocks it touches get memory, cleared, behind their device pages.
Unbacked device pages read zero and drop writes instead of crashing, and
the unmapped-device-access errors are logged 16 times, not once per page.
nx2:gpu 215 releases the device range and the blocks after the GPU is
done with them. The log reports GPU-only memory in use and its peak.
Mario Kart World, 180 s: no allocation failures; ~2.8 GB of terrain
blocks get memory instead of 6.5 GB of whole regions, next to 3 GB of
ordinary GPU-only regions. Terrain renders as before.
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/device_memory_manager.inc | 44 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp | 60 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h | 6 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nx2_stub_device.h | 53 | ||||
| -rw-r--r-- | src/core/hle/service/nx2_gsv.h | 156 | ||||
| -rw-r--r-- | src/video_core/memory_manager.cpp | 30 | ||||
| -rw-r--r-- | src/video_core/nx2.h | 40 |
7 files changed, 349 insertions, 40 deletions
diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc index d70444ef8c..d78a229e1f 100644 --- a/src/core/device_memory_manager.inc +++ b/src/core/device_memory_manager.inc @@ -425,10 +425,13 @@ void DeviceMemoryManager<Traits>::ReadBlock(DAddr address, void* dest_pointer, s WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic<u32> reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } std::memset(dest_pointer, 0, copy_amount); }, [&](size_t copy_amount, const u8* const src_ptr) { @@ -444,10 +447,13 @@ void DeviceMemoryManager<Traits>::WriteBlock(DAddr address, const void* src_poin WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic<u32> reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } }, [&](size_t copy_amount, u8* const dst_ptr) { std::memcpy(dst_ptr, src_pointer, copy_amount); @@ -484,10 +490,13 @@ void DeviceMemoryManager<Traits>::ReadBlockUnsafe(DAddr address, void* dest_poin WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic<u32> reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } std::memset(dest_pointer, 0, copy_amount); }, [&](size_t copy_amount, const u8* const src_ptr) { @@ -504,10 +513,13 @@ void DeviceMemoryManager<Traits>::WriteBlockUnsafe(DAddr address, const void* sr WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic<u32> reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } }, [&](size_t copy_amount, u8* const dst_ptr) { std::memcpy(dst_ptr, src_pointer, copy_amount); diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp index 2634902641..c4119b2ab1 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp @@ -63,7 +63,13 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> i Nx2Map(gpu_address, cpu_address, size); }, [this](u64 gpu_address, u64 size) { Nx2Unmap(gpu_address, size); }, - [this](u64 cpu_address, u64 size) { Nx2Release(cpu_address, size); }); + [this](u64 cpu_address, u64 size) { Nx2Release(cpu_address, size); }, + [this](u64 gpu_address, u64 size, bool create) { + Nx2Sparse(gpu_address, size, create); + }, + [this](u64 gpu_address, u64 cpu_address, u64 size) { + Nx2BackSparse(gpu_address, cpu_address, size); + }); } return result; } @@ -185,6 +191,58 @@ void nvhost_as_gpu::Nx2Release(u64 cpu_address, u64 size) { LOG_DEBUG(Service_NVDRV, "Switch 2 released backing {:#x}+{:#x}", cpu_address, size); } +void nvhost_as_gpu::Nx2Sparse(u64 gpu_address, u64 size, bool create) { + std::scoped_lock lock(mutex); + if (!gmmu) { + return; + } + auto& smmu = system.Host1x().MemoryManager(); + const u64 device_size = Common::AlignUp(size, 0x20000); + if (!create) { + const auto it = nx2_sparse_ranges.find(gpu_address); + if (it == nx2_sparse_ranges.end()) { + return; + } + const DAddr device_address = it->second.second; + nx2_sparse_ranges.erase(it); + // After the command lists that may still use it. + system.GPU().RunInOrder([gmmu = gmmu.get(), &smmu, gpu_address, size, device_address, + device_size] { + gmmu->Unmap(gpu_address, size); + smmu.Unmap(device_address, size); + smmu.Free(device_address, device_size); + }); + return; + } + const DAddr device_address = smmu.Allocate(device_size); + if (device_address == 0) { + LOG_CRITICAL(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x}: out of SMMU space", + gpu_address, size); + return; + } + nx2_sparse_ranges[gpu_address] = {size, device_address}; + gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false); + LOG_DEBUG(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x} -> device {:#x}", gpu_address, + size, device_address); +} + +void nvhost_as_gpu::Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lock(mutex); + auto it = nx2_sparse_ranges.upper_bound(gpu_address); + auto* session = container.GetSession(nx2_session); + if (it == nx2_sparse_ranges.begin() || session == nullptr) { + return; + } + --it; + const auto& [start, range] = *it; + const auto& [region_size, device_address] = range; + if (gpu_address + size > start + region_size) { + return; + } + system.Host1x().MemoryManager().Map(device_address + (gpu_address - start), cpu_address, size, + session->asid, true); +} + void nvhost_as_gpu::OnClose(DeviceFD fd) {} // Switch 2 NvRm (with characteristics flag 26) asks for the read-only syncpoint shim: a GPU page diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h index d64ec85dec..69050bb677 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h @@ -241,6 +241,12 @@ private: }; std::map<std::pair<u64, u64>, Nx2DeviceRange> nx2_device_ranges; void Nx2Release(u64 cpu_address, u64 size); + + // Switch 2 sparse regions: a device range per region, mapped for the GPU when it's created; + // blocks get memory behind their device pages as the GPU writes them. + void Nx2Sparse(u64 gpu_address, u64 size, bool create); + void Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size); + std::map<u64, std::pair<u64, DAddr>> nx2_sparse_ranges; ///< GPU address -> (size, device) }; } // namespace Service::Nvidia::Devices diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h index 08c21f09e3..989bf61682 100644 --- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h +++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h @@ -40,6 +40,10 @@ public: using Listener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>; using UnmapListener = std::function<void(u64 gpu_address, u64 size)>; using ReleaseListener = std::function<void(u64 cpu_address, u64 size)>; + /// A sparse region is created (true) or destroyed (false). + using SparseListener = std::function<void(u64 gpu_address, u64 size, bool create)>; + /// A block of a sparse region gets memory [cpu_address, +size). + using BackListener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>; static Nx2GpuMemoryRegistry& Get() { static Nx2GpuMemoryRegistry registry; @@ -83,14 +87,55 @@ public: } } + // Sparse regions (nx2:gpu 214 with the sparse flag): the GPU range reaches device memory of its + // own from the start, and each block gets memory when the GPU first writes it (BackSparse). + // The GPU mapping never changes, so the texture cache doesn't rebuild the images over it as + // blocks get memory; unbacked blocks read zero. + void CreateSparse(u64 gpu_address, u64 size) { + std::scoped_lock lk{mutex}; + sparse_regions[gpu_address] = size; + for (auto& [owner, listener] : listeners) { + if (listener.sparse) { + listener.sparse(gpu_address, size, true); + } + } + } + + void DestroySparse(u64 gpu_address, u64 size) { + std::scoped_lock lk{mutex}; + sparse_regions.erase(gpu_address); + for (auto& [owner, listener] : listeners) { + if (listener.sparse) { + listener.sparse(gpu_address, size, false); + } + } + } + + void BackSparse(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lk{mutex}; + for (auto& [owner, listener] : listeners) { + if (listener.back) { + listener.back(gpu_address, cpu_address, size); + } + } + } + void AddListener(const void* owner, Listener listener, UnmapListener unmap_listener, - ReleaseListener release_listener = {}) { + ReleaseListener release_listener = {}, SparseListener sparse_listener = {}, + BackListener back_listener = {}) { std::scoped_lock lk{mutex}; for (const auto& m : mappings) { listener(m.gpu_address, m.cpu_address, m.size); } + if (sparse_listener) { + for (const auto& [gpu_address, size] : sparse_regions) { + sparse_listener(gpu_address, size, true); + } + } listeners.insert_or_assign(owner, Listeners{std::move(listener), std::move(unmap_listener), - std::move(release_listener)}); + std::move(release_listener), + std::move(sparse_listener), + std::move(back_listener)}); } void RemoveListener(const void* owner) { @@ -107,6 +152,7 @@ public: } std::scoped_lock lk{mutex}; mappings.clear(); + sparse_regions.clear(); } // Device (SMMU) address of a CPU range an address space mapped; used to present images. @@ -144,6 +190,8 @@ private: Listener map; UnmapListener unmap; ReleaseListener release; + SparseListener sparse; + BackListener back; }; // Drops [gpu_address, +size) from the recorded mappings, keeping the parts outside it. @@ -169,6 +217,7 @@ private: std::mutex mutex; std::vector<Mapping> mappings; + std::map<u64, u64> sparse_regions; ///< GPU address -> size std::map<const void*, Listeners> listeners; }; diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 3f1f25b6bf..f3cc87ac4d 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -24,9 +24,11 @@ #include <vector> #include "common/alignment.h" +#include "common/div_ceil.h" #include "common/settings.h" #include "common/thread.h" #include "core/core.h" +#include "core/device_memory.h" #include "video_core/rasterizer_interface.h" #include "video_core/renderer_base.h" #include "core/frontend/framebuffer_layout.h" @@ -314,9 +316,15 @@ public: {214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateSparseRegion"}, {215, &INx2GpuSession::SubDestroySparseRegion, "DestroySparseRegion"}, }); + if (LazySparseRegions()) { + Tegra::Nx2SetSparseBacking( + [this](u64 gpu_address, u64 size) { BackSparseWrite(gpu_address, size); }); + } } ~INx2GpuSession() override { + // Waits for a backing the GPU thread may be doing. + Tegra::Nx2SetSparseBacking({}); for (auto* event : m_events) { if (event != nullptr) { m_context.CloseEvent(event); @@ -532,11 +540,23 @@ private: w[1], w[2], w[3], w[4], w[5], w[6], w[7], w[8], w[9], w[10], w[11], w[12]); if (last > start && ((last + 1 - start) % Core::Memory::YUZU_PAGESIZE) == 0) { const u64 size = last + 1 - start; + std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); - if (const auto address = BackGpuOnlyRegion(start, size)) { + if ((w[7] & SparseRegionFlag) != 0 && LazySparseRegions()) { + // Title 1's terrain texture arrays and layer pools (170 MB-5.8 GB, mostly empty): + // backed whole, they ran the pool dry and the later ones got no memory at all. + // The region gets a device range of its own, unbacked pages read zero, and + // BackSparseWrite gives a block memory before the GPU writes into it. + m_sparse_regions[start] = {size, std::vector<u8>(Common::DivCeil(size, SparseBlockSize))}; + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size); + Tegra::Nx2AddSparseSpan(start, size); + LOG_INFO(Service, "nx2:gpu 214 sparse region [{:#x}, {:#x}] backed as the GPU writes it", + start, last); + } else if (const auto address = BackGpuOnlyRegion(start, size)) { Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(start, *address, size); - LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed at {:#x}", - start, last, *address); + LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed at {:#x} " + "({} MiB of GPU-only memory in use, peak {} MiB)", + start, last, *address, m_gpu_only_bytes >> 20, m_gpu_only_peak >> 20); } } else { LOG_WARNING(Service, "nx2:gpu 214 unexpected region [{:#x}, {:#x}]", start, last); @@ -556,17 +576,30 @@ private: const u64 last = u64{w[5]} | (u64{w[6]} << 32); LOG_DEBUG(Service, "nx2:gpu 215 destroy sparse region [{:#x}, {:#x}] words {:#x} {:#x} {:#x} {:#x}", first, last, w[1], w[2], w[7], w[8]); + std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); if (last > first) { - Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(first, last - first + 1); - const auto region = m_gpu_only_regions.find(first); - if (region != m_gpu_only_regions.end() && region->second.size == last - first + 1) { + if (m_sparse_regions.erase(first) != 0) { + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DestroySparse(first, last - first + 1); + } else { + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(first, last - first + 1); + } + // The region's backing: all of it, or the blocks a sparse region got as it was written. + std::shared_ptr<std::atomic<bool>> done; + for (auto region = m_gpu_only_regions.lower_bound(first); + region != m_gpu_only_regions.end() && region->first <= last;) { + if (region->first + region->second.size - 1 > last) { + ++region; + continue; + } Nvidia::Devices::Nx2GpuMemoryRegistry::Get().ReleaseBacking( region->second.cpu_address, region->second.size); - auto done = std::make_shared<std::atomic<bool>>(false); - system.GPU().RunInOrder([done] { done->store(true, std::memory_order_release); }); - m_released_regions.push_back({std::move(region->second), std::move(done)}); - m_gpu_only_regions.erase(region); + if (!done) { + done = std::make_shared<std::atomic<bool>>(false); + system.GPU().RunInOrder([done] { done->store(true, std::memory_order_release); }); + } + m_released_regions.push_back({std::move(region->second), done}); + region = m_gpu_only_regions.erase(region); } } SignalAfterGpu(w[8]); @@ -590,11 +623,13 @@ private: return true; // still mapped: keep its pages } region.pages->Close(system.Kernel()); + m_gpu_only_bytes -= region.size; return true; }); } - std::optional<u64> BackGpuOnlyRegion(u64 gpu_address, u64 size) { + std::optional<u64> BackGpuOnlyRegion(u64 gpu_address, u64 size, bool log_failure = true, + bool clear = false) { using Core::Memory::YUZU_PAGESIZE; auto& kernel_core = system.Kernel(); auto* const process = system.ApplicationProcess(); @@ -611,18 +646,36 @@ private: pg.get(), size / YUZU_PAGESIZE, Kernel::KMemoryManager::EncodeOption(Kernel::KMemoryManager::Pool::SystemNonSecure, Kernel::KMemoryManager::Direction::FromBack)))) { - LOG_ERROR(Service, "nx2:gpu 214 could not allocate {:#x} bytes", size); + if (log_failure) { + LOG_ERROR(Service, "nx2:gpu 214 could not allocate {:#x} bytes", size); + } return std::nullopt; } + if (clear) { + // Cheap for memory that's still zero (most of it): only reused pages get cleared. + for (const auto& block : *pg) { + system.DeviceMemory().buffer.ClearBackingRegion( + GetInteger(block.GetAddress()) - Core::DramMemoryMap::Base, block.GetSize(), 0); + } + } auto& page_table = process->GetPageTable(); const u64 region_start = GetInteger(page_table.GetAliasCodeRegionStart()); const u64 region_size = page_table.GetAliasCodeRegionSize(); constexpr u64 Alignment = 0x200000; // 2 MiB - for (u64 offset = 0; offset + size <= region_size; offset += Alignment) { + // From where the last backing went (sparse regions back hundreds of blocks), then wrapping. + const u64 slots = region_size / Alignment; + for (u64 i = 0; i < slots; ++i) { + const u64 offset = ((m_alias_hint + i) % slots) * Alignment; + if (offset + size > region_size) { + continue; + } const Common::ProcessAddress address{region_start + offset}; if (R_SUCCEEDED(page_table.MapPageGroup(address, *pg, Kernel::KMemoryState::IoMemory, Kernel::KMemoryPermission::UserReadWrite))) { + m_alias_hint = (offset + Common::AlignUp(size, Alignment)) / Alignment; m_gpu_only_regions[gpu_address] = {GetInteger(address), size, std::move(pg)}; + m_gpu_only_bytes += size; + m_gpu_only_peak = (std::max)(m_gpu_only_peak, m_gpu_only_bytes); return GetInteger(address); } } @@ -632,6 +685,83 @@ private: return std::nullopt; } + // Runs before a GPU-side write that may land in a sparse region (Tegra::Nx2BackSparseWrite, + // 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) { + std::scoped_lock lk{m_region_mutex}; + const u64 end = gpu_address + size; + auto it = m_sparse_regions.upper_bound(gpu_address); + if (it != m_sparse_regions.begin()) { + --it; + } + for (; it != m_sparse_regions.end() && it->first < end; ++it) { + auto& [start, region] = *it; + if (start + region.size <= gpu_address) { + continue; + } + const u64 first_block = ((std::max)(gpu_address, start) - start) / SparseBlockSize; + const u64 last_block = ((std::min)(end, start + region.size) - 1 - start) / SparseBlockSize; + for (u64 block = first_block; block <= last_block; ++block) { + if (region.blocks[block] != BlockEmpty) { + continue; + } + const u64 block_address = start + block * SparseBlockSize; + const u64 block_size = (std::min)(SparseBlockSize, region.size - block * SparseBlockSize); + const auto address = BackGpuOnlyRegion(block_address, block_size, false, true); + if (!address) { + region.blocks[block] = BlockFailed; // out of memory: not again for every write + if (!region.warned) { + region.warned = true; + LOG_WARNING(Service, "nx2:gpu sparse region at {:#x}: pool exhausted, GPU writes " + "to its unbacked blocks are dropped", + start); + } + continue; + } + region.blocks[block] = BlockBacked; + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, *address, + block_size); + m_sparse_backed_bytes += block_size; + if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) { + LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only " + "memory in use, peak {} MiB", + m_sparse_blocks, m_sparse_backed_bytes >> 20, m_gpu_only_bytes >> 20, + m_gpu_only_peak >> 20); + } + } + } + } + + // EDEN_NX2_SPARSE_LAZY=0 backs sparse regions whole, as before. + static bool LazySparseRegions() { + static const bool lazy = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_LAZY"); + return value == nullptr || value[0] != '0'; + }(); + return lazy; + } + + static constexpr u32 SparseRegionFlag = 0x200; ///< Word 7 of 214 (0x60c arrays, 0xa0c pools) + static constexpr u64 SparseBlockSize = 4ULL << 20; + static constexpr u8 BlockEmpty = 0; + static constexpr u8 BlockBacked = 1; + static constexpr u8 BlockFailed = 2; + struct SparseRegion { + u64 size; + std::vector<u8> blocks; ///< BlockEmpty / BlockBacked / BlockFailed per SparseBlockSize + bool warned{}; ///< Logged that the pool ran out for its blocks + }; + std::map<u64, SparseRegion> 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_gpu_only_bytes{}; ///< Backing of GPU-only regions and sparse blocks right now + u64 m_gpu_only_peak{}; + u64 m_alias_hint{}; ///< 2 MiB slot after the last backing in the alias code region + /// Regions are created and destroyed on the service thread and backed on the GPU thread. + std::recursive_mutex m_region_mutex; + struct GpuOnlyRegion { u64 cpu_address; u64 size; diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index 2dd8b04656..9111ba10d2 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -14,6 +14,7 @@ #include "core/hle/kernel/k_process.h" #include "memory_manager.h" #include "video_core/guest_memory.h" +#include "video_core/nx2.h" #include "video_core/host1x/host1x.h" #include "video_core/invalidation_accumulator.h" #include "video_core/memory_manager.h" @@ -119,7 +120,6 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de if (entry_type == EntryType::Mapped) { page_table.ReserveRange(gpu_addr, size); } - for (u64 offset{}; offset < size; offset += page_size) { // The caches hear about each run of pages whose entry changed in one call, not page by page // (a 1.45 GB Switch 2 region made ~370,000 texture cache scans). GPUVAddr modified_start{}; @@ -130,6 +130,7 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de modified_size = 0; } }; + for (u64 offset{}; offset < size; offset += page_size) { const GPUVAddr current_gpu_addr = gpu_addr + offset; [[maybe_unused]] const auto current_entry_type = GetEntry(current_gpu_addr, false); SetEntry(current_gpu_addr, entry_type, false); @@ -149,14 +150,13 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de } remaining_size -= page_size; } - kind_map.Map(gpu_addr, gpu_addr + size, kind); flush_modified(); + kind_map.Map(gpu_addr, gpu_addr + size, kind); return gpu_addr; } GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr dev_addr, size_t size, PTEKind kind, MemoryManager::EntryType entry_type) { [[maybe_unused]] u64 remaining_size{size}; - for (u64 offset{}; offset < size; offset += big_page_size) { GPUVAddr modified_start{}; u64 modified_size{}; const auto flush_modified = [&] { @@ -165,6 +165,7 @@ GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr modified_size = 0; } }; + for (u64 offset{}; offset < size; offset += big_page_size) { const GPUVAddr current_gpu_addr = gpu_addr + offset; [[maybe_unused]] const auto current_entry_type = GetEntry(current_gpu_addr, true); SetEntry(current_gpu_addr, entry_type, true); @@ -201,8 +202,8 @@ GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr } remaining_size -= big_page_size; } - { flush_modified(); + { std::unique_lock<std::mutex> lock(guard); kind_map.Map(gpu_addr, gpu_addr + size, kind); } @@ -300,6 +301,12 @@ void MemoryManager::Write(GPUVAddr addr, T data) { std::memcpy(page_pointer, &data, sizeof(T)); return; } + // A Switch 2 sparse block nobody wrote yet gets its memory now. + Tegra::Nx2BackSparseWrite(addr, sizeof(T)); + if (auto page_pointer{GetPointer(addr)}; page_pointer) { + std::memcpy(page_pointer, &data, sizeof(T)); + return; + } ASSERT(false); } @@ -411,8 +418,12 @@ void MemoryManager::ReadBlockImpl(GPUVAddr gpu_src_addr, void* dest_buffer, std: if (!unsafe) { rasterizer->FlushRegion(dev_addr_base, copy_amount, which); } - u8* physical = memory.GetPointer<u8>(dev_addr_base); - std::memcpy(dest_buffer, physical, copy_amount); + // A device page without memory (a sparse block nobody wrote yet) reads zero. + if (const u8* physical = memory.GetPointer<u8>(dev_addr_base)) [[likely]] { + std::memcpy(dest_buffer, physical, copy_amount); + } else { + std::memset(dest_buffer, 0, copy_amount); + } dest_buffer = static_cast<u8*>(dest_buffer) + copy_amount; }; auto mapped_big = [&](std::size_t page_index, std::size_t offset, std::size_t copy_amount) { @@ -454,6 +465,8 @@ 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); 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<const u8*>(src_buffer) + copy_amount; }; @@ -462,8 +475,9 @@ void MemoryManager::WriteBlockImpl(GPUVAddr gpu_dest_addr, const void* src_buffe if (!unsafe) { rasterizer->InvalidateRegion(dev_addr_base, copy_amount, which); } - u8* physical = memory.GetPointer<u8>(dev_addr_base); - std::memcpy(physical, src_buffer, copy_amount); + if (u8* physical = memory.GetPointer<u8>(dev_addr_base)) [[likely]] { + std::memcpy(physical, src_buffer, copy_amount); + } src_buffer = static_cast<const u8*>(src_buffer) + copy_amount; }; auto mapped_big = [&](std::size_t page_index, std::size_t offset, std::size_t copy_amount) { diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 04a01e5314..58c9b17bcf 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -6,6 +6,7 @@ #include <algorithm> #include <array> #include <atomic> +#include <functional> #include <mutex> #include <vector> @@ -53,6 +54,45 @@ 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<void(u64 gpu_address, u64 size)>; +inline std::recursive_mutex g_nx2_sparse_backing_mutex; +inline Nx2SparseBackingHandler g_nx2_sparse_backing; +inline std::atomic<u64> g_nx2_sparse_low{~u64{0}}; +inline std::atomic<u64> g_nx2_sparse_high{0}; + +inline void Nx2SetSparseBacking(Nx2SparseBackingHandler handler) { + std::scoped_lock lk{g_nx2_sparse_backing_mutex}; + g_nx2_sparse_backing = std::move(handler); + g_nx2_sparse_low.store(~u64{0}); + g_nx2_sparse_high.store(0); +} + +/// Widens the span that writes are checked against (a new lazily backed region). +inline void Nx2AddSparseSpan(u64 gpu_address, u64 size) { + u64 low = g_nx2_sparse_low.load(); + while (gpu_address < low && !g_nx2_sparse_low.compare_exchange_weak(low, gpu_address)) { + } + u64 high = g_nx2_sparse_high.load(); + while (gpu_address + size > high && + !g_nx2_sparse_high.compare_exchange_weak(high, gpu_address + size)) { + } +} + +inline void Nx2BackSparseWrite(u64 gpu_address, u64 size) { + 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); + } +} + /// GPU VA in the first page that mirrors a release at `address`, or 0. inline u64 Nx2SemaphoreMirror(u64 address) { for (const auto& region : g_nx2_semaphore_regions) { |
