diff --git a/dm/games/0400486024DEC000/README.md b/dm/games/0400486024DEC000/README.md index 2b20a24fee..0a0ae6ef6f 100644 --- a/dm/games/0400486024DEC000/README.md +++ b/dm/games/0400486024DEC000/README.md @@ -112,6 +112,7 @@ From an empty save: at about 45 s `A` (title), `A` (New Game), then the movie. F - [Native mesh pipelines compiled at their first draw](../../../docs/reference/shaders.md#native-mesh-pipelines-compiled-at-their-first-draw): resolved. The foliage, shadows and enemies drawn with task and mesh shaders stalled at their first draw on a cold Metal cache (59.5 s of builds at the Kokiri Forest entry on an M5 Max, one frame of 42.9 s), as nothing recorded or kept their pipelines; `pso_manifest.tsv` now carries 123 `M` rows (Kokiri Forest, Hyrule Field, Castle Town, Hyrule Castle), and with the prepare step they are built before the first frame. Pipelines outside the list still stall once, then go into `pso_user.tsv`. - [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. - [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. +- [Compute dispatches ordered by the game's own barriers](../../../docs/reference/rendering.md#compute-dispatches-ordered-by-the-games-own-barriers): most of the frame's compute runs at its start with no barriers between the dispatches, each of which waited for the one before; the profile sets `dispatch_overlap on` (`kokiri-deck`: GPU p50 15.63 → 14.76 ms). - [A fixed frame step plays in slow motion on a slow Mac](../../../docs/reference/frame-rate.md#a-fixed-frame-step-plays-in-slow-motion-on-a-slow-mac): the frame-ratio division. - [Frame-step stubs placed by their function](../../../docs/reference/rewriter.md#frame-step-stubs-placed-by-their-function) diff --git a/dm/games/0400486024DEC000/game.tsv b/dm/games/0400486024DEC000/game.tsv index e0c8ec0cdc..b1810c248f 100644 --- a/dm/games/0400486024DEC000/game.tsv +++ b/dm/games/0400486024DEC000/game.tsv @@ -49,3 +49,6 @@ draw_barriers game # As Tears of the Kingdom: the terrain's height, blend and hole maps are written by the CPU into textures after they are created. Without these keys they stay zero: holes in the ground and the wrong materials tex_flush_upload on tex_dirty_upload on +# The game's compute dispatches with no barrier of its between them run side by side (nvn_mtl.m dispatch_enc): on the kokiri-deck route GPU p50 15.63 -> 14.76 ms (docked, +# Apple M4 Pro, three runs a side), the pictures alike +dispatch_overlap on diff --git a/dm/games/README.md b/dm/games/README.md index d003204382..4af422b0b7 100644 --- a/dm/games/README.md +++ b/dm/games/README.md @@ -246,6 +246,7 @@ One row per profile; the notes are each profile's `README.md`. | `tex_dyn_slots` | `on` / `off` (the default): a shader whose buffer-texture handle the translator could not trace to one slot gets Ryujinx's fallback arrays (`__cb0_40000000`: 32 buffer textures and samplers indexed by the handle's texture / sampler ID; any other sampled texture indexes the whole pool without a key, [Texture handles from game memory index the pools](../../docs/reference/shaders.md#texture-handles-from-game-memory-index-the-pools)). With the key the replay puts slot indices in that shader's c2 texture and sampler tables (texture slot r → r, sampler slot r → r << 20) and binds slot k at element k, so the shader's own arithmetic picks the binding (`NVN_RB_TEXD` / `NVN_RB_SAMPD` in `nvn_mtl.m`). Right only where the handle comes from c2's tables: Octopath Traveler 0's 61 such vertex shaders read separate texture slots 0-10 and samplers 0-4 there and nothing else (FINAL FANTASY RESONANCE's 2,077 the texture table words 0x168-0x1c0 and sampler table words 0x568-0x590). Off, such shaders do not load. Not part of the shader chain version | | `gpu_timestamps` | What a timestamp report (`nvnCommandBufferReportCounter`) records: `submission` (the default) = the time the whole submission completed, the same for every report in it; `segment` = the GPU start (`TIMESTAMP_TOP`) or end (the other types) of the segment the report was encoded in (the host commits a submission in segments of 64 draws, `sub_cut` in `nvn_mtl.m`), so two reports of one submission bracket the GPU time between them, as on hardware. Breath of the Wild needs `segment`: its frame-interval controller and its dynamic resolution read the GPU time between the frame's first and last report, and with one time for both they read 0 (the game kept 60 fps in slow motion when the GPU could not keep up). `DM_NVN_GPU_TS=segment` / `submission` overrides it, also from `Documents/env.txt` | | `draw_barriers` | `full` (the default) / `game`: how draw passes are ordered. `full`: each waits for all earlier GPU work. `game`: by the game's own barriers (NVN asks for one before results of rendering are read), so a pass's vertex work overlaps the previous pass's fragments. Ocarina of Time's GPU time 25 → 18 ms, about 180 full barriers a frame halved. Only vertex-stage reads of the previous pass's output without a barrier change order. `DM_NVN_BARRIER` overrides it | +| `dispatch_overlap` | `off` (the default) / `on`: the game's compute dispatches with no game barrier and nothing else between them run side by side instead of each waiting for the one before ([Compute dispatches ordered by the game's own barriers](../../docs/reference/rendering.md#compute-dispatches-ordered-by-the-games-own-barriers)). `DM_NVN_DISPATCH_OVERLAP` overrides it | | `vconv_range` | `on` / `off` (the default): vertex attributes the replay converts on the CPU (integer shader inputs fed float-type formats and the like, `vconv_bind` in `nvn_mtl.m`) are converted only over the vertices each draw reads (first / count, or the smallest and largest index of a direct indexed draw, kept while the index pages stay unwritten) instead of the whole bound range. Age of Calamity binds whole mesh pools: in a battle that was millions of vertices a frame. Indirect draws, per-instance streams and indices with a GPU write queued on them keep the whole range. `DM_NVN_VCONV_CHECK=1` scans the indices again at every reuse and reports differences; `DM_NVN_VCONV_RANGE=0` / `1` overrides it | | `shader_key_skip` | `+` (none by default): control-section bytes every shader key hashes as zeros, in `shader_split.py` (`key_ctl`) and the host (`hle_nvn.c` `keys_hash`), for an engine that shares one control section among shaders that differ only there. Batman: Arkham Knight: `0x770+0x78`, GLSLC's compile information (+0x778..+0x791 and +0x7d0..+0x7e7); without it the game asks for shaders the map does not have, each the control section of one record with the code area of another. It must stay clear of `+0x700..+0x75f`, which the tools read; it is a shader chain input, as every `shader_*` key | | `shader_groups`, `shader_note` | `shader_split.py` source groups and the map header note (Engage: `engine` for `data.unity3d` and `Resources/`, `assets` for the Addressables bundles). Both are shader inputs, the note too: changing either one, even by a word, builds every shader of the game again. Keep them as they are when a provisional profile moves into `dm/games` | diff --git a/dm/games/game.py b/dm/games/game.py index 53e23ee897..6435339871 100644 --- a/dm/games/game.py +++ b/dm/games/game.py @@ -193,6 +193,8 @@ def header_one(g): sko, skl = (int(m[1], 0), int(m[2], 0)) if m else (0, 0) rcd = g.get('render_cond', 'off') # NVN conditional rendering evaluated at replay, timestamp reports with counter 0 (nvn_mtl.m OP_RENDER_COND): on / off if rcd not in ('on', 'off'): sys.exit(f'{g.id}: render_cond must be on or off: {rcd}') + dov = g.get('dispatch_overlap', 'off') # the game's dispatches with no barrier of its between them run side by side in the copy encoder (nvn_mtl.m dispatch_enc; DM_NVN_DISPATCH_OVERLAP overrides): on / off + if dov not in ('on', 'off'): sys.exit(f'{g.id}: dispatch_overlap must be on or off: {dov}') tfu = g.get('tex_flush_upload', 'off') # textures written by the CPU into a pool and published with nvnMemoryPoolFlushMappedRange are uploaded like packaged ones (hle_nvn.c): on / off if tfu not in ('on', 'off'): sys.exit(f'{g.id}: tex_flush_upload must be on or off: {tfu}') tdu = g.get('tex_dirty_upload', 'off') # with tex_flush_upload: a registered texture the CPU writes after init without any flush is uploaded too (dirty pages, hle_nvn.c nvn_ft_written): on / off @@ -259,6 +261,7 @@ def header_one(g): f'#define DM_GAME_TEX_FLUSH_UPLOAD {int(tfu == "on")}', f'#define DM_GAME_SHADER_KEY_SKIP_OFF {sko:#x}', f'#define DM_GAME_SHADER_KEY_SKIP_LEN {skl:#x}', f'#define DM_GAME_RENDER_COND {int(rcd == "on")}', + f'#define DM_GAME_DISPATCH_OVERLAP {int(dov == "on")}', f'#define DM_GAME_TEX_DIRTY_UPLOAD {int(tdu == "on")}', f'#define DM_GAME_TEX_DYN_SLOTS {int(tds == "on")}', f'#define DM_GAME_GPU_TS_SEGMENT {int(gts == "segment")}', diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index d69bfb7386..a8166b3403 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -1559,6 +1559,8 @@ static inline id enc_b_begin(id cb) { // allocator gave out overlapping blocks, see hle_nvn.c zcull_size; NVN runs copies in order for any game) enum { BW_MAX = 64 }; static uint64_t g_bw_lo[BW_MAX], g_bw_hi[BW_MAX], g_br_lo[BW_MAX], g_br_hi[BW_MAX]; static int g_bw_n, g_br_n, g_blit_ops; static uint64_t g_blit_barriers; +static int g_blit_disponly = 1; // since the copy encoder's last barrier only the game's dispatches went into it (dispatch_enc) +static int g_cs_gbar; // the game called nvnCommandBufferBarrier since its last dispatch (dispatch_enc) static uint64_t g_blit_splits; // copies / buffer updates / fills that ended an open draw pass (the residency set line; a pass ended this way stores and the next one reloads its attachments) uint64_t mtl_blit_splits(void) { return g_blit_splits; } static int enc_open(void); @@ -1578,7 +1580,7 @@ static void blit_barrier(void) { static int off = -1; if (off < 0) { const char *e = getenv("DM_NVN_BLIT_ORDER"); off = e && !strcmp(e, "0"); if (off) MLOG("DM_NVN_BLIT_ORDER=0: no barriers inside the copy encoder (diagnostic; brings back the fountain's vertical stripes)"); } // Only the stages a compute encoder has: MTLStageAll here fails Metal validation (macOS 27: "must be a valid combination of MTLStages for the current encoder type") if (!off) [g_blit barrierAfterEncoderStages:MTLStageDispatch | MTLStageBlit beforeEncoderStages:MTLStageDispatch | MTLStageBlit visibilityOptions:blit_vis()]; - g_bw_n = g_br_n = 0; g_blit_ops = 0; g_blit_barriers++; + g_bw_n = g_br_n = 0; g_blit_ops = 0; g_blit_barriers++; g_blit_disponly = 1; } uint64_t mtl_blit_barriers(void) { return g_blit_barriers; } static int bw_hit(uint64_t lo, uint64_t hi) { for (int i = 0; i < g_bw_n; i++) if (lo < g_bw_hi[i] && g_bw_lo[i] < hi) return 1; return 0; } @@ -1617,7 +1619,7 @@ static void drw_note(uint64_t lo, uint64_t n) { // range-based batching, including separate Metal buffers over one CPU range. if (nvn_sparse_alias(rlo, rhi) || nvn_sparse_alias(wlo, whi)) { wlo = 0; whi = ~0ull; } if (g_blit_ops && (g_bw_n == BW_MAX || g_br_n == BW_MAX || (rhi > rlo && bw_hit(rlo, rhi)) || bw_hit(wlo, whi) || br_hit(wlo, whi))) blit_barrier(); - g_bw_lo[g_bw_n] = wlo; g_bw_hi[g_bw_n++] = whi; g_blit_ops++; g_gp_copies++; + g_bw_lo[g_bw_n] = wlo; g_bw_hi[g_bw_n++] = whi; g_blit_ops++; g_gp_copies++; g_blit_disponly = 0; if (rhi > rlo) { g_br_lo[g_br_n] = rlo; g_br_hi[g_br_n++] = rhi; } return e; } @@ -1625,7 +1627,7 @@ static void drw_note(uint64_t lo, uint64_t n) { static id blit(void) { id e = blit_raw(); if (g_blit_ops) blit_barrier(); - g_bw_lo[0] = 0; g_bw_hi[0] = ~0ull; g_bw_n = 1; g_br_n = 0; g_blit_ops = 1; // record as "everything written": the next operation always gets a barrier + g_bw_lo[0] = 0; g_bw_hi[0] = ~0ull; g_bw_n = 1; g_br_n = 0; g_blit_ops = 1; g_blit_disponly = 0; // record as "everything written": the next operation always gets a barrier return e; } @@ -13042,10 +13044,18 @@ static int dispatch_own(void) { if (v < 0) { const char *e = getenv("DM_NVN_DISPATCH_OWN"); v = (e && !strcmp(e, "1")) || pg_on(); if (v) MLOG("compute dispatches: each in an encoder of its own (DM_NVN_DISPATCH_OWN=1 or DM_NVN_PASSGPU)"); } return v; } -static id dispatch_enc(void) { +// DM_NVN_DISPATCH_OVERLAP (profile key dispatch_overlap): two of the game's dispatches with nothing between them in the copy encoder and no nvnCommandBufferBarrier of the game's +// between them run side by side, as NVN lets them: it orders a dispatch after earlier work only through a barrier. A copy, fill or inline update between them, a host dispatch +// (dispatch_enc's game 0) or a game barrier keeps the barrier, and what comes after a dispatch other than a dispatch still waits for it (everything written). See docs/reference/rendering.md, +// "Compute dispatches ordered by the game's own barriers" (Ocarina of Time) +static uint64_t g_disp_beside; static int g_disp_last_beside; // dispatches placed beside the one before (t_dispatch_overlap); whether the last one was +static int dispatch_overlap(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_DISPATCH_OVERLAP"); v = e ? atoi(e) != 0 : DM_GAME_DISPATCH_OVERLAP; if (v) MLOG("dispatch overlap: the game's dispatches with no barrier of its between them run side by side (DM_NVN_DISPATCH_OVERLAP=0 orders each after the one before)"); } return v; } +static id dispatch_enc(int game) { // game: one of the game's dispatches (0: the host's own, ordered as a copy is) id e = blit_open(); g_gp_disp++; - if (g_blit_ops) blit_barrier(); - g_bw_lo[0] = 0; g_bw_hi[0] = ~0ull; g_bw_n = 1; g_br_n = 0; g_blit_ops = 1; // recorded as "everything written": the next copy or dispatch gets a barrier + int beside = game && g_blit_ops && g_blit_disponly && !g_cs_gbar && dispatch_overlap(); + if (g_blit_ops && !beside) blit_barrier(); + if (game) { g_disp_last_beside = beside; if (beside) g_disp_beside++; g_cs_gbar = 0; } + g_bw_lo[0] = 0; g_bw_hi[0] = ~0ull; g_bw_n = 1; g_br_n = 0; g_blit_ops = 1; g_blit_disponly = game; // recorded as "everything written": the next copy, or a dispatch after a game barrier, gets a barrier if (!g_blit_ct) { [e setArgumentTable:g_ct]; g_blit_ct = 1; } args_dirty_all(); return e; @@ -13103,7 +13113,8 @@ static void dispatch(const p_dispatch *dp) { // or before dispatch_enc's copy-to-dispatch barrier: otherwise the upgrade opens a second encoder, or its copy races the dispatch (#1743). encode_args(nil, cs, 5, 0); if (own && g_blit) { enc_b_end(g_blit); g_blit = nil; } - id ce = own ? pg_compute() : (id)dispatch_enc(); + id ce = own ? pg_compute() : (id)dispatch_enc(1); + PD("X %llu dispatch %s groups %llux%llux%llu%s%s", g_stat_pass, cs->key, (unsigned long long)dp->x, (unsigned long long)dp->y, (unsigned long long)dp->z, dp->ind ? " indirect" : "", own ? " (own encoder)" : g_disp_last_beside ? " beside the one before" : " after a barrier"); enc_c_pso(ce, ps); if (cs->rf.tg_spill) { // threadgroup memory past Metal's 32 KB, in a device buffer (dmsh_build.py tg_spill): a slice per workgroup, the workgroup's index modulo DMSH_TG_GROUPS uint64_t groups = dp->ind ? DMSH_TG_GROUPS : MIN((uint64_t)dp->x * dp->y * dp->z, (uint64_t)DMSH_TG_GROUPS); @@ -13141,7 +13152,7 @@ static int rb_swap_gpu(uint64_t src, size_t row, size_t img, uint64_t dst, size_ id pb; size_t po = arena_alloc(48, &pb); struct { uint64_t s, d; uint32_t w, h, row, img, drow, dimg, shift; } *p = (void *)((uint8_t *)pb.contents + po); *p = (typeof(*p)){src, dst, (uint32_t)(wb / 2), (uint32_t)rows, (uint32_t)row, (uint32_t)img, (uint32_t)drow, (uint32_t)dimg, (uint32_t)shift}; - id e = dispatch_enc(); + id e = dispatch_enc(0); [g_ct setAddress:pb.gpuAddress + po atIndex:0]; enc_c_pso(e, ps); enc_c_dispatch(e, MTLSizeMake((wb / 2 + 63) / 64, rows, nz), MTLSizeMake(64, 1, 1)); static int once; if (!once++) MLOG("RGB565-family copy between guest memory and a host texture: red and blue exchanged on the GPU (first case %zux%zux%zu texels)", wb / 2, rows, nz); @@ -14213,7 +14224,7 @@ void mtl_exec(const uint8_t *p) { PD("I %llu barrier bits %#x%s", g_stat_pass, bits, split ? ": ends the pass (shader writes)" : order ? " (render pass open; a later shader write is ordered after its draws)" : g_enc ? " (render pass open)" : ""); if (split) { rp_end(); static int once; if (!once++) MLOG("a game barrier (bits %#x) after shader writes in a draw pass: the pass ends there (first case pass %llu)", bits, (unsigned long long)g_stat_pass); } else if (order) g_pass_order |= (bits & NVN_BARRIER_ORDER_PRIMITIVES_BIT ? 1 : 0) | (bits & (NVN_BARRIER_ORDER_FRAGMENTS_BIT | NVN_BARRIER_ORDER_FRAGMENTS_TILED_BIT) ? 2 : 0); - if (bits) { g_game_barrier = 1; g_fb_bar++; } break; } // g_fb_bar: snapshots taken before it are stale (see fb_prepare) + if (bits) { g_game_barrier = 1; g_fb_bar++; g_cs_gbar = 1; } break; } // g_fb_bar: snapshots taken before it are stale (see fb_prepare); g_cs_gbar: dispatch_enc case OP_SET_TEX_POOL: case OP_SET_SAMP_POOL: state_op(h.op, a); break; default: break; } diff --git a/dm/gfx/tests/Makefile b/dm/gfx/tests/Makefile index a76496f1e6..f254f1cdf4 100644 --- a/dm/gfx/tests/Makefile +++ b/dm/gfx/tests/Makefile @@ -121,6 +121,10 @@ t_link_ticks: t_link_ticks.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_link_ticks.m $(filter-out ../nvn/hle_nvn.c,$(MTL_T_SRC)) $(FW) t_cpu_tex: t_cpu_tex.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_cpu_tex.m $(filter-out ../nvn/hle_nvn.c,$(MTL_T_SRC)) $(FW) +# The game's dispatches side by side in the copy encoder where no barrier of its and nothing else lies between them (dispatch_enc, DM_NVN_DISPATCH_OVERLAP) +t_dispatch_overlap: t_dispatch_overlap.m $(MTL_T_DEP) + clang $(CFLAGS) -fobjc-arc -o $@ t_dispatch_overlap.m $(MTL_T_SRC) $(FW) +check_t_dispatch_overlap: RUN_ENV = MTL_DEBUG_LAYER=1 .PHONY: check_t_cpu_tex check_t_cpu_tex: t_cpu_tex MTL_DEBUG_LAYER=1 ./t_cpu_tex --metal diff --git a/dm/gfx/tests/t_dispatch_overlap.m b/dm/gfx/tests/t_dispatch_overlap.m new file mode 100644 index 0000000000..3d1077bf31 --- /dev/null +++ b/dm/gfx/tests/t_dispatch_overlap.m @@ -0,0 +1,34 @@ +// The game's dispatches in the copy encoder (dispatch_enc, DM_NVN_DISPATCH_OVERLAP): two with nothing between them and no barrier of the game's run side by side; a game barrier, +// a copy between them or a host dispatch keeps the barrier (counted by the copy encoder's barriers, g_blit_barriers) +#import +#include "../nvn/nvn_mtl.m" +void *front_metal_layer(void) { return NULL; } +module_t *module_of(const void *p) { (void)p; return NULL; } +module_t *g_mod; int g_nmod; +void svc_thread_dump(void) {} +uint64_t now_ns(void) { return bp_now(); } + +static int checks, failed; +#define CHECK(c, label) do { checks++; if (!(c)) { failed++; fprintf(stderr, "FAIL: %s (line %d)\n", label, __LINE__); } } while (0) + +int main(void) { @autoreleasepool { + setenv("DM_NVN_DISPATCH_OVERLAP", "1", 1); + mtl_init(); + mtl_sub_begin(); + uint64_t b0 = g_blit_barriers; + dispatch_enc(1); + CHECK(g_blit_barriers == b0, "the first dispatch in the encoder needs no barrier"); + dispatch_enc(1); dispatch_enc(1); + CHECK(g_blit_barriers == b0 && g_disp_beside == 2, "dispatches with nothing between them run side by side"); + g_cs_gbar = 1; dispatch_enc(1); + CHECK(g_blit_barriers == b0 + 1 && !g_cs_gbar, "a game barrier between them keeps the barrier"); + blit_buf(0, 0, 0x1000, 0x2000); dispatch_enc(1); + CHECK(g_blit_barriers == b0 + 3, "a copy after a dispatch waits for it, and the dispatch after the copy waits for the copy"); + uint64_t b1 = g_blit_barriers; dispatch_enc(0); + CHECK(g_blit_barriers == b1 + 1, "a host dispatch is ordered as a copy is"); + uint64_t b2 = g_blit_barriers; dispatch_enc(1); + CHECK(g_blit_barriers == b2 + 1, "a game dispatch after a host dispatch waits for it"); + CHECK(mtl_wait(mtl_sub_commit(), 5000000000ull) && !mtl_last_commit_error(), "the submission completes"); + printf("t_dispatch_overlap: %d checks, %d failed\n", checks, failed); + return failed != 0; +} } diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 1386469d95..021988a619 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -760,6 +760,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - 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 ends in :`). → [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) - GPU time per frame doubles with many small dispatches (Age of Calamity: GPU 9 → 18 ms a frame on the Mac; the iPhone's 15 → 51 ms, 17 fps). → [Compute dispatches share the copy encoder](../reference/rendering.md#compute-dispatches-share-the-copy-encoder) +- a frame that runs many compute dispatches back to back spends much of their GPU time waiting: each dispatch starts only when the one before it has finished, also where the game put no barrier between them. → [Compute dispatches ordered by the game's own barriers](../reference/rendering.md#compute-dispatches-ordered-by-the-games-own-barriers) - with `DM_NVN_DISPATCH_OWN=1` the host crashes in the Metal driver (`CRASH sig=11 addr=0x98` in `AGX::ComputeContext<…>::prepareForEnqueue(bool)`) at a compute shader's first read through a texture view of another format; by default (dispatches sharing the copy encoder) that read can see the texture's old pixels. Metal validation reports `Command buffer already has an open command encoder` for the first. → [Compute bindings prepare texture copies before the dispatch encoder](../reference/rendering.md#compute-bindings-prepare-texture-copies-before-the-dispatch-encoder) - dialog boxes with holes the size of their text, and white specks where text faded. → [Fragment-ordering barriers between draws that write images](../reference/rendering.md#fragment-ordering-barriers-between-draws-that-write-images) - a texture copied from a staging buffer shows another texture's blocks (Catherine: Full Body's cutscene subtitle band a hard-edged dark box with a magenta stroke through it). → [A segment committed after a buffer → texture copy](../reference/rendering.md#a-segment-committed-after-a-buffer--texture-copy) diff --git a/docs/env_switches.tsv b/docs/env_switches.tsv index 399702b374..47ec5dd66f 100644 --- a/docs/env_switches.tsv +++ b/docs/env_switches.tsv @@ -134,6 +134,7 @@ DM_NVN_DEFER_AB diagnostic `=1` keeps the deferred pass merging (with `DM_NVN_TB DM_NVN_DEPTHOVR diagnostic `=,…`: those draws compare depth with `ALWAYS`; with `DM_NVN_HILITE`, tells "the depth test rejects every fragment" from "nothing rasterizes" DM_NVN_DIRECT_UPLOAD switch 0 disables bounded direct RGBA16F array uploads and keeps staged copies; on by default for supported non-sparse textures. DM_NVN_DIRTYCHECK diagnostic `=1` fully hashes pool content on every use to find changes the dirty-page tracking missed +DM_NVN_DISPATCH_OVERLAP switch `1` runs the game's compute dispatches with no barrier of its and nothing else between them side by side in the copy encoder, as NVN allows; `0` orders each after the one before (the default unless the profile sets dispatch_overlap: Ocarina of Time does) DM_NVN_DISPATCH_OWN switch `=1` gives every compute dispatch a compute encoder of its own with full barriers, as before (by default dispatches share the copy encoder); `DM_NVN_PASSGPU` does the same DM_NVN_DUAL_WANT path `=`: appends the key of each pixel shader a pipeline blends with its output 1 as the second source, while it has no dual-source entry point, to that file, once per key (the profile's `dual_want.tsv`) DM_NVN_ENC_LABEL diagnostic `=1` labels every render encoder `p x d` for Instruments traces diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index ba4c9bb271..553ad47903 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -1230,6 +1230,13 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E - **Found on:** [Hyrule Warriors: Age of Calamity](../../dm/games/01002B00111A2000/README.md): after its first cutscene the first battle runs about 900–1,500 small dispatches a frame on the Mac (an iPhone log's call counts suggest more), each after a game barrier, at the same draws and passes; GPU 7.3 → 8.1 ms across the cutscene with the change. - **Note:** `DM_NVN_DISPATCH_OWN=1` (and `DM_NVN_PASSGPU`, which times each dispatch) gives each dispatch its own encoder again. +### Compute dispatches ordered by the game's own barriers + +- **Symptom:** a frame that runs many compute dispatches back to back spends much of their GPU time waiting: each dispatch starts only when the one before it has finished, also where the game put no barrier between them. +- **Cause:** `dispatch_enc` puts a barrier inside the shared copy encoder before every dispatch ([Compute dispatches share the copy encoder](#compute-dispatches-share-the-copy-encoder)). That is more ordering than NVN gives: it orders a dispatch after earlier work only through `nvnCommandBufferBarrier`, as the draw passes of a game with `draw_barriers game` are ordered. +- **Fix:** the profile key `dispatch_overlap` (`DM_NVN_DISPATCH_OVERLAP` overrides it), in `dispatch_enc`. Two of the game's dispatches with nothing else between them in the copy encoder and no game barrier between them run side by side. A copy, fill or inline update between them, a dispatch of the host's own, or a game barrier keeps the barrier, and whatever follows a dispatch other than another dispatch still waits for it. `dm/gfx/tests/t_dispatch_overlap.m` checks where the barriers go. +- **Found on:** [The Legend of Zelda: Ocarina of Time](../../dm/games/0400486024DEC000/README.md#found-on-this-game), which runs most of its compute at the start of a frame with no barriers between the dispatches. Its `kokiri-deck` route (`scripts/bench.py`, docked, Apple M4 Pro, three runs a side, `DM_NVN_DISPATCH_OVERLAP=0` against the profile's `on`): GPU p50 15.63 → 14.76 ms, p95 16.43 → 15.66 ms, frames over budget 63 → 36, the pictures alike. + ### Compute bindings prepare texture copies before the dispatch encoder - **Symptom:** with `DM_NVN_DISPATCH_OWN=1` the host crashes in the Metal driver (`CRASH sig=11 addr=0x98` in `AGX::ComputeContext<…>::prepareForEnqueue(bool)`) at a compute shader's first read through a texture view of another format; by default (dispatches sharing the copy encoder) that read can see the texture's old pixels. Metal validation reports `Command buffer already has an open command encoder` for the first.