1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <functional>
#include <map>
#include <mutex>
#include <utility>
#include <vector>
#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<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
// 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<std::atomic<u64>, 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<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}};
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, 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<const u8*>(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<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;
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<bool>((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<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) ||
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<Waiter> waiters;
std::atomic<size_t> count{};
};
} // namespace Tegra
|