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/04008E001F098000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ There is no game-specific patch and no rewriter cut. Everything else it needed i

- In the doubles match on the M6 (window, 2026-11-12): frame time p50 16.67 / p95 17.3-17.9 ms, NVN submit and present about 3.4 ms, CPU about 92% of one core.
- bymaxmarc's measurements on the M2 Max (scene loads of 110-225 ms, a 250 ms GPU frame at boot) are in [PR #2387](https://github.com/seedds/nxnative/pull/2387)'s note.
- **Two players, pool tables.** In bymaxmarc's two-player skate session (M2 Max, 2026-11-12) the texture pool table (33,024 IDs) was built whole about 3 times a frame from the game's first sparse mapping, at about frame 3,900: 14.7-15.4 ms of a 25 ms submit, with the dynamic resolution at its floor (854x240 a player). Every refusal to patch was `a sparse mapping` (213,859 of 214,245 by frame 8,000, `sparse mappings 12`): the host refused a patch over any storage change once any mapping had been made. bymaxmarc's two-player Avioneta session of 2026-11-11 built the table about 3 times a frame for 13.9 ms of a 23 ms submit, at 30 and then 24 fps, before the log split the refusals by cause; likely the same mapping, not confirmed. A table is now built whole only over a mapping made since its walk ([Pool tables patched instead of built whole](../../../docs/reference/nvn-objects.md#pool-tables-patched-instead-of-built-whole)); in a player's 11-minute session with the change on 2026-11-12, two-player skate and two-player Avioneta with no crash, the `pool tables up to frame` lines counted 0.00 tables built whole a frame (about 3 before), at 30 fps (present interval 2); two-player Avioneta held mostly 2134x600 a view, while skate's dynamic resolution stayed at its 854x240 floor a view, which is not the tables but the GPU clock at 30 fps (a separate change).
- Out of focus the game pauses and its GPU queues wait on it for 6-10 s at a time (`[RED] queue 4: an event wait has held it …`); runs set `DM_FOCUS_KEEP=1`.

## Motion controls
Expand Down Expand Up @@ -70,4 +71,5 @@ The work folder `04008E001F098000-work` beside the dump has `run/boot.sh <log> <
- [The fragment's shading-rate size reads 1 x 1](../../../docs/reference/shaders.md#the-fragments-shading-rate-size-reads-1-x-1)
- [A render-target view of another class: the texture is upgraded, not drawn in its base format](../../../docs/reference/rendering.md#a-render-target-view-of-another-class-the-texture-is-upgraded-not-drawn-in-its-base-format)
- [Conditional rendering evaluated at replay](../../../docs/reference/rendering.md#conditional-rendering-evaluated-at-replay)
- [Pool tables patched instead of built whole](../../../docs/reference/nvn-objects.md#pool-tables-patched-instead-of-built-whole) (a sparse mapping long before)
- [Tests of one uniform word folded into switches](../../../docs/reference/shaders.md#tests-of-one-uniform-word-folded-into-switches) and [Specialized second functions in the offline pipeline archive](../../../docs/reference/shaders.md#specialized-second-functions-in-the-offline-pipeline-archive)
6 changes: 3 additions & 3 deletions dm/gfx/nvn/hle_nvn.c
Original file line number Diff line number Diff line change
Expand Up @@ -2013,9 +2013,9 @@ static void power_line(uint64_t frame) {
if (fs_ != pfs || fr != pfr || ft != pft) LOG("[nvn] same-pass feedback up to frame %llu, per frame: %.1f snapshots taken, %.1f draws reused one, %.1f read the tile (%.1f waited for their pipeline)",
frame, (fs_ - pfs) / nf, (fr - pfr) / nf, (ft - pft) / nf, (fw - pfw) / nf);
pfs = fs_; pfr = fr; pft = ft; pfw = fw;
void mtl_pool_stats(uint64_t s[6]); static uint64_t pps[6]; uint64_t ps[6]; mtl_pool_stats(ps); // the tables of handles traced to no slot (nvn_mtl.m pool_gpu)
if (ps[0] != pps[0] || ps[2] != pps[2] || ps[3] != pps[3]) LOG("[nvn] pool tables up to frame %llu, per frame: %.2f built whole (%.2f ms), %.2f patched, %.2f kept; of these two %.2f over memory pool changes, %.2f over a new submission",
frame, (ps[0] - pps[0]) / nf, (ps[1] - pps[1]) / 1e3 / nf, (ps[2] - pps[2]) / nf, (ps[3] - pps[3]) / nf, (ps[4] - pps[4]) / nf, (ps[5] - pps[5]) / nf);
void mtl_pool_stats(uint64_t s[8]); static uint64_t pps[8]; uint64_t ps[8]; mtl_pool_stats(ps); // the tables of handles traced to no slot (nvn_mtl.m pool_gpu)
if (ps[0] != pps[0] || ps[2] != pps[2] || ps[3] != pps[3]) LOG("[nvn] pool tables up to frame %llu, per frame: %.2f built whole (%.2f ms), %.2f patched, %.2f kept; of these two %.2f over memory pool changes, %.2f over a new submission; registrations %.1f, storage changes %.1f",
frame, (ps[0] - pps[0]) / nf, (ps[1] - pps[1]) / 1e3 / nf, (ps[2] - pps[2]) / nf, (ps[3] - pps[3]) / nf, (ps[4] - pps[4]) / nf, (ps[5] - pps[5]) / nf, (ps[6] - pps[6]) / nf, (ps[7] - pps[7]) / nf);
memcpy(pps, ps, sizeof pps);
void mtl_gsb_stats(uint64_t *, uint64_t *); static uint64_t pgn, pgb; uint64_t gn, gb; mtl_gsb_stats(&gn, &gb); // GPU textures' writes stored into their storage (nvn_mtl.m gsb_store)
if (gn != pgn) LOG("[nvn] GPU texture storage up to frame %llu: %llu regions, %.1f MB written into their storage on the GPU (%.2f regions, %.2f MB per frame)",
Expand Down
33 changes: 20 additions & 13 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -6326,14 +6326,17 @@ void mtl_prog_snapshot(prog_t *copy,const prog_t *source) {
// Texture ID views are valid for an epoch (view_ser), not for every serial table change: ser_sync takes the notes (own_note) and sends only the IDs registered with a noted serial
// down the slow path; a change without a note starts a new epoch, which sends them all (what every g_ser_gen bump did before). A battle map in Fire Emblem: Fortune's Weave changes a
// few serials a frame (the per-stage program ring in bind_stages among them), and each change made the next pool walk (pool_gpu) resolve all ~5,000 IDs again, 2 ms a frame.
// Once any sparse mapping exists (nvn_sparse_gen) every change starts a new epoch, as before: a sparse texture's page-table changes bump no generation the ID cache follows. Under g_rmu
static uint64_t g_treg_ep = 1, g_ser_synced = 1;
// A sparse mapping since the last sync (nvn_sparse_gen) starts a new epoch too: a sparse texture's page-table changes bump no generation the ID cache follows, and its view is checked
// against them only on the slow path. Only a mapping since the last sync: once any mapping existed every change started a new epoch, and Nintendo Switch Sports Resort, which maps
// a sparse pool a few times a minute in two-player skate, resolved its 17,000 texture IDs again at each of its 60 storage changes a frame. Under g_rmu
static uint64_t g_treg_ep = 1, g_ser_synced = 1, g_sparse_synced;
static void ser_sync(void) {
uint64_t s[SER_NOTES];
uint64_t sg; unsigned n = own_notes_take(s, &sg);
if (sg - g_ser_synced != n || nvn_sparse_gen()) g_treg_ep++;
uint64_t vg = nvn_sparse_gen();
if (sg - g_ser_synced != n || vg != g_sparse_synced) g_treg_ep++;
else for (unsigned i = 0; i < n; i++) for (int k = (int)(uintptr_t)nvn_hm_get(&g_treg_serial, &s[i]); k; k = g_treg[k - 1].next) g_treg[k - 1].view_ser = 0;
g_ser_synced = sg;
g_ser_synced = sg; g_sparse_synced = vg;
}
static inline uint64_t treg_ep(void) { if (g_ser_gen != g_ser_synced) ser_sync(); return g_treg_ep; }
static int *g_treg_transient; static size_t g_ntreg_transient, g_treg_transient_cap;
Expand Down Expand Up @@ -10133,7 +10136,7 @@ static void warm_start(void) {
// 1.4-1.6 a frame of 9 ms, 50-55 fps for five seconds; PR #2389)
enum { POOL_IDS_MAX = 8192 }; // the IDs a patch takes at most, and a list holds
typedef struct { int *v, n, cap; } pool_ids;
typedef struct { __strong id<MTLBuffer> buf; uint64_t gpu, reg, ser, frame, sub; int samp, dtt, dpf, base, left, left_req; pool_ids dead, trans; int over; } pool_snap; // over: its last walk found the budget full, so what it left out has its stand-in (pool_stand)
typedef struct { __strong id<MTLBuffer> buf; uint64_t gpu, reg, ser, frame, sub; int samp, dtt, dpf, base, left, left_req; pool_ids dead, trans; int over; uint64_t vgen; } pool_snap; // over: its last walk found the budget full, so what it left out has its stand-in (pool_stand); vgen: the sparse mappings (nvn_sparse_gen) before its walk
static pool_snap g_psnap[POOL_SNAPS]; static unsigned g_psnap_next;
static uint64_t g_psnap_gen = 1; // bumped by each rebuild or patch: an argument block holding a pool address is not reused past it (encode_args_)
static uint64_t g_stat_pool_builds; static double g_stat_pool_ms;
Expand All @@ -10142,7 +10145,9 @@ static void warm_start(void) {
// [2] resized, [3] a change without a note, [4] a new submission over more than POOL_IDS_MAX retired IDs. pchg / schg: of those patched or kept, the ones over memory pool changes
// and over a new submission
static uint64_t g_stat_pool_patched, g_stat_pool_kept, g_stat_pool_nopatch[5], g_stat_pool_pchg, g_stat_pool_schg;
void mtl_pool_stats(uint64_t s[6]) { s[0] = g_stat_pool_builds; s[1] = (uint64_t)(g_stat_pool_ms * 1000); s[2] = g_stat_pool_patched; s[3] = g_stat_pool_kept; s[4] = g_stat_pool_pchg; s[5] = g_stat_pool_schg; } // the power line's pool table counts (hle_nvn.c)
// [0] split by its cause: registrations past their log (REG_LOG), storage changes past theirs (SER_LOG), a sparse mapping since the table was made, more than 8192 IDs to take again. Which one decides how a game that builds its tables whole every frame can be helped
static uint64_t g_stat_pool_many[4];
void mtl_pool_stats(uint64_t s[8]) { s[0] = g_stat_pool_builds; s[1] = (uint64_t)(g_stat_pool_ms * 1000); s[2] = g_stat_pool_patched; s[3] = g_stat_pool_kept; s[4] = g_stat_pool_pchg; s[5] = g_stat_pool_schg; s[6] = atomic_load(&g_reg_gen); s[7] = atomic_load(&g_ser_gen); } // the power line's pool table counts (hle_nvn.c)
static void ids_push(pool_ids *l, int id_) { // past POOL_IDS_MAX the list is dropped (n -1)
if (l->n < 0) return;
if (l->n == POOL_IDS_MAX) { free(l->v); *l = (pool_ids){NULL, -1, 0}; return; }
Expand Down Expand Up @@ -10447,7 +10452,7 @@ static uint64_t pool_entry(int samp, int dtt, int id_, uint64_t limit, int *gone
}
static uint64_t pool_gpu(int samp, int dtt, int dpf, int base, __unsafe_unretained id<MTLBuffer> *out) {
pool_seen_scan();
uint64_t rg = atomic_load(&g_reg_gen), sg = samp ? 0 : atomic_load(&g_ser_gen); // a sampler table follows registrations alone
uint64_t rg = atomic_load(&g_reg_gen), sg = samp ? 0 : atomic_load(&g_ser_gen), vg = samp ? 0 : nvn_sparse_gen(); // a sampler table follows registrations alone
pool_snap *p = NULL, *free_ = NULL;
for (int i = 0; i < POOL_SNAPS && !p; i++) { pool_snap *q = &g_psnap[i];
if (!q->buf) { if (!free_) free_ = q; }
Expand All @@ -10457,11 +10462,13 @@ static uint64_t pool_gpu(int samp, int dtt, int dpf, int base, __unsafe_unretain
if (p && p->reg == rg && p->ser == sg && !stale && !walk_on) { *out = p->buf; return p->gpu; }
// Only registrations and noted storage changes since the table was built (each generation in its log), the IDs without storage after a pool change and the transient IDs in a
// new submission: take those IDs again, by the walk's rules and within what this frame may still make. The table is kept when none of them changes an entry; otherwise the old
// one is copied and patched, as draws already encoded read the old one. A sparse texture's page table follows no note (ser_sync), so then it is built whole. Ocarina of Time,
// one is copied and patched, as draws already encoded read the old one. A sparse texture's page table follows no note (ser_sync), so a storage change after a sparse mapping the
// table was walked before builds it whole (the walk sees the new epoch); with no mapping since, its entries change only by the notes, as without sparse pools. Ocarina of Time,
// a seven-minute walk through Kokiri Forest: building the tables whole took about 53 s (61,000 builds)
int nn = (samp ? g_nsreg : g_ntreg) - base; if (nn < 1) nn = 1;
if (p && !walk_on) do {
if (rg - p->reg >= REG_LOG || sg - p->ser >= SER_LOG || (!samp && p->ser != sg && nvn_sparse_gen())) { g_stat_pool_nopatch[0]++; break; }
{ int why = rg - p->reg >= REG_LOG ? 0 : sg - p->ser >= SER_LOG ? 1 : !samp && p->ser != sg && p->vgen != vg ? 2 : -1;
if (why >= 0) { g_stat_pool_nopatch[0]++; g_stat_pool_many[why]++; break; } }
if (*(const uint32_t *)p->buf.contents != (uint32_t)nn) { g_stat_pool_nopatch[2]++; break; }
static int *ids; static size_t idcap; size_t nid = 0; int known = 1, pools = 0;
#define POOL_ID(x) do { if (nid == idcap) { idcap = idcap ? idcap * 2 : 256; ids = realloc_or_die(ids, idcap * sizeof *ids, "the pool table patch"); } ids[nid++] = (x); } while (0)
Expand All @@ -10487,7 +10494,7 @@ static uint64_t pool_gpu(int samp, int dtt, int dpf, int base, __unsafe_unretain
}
#undef POOL_ID
if (!known) { g_stat_pool_nopatch[3]++; break; }
if (nid >= POOL_IDS_MAX) { g_stat_pool_nopatch[0]++; break; }
if (nid >= POOL_IDS_MAX) { g_stat_pool_nopatch[0]++; g_stat_pool_many[3]++; break; }
uint64_t *e = (uint64_t *)((uint8_t *)p->buf.contents + DMSH_POOL_HEAD), dummy = e[nn], room = samp ? UINT64_MAX : pool_room(), spent = g_walk_spent;
int gone = 0, left = 0, left_req = 0; id<MTLBuffer> b = nil; uint64_t *be = NULL;
pool_ids nd = {0}, nt = {0}; ids_begin(0); ids_begin(1);
Expand Down Expand Up @@ -10535,7 +10542,7 @@ static uint64_t pool_gpu(int samp, int dtt, int dpf, int base, __unsafe_unretain
e[n] = dummy;
pool_table_give(p->buf); free(p->dead.v); free(p->trans.v);
// Retired registrations drop their views at submission end; their resource IDs cannot outlive that submission (trans, taken again by the next one)
*p = (pool_snap){b, b.gpuAddress, rg, sg, atomic_load(&g_cur_frame), nt.n ? g_sub_serial : 0, samp, dtt, dpf, base, left, left_req, nd, nt, samp || g_walk_over};
*p = (pool_snap){b, b.gpuAddress, rg, sg, atomic_load(&g_cur_frame), nt.n ? g_sub_serial : 0, samp, dtt, dpf, base, left, left_req, nd, nt, samp || g_walk_over, vg};
g_psnap_gen++;
static mach_timebase_info_data_t tb; if (!tb.denom) mach_timebase_info(&tb);
double ms = (double)(mach_absolute_time() - t0) * tb.numer / tb.denom / 1e6; g_stat_pool_ms += ms;
Expand All @@ -10555,11 +10562,11 @@ static uint64_t pool_gpu(int samp, int dtt, int dpf, int base, __unsafe_unretain
if (g_stat_pool_builds++ < 16 || g_stat_pool_builds % 1000 == 0 || log_gone)
MLOG("pool for handles traced to no slot: %s pool (texture type %d, class %d) at base %d, %d IDs of which %d hold %s and %d have no storage (memory pool finalized or gone), built in %.2f ms (%llu builds, %.0f ms in all; "
"built because new %llu, registration %llu, storage %llu, retired IDs %llu, textures left out %llu; taken alone: %llu tables patched, %llu kept (%llu over memory pool changes, %llu over a new submission); "
"not: too many or sparse %llu, a pool change over too many IDs without storage %llu, a new submission over too many retired IDs %llu, resized %llu, a change without a note %llu)", samp ? "sampler" : "texture",
"not: too many or sparse %llu (registrations past the log %llu, storage changes past the log %llu, a sparse mapping %llu, IDs past 8192 %llu; registrations so far %llu, storage changes so far %llu, sparse mappings %llu), a pool change over too many IDs without storage %llu, a new submission over too many retired IDs %llu, resized %llu, a change without a note %llu)", samp ? "sampler" : "texture",
samp ? -1 : dtt, samp ? -1 : dpf, base, n, real, samp ? "samplers" : "textures of the type", gone, ms, (unsigned long long)g_stat_pool_builds, g_stat_pool_ms,
(unsigned long long)g_stat_pool_why[0], (unsigned long long)g_stat_pool_why[1], (unsigned long long)g_stat_pool_why[2], (unsigned long long)g_stat_pool_why[3], (unsigned long long)g_stat_pool_why[4],
(unsigned long long)g_stat_pool_patched, (unsigned long long)g_stat_pool_kept, (unsigned long long)g_stat_pool_pchg, (unsigned long long)g_stat_pool_schg,
(unsigned long long)g_stat_pool_nopatch[0], (unsigned long long)g_stat_pool_nopatch[1], (unsigned long long)g_stat_pool_nopatch[4], (unsigned long long)g_stat_pool_nopatch[2], (unsigned long long)g_stat_pool_nopatch[3]);
(unsigned long long)g_stat_pool_nopatch[0], (unsigned long long)g_stat_pool_many[0], (unsigned long long)g_stat_pool_many[1], (unsigned long long)g_stat_pool_many[2], (unsigned long long)g_stat_pool_many[3], (unsigned long long)atomic_load(&g_reg_gen), (unsigned long long)atomic_load(&g_ser_gen), (unsigned long long)nvn_sparse_gen(), (unsigned long long)g_stat_pool_nopatch[1], (unsigned long long)g_stat_pool_nopatch[4], (unsigned long long)g_stat_pool_nopatch[2], (unsigned long long)g_stat_pool_nopatch[3]);
*out = b; return p->gpu;
}
static id<MTLBuffer> dummy_buf(void) { static id<MTLBuffer> b; if (!b) { b = [g_dev newBufferWithLength:64 << 10 options:MTLResourceStorageModeShared]; res_add(b); } return b; }
Expand Down
Loading
Loading