From cae09a2bcd67fe8239337b3c024dcbfe94fd6347 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 07:08:09 -0500 Subject: [PATCH] Add floor_watch: hardware watchpoints on the floor the scan finds log which game code reads and writes it, at 60 fps and again at 120 fps or more. floor_watch.c takes up to twelve of the floor scan's survivors (the platform's rigid body controller unit first, then one per object at the pivot's height), watches them four at a time through the debug registers on every thread (platform.h plat_watch_set, Windows only), counts each access's call chain in a lock-free table and logs the chains with the executable's symbol names after each pass. prof_syms gains a callback form for that. Co-Authored-By: Claude Opus 5.5 --- nx2navi.ini.example | 4 + runtime/cfg.c | 6 +- runtime/floor_watch.c | 297 ++++++++++++++++++++++++++++++++++++ runtime/floor_watch.h | 13 ++ runtime/frame_rate.c | 22 ++- runtime/platform/linux.c | 6 + runtime/platform/platform.h | 10 ++ runtime/platform/win32.c | 115 ++++++++++++++ runtime/prof_syms.c | 13 +- runtime/prof_syms.h | 4 + tests/frame_step_check.c | 10 ++ 11 files changed, 492 insertions(+), 8 deletions(-) create mode 100644 runtime/floor_watch.c create mode 100644 runtime/floor_watch.h diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 723c251e..c1e86f92 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -971,6 +971,10 @@ # then on. The snapshot holds the game for about a second once. 1 writes 20000 lines, a larger number that many; 0 is # off. # floor_scan = 0 +# Diagnosis (Windows): 1 also runs floor_scan and, once it has found the floor, logs which code of the game reads and +# writes it: hardware watchpoints on its addresses for a few seconds at 60 fps, and again at the next stretch at 120 +# fps or more with Link on the floor. Each pass slows the game for its few seconds. 0 is off. +# floor_watch = 0 # 1: with trace = perf, walks of the process's address space count its private and write-combined memory every 600 # frames (the "mem: process" line) and where its committed memory lies every 3600 frames (the "commit census" lines). # Each walk holds the address-space lock for 100-240 ms and delays the next frame by as much, so they are off unless diff --git a/runtime/cfg.c b/runtime/cfg.c index ac73e1f2..962c08e3 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -33,7 +33,7 @@ static int save_ok = 1; // the last cfg_save_to wrote (o // Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is // logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line. static const char *const known[] = { - "CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_BUDGET", "ONESHOT_WAIT_MS", "OCARINA_STEP", "PHYSICS_STEPS", "PHYSICS_STEPS_FULL", "PICKUP_COUNT", "WATER_STEPS", "SOUND_STEPS", "FLOOR_PROBE", "FLOOR_SCAN", "FLOOR_CARRY", "MOVIE_PTS", "STEP_STEADY", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c) + "CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_BUDGET", "ONESHOT_WAIT_MS", "OCARINA_STEP", "PHYSICS_STEPS", "PHYSICS_STEPS_FULL", "PICKUP_COUNT", "WATER_STEPS", "SOUND_STEPS", "FLOOR_PROBE", "FLOOR_SCAN", "FLOOR_WATCH", "FLOOR_CARRY", "MOVIE_PTS", "STEP_STEADY", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c) "AA", "AA_LOG", "AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CATCHUP", "AUDIO_CHANNELS", "AUDIO_DEVICE", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_RAMP", "AUDIO_STATS", "BARRIER_GAME", "MESH_AHEAD", "MESH_FALLBACK", "MESH_FALLBACK_MB", "MESH_FALLBACK_WORKERS", "PROBE_MESHFB_DIR", "BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT", @@ -100,7 +100,7 @@ static const struct { const char *key, *section; } sections[] = { {"VRAM_HEAP_LIMIT", "performance"}, {"VRAM_RESERVE_MB", "performance"}, {"UPLOAD_BATCH_MB", "performance"}, {"MESH_EMULATION", "performance"}, {"MESH_EMU_MB", "performance"}, {"MESH_EMU_TASKS", "performance"}, {"MESH_EMU_BATCH", "performance"}, {"VEC_INDEX_FIX", "performance"}, {"GUEST_OPT", "performance"}, {"PASS_TIMES", "diagnostics"}, {"DUMP_KEY", "diagnostics"}, - {"FLOOR_PROBE", "diagnostics"}, {"FLOOR_SCAN", "diagnostics"}, + {"FLOOR_PROBE", "diagnostics"}, {"FLOOR_SCAN", "diagnostics"}, {"FLOOR_WATCH", "diagnostics"}, {"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"RTSS", "frame rate"}, {"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"}, {"PHYSICS_STEPS", "frame rate"}, {"PICKUP_COUNT", "frame rate"}, {"WATER_STEPS", "frame rate"}, @@ -250,6 +250,8 @@ static const CfgMeta metas[] = { {"FLOOR_PROBE", "0", NULL, RESTART}, // frame_rate.c floor_scan: finds the moving floor under Link in guest memory (0 off, 1 = 20000 lines, or a count) {"FLOOR_SCAN", "0", NULL, RESTART}, + // floor_watch.c: hardware watchpoints on the floor the scan finds, at 60 fps and at 120 or more (0 off, 1 on) + {"FLOOR_WATCH", "0", "0|1", RESTART}, {"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR|pl", RESTART}, {"OPERATION_MODE", "docked", "docked|handheld", LIVE}, {"TEXT_SPEED", "normal", "normal|fast|instant", LIVE}, // text_speed.c: the message window's typing diff --git a/runtime/floor_watch.c b/runtime/floor_watch.c new file mode 100644 index 00000000..e85ee392 --- /dev/null +++ b/runtime/floor_watch.c @@ -0,0 +1,297 @@ +// NX2NAVI_FLOOR_WATCH (floor_watch in nx2navi.ini, 0 by default; diagnosis, 1.1.1, Windows): which code reads and +// writes the moving floor under Link, at 60 fps and above it. It takes its addresses from frame_rate.c's floor scan +// (which it switches on), once the scan has the values below Link that move with him at 60 fps: +// - the candidates: up to two survivors inside the platform's rigid body controller unit (the object with vtable +// main+0x4bf8a88, where the platform's own position is kept), then one survivor of each distinct object (more than +// 0x200 bytes from the ones taken) at the height most survivors share below Link (the platform's pivot, 2 m under +// him on the Deku Tree's floating platform), FW_CAND in all; +// - a pass watches them four at a time (the debug registers, platform.h plat_watch_set) for FW_ROUND_NS each, every +// read or write of the 8 bytes at a survivor (its x and y; 4 bytes, x alone, when it is not 8-byte aligned). Each +// access counts its call chain (the instruction after it and three return addresses) in a lock-free table, with the +// candidate and the kind: a write when the value changed since the candidate's last access, else a read; +// - the first pass runs as soon as the candidates are known (the scan arms at 60 fps); the second at the next stretch +// of 120 presents at 120 fps or more (frames per update at most 0.5) with Link on the floor (his height over the +// pivot within 0.2 m of what it was at the first pass, at most 4 m from it sideways). After each pass the log gets +// the pass's frame rate, each candidate's reads and writes a present, and its FW_SHOW most counted chains, each +// frame named by the executable's symbol table (lifted functions are f_, shown with their main +// offset), so the code that reads the floor at 60 fps and not at 120 can be compared. +// A round ends early after FW_ROUND_HITS accesses; after that each thread that traps again has its watchpoints cleared. +// Each access costs an exception (several microseconds), so a pass slows the game for its few seconds; symbol lookup +// after a pass holds one present for up to a second. Off it costs one test a present in frame_rate.c. +#include +#include +#include +#include +#include +#include + +#include "cfg.h" +#include "floor_watch.h" +#include "host.h" +#include "layout.h" +#include "platform/platform.h" +#include "prof_syms.h" + +#define FW_CAND 12 // candidate addresses, four a round +#define FW_ROUND_NS 1500000000ull +#define FW_ROUND_HITS 200000 +#define FW_SLOTS 8192 // call chains, open addressing; a full table drops new ones +#define FW_SHOW 40 // chains logged a pass +#define FW_PASSES 2 +#define FW_HIGH_FRAMES 0.5f // the second pass: at most this many 60 Hz frames per update (120 fps) +#define FW_HIGH_STRETCH 120 + +typedef struct { + _Atomic uint64_t key; // 0: free + _Atomic uint64_t n; + uint64_t frame[PLAT_WATCH_DEPTH]; + uint8_t cand, kind, depth; +} FwChain; + +static FwChain tab[FW_SLOTS]; +static _Atomic uint64_t fw_hits, fw_dropped, fw_reads[FW_CAND], fw_writes[FW_CAND]; +static _Atomic uint64_t last_val[4]; +static int slot_cand[4], slot_len[4]; +static uint64_t cand[FW_CAND]; +static int cand_len[FW_CAND], cand_unit[FW_CAND], ncand, floor_ref = -1; +static float cand_y[FW_CAND], dy0; +static uint64_t round_presents[(FW_CAND + 3) / 4]; +static struct { + int state; // 0 no candidates, 1 waiting for a pass's frame rate, 2 a pass runs, 3 done + int pass, round, threads; + uint64_t pass_t0, round_t0, presents, on_floor, stretch, hits, rounds_armed; + double frames_sum; +} w; + +int floor_watch_wanted(void) { + static int wanted = -1; + if (wanted < 0) { + const char *e = cfg_get("NX2NAVI_FLOOR_WATCH"); + wanted = RT_V111 && e && *e && strcmp(e, "0") != 0; + if (wanted) rt_log("floor watch: on; it waits for the floor scan's addresses"); + } + return wanted; +} + +// On the thread that made the access, inside its exception handling: no lock, no allocation, no log. +static int fw_hit(int slot, const uint64_t *frames, int depth) { + if (atomic_fetch_add_explicit(&fw_hits, 1, memory_order_relaxed) >= FW_ROUND_HITS) return 1; + int ci = slot_cand[slot]; + // the value now, read through the system (a read from here would trap again): a write changed it + uint64_t v = 0; + plat_mem_read(cand[ci], &v, (size_t)slot_len[slot]); + int kind = atomic_exchange_explicit(&last_val[slot], v, memory_order_relaxed) != v; + atomic_fetch_add_explicit(kind ? &fw_writes[ci] : &fw_reads[ci], 1, memory_order_relaxed); + uint64_t key = 0xcbf29ce484222325ull ^ (uint64_t)(ci * 2 + kind); + for (int d = 0; d < depth; d++) key = (key ^ frames[d]) * 0x100000001b3ull; + key = ((key ^ (uint64_t)depth) * 0x100000001b3ull) | 1; + uint32_t i = (uint32_t)(key >> 40) & (FW_SLOTS - 1); + for (int k = 0; k < 64; k++, i = (i + 1) & (FW_SLOTS - 1)) { + uint64_t have = atomic_load_explicit(&tab[i].key, memory_order_acquire); + if (!have) { + if (atomic_compare_exchange_strong_explicit(&tab[i].key, &have, key, memory_order_acq_rel, + memory_order_acquire)) { + memcpy(tab[i].frame, frames, sizeof(uint64_t) * (size_t)depth); + tab[i].depth = (uint8_t)depth, tab[i].cand = (uint8_t)ci, tab[i].kind = (uint8_t)kind; + atomic_fetch_add_explicit(&tab[i].n, 1, memory_order_relaxed); + return 0; + } + } + if (have == key) { + atomic_fetch_add_explicit(&tab[i].n, 1, memory_order_relaxed); + return 0; + } + } + atomic_fetch_add_explicit(&fw_dropped, 1, memory_order_relaxed); + return 0; +} + +int floor_watch_found(const uint64_t *addr, const float *val, const int *unit, int n, const float link[3]) { + if (!floor_watch_wanted() || w.state) return 0; + ncand = 0; + for (int k = 0; k < n && ncand < 2; k++) // the controller unit's own triplets + if (unit[k]) cand_y[ncand] = val[3 * k + 1], cand_unit[ncand] = 1, cand[ncand++] = addr[k]; + // the height most survivors share below Link: the pivot that the floor's copies (body, matrices) hold + int best = -1, best_n = 0; + for (int k = 0; k < n; k++) { + if (!(val[3 * k + 1] < link[1] - 0.3f)) continue; + int m = 0; + for (int j = 0; j < n; j++) m += fabsf(val[3 * j + 1] - val[3 * k + 1]) < 0.02f; + if (m > best_n) best = k, best_n = m; + } + for (int k = 0; best >= 0 && k < n && ncand < FW_CAND; k++) { + if (fabsf(val[3 * k + 1] - val[3 * best + 1]) >= 0.02f) continue; + int near = 0; + for (int j = 0; j < ncand; j++) near |= (addr[k] > cand[j] ? addr[k] - cand[j] : cand[j] - addr[k]) <= 0x200; + if (near) continue; + if (floor_ref < 0) floor_ref = ncand; + cand_y[ncand] = val[3 * k + 1], cand_unit[ncand] = 0, cand[ncand++] = addr[k]; + } + for (int j = 0; j < ncand; j++) cand_len[j] = cand[j] % 8 ? 4 : 8; + if (!ncand || floor_ref < 0) { + rt_log("floor watch: the scan's %d survivors hold no height below Link to watch; off", n); + w.state = 3; + return 0; + } + dy0 = link[1] - cand_y[floor_ref]; + rt_log("floor watch: %d addresses to watch, four at a time; Link %.3f m above the floor's pivot at %#llx", ncand, + dy0, (unsigned long long)cand[floor_ref]); + for (int j = 0; j < ncand; j++) + rt_log("floor watch: address %d: %#llx (%d bytes), y %.3f%s", j, (unsigned long long)cand[j], cand_len[j], + cand_y[j], cand_unit[j] ? ", in the rigid body controller unit (vtable main+0x4bf8a88)" : ""); + w.state = 1; + return 1; +} + +// Link on the floor: his height over the floor's pivot as at the first pass, and not far from it sideways +static int on_floor(const float link[3]) { + float p[3]; + if (!plat_mem_read(cand[floor_ref], p, sizeof p) || !isfinite(p[0]) || !isfinite(p[1]) || !isfinite(p[2])) return 0; + return fabsf(link[1] - p[1] - dy0) < 0.2f && hypotf(link[0] - p[0], link[2] - p[2]) < 4.0f; +} + +static int round_arm(void) { + uint64_t a[4]; + int len[4], n = 0; + for (int k = 0; k < 4; k++) { + int ci = w.round * 4 + k; + uint64_t v = 0; + if (ci >= ncand || !plat_mem_read(cand[ci], &v, (size_t)cand_len[ci])) continue; // gone with its area + slot_cand[n] = ci, slot_len[n] = cand_len[ci]; + atomic_store_explicit(&last_val[n], v, memory_order_relaxed); + a[n] = cand[ci], len[n] = cand_len[ci], n++; + } + atomic_store_explicit(&fw_hits, 0, memory_order_relaxed); + if (!n) return 0; + int t = plat_watch_set(a, len, n, fw_hit); + if (t > w.threads) w.threads = t; + if (t >= 0) w.rounds_armed++; + return t; +} + +// ---- the report of a pass +typedef struct { uint64_t addr; char name[48]; uint32_t off; } FwName; +static FwName *names; +static int nnames; + +static void name_one(void *ctx, uint64_t addr, const char *name, uint32_t off) { + (void)ctx; + if (!names || nnames >= FW_SHOW * PLAT_WATCH_DEPTH) return; + FwName *f = &names[nnames++]; + f->addr = addr, f->off = off; + snprintf(f->name, sizeof f->name, "%s", name); +} + +static int frame_text(char *out, size_t cap, uint64_t a) { + for (int i = 0; i < nnames; i++) { + if (names[i].addr != a) continue; + // a lifted function is named after its guest address: f_7002c215e0 is main+0x2c215e0 + const char *nm = names[i].name; + uint64_t guest = !strncmp(nm, "f_", 2) ? strtoull(nm + 2, NULL, 16) : 0; + if (guest >= MOD_MAIN_BASE && guest < MOD_MAIN_BASE + MOD_MAIN_SIZE) + return snprintf(out, cap, "%s+0x%x (main+%#llx)", nm, names[i].off, + (unsigned long long)(guest - MOD_MAIN_BASE)); + return snprintf(out, cap, "%s+0x%x", nm, names[i].off); + } + char mod[64]; + uintptr_t base = 0; + if (plat_module_of((uintptr_t)a, mod, sizeof mod, &base)) + return snprintf(out, cap, "%s+%#llx", mod, (unsigned long long)(a - base)); + return snprintf(out, cap, "%#llx", (unsigned long long)a); +} + +static int by_count(const void *x, const void *y) { + uint64_t a = atomic_load_explicit(&(*(FwChain *const *)x)->n, memory_order_relaxed), + b = atomic_load_explicit(&(*(FwChain *const *)y)->n, memory_order_relaxed); + return a < b ? 1 : a > b ? -1 : 0; +} + +static void report(uint64_t t) { + double secs = (t - w.pass_t0) / 1e9, frames = w.presents ? w.frames_sum / w.presents : 0; + rt_log("floor watch: pass %d at %.3f frames per update on average (%.0f fps): %.2f s in %llu rounds on up to %d " + "threads, %llu presents (%llu with Link on the floor), %llu accesses, %llu chains not kept", + w.pass + 1, frames, frames > 0 ? 60.0 / frames : 0.0, secs, (unsigned long long)w.rounds_armed, w.threads, + (unsigned long long)w.presents, (unsigned long long)w.on_floor, (unsigned long long)w.hits, + (unsigned long long)atomic_load(&fw_dropped)); + for (int j = 0; j < ncand; j++) { + uint64_t r = atomic_load(&fw_reads[j]), wr = atomic_load(&fw_writes[j]), p = round_presents[j / 4]; + rt_log("floor watch: address %d %#llx: %llu reads, %llu writes in %llu presents (%.2f and %.2f a present)", j, + (unsigned long long)cand[j], (unsigned long long)r, (unsigned long long)wr, (unsigned long long)p, + p ? (double)r / p : 0.0, p ? (double)wr / p : 0.0); + } + static FwChain *list[FW_SLOTS]; + int n = 0; + for (int i = 0; i < FW_SLOTS; i++) + if (atomic_load(&tab[i].key) && atomic_load(&tab[i].n)) list[n++] = &tab[i]; + qsort(list, (size_t)n, sizeof *list, by_count); + if (n > FW_SHOW) n = FW_SHOW; + // the frames' names from the executable's own symbol table + uint64_t addrs[FW_SHOW * PLAT_WATCH_DEPTH]; + size_t na = 0; + for (int i = 0; i < n; i++) + for (int d = 0; d < list[i]->depth; d++) addrs[na++] = list[i]->frame[d]; + char exe[1024]; + if (!names) names = malloc(sizeof *names * FW_SHOW * PLAT_WATCH_DEPTH); + nnames = 0; + if (names && na && plat_exe_path(exe, sizeof exe) && + prof_syms_each(exe, (uint64_t)plat_exe_base(), addrs, na, name_one, NULL) < 0) + rt_log("floor watch: the executable has no symbol table: frames stay unnamed"); + for (int i = 0; i < n; i++) { + const FwChain *c = list[i]; + uint64_t cnt = atomic_load(&c->n), p = round_presents[c->cand / 4]; + char line[1024]; + int o = snprintf(line, sizeof line, "floor watch: %llu (%.2f a present) %s of address %d:", + (unsigned long long)cnt, p ? (double)cnt / p : 0.0, c->kind ? "writes" : "reads", c->cand); + for (int d = 0; d < c->depth && o > 0 && o < (int)sizeof line - 2; d++) { + o += snprintf(line + o, sizeof line - o, "%s", d ? " < " : " "); + if (o < (int)sizeof line) o += frame_text(line + o, sizeof line - o, c->frame[d]); + } + rt_log("%s", line); + } +} + +static void pass_start(uint64_t t) { + memset(tab, 0, sizeof tab); + for (int j = 0; j < FW_CAND; j++) atomic_store(&fw_reads[j], 0), atomic_store(&fw_writes[j], 0); + memset(round_presents, 0, sizeof round_presents); + atomic_store(&fw_dropped, 0); + w.round = 0, w.presents = w.on_floor = w.hits = w.rounds_armed = 0, w.frames_sum = 0, w.threads = 0; + w.pass_t0 = w.round_t0 = t; + if (round_arm() < 0) { + rt_log("floor watch: no hardware watchpoints here (or they could not be set); off"); + w.state = 3; + return; + } + rt_log("floor watch: pass %d starts", w.pass + 1); + w.state = 2; +} + +int floor_watch_present(const float link[3], int link_ok, float frames) { + if (w.state == 0 || w.state == 3) return w.state == 0; + uint64_t t = plat_now_ns(); + if (w.state == 1) { // the pass's frame rate, with Link on the floor + int band = w.pass == 0 ? fabsf(frames - 1.0f) < 0.05f : frames <= FW_HIGH_FRAMES; + w.stretch = band && link_ok && on_floor(link) ? w.stretch + 1 : 0; + if (w.stretch >= (w.pass == 0 ? 1u : FW_HIGH_STRETCH)) pass_start(t); + return w.state != 3; + } + w.presents++, round_presents[w.round]++; + w.frames_sum += frames; + if (link_ok && on_floor(link)) w.on_floor++; + uint64_t hits = atomic_load_explicit(&fw_hits, memory_order_relaxed); + if (t - w.round_t0 < FW_ROUND_NS && hits < FW_ROUND_HITS) return 1; + plat_watch_set(NULL, NULL, 0, NULL); + w.hits += hits < FW_ROUND_HITS ? hits : FW_ROUND_HITS; + if (++w.round * 4 < ncand) { + w.round_t0 = t; + round_arm(); + return 1; + } + report(t); + w.stretch = 0; + w.state = ++w.pass < FW_PASSES ? 1 : 3; + if (w.state == 1) rt_log("floor watch: pass 2 waits for %d presents at 120 fps or more with Link on the floor", + FW_HIGH_STRETCH); + else rt_log("floor watch: done"); + return w.state != 3; +} diff --git a/runtime/floor_watch.h b/runtime/floor_watch.h new file mode 100644 index 00000000..4694a292 --- /dev/null +++ b/runtime/floor_watch.h @@ -0,0 +1,13 @@ +#pragma once +#include + +// NX2NAVI_FLOOR_WATCH (floor_watch.c): which code reads and writes the floor under Link, from hardware watchpoints on +// the addresses frame_rate.c's floor scan found. Diagnosis, 1.1.1, Windows; off by default. +int floor_watch_wanted(void); // the setting, read once: 1 on +// The scan's survivors once it has them (frame_rate.c floor_scan): n addresses of float triplets, their values at +// that present, whether each lies in the platform's rigid body controller unit, and Link's position. 1 when the watch +// runs from now on. +int floor_watch_found(const uint64_t *addr, const float *val, const int *unit, int n, const float link[3]); +// Each present from then on (frame_rate.c frame_step_present), with the game's frames per update. 0 once the watch is +// done. +int floor_watch_present(const float link[3], int link_ok, float frames); diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index c0561407..912f2bd5 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -49,6 +49,7 @@ #include "debug_tools.h" #include "cfg.h" +#include "floor_watch.h" #include "gfx.h" #include "host.h" #include "input.h" @@ -1025,11 +1026,14 @@ static void floor_probe(void) { // keeps none arms the scan again, three times at most. From then on, once a second, it logs Link's position and move // over that second and one line per survivor (FLOOR_SCAN_SHOW at most) with its address, position and move over the // same second, so a stretch at a higher frame rate shows each one's own speed. The value is the number of lines to -// write (1 means 20000); off it costs one test per present. +// write (1 means 20000); off it costs one test per present. NX2NAVI_FLOOR_WATCH switches the scan on too and gets its +// survivors when the logging starts, with those that lie in the platform's rigid body controller unit marked +// (floor_watch.c). #define FLOOR_SCAN_ARM 30 #define FLOOR_SCAN_MAX 2000000 // candidate triplets a snapshot keeps #define FLOOR_SCAN_SHOW 64 // survivors logged each second static int floor_scan_left = -1; // lines still to write; -1 before the setting was read +static int floor_watching; // floor_watch.c has the survivors and is not done (NX2NAVI_FLOOR_WATCH) static struct { int state, run, tries; // state 0: waiting for a drift, 1: filtering, 2: logging float link0[3], last[3], start[3], path, sec_link[3]; @@ -1201,6 +1205,7 @@ static void floor_scan(const float link[3], int link_ok) { if (floor_scan_left < 0) { const char *e = cfg_get("NX2NAVI_FLOOR_SCAN"); int n = e && *e ? atoi(e) : 0; + if (n <= 0 && floor_watch_wanted()) n = 1; // the watch takes its addresses from the scan floor_scan_left = !RT_V111 || n <= 0 ? 0 : n == 1 ? 20000 : n; if (floor_scan_left) rt_log("floor scan: on for %d lines", floor_scan_left); } @@ -1267,6 +1272,14 @@ static void floor_scan(const float link[3], int link_ok) { for (int k = 1; k < fs.n; k++) if (fs.val[3 * k + 1] < fs.val[3 * deep + 1]) deep = k; floor_scan_dump(fs.addr[deep]); + if (floor_watch_wanted()) { // the survivors in the platform's rigid body controller unit (floor_watch.c) + int unit[FLOOR_SCAN_SHOW]; + for (int k = 0; k < fs.n; k++) { + uint64_t at = 0; + unit[k] = vtable_below(fs.addr[k], &at) == MOD_MAIN_BASE + 0x4bf8a88; + } + floor_watching = floor_watch_found(fs.addr, fs.val, unit, fs.n, link); + } memcpy(fs.sec_link, link, 12); fs.sec_ns = plat_now_ns(); fs.state = 2; @@ -1326,10 +1339,11 @@ void frame_step_present(uint64_t display_ns, int interval) { memory_order_relaxed); world_clock(dt, step); floor_probe(); - if (floor_scan_left) { // after its first read the scan costs this test alone while off + if (floor_scan_left || floor_watching) { // after its first read the scan costs this test alone while off float link[3]; - int ok = floor_scan_left > 0 ? player_pos(link) : 0; - floor_scan(link, ok); + int ok = floor_scan_left > 0 || floor_watching ? player_pos(link) : 0; + if (floor_scan_left) floor_scan(link, ok); + if (floor_watching) floor_watching = floor_watch_present(link, ok, frame_step_frames()); } if (rt_trace & RT_TRACE_PERF) { time_dump(); diff --git a/runtime/platform/linux.c b/runtime/platform/linux.c index cbee34ff..f8d4ef7a 100644 --- a/runtime/platform/linux.c +++ b/runtime/platform/linux.c @@ -332,6 +332,12 @@ int plat_intr_resume(PlatIntr *t, uint64_t to) { return 1; } +// Hardware data watchpoints are Windows' only (platform.h plat_watch_set): the diagnosis that uses them is off here. +int plat_watch_set(const uint64_t *addr, const int *len, int n, PlatWatchHit hit) { + (void)addr, (void)len, (void)n, (void)hit; + return -1; +} + // ---- fibers: a stack switch of our own. glibc's swapcontext saves and restores the signal mask with a system call // on every switch, and the game switches fibers often. fiber_swap saves the callee-saved registers, the MXCSR and the // x87 control word on the current stack, stores the stack pointer in *save and loads the other one. A new fiber's diff --git a/runtime/platform/platform.h b/runtime/platform/platform.h index ff2a170c..f63f733a 100644 --- a/runtime/platform/platform.h +++ b/runtime/platform/platform.h @@ -75,6 +75,16 @@ int plat_intr_stop(PlatIntr *t, uint64_t *rip, uint64_t *rsp); // on where it stopped). int plat_intr_resume(PlatIntr *t, uint64_t to); +// Hardware data watchpoints (diagnosis: floor_watch.c). Up to four addresses, each watched for any read or write of its +// len bytes (1, 2, 4 or 8, the address aligned to it), on every thread the process has when they are set; threads +// started later have none. After each access hit(slot, frames, depth) runs on the thread that made it, inside its +// exception handling: frames[0] is the instruction after the access, then up to PLAT_WATCH_DEPTH - 1 return addresses. +// hit must not lock, allocate or log, and its nonzero return clears that thread's watchpoints. n = 0 clears them on +// every thread. Returns the threads changed; -1 where the platform has none (Linux) or the change failed. +#define PLAT_WATCH_DEPTH 4 +typedef int (*PlatWatchHit)(int slot, const uint64_t *frames, int depth); +int plat_watch_set(const uint64_t *addr, const int *len, int n, PlatWatchHit hit); + // Fibers: execution contexts with stacks of their own that threads switch between; a fiber may resume on another // thread. Each switch keeps the callee-saved registers and the floating-point control state. typedef struct PlatFiber PlatFiber; diff --git a/runtime/platform/win32.c b/runtime/platform/win32.c index 52b45945..a898f6a3 100644 --- a/runtime/platform/win32.c +++ b/runtime/platform/win32.c @@ -15,6 +15,7 @@ #include #include #include +#include // CreateToolhelp32Snapshot (plat_watch_set) #include #include #include @@ -200,6 +201,120 @@ int plat_intr_resume(PlatIntr *t, uint64_t to) { return ok; } +// ---- hardware data watchpoints (platform.h plat_watch_set): the debug registers DR0-DR3 hold the addresses and DR7 +// enables them, each for reads and writes (R/W 11) of its length. A thread's debug registers are part of its context: +// a helper thread lists the process's threads, then stops each, sets them with SetThreadContext and lets it go on; it +// opens every handle before it stops anything and takes no lock while a thread is stopped, so a thread stopped while +// holding the heap or loader lock cannot block it. The caller waits for the helper, which also sets the caller's own +// registers. An access traps after the instruction that made it, as EXCEPTION_SINGLE_STEP with the slot's bit in DR6; +// watch_veh hands it on and writes the current set into the context it continues with. So a thread that traps after +// its registers were cleared, or whose continue would restore registers from before a change, ends up with the +// current set too. +static PlatWatchHit watch_fn; +static volatile LONG watch_on; +static DWORD64 watch_addr[4], watch_dr7; +static int watch_added; + +// frames[0] the instruction after the access, then the return addresses the unwind tables give +static int watch_unwind(const CONTEXT *at, uint64_t *frames) { + CONTEXT c = *at; + int d = 0; + frames[d++] = c.Rip; + while (d < PLAT_WATCH_DEPTH) { + DWORD64 base; + PRUNTIME_FUNCTION f = RtlLookupFunctionEntry(c.Rip, &base, NULL); + if (f) { + void *hd; + DWORD64 est; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, base, c.Rip, f, &c, &hd, &est, NULL); + } else { // a leaf function: the return address is on top + c.Rip = *(DWORD64 *)c.Rsp; + c.Rsp += 8; + } + if (!c.Rip) break; + frames[d++] = c.Rip; + } + return d; +} + +static LONG CALLBACK watch_veh(EXCEPTION_POINTERS *ep) { + if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_CONTINUE_SEARCH; + CONTEXT *c = ep->ContextRecord; + unsigned bits = (unsigned)(c->Dr6 & 0xf); + if (!bits) return EXCEPTION_CONTINUE_SEARCH; // a trap-flag step, not a watchpoint + int on = watch_on != 0, stop = !on; + if (on && watch_fn) { + uint64_t frames[PLAT_WATCH_DEPTH]; + int d = watch_unwind(c, frames); + for (int s = 0; s < 4; s++) + if (bits >> s & 1 && watch_dr7 >> (2 * s) & 1) stop |= watch_fn(s, frames, d); + } + c->Dr0 = watch_addr[0], c->Dr1 = watch_addr[1], c->Dr2 = watch_addr[2], c->Dr3 = watch_addr[3]; + c->Dr7 = stop ? 0 : watch_dr7; + c->Dr6 = 0; + c->ContextFlags |= CONTEXT_DEBUG_REGISTERS; + return EXCEPTION_CONTINUE_EXECUTION; +} + +#define WATCH_THREADS 4096 +static DWORD WINAPI watch_apply(void *arg) { + (void)arg; + static DWORD tids[WATCH_THREADS]; + static HANDLE hs[WATCH_THREADS]; + DWORD self = GetCurrentThreadId(), pid = GetCurrentProcessId(); + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (snap == INVALID_HANDLE_VALUE) return (DWORD)-1; + int nt = 0; + THREADENTRY32 te = {.dwSize = sizeof te}; + for (BOOL ok = Thread32First(snap, &te); ok && nt < WATCH_THREADS; ok = Thread32Next(snap, &te)) + if (te.th32OwnerProcessID == pid && te.th32ThreadID != self) tids[nt++] = te.th32ThreadID; + CloseHandle(snap); + int nh = 0; + for (int i = 0; i < nt; i++) { + HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_SET_CONTEXT, FALSE, tids[i]); + if (h) hs[nh++] = h; + } + static CONTEXT ctx __attribute__((aligned(16))); + memset(&ctx, 0, sizeof ctx); + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + ctx.Dr0 = watch_addr[0], ctx.Dr1 = watch_addr[1], ctx.Dr2 = watch_addr[2], ctx.Dr3 = watch_addr[3]; + ctx.Dr7 = watch_on ? watch_dr7 : 0; + DWORD done = 0; + for (int i = 0; i < nh; i++) { // nothing between the stop and the go-on but the context call + if (SuspendThread(hs[i]) != (DWORD)-1) { + if (SetThreadContext(hs[i], &ctx)) done++; + ResumeThread(hs[i]); + } + } + for (int i = 0; i < nh; i++) CloseHandle(hs[i]); + return done; +} + +int plat_watch_set(const uint64_t *addr, const int *len, int n, PlatWatchHit hit) { + if (n < 0) n = 0; + if (n > 4) n = 4; + if (!watch_added) { + if (!n) return 0; + if (!AddVectoredExceptionHandler(1, watch_veh)) return -1; + watch_added = 1; + } + DWORD64 dr7 = 0; + for (int i = 0; i < 4; i++) watch_addr[i] = i < n ? addr[i] : 0; + for (int i = 0; i < n; i++) { + DWORD64 size = len[i] == 8 ? 2 : len[i] == 4 ? 3 : len[i] == 2 ? 1 : 0; // DR7's LEN encoding + dr7 |= 1ull << (2 * i) | 3ull << (16 + 4 * i) | size << (18 + 4 * i); + } + // the hook and the registers are in place before watch_on says so, and a clear takes watch_on away first + if (n) watch_fn = hit, watch_dr7 = dr7; + InterlockedExchange(&watch_on, n > 0); + HANDLE t = CreateThread(NULL, 64 << 10, watch_apply, NULL, 0, NULL); + DWORD code = (DWORD)-1; + if (!t || WaitForSingleObject(t, 10000) != WAIT_OBJECT_0 || !GetExitCodeThread(t, &code)) code = (DWORD)-1; + if (t) CloseHandle(t); + if (code == (DWORD)-1 && n) InterlockedExchange(&watch_on, 0); + return code == (DWORD)-1 ? -1 : (int)code; +} + // ---- fibers: Windows fibers with FIBER_FLAG_FLOAT_SWITCH (the MXCSR and x87 control word switch with them). struct PlatFiber { void *h; diff --git a/runtime/prof_syms.c b/runtime/prof_syms.c index dbf098df..109729f6 100644 --- a/runtime/prof_syms.c +++ b/runtime/prof_syms.c @@ -24,7 +24,8 @@ static int cmp_u64(const void *a, const void *b) { static uint32_t rd32(const uint8_t *p) { return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; } static uint16_t rd16(const uint8_t *p) { return (uint16_t)(p[0] | p[1] << 8); } -int prof_syms_write(FILE *out, const char *exe_path, uint64_t base, const uint64_t *addr, size_t n) { +int prof_syms_each(const char *exe_path, uint64_t base, const uint64_t *addr, size_t n, + void (*fn)(void *ctx, uint64_t addr, const char *name, uint32_t offset), void *ctx) { FILE *f = fopen(exe_path, "rb"); if (!f) return -1; uint8_t h[0x400]; @@ -102,7 +103,7 @@ int prof_syms_write(FILE *out, const char *exe_path, uint64_t base, const uint64 const CSym *c = &syms[j - 1]; char sn[9] = {0}; const char *nm = c->str ? (c->str < strsz ? strtab + c->str : "?") : (memcpy(sn, c->sname, 8), sn); - fprintf(out, "sym %llx %s+0x%x\n", (unsigned long long)a[i], nm, rva - c->rva); + fn(ctx, a[i], nm, rva - c->rva); named++; } done: @@ -110,3 +111,11 @@ done: fclose(f); return named; } + +static void write_line(void *ctx, uint64_t addr, const char *name, uint32_t offset) { + fprintf((FILE *)ctx, "sym %llx %s+0x%x\n", (unsigned long long)addr, name, offset); +} + +int prof_syms_write(FILE *out, const char *exe_path, uint64_t base, const uint64_t *addr, size_t n) { + return prof_syms_each(exe_path, base, addr, n, write_line, out); +} diff --git a/runtime/prof_syms.h b/runtime/prof_syms.h index c583e8f4..fc9abef6 100644 --- a/runtime/prof_syms.h +++ b/runtime/prof_syms.h @@ -7,3 +7,7 @@ // the image loaded at base, from the COFF symbol table of the file at exe_path. Returns the lines written, -1 when the // file has no symbol table or cannot be read. int prof_syms_write(FILE *out, const char *exe_path, uint64_t base, const uint64_t *addr, size_t n); +// The same lookup with a callback in place of the lines: fn(ctx, address, function, offset) for each address named, in +// address order, once each. Returns the addresses named, -1 as above. +int prof_syms_each(const char *exe_path, uint64_t base, const uint64_t *addr, size_t n, + void (*fn)(void *ctx, uint64_t addr, const char *name, uint32_t offset), void *ctx); diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index e80a2260..5b733c72 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -68,6 +68,16 @@ int mem_copy_readable(uint8_t *const *dst, const uint64_t (*src)[2], int n, uint return 0; } uint64_t kern_now_ns(void) { return 0; } +// the floor watch (off here) +int floor_watch_wanted(void) { return 0; } +int floor_watch_found(const uint64_t *addr, const float *val, const int *unit, int n, const float link[3]) { + (void)addr, (void)val, (void)unit, (void)n, (void)link; + return 0; +} +int floor_watch_present(const float link[3], int link_ok, float frames) { + (void)link, (void)link_ok, (void)frames; + return 0; +} void plat_mutex_lock(PlatMutex *m) { (void)m; } // one thread here void plat_mutex_unlock(PlatMutex *m) { (void)m; }