mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
297
runtime/floor_watch.c
Normal file
297
runtime/floor_watch.c
Normal file
@@ -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_<guest address>, 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 <math.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
13
runtime/floor_watch.h
Normal file
13
runtime/floor_watch.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// 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);
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <restartmanager.h>
|
||||
#include <shellapi.h>
|
||||
#include <timeapi.h>
|
||||
#include <tlhelp32.h> // CreateToolhelp32Snapshot (plat_watch_set)
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user