// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include #include #include #include #include #include #include #include #include "common/common_types.h" namespace Tegra { // Set once a Switch 2 (Ampere, NVN2) driver is talking to the GPU. Its command streams use // Ampere classes and the Turing+ macro ISA, which Eden's Maxwell engines don't implement. inline std::atomic 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, (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((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, (u64{1} << (36 - Nx2WrittenPageBits)) / 64> g_nx2_command_list_pages{}; inline std::atomic 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 // releases that land in a second page. Every queue has such a region, so keep a small table. inline constexpr size_t Nx2MaxSemaphoreRegions = 32; inline std::array, Nx2MaxSemaphoreRegions> g_nx2_semaphore_regions{}; // starts inline void Nx2AddSemaphoreRegion(u64 start) { for (auto& region : g_nx2_semaphore_regions) { if (region.load() == start) { return; } } for (auto& region : g_nx2_semaphore_regions) { u64 expected = 0; if (region.compare_exchange_strong(expected, start)) { return; } } } /// The region's memory was destroyed: stop mirroring into it. inline void Nx2RemoveSemaphoreRegion(u64 start) { for (auto& region : g_nx2_semaphore_regions) { u64 expected = start; region.compare_exchange_strong(expected, 0); } } /// A new emulation session starts: no queue has a region yet. inline void Nx2ClearSemaphoreRegions() { for (auto& region : g_nx2_semaphore_regions) { region.store(0); } } // 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, 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}}; inline std::atomic 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, 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, static_cast(data)); } } // Which blocks of each sparse region have been written (got memory), for the texture cache: an // image over blocks nobody wrote is all zeros and needs no device memory. Its own lock, taken last: // the texture cache asks with its lock held, and nx2:gpu updates it holding only its region mutex. struct Nx2SparseRegionState { u64 size; u64 block_size; std::vector 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 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 g_nx2_sparse_state; inline void Nx2SparseRegionCreated(u64 gpu_address, u64 size, u64 block_size) { std::scoped_lock lk{g_nx2_sparse_state_mutex}; g_nx2_sparse_state[gpu_address] = {size, block_size, std::vector((size + block_size - 1) / block_size)}; } inline void Nx2SparseRegionDestroyed(u64 gpu_address) { std::scoped_lock lk{g_nx2_sparse_state_mutex}; g_nx2_sparse_state.erase(gpu_address); } /// A new emulation session: the previous game's regions are gone (nvdrv's Module clears them). inline void Nx2ClearSparseRegions() { std::scoped_lock lk{g_nx2_sparse_state_mutex}; g_nx2_sparse_state.clear(); } inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { std::scoped_lock lk{g_nx2_sparse_state_mutex}; if (const auto it = g_nx2_sparse_state.find(region_address); it != g_nx2_sparse_state.end()) { it->second.written[block] = true; } } /// 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> Nx2SparseDataRanges(u64 gpu_address, u64 size) { std::vector> 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& 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) || gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] { return false; } 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 false; } --it; return gpu_address + size <= it->first + it->second.size; } /// The GPU renders into [gpu_address, +size): count its blocks as written, so images made there /// later get memory (their contents live in the host image, not in guest memory). inline void Nx2SparseMarkWritten(u64 gpu_address, u64 size) { if (size == 0 || 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_state_mutex}; for (auto& [start, region] : g_nx2_sparse_state) { const u64 end = start + region.size; if (gpu_address >= end || gpu_address + size <= start) { continue; } const u64 first = ((std::max)(gpu_address, start) - start) / region.block_size; const u64 last = ((std::min)(gpu_address + size, end) - 1 - start) / region.block_size; for (u64 block = first; block <= last; ++block) { region.written[block] = true; } } } /// True when [gpu_address, +size) lies in one sparse region and none of it has been written. inline bool Nx2SparseUnwritten(u64 gpu_address, u64 size) { if (size == 0 || 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 false; } 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 false; } --it; const auto& [start, region] = *it; if (gpu_address + size > start + region.size) { return false; } const u64 last = (gpu_address + size - 1 - start) / region.block_size; for (u64 block = (gpu_address - start) / region.block_size; block <= last; ++block) { if (region.written[block]) { return false; } } return true; } /// 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) { const u64 start = region.load(std::memory_order_relaxed); if (start == 0) { continue; } if (address >= start + 0x1000 && address < start + 0x2000) { return address - 0x1000; } } return 0; } /// Channels suspended on a semaphore acquire. A console channel polls and sees every release; /// Eden writes releases in bursts when fences signal and resumes waiters only between command /// lists, so an acquire-equal could miss its value. Writes latch the waiters they satisfy. class Nx2SemaphoreWaiters { public: // Puller GpuSemaphoreOperation values. static constexpr u32 AcquireEqual = 0x1; static constexpr u32 AcquireGequal = 0x4; static constexpr u32 AcquireMask = 0x8; static Nx2SemaphoreWaiters& Get() { static Nx2SemaphoreWaiters waiters; return waiters; } void Add(const void* owner, u64 device_address, u32 value, u32 operation) { std::scoped_lock lk{mutex}; std::erase_if(waiters, [owner](const Waiter& w) { return w.owner == owner; }); waiters.push_back({owner, device_address, value, operation, false}); count.store(waiters.size(), std::memory_order_release); } void Remove(const void* owner) { if (count.load(std::memory_order_acquire) == 0) { return; } std::scoped_lock lk{mutex}; std::erase_if(waiters, [owner](const Waiter& w) { return w.owner == owner; }); count.store(waiters.size(), std::memory_order_release); } [[nodiscard]] bool Latched(const void* owner) { if (count.load(std::memory_order_acquire) == 0) { return false; } std::scoped_lock lk{mutex}; return std::ranges::any_of(waiters, [owner](const Waiter& w) { return w.owner == owner && w.latched; }); } /// A GPU write of `value` to the u32 at `device_address`. void OnWrite(u64 device_address, u32 value) { if (count.load(std::memory_order_acquire) == 0) { return; } std::scoped_lock lk{mutex}; for (auto& w : waiters) { if (w.address == device_address && !w.latched && Matches(w, value)) { w.latched = true; } } } private: struct Waiter { const void* owner; u64 address; u32 value; u32 operation; bool latched; }; static bool Matches(const Waiter& w, u32 current) { switch (w.operation) { case AcquireEqual: return current == w.value; case AcquireGequal: return current >= w.value; case AcquireMask: return (current & w.value) != 0; default: return false; } } std::mutex mutex; std::vector waiters; std::atomic count{}; }; } // namespace Tegra