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
1 change: 1 addition & 0 deletions dm/games/0400486024DEC000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions dm/games/0400486024DEC000/game.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions dm/games/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<stage>_<tb|s>_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` | `<offset>+<length>` (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` |
Expand Down
3 changes: 3 additions & 0 deletions dm/games/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")}',
Expand Down
29 changes: 20 additions & 9 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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; }
Expand Down Expand Up @@ -1617,15 +1619,15 @@ 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;
}
// texture copies / readbacks: barrier if anything came before, and later operations must wait for it too
static id<MTL4ComputeCommandEncoder> blit(void) {
id<MTL4ComputeCommandEncoder> 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;
}

Expand Down Expand Up @@ -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<MTL4ComputeCommandEncoder> 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<MTL4ComputeCommandEncoder> dispatch_enc(int game) { // game: one of the game's dispatches (0: the host's own, ordered as a copy is)
id<MTL4ComputeCommandEncoder> 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;
Expand Down Expand Up @@ -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<MTLComputeCommandEncoder> ce = own ? pg_compute() : (id<MTLComputeCommandEncoder>)dispatch_enc();
id<MTLComputeCommandEncoder> ce = own ? pg_compute() : (id<MTLComputeCommandEncoder>)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);
Expand Down Expand Up @@ -13141,7 +13152,7 @@ static int rb_swap_gpu(uint64_t src, size_t row, size_t img, uint64_t dst, size_
id<MTLBuffer> 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<MTL4ComputeCommandEncoder> e = dispatch_enc();
id<MTL4ComputeCommandEncoder> 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);
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions dm/gfx/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions dm/gfx/tests/t_dispatch_overlap.m
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>
#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;
} }
Loading
Loading