diff options
Diffstat (limited to 'src/video_core/nx2.h')
| -rw-r--r-- | src/video_core/nx2.h | 148 |
1 files changed, 144 insertions, 4 deletions
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 824dc76044..92714d52d2 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -9,6 +9,7 @@ #include <functional> #include <map> #include <mutex> +#include <utility> #include <vector> #include "common/common_types.h" @@ -19,6 +20,76 @@ namespace Tegra { // Ampere classes and the Turing+ macro ISA, which Eden's Maxwell engines don't implement. inline std::atomic<bool> g_nx2_ampere{false}; +// Device pages the GPU has written through the buffer cache (shader stores, copies), ever. Title 1 +// runs command lists the GPU writes each frame (20-byte entries its culling fills in); read +// without a flush they are stale and the draws behind them (trees) go missing. The bits never +// clear: the dispatch that rewrites a page may still be queued on the split's back when its front +// reads the command list. One bit per 4 KiB of the 36-bit device address space (2 MiB). +inline constexpr u64 Nx2WrittenPageBits = 12; +inline std::array<std::atomic<u64>, (u64{1} << (36 - Nx2WrittenPageBits)) / 64> g_nx2_gpu_written_pages{}; + +inline void Nx2MarkGpuWritten(u64 device_address, u64 size) { + if (size == 0) { + return; + } + const u64 last = std::min<u64>((device_address + size - 1) >> Nx2WrittenPageBits, + g_nx2_gpu_written_pages.size() * 64 - 1); + for (u64 page = device_address >> Nx2WrittenPageBits; page <= last; ++page) { + auto& word = g_nx2_gpu_written_pages[page / 64]; + const u64 bit = u64{1} << (page % 64); + if ((word.load(std::memory_order_relaxed) & bit) == 0) { + word.fetch_or(bit, std::memory_order_relaxed); + } + } +} + +// GPU-written device pages the front has read command lists from (same layout as above). The +// buffer cache copies these pages into host memory right after the compute or copy work that +// writes them and submits, so the front reads the copy once the GPU is done instead of draining +// the split and flushing the caches (BufferCache::Nx2ReadListCopy). +inline std::array<std::atomic<u64>, (u64{1} << (36 - Nx2WrittenPageBits)) / 64> g_nx2_command_list_pages{}; +inline std::atomic<bool> g_nx2_has_command_list_pages{false}; + +inline void Nx2MarkCommandListPage(u64 device_address) { + const u64 page = device_address >> Nx2WrittenPageBits; + if (page >= g_nx2_command_list_pages.size() * 64) { + return; + } + auto& word = g_nx2_command_list_pages[page / 64]; + const u64 bit = u64{1} << (page % 64); + if ((word.load(std::memory_order_relaxed) & bit) == 0) { + word.fetch_or(bit, std::memory_order_relaxed); + g_nx2_has_command_list_pages.store(true, std::memory_order_relaxed); + } +} + +[[nodiscard]] inline bool Nx2IsCommandListPage(u64 device_address) { + const u64 page = device_address >> Nx2WrittenPageBits; + if (page >= g_nx2_command_list_pages.size() * 64) { + return false; + } + return (g_nx2_command_list_pages[page / 64].load(std::memory_order_relaxed) >> (page % 64)) & 1; +} + +/// A new emulation session starts: no page has been written by its GPU yet. +inline void Nx2ClearGpuWrittenPages() { + for (auto& word : g_nx2_gpu_written_pages) { + word.store(0, std::memory_order_relaxed); + } + for (auto& word : g_nx2_command_list_pages) { + word.store(0, std::memory_order_relaxed); + } + g_nx2_has_command_list_pages.store(false, std::memory_order_relaxed); +} + +[[nodiscard]] inline bool Nx2IsGpuWritten(u64 device_address) { + const u64 page = device_address >> Nx2WrittenPageBits; + if (page >= g_nx2_gpu_written_pages.size() * 64) { + return false; + } + return (g_nx2_gpu_written_pages[page / 64].load(std::memory_order_relaxed) >> (page % 64)) & 1; +} + // NVN2 queues release their progress values into the second page of a two-page GPU-accessible // region and GPU acquires read the same offsets in the first page (the CPU polls the second). // Something on the console copies one into the other; until that is known, the puller mirrors @@ -58,8 +129,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<void(u64 gpu_address, u64 size)>; +// 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<void(u64 gpu_address, u64 size, const u8* data)>; 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}}; @@ -83,14 +155,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<const u8*>(data)); } } @@ -101,7 +173,13 @@ struct Nx2SparseRegionState { u64 size; u64 block_size; std::vector<bool> written; + /// Once the game maps its own memory into the region (nx2:gpu 216): which tiles it maps. + /// Only those hold data then (writes elsewhere are dropped, as on the console). + std::vector<bool> mapped; }; + +/// The granularity of the game's sparse tiles (nx2:gpu 216 maps 64 KiB pages). +inline constexpr u64 Nx2SparseTileSize = 64ULL << 10; inline std::mutex g_nx2_sparse_state_mutex; inline std::map<u64, Nx2SparseRegionState> g_nx2_sparse_state; @@ -129,6 +207,68 @@ 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) { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + const auto it = g_nx2_sparse_state.find(region_address); + if (it == g_nx2_sparse_state.end() || size == 0) { + return; + } + auto& region = it->second; + if (region.mapped.empty()) { + region.mapped.resize((region.size + Nx2SparseTileSize - 1) / Nx2SparseTileSize); + } + const u64 last = (std::min)((offset + size - 1) / Nx2SparseTileSize, u64{region.mapped.size() - 1}); + for (u64 tile = offset / Nx2SparseTileSize; tile <= last; ++tile) { + region.mapped[tile] = mapped; + } +} + +/// The parts of [gpu_address, +size), which lies in one sparse region, that can hold data: the +/// tiles the game maps there if it maps any, otherwise the blocks written. As (offset from +/// gpu_address, size) runs. +inline std::vector<std::pair<u64, u64>> Nx2SparseDataRanges(u64 gpu_address, u64 size) { + std::vector<std::pair<u64, u64>> ranges; + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + auto it = g_nx2_sparse_state.upper_bound(gpu_address); + if (it == g_nx2_sparse_state.begin()) { + return ranges; + } + --it; + const auto& [start, region] = *it; + if (size == 0 || gpu_address + size > start + region.size) { + return ranges; + } + const bool by_tile = !region.mapped.empty(); + const u64 unit = by_tile ? Nx2SparseTileSize : region.block_size; + const std::vector<bool>& present = by_tile ? region.mapped : region.written; + const u64 begin = gpu_address - start; + const u64 end = begin + size; + for (u64 index = begin / unit; index * unit < end && index < present.size(); ++index) { + if (!present[index]) { + continue; + } + const u64 run_begin = (std::max)(index * unit, begin) - begin; + const u64 run_end = (std::min)((index + 1) * unit, end) - begin; + if (!ranges.empty() && ranges.back().first + ranges.back().second == run_begin) { + ranges.back().second += run_end - run_begin; + } else { + ranges.emplace_back(run_begin, run_end - run_begin); + } + } + return ranges; +} + /// True when [gpu_address, +size) lies in one sparse region. inline bool Nx2InSparseRegion(u64 gpu_address, u64 size) { if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || |
