aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/nx2.h
diff options
context:
space:
mode:
Diffstat (limited to 'src/video_core/nx2.h')
-rw-r--r--src/video_core/nx2.h148
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) ||