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
32 changes: 30 additions & 2 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -1259,13 +1259,19 @@ static int pend_room(int nodes) {
return -1;
}
enum { SBW_GPU = 1, SBW_REPORT = 2 };
enum { REP_N = 256 };
static struct { uint64_t lo, hi; } g_rep[REP_N]; static unsigned g_nrep; // exact report ranges this submission; REP_N + 1 means use the page fallback
static uint32_t g_sbw_gen; // bumped by every write noted that this submission's shadows must respect (a cached shadow made before it is checked again)
static void drw_note(uint64_t lo, uint64_t n); // the writes since the last drain (below g_wr_epoch)
static void sbw_note(uint64_t lo, uint64_t n, int src) {
if (src==SBW_GPU) rc_gpu_write(lo,n);
if (src == SBW_GPU) { g_sbw_gen++; drw_note(lo, n); }
if (!n) { if (src == SBW_GPU) g_sbw_over = 1; return; }
uint64_t hi = lo + n;
if (src == SBW_REPORT) {
if (g_nrep < REP_N) { g_rep[g_nrep].lo = lo; g_rep[g_nrep].hi = hi; }
if (g_nrep <= REP_N) g_nrep++;
}
if (src == SBW_GPU && ((hi - 1) >> 12) - (lo >> 12) >= SBW_WPAGES) { // a report is 16 bytes, never wide
for (int i = 0; i < g_nsbw_w; i++) if (lo <= g_sbw_whi[i] && g_sbw_wlo[i] <= hi) { g_sbw_wlo[i] = MIN(lo, g_sbw_wlo[i]); g_sbw_whi[i] = MAX(hi, g_sbw_whi[i]); return; }
if (g_nsbw_w == SBW_WN) { g_sbw_over = 1; return; }
Expand All @@ -1284,7 +1290,7 @@ static int sbw_hits(uint64_t lo, uint64_t hi) {
// a binding counted as a write anywhere, and every partly covered uniform shadow after it in the submission was refused (BotW shrines: about 240 more passes a frame, GitHub issue #534)
static uint64_t sbw_len(uint64_t size, void *mb, uint64_t mo) { if (size) return size; uint64_t len = ((__bridge id<MTLBuffer>)mb).length; return len > mo ? len - mo : 0; }
static inline unsigned pend_bucket(uint64_t page) { return (unsigned)((page * 0x9E3779B97F4A7C15ull) >> (64 - PEND_HB_BITS)); }
static void pend_reset(void) { g_npend = g_pend_over = g_nupd = g_npnode = g_nwide = g_nsbw = g_nsbw_w = g_sbw_over = 0; if (!++g_pend_gen) { memset(g_pend_hgen, 0, sizeof g_pend_hgen); memset(g_sbwp, 0, sizeof g_sbwp); g_pend_gen = 1; } }
static void pend_reset(void) { g_nrep = 0; g_npend = g_pend_over = g_nupd = g_npnode = g_nwide = g_nsbw = g_nsbw_w = g_sbw_over = 0; if (!++g_pend_gen) { memset(g_pend_hgen, 0, sizeof g_pend_hgen); memset(g_sbwp, 0, sizeof g_sbwp); g_pend_gen = 1; } }
static void vc_gpu_write(uint64_t dst, uint64_t n); // kept index ranges under the written range are not trusted again (ix_range)
static int pend_add(uint64_t dst, uint64_t n, int kind) {
vc_gpu_write(dst, n); drw_note(dst, n ? n : 1);
Expand Down Expand Up @@ -1848,8 +1854,23 @@ static void gw_put_in(gw_ent *t, uint64_t lo, uint64_t hi, uint64_t fence, uint6
if (fence > t[s].fence) t[s].fence = fence; }
}
static void gw_put(uint64_t lo, uint64_t hi, uint64_t fence, uint64_t done) { gw_put_in(g_gw, lo, hi, fence, done); gw_put_in(g_gwx, lo, hi, fence, done); }
// Reports land on the CPU before g_done advances. Only their 16 bytes are unknown until then:
// claiming their whole page refused unrelated uniform shadows (Ori and the Will of the Wisps).
// The submit thread owns this ring; fences are inserted in submission order. Never replace a live slot.
enum { GR_N = 1024 };
static struct { uint64_t lo, hi, fence; } g_gr[GR_N]; static unsigned g_gr_next, g_gr_n;
static int gr_live(uint64_t lo, uint64_t hi, uint64_t done) {
if (hi <= lo) return 0;
for (unsigned i = 0; i < g_gr_n; i++) {
unsigned q = (g_gr_next + GR_N - 1 - i) % GR_N;
if (g_gr[q].fence <= done) break;
if (lo < g_gr[q].hi && g_gr[q].lo < hi) return 1;
}
return 0;
}
static int gw_live_in(const gw_ent *t, uint64_t lo, uint64_t hi) {
uint64_t done = g_done; // read without the lock as elsewhere: a stale value only refuses more
if (gr_live(lo, hi, done)) return 1; // both ledgers, including rc_source's reads of report timestamp bytes
if (g_gw_all > done) return 1;
for (int i = 0; i < GWW_N; i++) if (g_gww[i].fence > done && lo < g_gww[i].hi && g_gww[i].lo < hi) return 1;
for (uint64_t pg = lo >> 12; pg <= (hi - 1) >> 12; pg++) { unsigned s = gw_slot(pg); if ((t[s].page == pg || t[s].page == ~0ull) && t[s].fence > done) return 1; }
Expand Down Expand Up @@ -1882,7 +1903,14 @@ static void gw_commit(uint64_t fence) {
gw_put_in(g_gw, g_pend_lo[e], g_pend_hi[e], fence, done);
if (g_pend_kind[e] == 1 && g_pend_upd[e] && um_on()) { const upd_ent *u = &g_upd[g_pend_upd[e] - 1]; um_put(g_pend_lo[e], g_pend_hi[e], (const uint8_t *)u->buf.contents + u->off + (g_pend_lo[e] - u->lo), fence, done); }
else gw_put_in(g_gwx, g_pend_lo[e], g_pend_hi[e], fence, done); }
if (g_nsbw) for (int i = 0; i < SBW_N; i++) if (g_sbwp[i].gen == g_pend_gen) gw_put(g_sbwp[i].page << 12, (g_sbwp[i].page + 1) << 12, fence, done);
int rep_exact = g_nrep <= REP_N;
for (unsigned i = 0; rep_exact && i < g_nrep; i++) if (g_gr[(g_gr_next + i) % GR_N].fence > done) rep_exact = 0;
if (rep_exact) for (unsigned i = 0; i < g_nrep; i++) {
g_gr[g_gr_next].lo = g_rep[i].lo; g_gr[g_gr_next].hi = g_rep[i].hi; g_gr[g_gr_next].fence = fence;
g_gr_next = (g_gr_next + 1) % GR_N; if (g_gr_n < GR_N) g_gr_n++;
}
// Overflow leaves all this submission's report pages conservative; actual GPU writes always remain by page.
if (g_nsbw) for (int i = 0; i < SBW_N; i++) if (g_sbwp[i].gen == g_pend_gen && (!rep_exact || (g_sbwp[i].src & SBW_GPU))) gw_put(g_sbwp[i].page << 12, (g_sbwp[i].page + 1) << 12, fence, done);
for (int i = 0; i < g_nsbw_w; i++) gw_put(g_sbw_wlo[i], g_sbw_whi[i], fence, done);
}
// Whether um_overlay lays a remembered byte over [lo, hi): ubp_get then copies that shadow here, since the overlay waits for its copy (queued and waited for at once, on every such shadow)
Expand Down
55 changes: 55 additions & 0 deletions dm/gfx/tests/t_ubp_running.m
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,61 @@ int main(void) { @autoreleasepool {
commit_as_running();
}

// Ori's two 16-byte reports share a page with uniform blocks. Only their bytes are unknown,
// not the rest of that page; the report timestamp is protected from CPU source reads too.
g_done = running; running += 1000; g_fence = running;
memset(mem + 0x14000, 0x55, 0x3000);
uint64_t base = (uint64_t)mem + 0x14000;
mtl_sub_counter(base + 0x40, 0); mtl_sub_counter(base + 0x80, 0); commit_as_running();
update(0x14004, 0x76543210, 4);
{ const uint32_t want[] = {0x55555555, 0x76543210, 0x55555555}; words(made_shadow(0x14000, 12), want, 3, "a disjoint uniform beside two unfinished reports stays eligible"); }
CHECK(!made_shadow(0x14038, 32), "a uniform intersecting a report is refused");
CHECK(gw_live(base + 0x40, base + 0x41) && gw_live(base + 0x8f, base + 0x90), "both complete report records remain live");
CHECK(!gw_live(base + 0x30, base + 0x40) && !gw_live(base + 0x50, base + 0x60), "touching either report boundary is disjoint");
{ uint8_t out[8]; int unreliable = 0;
CHECK(rc_source(base + 0x48, 8, out, &unreliable) == 0, "a report timestamp cannot be read as completed CPU memory");
CHECK(rc_source(base + 0x60, 8, out, &unreliable) == 255, "disjoint CPU source bytes on the report page are readable"); }
commit_as_running();
uint64_t first_report = running;
running++; g_fence = running;
mtl_sub_counter(base + 0xc0, 0); commit_as_running();
g_done = first_report;
CHECK(!gw_live(base + 0x40, base + 0x50), "the earlier report retires at its own fence");
CHECK(gw_live(base + 0xc0, base + 0xd0), "a later report on that page stays live");
// A report crossing a page protects both pieces, without claiming neighboring bytes.
mtl_sub_counter(base + 0xff8, 0); commit_as_running();
CHECK(gw_live(base + 0xff8, base + 0xff9) && gw_live(base + 0x1007, base + 0x1008), "a crossing report protects both pages");
CHECK(!gw_live(base + 0xfe8, base + 0xff8) && !gw_live(base + 0x1008, base + 0x1018), "crossing-report neighbors stay disjoint");
// Real GPU writes still claim their page even when a report is also there.
sbw_note(base + 0x2040, 16, SBW_REPORT); sbw_note(base + 0x2080, 16, SBW_GPU); commit_as_running();
CHECK(gw_live(base + 0x2000, base + 0x2010), "a mixed report/GPU page keeps its full hazard");
g_done = running; running += 1000; g_fence = running;
// Many reports must retain every hazard, whether listed exactly or conservatively by page.
for (int i = 0; i < 2048; i++) mtl_sub_counter(base + (uint64_t)(i % 128) * 16, 0);
commit_as_running();
for (int i = 0; i < 128; i++) CHECK(gw_live(base + (uint64_t)i * 16, base + (uint64_t)i * 16 + 16), "overflow never loses an unfinished report");
g_done = running;
CHECK(!gw_live(base, base + 0x3000), "completed overflow and mixed-page hazards retire");
// Fill the cross-submission ring while every fence remains live. The next submission must
// fall back without overwriting any earlier record, then reuse only slots whose fence finished.
uint64_t ring_first = running + 1;
for (unsigned chunk = 0; chunk < GR_N / REP_N; chunk++) {
running++; g_fence = running;
for (unsigned i = 0; i < REP_N; i++) mtl_sub_counter(base + ((uint64_t)chunk * REP_N + i) * 32, 0);
commit_as_running();
}
CHECK(!gw_live(base + 16, base + 32), "a full exact ring still leaves disjoint bytes readable");
running++; g_fence = running; mtl_sub_counter(base + 0x9000, 0); commit_as_running();
CHECK(gw_live(base + 0x9010, base + 0x9020), "a full live ring falls back to the new report's page");
for (unsigned i = 0; i < GR_N; i++) CHECK(gw_live(base + (uint64_t)i * 32, base + (uint64_t)i * 32 + 16), "ring overflow preserves all older reports");
g_done = ring_first;
CHECK(!gw_live(base, base + 16), "only the first batch has retired");
CHECK(gw_live(base + REP_N * 32, base + REP_N * 32 + 16), "the next batch is still unfinished");
running++; g_fence = running; mtl_sub_counter(base + 0xa000, 0); commit_as_running();
CHECK(gw_live(base + 0xa000, base + 0xa010) && !gw_live(base + 0xa010, base + 0xa020), "a retired ring slot can hold a new exact report");
g_done = running;
CHECK(!gw_live(base, base + 0xb000), "all report hazards retire after completion");

pend_reset(); g_nubd = 0;
printf("t_ubp_running: %d checks, %d failed\n", checks, failed); fflush(stdout);
_exit(failed != 0);
Expand Down
1 change: 1 addition & 0 deletions docs/agents/symptoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
- many render passes with one draw each and high submit time: Metroid Dread's cutscene frames had about 180 passes with one draw each (about 5 ms of submit time per frame on the Mac). → [Inline uniform updates inside a render pass](../reference/rendering.md#inline-uniform-updates-inside-a-render-pass)
- hundreds of render passes a frame and heavy attachment traffic when a game updates a small part of a large uniform block before each draw group. → [Partial-cover uniform shadows keep passes open](../reference/rendering.md#partial-cover-uniform-shadows-keep-passes-open)
- a scene runs at 20–27 fps where similar ones hold 60, with the GPU time about three times its usual (draw counts unchanged). The `residency set` line shows `passes ended to land them for a read` in the tens of thousands, every one `refused … other GPU writes`, and about three times the usual `load-all store-all passes`. A burst of 1,200 such refusals at each world load is the short form. → [A storage buffer bound with size 0 counted as a write anywhere](../reference/rendering.md#a-storage-buffer-bound-with-size-0-counted-as-a-write-anywhere)
- partly covered uniform shadows are refused as `earlier submission running` although only disjoint counter reports on the same page are unfinished; each refusal ends a render pass. → [Counter reports falsely block other bytes on their page](../reference/rendering.md#counter-reports-falsely-block-other-bytes-on-their-page)
- the `residency set` line shows `passes ended to land them for a read` in the tens of thousands, every one `refused … earlier submission running`. The slower the GPU, the more passes, and each pass makes it slower: a scene stays at a very low frame rate once it falls in. → [Inline updates of a running submission refused uniform shadows](../reference/rendering.md#inline-updates-of-a-running-submission-refused-uniform-shadows)
- one scene of a game drops to 1-2 fps and stays there, with the GPU busy 400-600 ms a frame and about ten times the render passes of the scenes before it, many of one draw each on the same target. The `residency set` line shows `passes ended to land them for a read` in the tens of thousands, nearly all `refused … earlier submission running`, with some `unreadable`. A pass dump (`DM_NVN_PASSDUMP`) names `draw_` → `ubd_conflict` as the end of those passes (`E <pass> pass ends in <function>:<line>`). → [A submission past 8,192 queued writes counted as writing everywhere](../reference/rendering.md#a-submission-past-8192-queued-writes-counted-as-writing-everywhere)
- `stage 0 cb5 has no uniform buffer bound` (or another stage and slot, such as `stage 0 cb0`), once per run. → [An unbound uniform slot reads the zero bank](../reference/rendering.md#an-unbound-uniform-slot-reads-the-zero-bank)
Expand Down
2 changes: 2 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The README covers packing and playing. This page is for working on nxnative itse
| iOS app | One unsigned IPA for iPhone and iPad. The rewritten code is embedded in the executable; the shader libraries, the RomFS and the game icon are inside the app. A settings screen comes before the game (`Launcher.swift`, drawn from `dm/launcher.json`, which `dm/games/settings.py` writes from `dm/games/settings.tsv`); the host's loading starts at app launch meanwhile, and the frame rate and render scale go into the frame step state when the game starts (`loader.c` `dm_fps_state_apply`) |
| Known gaps | Conditional render is always drawn; [indirect-count multi-draws](reference/rendering.md#indirect-count-multi-draws) can wait for a GPU-written count when the records need conversion, cannot be GPU-masked, or exceed the mask table capacity; geometry shaders do not translate. TBDR optimizations are off by default (they don't understand bindless textures yet). No amiibo. On the Mac, Mario's menu and dialogue text render blank (fine on the iPhone) |

Counter reports keep their exact byte ranges live until completion, so unrelated uniforms on the same page can retain partial shadows. Overflow remains conservative, as do actual GPU writes ([counter-report hazards](reference/rendering.md#counter-reports-falsely-block-other-bytes-on-their-page)).

## Cheats

How a Switch with Atmosphère runs cheats, and where this host differs, is in [Cheats on a Switch](reference/cheats.md).
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,13 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md) (GitHub issues #534 and #598, shrines, MacBook Air M2 and M3). The Shrine of Resurrection save shows the load burst: the 300 frames around the world load had 1,200 refusals and 107 load-all store-all passes, now 0 and 47; `DM_NVN_UBP_CHECK=1` found 146,000 shadows equal to the GPU's memory, 0 different. The reporter's log shows the same burst at each load, and 240 refusals a frame in its slow stretches.
- **Check:** `make -C dm/gfx/tests GAME=<id> check_t_sbw` (16 checks: the size-0 outputs of that dispatch do not hit the uniform block's pool, a write of no known size still stops the tracking, the interval list's limit, and the next submission's view through `gw_live`).

### Counter reports falsely block other bytes on their page

- **Symptom:** partly covered uniform shadows are refused as `earlier submission running` although only disjoint counter reports on the same page are unfinished; each refusal ends a render pass.
- **Cause:** a report writes 16 bytes when its submission completes, but `gw_commit` recorded its entire 4 KB page as an unknown write. Uniform blocks sharing that page were unnecessarily refused.
- **Fix:** retain report byte ranges with their submission fences. Both GPU-write ledgers consult these ranges, including CPU copy-source reads of the report's timestamp field. Real GPU writes still claim pages. At most 256 reports per submission enter the 1,024-record ring; if any new record would replace a live one, or the submission's report list overflows, that submission keeps the old conservative page hazards. Completed records retire only after their report bytes have landed and `g_done` advances.
- **Found on:** [Ori and the Will of the Wisps, issue #1660](https://github.com/seedds/nxnative/issues/1660): two timestamp reports share a page with uniform blocks. No repository profile exists yet. The exact-range rule is validated with synthetic commands; the reported gameplay frame-rate improvement has not been independently measured.

### Inline updates of a running submission refused uniform shadows

- **Symptom:** the `residency set` line shows `passes ended to land them for a read` in the tens of thousands, every one `refused … earlier submission running`. The slower the GPU, the more passes, and each pass makes it slower: a scene stays at a very low frame rate once it falls in.
Expand Down
Loading