Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dm/games/0400486024DEC000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Measured on an M6 Mac with 16 GB, headless, on one walk with camera turns (`walk
- In a run that takes shots, each shot costs its frame 60–100 ms, and `DM_NVN_GPUPROF` and pass dumps slow the frames they cover.
- Resolved: the castle town market at 9–12 fps on a first visit, with the GPU at 24–26 ms a frame and the main thread busy searching the pipeline archives: [archive searches repeated while the build table is full](../../../docs/reference/frame-rate.md#archive-searches-repeated-while-the-build-table-is-full). Game v131072. On an M5 Mac (macOS 27.0.1, a host near `bd979ced`), CPU translation 113.8 → 6.4 ms a frame and failed searches 9,208 (30.5 s) → 5 per 300 frames, frame p50 107 → 28.5 ms. On an M4 Pro MacBook Pro (24 GB, a host near `1794a35`, real play), failed searches about 49,000 → 5–57 per 300 frames, frame p50 84 → 16–20 ms, main thread 854 → 440–600 ms/s, game time 0.605 → 1.000 of real time. In both, the build table stayed full (384 in flight). Neither is a controlled A/B: the second visits started with the first visit's pipelines prepared (489 more on the M4 Pro), which explains part of the lower GPU time. The drop in failed searches is the change's direct effect.
- Resolved: frames of 50–170 ms on entering the castle town, in a second run as in the first, with `new pipelines 1` or `0` and the time under `predictions`: [pulling stand-ins made for predicted keys that are never drawn](../../../docs/reference/frame-rate.md#pulling-stand-ins-made-for-predicted-keys-that-are-never-drawn). Game v131072, M4 Pro, the town entry route (headless, `DM_FPS_HOLD=off`): the three predictions of the route 30.8, 104.4 and 54.5 ms → 0.5, 2.0 and 1.4 ms, pulling pipelines made 244 → 1; frames 910, 1458 and 1787 (48, 132 and 71 ms) are no longer long. The long frames left on the route are texture uploads.
- Resolved: frames of 30–64 ms while walking into the castle town, with `new pipelines 0` and `6 re-uploads, 227.4 MB / 32.0 ms`: the loader fills a 1800x1380x8 R16 array (38.5 MB) and three block-compressed textures over several frames while they are drawn, and each use read the whole texture again, up to 8 times in one submission: [a texture written while it is drawn read whole at every use](../../../docs/reference/frame-rate.md#a-texture-written-while-it-is-drawn-read-whole-at-every-use). Now one look a submission and only the written layers: 25 ms at most there. The single-image textures (3072x2096 BC5, 3056x1152) are still read whole, once a submission (open).
- Open: frames of 100–650 ms when an area first loads (640 first uploads, 321 MB, in one frame of 294 ms; another 640 ms of submit with 68 MB).
- `DM_NVN_TBDR=1` was tried and does not help: p95 19 ms on the same walk, against 17.8.
- The first long run on a new chain is slower (p50 60–114 ms), while `MTLCompilerService` compiles pipelines; the next run is not.
Expand Down Expand Up @@ -132,6 +133,7 @@ Its 37 kernels that correct samples, NDSR's network among them, gave about 0.25
- [Early fragment tests from the control section](../../../docs/reference/shaders.md#early-fragment-tests-from-the-control-section): 746 of its 22,352 pixel shaders set the NVN2 flag (control +0x5b), none of them writes depth; before, no NVN2 shader got the attribute.
- [Archive searches repeated while the build table is full](../../../docs/reference/frame-rate.md#archive-searches-repeated-while-the-build-table-is-full): the castle town market at about 12 fps while the GPU had time to spare.
- [Pulling stand-ins made for predicted keys that are never drawn](../../../docs/reference/frame-rate.md#pulling-stand-ins-made-for-predicted-keys-that-are-never-drawn): resolved, frames of 50–170 ms on entering the castle town in every run, with one new pipeline or none.
- [A texture written while it is drawn read whole at every use](../../../docs/reference/frame-rate.md#a-texture-written-while-it-is-drawn-read-whole-at-every-use): resolved, frames of 31–59 ms on entering Hyrule Castle Town, a 1800x1380x8 R16 array (38.5 MB) and three block-compressed textures re-uploaded 31–52 times while the loader wrote them, the array 16–17 times whole; 16–17 re-uploads after, the array's 7–9 at 4.7–14.2 MB, one frame of 25 ms at most.
- [One Pro Controller read as two players](../../../docs/reference/services.md#one-pro-controller-read-as-two-players): resolved, no music in Kokiri Forest gameplay and its cutscenes; the controller applet opened over and over and the game kept its music suspended while it had focus.
- [A keyboard receiver that declares a gamepad takes player 1](../../../docs/reference/services.md#a-keyboard-receiver-that-declares-a-gamepad-takes-player-1): current until the reporter retests ([issue #1657](https://github.com/seedds/nxnative/issues/1657)). A Keychron Link receiver was player 1 and the Pro Controller player 2, so the game took no input; a raw HID pad is now a player from its first button press.
- [Compute bindings prepare texture copies before the dispatch encoder](../../../docs/reference/rendering.md#compute-bindings-prepare-texture-copies-before-the-dispatch-encoder): resolved, `DM_NVN_DISPATCH_OWN=1` crashed in the Metal driver (`AGX::ComputeContext::prepareForEnqueue`) during the Kokiri Forest load ([issue #1743](https://github.com/seedds/nxnative/issues/1743)); on an M2 Pro 5 of 5 runs crashed before the fix and 3 of 3 loaded the forest after it.
Expand Down
14 changes: 11 additions & 3 deletions dm/gfx/nvn/nvn_dirty.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,17 @@ static void stamp_page(range_t *r, uintptr_t p) {
// must invalidate that cached clock, as the page stays writable until the next upload protects it again.
atomic_fetch_add(&g_now, 1);
}
// Writes by the thread that replays submissions (nvn_dirty_own_thread), by fault or as host writes into a tracked range: they are in order with the commands it replays, where a write by any other thread
// races it (nvn_mtl.m looks at a texture's pages once a submission unless this moved). Not the pages nvn_dirty_close finds changed: another thread or the GPU wrote those
static _Atomic(pthread_t) g_own; static _Atomic uint64_t g_own_writes;
static void own_note(void) { pthread_t o = atomic_load_explicit(&g_own, memory_order_relaxed); if (o && pthread_equal(o, pthread_self())) atomic_fetch_add(&g_own_writes, 1); }
void nvn_dirty_own_thread(void) { atomic_store(&g_own, pthread_self()); }
uint64_t nvn_dirty_own_writes(void) { return atomic_load(&g_own_writes); }
static _Atomic uint64_t g_sparse_writes; // writes to pages protected for a placement sparse tile (nvn_dirty_sparse_writes)
static void sparse_note(range_t *r, uintptr_t p) { _Atomic uint8_t *sp = atomic_load(&r->sparse); if (sp && atomic_exchange(&sp[p], 0)) atomic_fetch_add(&g_sparse_writes, 1); }
static void unprotect_page(range_t *r, uintptr_t p) { // The caller has already changed the state from PROT to BUSY
mp(r->base + (p << g_shift), g_pg, PROT_READ | PROT_WRITE);
stamp_page(r, p); sparse_note(r, p);
stamp_page(r, p); sparse_note(r, p); own_note();
atomic_store(&r->state[p], PG_NONE);
}
// Take a protected or open page for unprotecting (-> BUSY): 1 = taken; 0 = not, with the state found in *st
Expand Down Expand Up @@ -124,14 +130,15 @@ static void release_pages(range_t *r, uintptr_t p, uintptr_t q, _Atomic uint64_t
}
}
static void prep_write(void *addr, size_t len) {
uintptr_t a = (uintptr_t)addr, e = a + len; int n = atomic_load(&g_nr);
uintptr_t a = (uintptr_t)addr, e = a + len; int n = atomic_load(&g_nr), in = 0;
if (!len) return;
for (int i = 0; i < n; i++) {
range_t *r = rg(i);
if (!live_at(i) || e <= r->base || a >= r->end) continue;
uintptr_t lo = a > r->base ? a : r->base, hi = e < r->end ? e : r->end;
uintptr_t lo = a > r->base ? a : r->base, hi = e < r->end ? e : r->end; in = 1;
release_pages(r, (lo - r->base) >> g_shift, (hi - 1 - r->base) >> g_shift, &g_preps);
}
if (in) own_note(); // whatever its pages' state: one another thread wrote first is writable already
}

void nvn_dirty_add_range(const void *mem, size_t size) {
Expand Down Expand Up @@ -296,6 +303,7 @@ void nvn_dirty_protect_sparse(const void *addr, size_t len) {
}
uint64_t nvn_dirty_sparse_writes(void) { return atomic_load(&g_sparse_writes); }
uint64_t nvn_dirty_now(void) { return atomic_load(&g_now); }
void nvn_dirty_touch(void) { atomic_fetch_add(&g_now, 1); }
// As nvn_dirty_since, with the caller's last range kept in *span (rechecked, scanned again only if it no longer holds the span; a slot reused by another pool over the same memory is as
// good): a packaged texture keeps its handle (the scan over every range ran twice per texture lookup), and a sparse texture's page-table walk (hle_nvn.c vpt_walk) asks once per run of
// physical pages, which mostly lie in one pool, where find_span scans every registered range (about 4,300 in a Mario Kart World race)
Expand Down
3 changes: 3 additions & 0 deletions dm/gfx/nvn/nvn_dirty.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ int nvn_dirty_tracked(const void *addr, size_t len);
void nvn_dirty_protect(const void *addr, size_t len); // Set the covered pages read-only
void nvn_dirty_protect_sparse(const void *addr, size_t len); // The same, for the pages behind a placement sparse tile: a later write to one moves nvn_dirty_sparse_writes
uint64_t nvn_dirty_sparse_writes(void); // Writes so far to pages protected that way
void nvn_dirty_own_thread(void); // The calling thread is the one that replays submissions
uint64_t nvn_dirty_own_writes(void); // Writes to protected pages by that thread so far (faults and host writes): the ones in order with what it replays
void nvn_dirty_touch(void); // Moves the generation as a write does, with no page written: whoever remembers "nothing written since" by it asks its pages again
uint64_t nvn_dirty_now(void); // Current generation: take the content's stamp before nvn_dirty_protect and before reading it
int nvn_dirty_since(const void *addr, size_t len, uint64_t stamp); // Covered pages were written after the stamp -> 1
const void *nvn_dirty_span(const void *addr, size_t len); // The tracked range holding the span (a handle to keep; NULL = untracked)
Expand Down
Loading
Loading