diff --git a/dm/games/01007EF00011E000/README.md b/dm/games/01007EF00011E000/README.md index 6947229..84d77a9 100644 --- a/dm/games/01007EF00011E000/README.md +++ b/dm/games/01007EF00011E000/README.md @@ -183,6 +183,7 @@ Before publishing, the fixes were integrated with main's pipeline archive and GP General host changes first needed here; each applies to every game: +- **Shared performance work, checked independently of prewarm data:** [CPU deswizzle copies use slice and row offsets](../../../docs/reference/nvn-objects.md#cpu-deswizzle-copies-use-slice-and-row-offsets) and [Host symbol indexes avoid repeated name scans](../../../docs/reference/services.md#host-symbol-indexes-avoid-repeated-name-scans). [PR #261](https://github.com/seedds/nxnative/pull/261) reports M2 iPad Pro gameplay on update 1.9.0; local verification covers synthetic correctness and CPU microbenchmarks, without a new game/device run. - **Final #158/#213 gameplay validation:** [Current results](#final-synchronization-and-pass-validation-2026-10-26-current) distinguish the retained pass savings from the timing gains not demonstrated on this Mac. - **Resolved: the IPA omitted pulling libraries with `pso_mtl4 on`, and explicitly including them failed under macOS Bash 3.2.** See [Pulling vertex inputs](../../../docs/reference/rendering.md#pulling-vertex-inputs). diff --git a/dm/gfx/tex/README.md b/dm/gfx/tex/README.md index 4e913d2..8b544bf 100644 --- a/dm/gfx/tex/README.md +++ b/dm/gfx/tex/README.md @@ -36,6 +36,7 @@ make -C dm/gfx/tex/tests run # run t_fmt_mtl and t_tex |---|---| | A round trip | Writes random data into each level and layer and reads it back out; must be byte-for-byte equal. Covers 2D, 2D array and 3D, several bytes-per-block values and non-power-of-two sizes | | B address formula | The result of `nvn_bl_offset()` equals the address computed by hand from the public layout rules | + | D differential copies | Both copy directions match a byte-address oracle for 1,029 deterministic layouts, including padded pitches, byte tails, sparse mip tails, packaged block dimensions, arrays and cubes; untouched storage and row padding are compared too | | E hand-made 3D | Places markers at hand-computed addresses, checks where they land after deswizzling, and checks block height, block depth and storage size | The A round trip can only prove "one-to-one and within bounds"; it cannot prove that the layout formula is right. What backs it up is the hand-computed addresses of groups B and E, and the games that render right. diff --git a/dm/gfx/tex/nvn_deswizzle.c b/dm/gfx/tex/nvn_deswizzle.c index 3c8a199..8b5aefb 100644 --- a/dm/gfx/tex/nvn_deswizzle.c +++ b/dm/gfx/tex/nvn_deswizzle.c @@ -47,15 +47,31 @@ static int move(const nvn_texdesc *d, unsigned level, unsigned layer, uint8_t *b if (pitch < row_bytes) return -1; bl += base; uint32_t lb = nvn_tex_level_gobs(d, level) * 64; // the row as laid out (padded in a sparse texture's tiled levels) if (lb < row_bytes) lb = row_bytes; - for (uint32_t z = 0; z < slices; z++) + // Hoist nvn_bl_offset's slice and row terms out of the segment loop. The block height and depth are powers of two, + // so their division / remainder become shifts and masks. At x = 16*j the block column is j >> 2, and the segment + // has one of four positions inside the GOB (x%64 = 0/16/32/48 -> 0/32/256/288); the y part depends only on y%8. + static const uint16_t kox[4] = {0, 32, 256, 288}; // x%16 is always 0 here: segments start at multiples of 16 + static const uint16_t koy[8] = {0, 16, 64, 80, 128, 144, 192, 208}; // (y%8/2)*64 + (y%2)*16 + uint32_t bh = 1u << th, bd = 1u << td; + uint64_t block = 512ull * bh * bd, gobs_x = (uint64_t)(lb + 63) >> 6, blocks_y = ((uint64_t)rows + 8 * bh - 1) >> (3 + th); + for (uint32_t z = 0; z < slices; z++) { + uint64_t zb = (uint64_t)(z >> td) * gobs_x * blocks_y * block + (uint64_t)(z & (bd - 1)) * bh * 512; for (uint32_t y = 0; y < rows; y++) { + uint64_t p = (uint64_t)(y >> (3 + th)) * gobs_x * block + (uint64_t)((y & (8 * bh - 1)) >> 3) * 512 + koy[y & 7]; uint8_t *row = lin + ((size_t)z * rows + y) * pitch; - for (uint32_t x = 0; x < row_bytes; x += 16) { // 16-byte segments: 16 bytes are contiguous within a GOB, and 16 divides all block widths - uint32_t n = row_bytes - x < 16 ? row_bytes - x : 16; - uint8_t *p = bl + nvn_bl_offset(x, y, z, lb, rows, th, td); - if (to_linear) memcpy(row + x, p, n); else memcpy(p, row + x, n); + uint64_t yb = zb + p; // the row's segments: block column (j >> 2) at yb + j>>2 times block + kox[j & 3] + uint32_t nfull = row_bytes >> 4, tail = row_bytes & 15; + // The full 16-byte segments take a constant-size copy (a memcpy of a runtime length is a call per segment); + // a row whose byte count is not divisible by 16 has a tail (e.g. R8 or RGB formats at an odd width). + if (to_linear) { + for (uint32_t j = 0; j < nfull; j++) memcpy(row + (j << 4), bl + yb + ((uint64_t)(j >> 2) * block) + kox[j & 3], 16); + if (tail) memcpy(row + (nfull << 4), bl + yb + ((uint64_t)(nfull >> 2) * block) + kox[nfull & 3], tail); + } else { + for (uint32_t j = 0; j < nfull; j++) memcpy(bl + yb + ((uint64_t)(j >> 2) * block) + kox[j & 3], row + (j << 4), 16); + if (tail) memcpy(bl + yb + ((uint64_t)(nfull >> 2) * block) + kox[nfull & 3], row + (nfull << 4), tail); } } + } return 0; } diff --git a/dm/gfx/tex/tests/t_tex.c b/dm/gfx/tex/tests/t_tex.c index 0fe9773..349ea9b 100644 --- a/dm/gfx/tex/tests/t_tex.c +++ b/dm/gfx/tex/tests/t_tex.c @@ -45,7 +45,63 @@ static void roundtrip(const char *tag, int target, int fmt, uint32_t w, uint32_t free(st); } +// Compare both copy directions against byte addresses, independently of move()'s segment arithmetic. +// Comparing all storage bytes and padded rows also checks that tails leave unrelated data untouched. +static void differential(const nvn_texdesc *d, unsigned level, unsigned layer) { + uint32_t rb, rows, slices, bpb; uint64_t base; unsigned th, td; + int rc = nvn_deswizzle_geom(d, level, layer, &rb, &rows, &slices, &base, &th, &td, &bpb); + CHECK(!rc, "D geometry failed"); if (rc) return; + nvn_texlayout L; nvn_tex_layout(d, &L); + size_t pitch = rb + 1 + (rnd() & 31), nlin = pitch * rows * slices, nst = L.size + 32; + uint8_t *storage = malloc(nst), *expected = malloc(nst), *linear = malloc(nlin + 32), *out = malloc(nlin + 32); + for (size_t i = 0; i < nst; i++) storage[i] = rnd(); + memset(linear, 0xAB, nlin + 32); memset(out, 0xAB, nlin + 32); + uint32_t lb = nvn_tex_level_gobs(d, level) * 64; + if (lb < rb) lb = rb; + for (uint32_t z = 0; z < slices; z++) for (uint32_t y = 0; y < rows; y++) for (uint32_t x = 0; x < rb; x++) + linear[((size_t)z * rows + y) * pitch + x] = storage[base + nvn_bl_offset(x, y, z, lb, rows, th, td)]; + CHECK(!nvn_deswizzle(d, level, layer, storage, out, pitch) && !memcmp(linear, out, nlin + 32), + "D read differs target %d format %#x flags %#x mip %u layer %u", d->target, d->format, d->flags, level, layer); + for (size_t i = 0; i < nlin; i++) linear[i] = rnd(); + memcpy(expected, storage, nst); + for (uint32_t z = 0; z < slices; z++) for (uint32_t y = 0; y < rows; y++) for (uint32_t x = 0; x < rb; x++) + expected[base + nvn_bl_offset(x, y, z, lb, rows, th, td)] = linear[((size_t)z * rows + y) * pitch + x]; + CHECK(!nvn_swizzle(d, level, layer, linear, pitch, storage) && !memcmp(storage, expected, nst), + "D write differs target %d format %#x flags %#x mip %u layer %u", d->target, d->format, d->flags, level, layer); + CHECK(nvn_swizzle(d, level, layer, linear, rb - 1, storage) == -1 && !memcmp(storage, expected, nst), + "D short source pitch accepted"); + CHECK(nvn_deswizzle(d, level, layer, storage, out, rb - 1) == -1, + "D short destination pitch accepted"); + free(storage); free(expected); free(linear); free(out); +} + +static void differential_shapes(void) { + static const int formats[] = {0x01, 0x0D, 0x19, 0x1D, 0x22, 0x25, 0x29, 0x2E, 0x42, 0x44, 0x82, 0x85}; + static const int targets[] = {0, 1, 2, 3, 4, 7, 8, 9}; + static const uint32_t widths[] = {1, 3, 7, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 129}; + unsigned cases = 0; + for (unsigned i = 0; i < 256; i++) { + nvn_texdesc d = {0}; + d.target = targets[i % 8]; d.format = formats[(i / 8) % 12]; + d.width = widths[rnd() % 14]; d.height = 1 + rnd() % 65; d.depth = 1 + rnd() % 9; d.levels = 4; + if (d.target == 3) d.height = 3; + if (d.target == 9) d.depth = 12; + if ((i / 8) & 1) d.pkg_block_h = 1 + (rnd() % 4) + (d.target == 2 ? (rnd() % 3) * 16 : 0); + if ((i / 16) & 1) d.flags = 0x20; + nvn_texlayout L; nvn_tex_layout(&d, &L); + for (unsigned l = 0; l < d.levels; l++) { + differential(&d, l, L.layers - 1); cases++; + } + } + // Sparse levels wide enough for padded GOB columns, followed by their mip tails. + nvn_texdesc sparse = {1, 0x7D, 0x20, 0, 1024, 1024, 1, 5, 0, 0}; + for (unsigned l = 0; l < sparse.levels; l++) { differential(&sparse, l, 0); cases++; } + printf(" D %u layouts: both directions equal byte-address oracle, including pitch padding and tails\n", cases); +} + int main(void) { + printf("D differential copy checks\n"); + differential_shapes(); printf("L linear render targets without the separate LINEAR flag\n"); { // Persona 5 Royal asks for 16x16 RGBA8, flags 0x44, stride 128: 2 KB, diff --git a/dm/host/hle.c b/dm/host/hle.c index 52c2f8b..be261ea 100644 --- a/dm/host/hle.c +++ b/dm/host/hle.c @@ -1,12 +1,10 @@ // Import resolution (implemented ones go to the host, the rest go to named stubs) and host gate dispatch. -#include "dm.h" +#include "hle.h" #include #include #include #include -typedef struct { const char *name; void *fn; } hle_ent; - // ---------- Unimplemented imports: named stubs (the statically assembled stub table in entry.S, the k-th = movz x9,#k; b unimpl_entry) ---------- #define MAX_STUB 4096 // Matches the .rept count in entry.S static unsigned g_nstub; @@ -25,31 +23,66 @@ uint64_t unimpl_c(unsigned idx, uint64_t *regs) { // Per module, one cache of symbol number → resolved address static uint64_t g_cache[MAX_MOD][8192]; +// Name hash over the swhle section, built at the first lookup. Startup resolution is single-threaded; +// runtime NRO relocation holds the loader's module write lock, so later lookups and stub insertions are serialized too. +// resolve_import used to strcmp its way through the whole section for every import no module defines, and +// hle_implements for every weak one: 270 entries (the HLE hooks and the libc / Vulkan / GL passthrough tables of +// hle_table.c / hle_vk.m / hle_gl.m) per lookup. These were the last by-name linear scans of the loading phase +// whose module symbol tables module_sym's hash already fixed (loader.c build_symhash, whose comment measured +// that phase at 77-86 ms of the ① load before it) +static uint32_t name_h(const char *s) { uint32_t h = 2166136261u; while (*s) h = (h ^ (uint8_t)*s++) * 16777619u; return h; } +static unsigned *g_hle_idx, g_hle_mask; static const hle_ent *g_hle_sec; +static const hle_ent *hle_find(const char *name) { + if (!g_hle_mask) { + unsigned long secsz; + g_hle_sec = (const hle_ent *)getsectiondata(&_mh_execute_header, "__DATA", "swhle", &secsz); + unsigned n = g_hle_sec ? (unsigned)(secsz / sizeof *g_hle_sec) : 0; + unsigned cap = 16; while (cap < n * 2) cap <<= 1; + g_hle_idx = calloc(cap, sizeof *g_hle_idx); g_hle_mask = cap - 1; + for (unsigned i = 0; i < n; i++) // the first entry of a duplicated name wins, as the linear scan did + for (unsigned j = name_h(g_hle_sec[i].name) & g_hle_mask;; j = (j + 1) & g_hle_mask) { + if (!g_hle_idx[j]) { g_hle_idx[j] = i + 1; break; } + if (!strcmp(g_hle_sec[g_hle_idx[j] - 1].name, g_hle_sec[i].name)) break; + } + } + if (!g_hle_sec) return NULL; + for (unsigned j = name_h(name) & g_hle_mask; g_hle_idx[j]; j = (j + 1) & g_hle_mask) + if (!strcmp(g_hle_sec[g_hle_idx[j] - 1].name, name)) return &g_hle_sec[g_hle_idx[j] - 1]; + return NULL; +} + uint64_t resolve_import(module_t *m, const char *name) { - static const hle_ent *sec; static unsigned long secsz; - if (!sec) sec = (const hle_ent *)getsectiondata(&_mh_execute_header, "__DATA", "swhle", &secsz); - for (unsigned i = 0; i < secsz / sizeof(hle_ent); i++) - if (!strcmp(sec[i].name, name)) { - if (g_flag.imports) { Dl_info di; dladdr(sec[i].fn, &di); LOG("import %s -> %s", name, di.dli_sname); } - void *hlecount_wrap(const char *, const char *, void *); return (uint64_t)hlecount_wrap("HLE", name, sec[i].fn); // With DM_HLECOUNT it is a counting stub (including the libc passthrough table) - } - // Reuse the stub if one was already allocated for the same name + const hle_ent *e = hle_find(name); + if (e) { + if (g_flag.imports) { Dl_info di; dladdr(e->fn, &di); LOG("import %s -> %s", name, di.dli_sname); } + void *hlecount_wrap(const char *, const char *, void *); return (uint64_t)hlecount_wrap("HLE", name, e->fn); // With DM_HLECOUNT it is a counting stub (including the libc passthrough table) + } + // Reuse the stub if one was already allocated for the same name (its own hash: the reuse scan over every stub was quadratic in the stub count) void *hlecount_wrap(const char *, const char *, void *); - for (unsigned k = 0; k < g_nstub; k++) - if (!strcmp(g_stub_name[k], name)) return (uint64_t)hlecount_wrap("STUB", name, (void *)&unimpl_stubs[k]); + static unsigned *stub_idx, stub_mask; + if (stub_idx) + for (unsigned j = name_h(name) & stub_mask; stub_idx[j]; j = (j + 1) & stub_mask) + if (!strcmp(g_stub_name[stub_idx[j] - 1], name)) return (uint64_t)hlecount_wrap("STUB", name, (void *)&unimpl_stubs[stub_idx[j] - 1]); if (g_nstub >= MAX_STUB) die("stub overflow"); unsigned k = g_nstub++; g_stub_name[k] = name; + if (!stub_idx || (unsigned)g_nstub * 2 > stub_mask + 1) { // the first stub, or past half full: (re)build at twice the size + unsigned cap = stub_idx ? (stub_mask + 1) * 2 : 512; + free(stub_idx); stub_idx = calloc(cap, sizeof *stub_idx); stub_mask = cap - 1; + for (unsigned i = 0; i < (unsigned)g_nstub; i++) + for (unsigned j = name_h(g_stub_name[i]) & stub_mask;; j = (j + 1) & stub_mask) { + if (!stub_idx[j]) { stub_idx[j] = i + 1; break; } + if (!strcmp(g_stub_name[stub_idx[j] - 1], g_stub_name[i])) break; // a duplicated name: keep the lower index + } + } else + for (unsigned j = name_h(name) & stub_mask;; j = (j + 1) & stub_mask) + if (!stub_idx[j]) { stub_idx[j] = k + 1; break; } (void)m; return (uint64_t)hlecount_wrap("STUB", name, (void *)&unimpl_stubs[k]); } // The host implements this import (the swhle table), without allocating a stub for it -int hle_implements(const char *name) { - unsigned long secsz; const hle_ent *sec = (const hle_ent *)getsectiondata(&_mh_execute_header, "__DATA", "swhle", &secsz); - for (unsigned i = 0; sec && i < secsz / sizeof(hle_ent); i++) if (!strcmp(sec[i].name, name)) return 1; - return 0; -} +int hle_implements(const char *name) { return hle_find(name) != NULL; } void stubs_report(void) { LOG("stubs=%u", g_nstub); } diff --git a/dm/host/loader.c b/dm/host/loader.c index 900a1fd..cd4af6a 100644 --- a/dm/host/loader.c +++ b/dm/host/loader.c @@ -148,20 +148,28 @@ static uint64_t resolve_sym(module_t *m, const char *name) { // Forced take-over table (hle.h HLE_FORCE, the __DATA,swforce section): a hit overrides the definition inside the module static const hle_force_ent *g_force; static unsigned g_nforce; static unsigned *g_force_hits; +static unsigned *g_fidx, g_fmask; // name hash over the table: force_lookup runs for every named relocation, and each lookup used to strcmp its way through all 722 entries (the swhle table's scan is hashed the same way, hle.c) static void force_init(void) { unsigned long sz; g_force = (const hle_force_ent *)getsectiondata(&_mh_execute_header, "__DATA", "swforce", &sz); g_nforce = g_force ? (unsigned)(sz / sizeof *g_force) : 0; g_force_hits = calloc(g_nforce + 1, sizeof *g_force_hits); - for (unsigned i = 0; i < g_nforce; i++) - for (unsigned j = i + 1; j < g_nforce; j++) - if (!strcmp(g_force[i].name, g_force[j].name)) die("duplicate entry in the forced takeover list: %s", g_force[i].name); + unsigned cap = 16; while (cap < g_nforce * 2) cap <<= 1; + g_fidx = calloc(cap, sizeof *g_fidx); g_fmask = cap - 1; + for (unsigned i = 0; i < g_nforce; i++) // insertion doubles as the duplicate check (a wrong list must still fail loudly, at once) + for (unsigned j = sym_hash(g_force[i].name) & g_fmask;; j = (j + 1) & g_fmask) { + if (!g_fidx[j]) { g_fidx[j] = i + 1; break; } + if (!strcmp(g_force[g_fidx[j] - 1].name, g_force[i].name)) die("duplicate entry in the forced takeover list: %s", g_force[i].name); + } } static void *force_lookup(const char *name) { - for (unsigned i = 0; i < g_nforce; i++) + if (!g_fmask) return NULL; + for (unsigned j = sym_hash(name) & g_fmask; g_fidx[j]; j = (j + 1) & g_fmask) { + unsigned i = g_fidx[j] - 1; if (!strcmp(g_force[i].name, name)) { if (!g_force_hits[i]++ && g_flag.imports) LOG("force %s (%s)", name, g_force[i].why); void *hlecount_wrap(const char *, const char *, void *); return hlecount_wrap("FORCE", name, g_force[i].fn); // A counting stub when DM_HLECOUNT is set } + } return NULL; } // Generic host: the list serves several games, so entries a game does not reference are only noted; with DM_FORCE_STRICT=1 it still fails loudly (for checking a mistyped list) diff --git a/dm/host/tests/Makefile b/dm/host/tests/Makefile index c3debc4..5e22265 100644 --- a/dm/host/tests/Makefile +++ b/dm/host/tests/Makefile @@ -33,7 +33,14 @@ run: $(B)/t_heap_size $(B)/t_touch $(B)/t_gesture $(B)/t_oe $(B)/t_fs $(B)/t_fs_ $(B)/t_haptics $(B)/t_haptics_apple $(B)/t_nro + $(B)/t_symbols run: $(B)/t_nro +run: $(B)/t_symbols +$(B)/t_symbols: t_symbols.c ../hle.c ../loader.c ../hle.h ../dm.h $(GAME_H) | $(B) + clang -O1 -g -Wall -Werror -Wno-unused-function -Wno-deprecated-declarations -I.. $(GINC) -o $@ t_symbols.c +.PHONY: symbols +symbols: $(B)/t_symbols + $(B)/t_symbols run: $(B)/t_system_event run: $(B)/t_audren_work $(B)/t_audren_work: t_audren_work.c ../hle_audren.c ../hle.h ../dm.h $(GAME_H) | $(B) diff --git a/dm/host/tests/t_symbols.c b/dm/host/tests/t_symbols.c new file mode 100644 index 0000000..4dfefc8 --- /dev/null +++ b/dm/host/tests/t_symbols.c @@ -0,0 +1,108 @@ +// Exercise the real HLE, forced-takeover and stub indexes with synthetic Mach-O sections; no game files required. +#include "../hle.h" +#include +#include +#include +#include +#include +#include + +static hle_ent entries[4]; +static hle_force_ent forces[3]; +static int have_sections; +static uint8_t *test_section(const struct mach_header_64 *mh, const char *seg, const char *sect, unsigned long *size) { + (void)mh; assert(!strcmp(seg, "__DATA")); + *size = 0; + if (!have_sections) return NULL; + if (!strcmp(sect, "swhle")) { *size = sizeof entries; return (uint8_t *)entries; } + assert(!strcmp(sect, "swforce")); *size = sizeof forces; return (uint8_t *)forces; +} +#define getsectiondata test_section +#include "../hle.c" +#include "../loader.c" +#undef getsectiondata + +struct dm_flags g_flag; +uint64_t unimpl_stubs[MAX_STUB]; +uint64_t now_ns(void) { return 0; } +uint64_t guest_ns(void) { return 0; } +void guest_clock_read_wait(void) {} +void hlecount_seal(void) {} +void thr_svc_enter(uint64_t *r, uint64_t a) { (void)r; (void)a; } +void thr_svc_leave(void) {} +void dm_spin_wait(uint64_t r, uint64_t a) { (void)r; (void)a; } +uint64_t svc_dispatch(int n, uint64_t *r, uint64_t a) { (void)n; (void)r; (void)a; return 0; } +static const char *wrap_kind, *wrap_name; +static unsigned wraps; +void *hlecount_wrap(const char *kind, const char *name, void *fn) { + wrap_kind = kind; wrap_name = name; wraps++; return fn; +} + +static void reset_sections(void) { + free(g_hle_idx); g_hle_idx = NULL; g_hle_mask = 0; g_hle_sec = NULL; + free(g_fidx); g_fidx = NULL; g_fmask = 0; + free(g_force_hits); g_force_hits = NULL; +} + +// Failures run in a child, so duplicate rejection and the hard stub limit are checked without killing the suite. +static void expect_abort(int duplicate) { + pid_t pid = fork(); assert(pid >= 0); + if (!pid) { + struct rlimit lim = {0, 0}; setrlimit(RLIMIT_CORE, &lim); + freopen("/dev/null", "w", stderr); + if (duplicate) { forces[2] = forces[0]; force_init(); } + else resolve_import(NULL, "one_more_missing_symbol"); + _exit(0); + } + int status; assert(waitpid(pid, &status, 0) == pid); + assert(WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT); +} + +int main(void) { + // Empty/missing sections must still give misses after lazy initialization. + assert(!hle_implements("absent")); assert(!hle_implements("absent")); + force_init(); assert(!force_lookup("absent")); reset_sections(); + + // Three different names in the same initial bucket exercise probing and a duplicate behind a collision. + char names[3][32]; unsigned found = 0; + for (unsigned i = 0; found < 3; i++) { + char candidate[32]; snprintf(candidate, sizeof candidate, "symbol_%u", i); + if ((name_h(candidate) & 15) == 15) strcpy(names[found++], candidate); + } + int values[5]; + entries[0] = (hle_ent){names[0], &values[0]}; + entries[1] = (hle_ent){names[1], &values[1]}; + entries[2] = (hle_ent){names[1], &values[2]}; + entries[3] = (hle_ent){names[2], &values[3]}; + for (unsigned i = 0; i < 3; i++) forces[i] = (hle_force_ent){names[i], &values[i], "test"}; + have_sections = 1; force_init(); + for (unsigned i = 0; i < 3; i++) { + unsigned before = wraps; + assert(hle_implements(names[i]) && wraps == before); + uint64_t want = (uint64_t)&values[i == 2 ? 3 : i]; + assert(resolve_import(NULL, names[i]) == want); + assert(!strcmp(wrap_kind, "HLE") && !strcmp(wrap_name, names[i])); + assert(force_lookup(names[i]) == &values[i]); + assert(!strcmp(wrap_kind, "FORCE") && !strcmp(wrap_name, names[i])); + assert(g_force_hits[i] == 1); + } + assert(!hle_implements("absent") && !force_lookup("absent") && !g_nstub); + assert(force_lookup(names[0]) == &values[0] && g_force_hits[0] == 2); + expect_abort(1); + + // Fill through every growth boundary, then query in reverse using different pointers to the same names. + static char missing[MAX_STUB][32]; + for (unsigned i = 0; i < MAX_STUB; i++) { + snprintf(missing[i], sizeof missing[i], "missing_%u", i); + assert(resolve_import(NULL, missing[i]) == (uint64_t)&unimpl_stubs[i]); + assert(!strcmp(wrap_kind, "STUB") && !strcmp(wrap_name, missing[i])); + } + for (unsigned i = MAX_STUB; i-- > 0;) { + char copy[32]; strcpy(copy, missing[i]); + assert(resolve_import(NULL, copy) == (uint64_t)&unimpl_stubs[i]); + } + assert(g_nstub == MAX_STUB); expect_abort(0); + reset_sections(); + puts("Symbol indexes: collisions, duplicate precedence/rejection, misses, wrappers and 4096 reusable stubs: OK"); + return 0; +} diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 8ec1ee2..ee69b89 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -156,6 +156,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - `std::terminate` (`Break` from `sdk+0x5bfa48`) at the first real-time read (right after the first-boot dialog, `main+0x3f3f68` → `system_clock::now()`). → [`clock_gettime(CLOCK_REALTIME)` through the time source pointer](../reference/services.md#clock_gettimeclock_realtime-through-the-time-source-pointer) - after switching apps and back, one guest thread at 100% in a tight loop (`main+0x9abd0`, `x10 = 0`); the game freezes, picture still, sound on. Mac repro `DM_OE_SCRIPT=bg=1@20,bg=0@25`. → [Guest clock reads wait while the app is in the background](../reference/services.md#guest-clock-reads-wait-while-the-app-is-in-the-background) - abort in `sm::Initialize` under `nn::crypto::GenerateCryptographicallyRandomBytes` (Unicorn Overlord: `std::random_device` → the `sdk`'s own `getentropy` → `InitializeCsrng`; Z-A: the first call in `nnMain`, `main+0x1734b4`). → [Cryptographically random bytes](../reference/services.md#cryptographically-random-bytes) +- relocation repeatedly scans all HLE and forced-takeover entries, and named-stub reuse becomes quadratic as more imports are missing. → [Host symbol indexes avoid repeated name scans](../reference/services.md#host-symbol-indexes-avoid-repeated-name-scans) - a library's `free` rejects a pointer (`CentralHeap::Free` → `NotNMallocPtrError`). → [Global symbol resolution in load order](../reference/services.md#global-symbol-resolution-in-load-order) - a weakly imported function the game calls although nothing defines it: an `UNIMPL` line, then an abort. → [Undefined weak imports bind to 0](../reference/services.md#undefined-weak-imports-bind-to-0) - crash in `nn::ro::InitializeWithPortName`. → [`nn::ro` and its port](../reference/services.md#nnro-and-its-port) @@ -256,6 +257,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - frames stop, no crash, the main thread sits in `nvnSyncWait(sync, -1)`, and the watchdog's wait-state line shows the main fence one short (`main fence submitted N / completed N-1`). → [A GPU command buffer that never completes](../reference/nvn-objects.md#a-gpu-command-buffer-that-never-completes) - the frame's blocking SyncWait on its newest fence wakes about 0.26–0.29 ms after the GPU end (M2 Max; the wait ledger, `DM_NVN_WAITS=1`), on the critical path of every frame. → [The game's SyncWait wakes on the GPU's own signal of its fence](../reference/nvn-objects.md#the-games-syncwait-wakes-on-the-gpus-own-signal-of-its-fence) - loading screens many times slower on the iPhone than on the Mac (Three Houses' first loading screen 78 s on the iPhone, 5 s on the Mac). → [`nvnQueueFinish` waits for the GPU, not the display](../reference/nvn-objects.md#nvnqueuefinish-waits-for-the-gpu-not-the-display) +- CPU texture uploads spend time recalculating block-linear addresses and calling variable-length `memcpy` for every 16-byte segment. → [CPU deswizzle copies use slice and row offsets](../reference/nvn-objects.md#cpu-deswizzle-copies-use-slice-and-row-offsets) - driver comparisons report the wrong block dimensions for packaged 2D / 3D textures, or too little storage for linear block-compressed textures (64x64 BC1 with stride 128: 2048 bytes instead of 8192). → [Packaged texture block dimensions and linear storage rows](../reference/nvn-objects.md#packaged-texture-block-dimensions-and-linear-storage-rows) - menus and dialogue boxes draw but contain no text, while images and pictogram digits are fine; the sky is black (CPU-written linear colour ramps). → [Linear textures in CPU-visible pools](../reference/nvn-objects.md#linear-textures-in-cpu-visible-pools) - a trap during graphics initialization in `SetupSurfaceInfoMemoryInside`, asserting `ctx.memorySize <= localmemsetupinfo.memorySize`. → [Linear render-target storage without the LINEAR flag](../reference/nvn-objects.md#linear-render-target-storage-without-the-linear-flag) diff --git a/docs/reference/nvn-objects.md b/docs/reference/nvn-objects.md index e080a9a..d8a2b69 100644 --- a/docs/reference/nvn-objects.md +++ b/docs/reference/nvn-objects.md @@ -182,6 +182,14 @@ Games read this count as a position in their own command memory and size their h ## CPU-written textures +### CPU deswizzle copies use slice and row offsets + +- **Symptom:** CPU texture uploads spend time recalculating block-linear addresses and calling variable-length `memcpy` for every 16-byte segment. +- **Cause:** `move` called `nvn_bl_offset` inside its innermost loop, repeating slice/row calculations and division by runtime block dimensions. +- **Fix:** hoist slice and row offsets, express power-of-two block dimensions as shifts/masks, and use the four fixed x offsets and eight fixed y offsets within a GOB. Full segments use constant-size 16-byte copies; byte counts not divisible by 16 retain a tail copy. ASTC blocks are always 16 bytes irrespective of their texel dimensions. Both copy directions preserve the layout, sparse row padding and mip/layer offsets. +- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md#found-on-this-game), reported in [PR #261](https://github.com/seedds/nxnative/pull/261). +- **Check:** `t_tex` compares both directions against the byte-address formula for 1,029 deterministic layouts, including 1D/2D/3D, arrays/cubes, packaged block dimensions, sparse mip tails, 1/2/3/4/6/8/12/16-byte formats, padded pitches and partial segments. All 5,334 checks pass, including under AddressSanitizer and UBSan. A local arm64 Mac comparison against the old implementation, Apple clang 21 at the host's `-O1`, measured 7.4–7.7 GB/s before and 35.1–37.6 GB/s after (4.6–5.1x) across 1024x1024 sparse ASTC 6x6, 2048x2048 BC7, 1920x1080 RGBA8 and 1280x720 RGBA16F. Each result is the median of seven runs copying about 128 MiB, with implementation order alternated. This is a CPU microbenchmark; no new gameplay run here. + ### Packaged texture block dimensions and linear storage rows - **Symptom:** driver comparisons report the wrong block dimensions for packaged 2D / 3D textures, or too little storage for linear block-compressed textures (64x64 BC1 with stride 128: 2048 bytes instead of 8192). diff --git a/docs/reference/services.md b/docs/reference/services.md index 536d1e8..3e20681 100644 --- a/docs/reference/services.md +++ b/docs/reference/services.md @@ -549,6 +549,14 @@ Most service calls can be found before they crash, before the first boot or the ## Modules and symbols +### Host symbol indexes avoid repeated name scans + +- **Symptom:** relocation repeatedly scans all HLE and forced-takeover entries, and named-stub reuse becomes quadratic as more imports are missing. +- **Cause:** module exports already had a name index, but `resolve_import`, `hle_implements`, `force_lookup` and stub reuse still searched their tables linearly. +- **Fix:** open-addressed name indexes store entry numbers. HLE duplicates retain the first entry; forced-takeover duplicates still abort. Stub insertion grows its index above half capacity and retains existing stub numbers. Resolution is serialized at startup and by the module write lock during runtime NRO loading; module string tables stay mapped across unload. +- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md#found-on-this-game), reported in [PR #261](https://github.com/seedds/nxnative/pull/261). +- **Check:** `make -C dm/host/tests GAME=010015100B514000 symbols nro` covers collisions that wrap around the table, duplicate precedence/rejection, absent sections and names, counting-wrapper selection, all 4,096 stubs through growth and reuse at capacity, and NRO relocation/lifecycle. The symbol test also passes AddressSanitizer and UBSan. A local arm64 Mac synthetic 270-entry HLE table with 25% misses, Apple clang 21 at `-O1`, took 375–442 ns per linear lookup versus 15 ns indexed (25–29x; three runs of 360,000 lookups). This measures lookup cost, not overall game loading time; no new gameplay run here. + ### Global symbol resolution in load order - **Symptom:** a library's `free` rejects a pointer (`CentralHeap::Free` → `NotNMallocPtrError`).