From 750034b7fdab8e20ca2f18f68c42f3b04b0d42cb Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 03:42:05 -0500 Subject: [PATCH] Run the water and floor surface simulations at their own 60 steps a second above 60 fps on 1.1.1. The physics' two water managers and the game's interactive floor simulation (the Deku Tree's ground webs) step their surfaces in fixed steps of 1/60 s, as many whole steps as the frame's time holds, and drop the fraction, so every frame shorter than 1/60 s ran none and the surfaces stood still above 60 fps. The new set owater (water_steps, on by default) calls the page between the division and the game's cut, which keeps the fraction per object; at 60 fps the count is the game's. Co-Authored-By: Claude Opus 5.5 --- docs/building.md | 8 ++++--- nx2navi.ini.example | 4 ++++ recomp/patches-v111.txt | 25 +++++++++++++++++++++ runtime/cfg.c | 7 ++++-- runtime/frame_rate.c | 40 +++++++++++++++++++++++++++++++++- runtime/host.h | 1 + runtime/patches.c | 5 +++-- runtime/sprint.c | 4 ++-- tests/frame_step_check.c | 46 ++++++++++++++++++++++++++++++++++++++- tests/frame_step_check.py | 28 +++++++++++++++++++++++- 10 files changed, 156 insertions(+), 12 deletions(-) diff --git a/docs/building.md b/docs/building.md index fa437580..b5d139d6 100644 --- a/docs/building.md +++ b/docs/building.md @@ -462,12 +462,14 @@ and runs each step as a start of the game on a profile it makes: the export, an exported, its backup and swap at the next start, the zips it refuses and Cancel. No game files. A change to the step Ocarina is given each frame (`runtime/frame_rate.c` `ocarina_step`), or to the whole numbers the -game's counters get out of it (`whole_take`, `frame_rate_day_host`, `frame_rate_water_host`), passes +game's counters get out of it (`whole_take`, `frame_rate_day_host`, `frame_rate_water_host`, `frame_rate_sim_host`), passes `python tests\frame_step_check.py`, which builds `frame_rate.c` with the rest of the runtime stubbed and feeds it display times: a held 60 fps limit gives exactly one frame every frame, a 240 fps limit never less than a quarter frame, uneven frame times one steady step, and game time follows the clock. It calls the two hooks as their patch -sites do: at 144 fps each of 64 water objects gets its ripple steps and the day its seconds, and a step of one frame -gives what the game computes. No game files. +sites do: at 144 fps each of 64 water objects gets its ripple steps and the day its seconds, at 120, 144 and 240 fps +the water managers and floors of the set `owater` get 60 fixed steps a second with only s0 changed, and a step of one +frame gives what the game computes. It also checks that each of those sites in `recomp/patches-v111.txt` ends in one +`bl` to its page slot. No game files. A change to how the mixer applies volumes (`runtime/audio_mixer.c` `accumulate_ramp`, the voice and splitter paths of `audio_mixer_render`) or to what it owes the device after a stall (`ring_pull`, `audio_mixer_owed`) passes diff --git a/nx2navi.ini.example b/nx2navi.ini.example index ac90f29e..8cca213f 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -48,6 +48,10 @@ # rounded that step down to nothing: the window stayed until the area was left and its counting sound went on. 1 lets # it count at least one a frame; 0 is the game's own count. # pickup_count = 1 +# Water surfaces and the springy floors (the Deku Tree's webs) are simulated in fixed steps of 1/60 s, as many as the +# frame's time holds, and the game dropped the rest: above 60 fps every frame ran none, so they stood still under Link. +# 1 keeps the rest for the next frame, 60 steps a second at any frame rate; 0 is the game's own count. Read at start. +# water_steps = 1 # A movie's narration and fades follow the picture on screen: 1 counts their time from the shown picture's # timestamp, so they stay with it at any frame rate (with movie_fps = 0 too) and wait when the picture stalls; 0 counts # the game's redraws, as before, which keeps them with the music when the picture stalls. Read at start; needs game diff --git a/recomp/patches-v111.txt b/recomp/patches-v111.txt index 77726d15..9514080a 100644 --- a/recomp/patches-v111.txt +++ b/recomp/patches-v111.txt @@ -79,6 +79,31 @@ site osolver main 38afd10 bd440d02 1e2a1002 # the same three words in the seco site osolver main 38afd1c 1f010800 1f218800 site osolver main 38afd20 1e380001 1e280001 +# Fixed steps of the height-field surfaces: the physics' two water managers, phive::InteractiveWaterMgr (its update +# main+0x3875f20, the vtable slot at main+0x4c32f90) and phive::ResponsiveWaterMgr (main+0x3882660, the slot at +# main+0x4c33a88; the names are those of their postUpdateWorld profiler scopes, and their bodies are named +# InteractiveWater_* and ResponsiveWater_* WaterBody, IceBody and SandBody), and the game's interactive floor +# simulation built on the same base (main+0x632cb0, the slot at main+0x4c338e0; its object is made at main+0x990040 by +# the code that sets up the InteractiveFloorBody, main+0x98fb70; the Deku Tree's ground webs are such a floor, the +# "SimulationCarpet" of DgnObj_Spiderweb_Ground's InteractiveFloorControllerExtensionParam, 24 x 24 points) simulate +# their surfaces in fixed steps of 1/60 s. Each update takes the frame's seconds (s0, kept in s8), divides them by 1/60 +# (main+0x3876014, main+0x38827f8, main+0x632cfc) and cuts the quotient to a whole number of steps, n = trunc(seconds * +# 60) (fcvtzs into w8, w8 and w22); it keeps n, and min(max(n, 0), 3) as the count of steps it queues, each with a step +# of 1/60 s (main+0x38764d8, main+0x3882cb0, main+0x632d40), and skips the whole simulation while n is under 1 +# (main+0x3876230, main+0x38829e8, main+0x632f20). Nothing keeps the fraction: at 60 fps every update runs one step, at +# 30 two, and every update shorter than 1/60 s none, so above 60 fps those surfaces stood still whatever pushed into +# them, a web under Link among them. After the division each site calls the page's +0xf20 +# (runtime/frame_rate.c frame_rate_sim_host): s0 becomes the whole steps in the quotient plus a remainder kept for the +# object (x19, the function's x0 at all three), as a float, and the game's own cut and clamp follow. So the surfaces +# run 60 steps a second at any frame rate, and an update of exactly 1/60 s gives what the game computes. The host +# writes only s0's low lane, which the division wrote; x30 is the function's own, saved by its prologue +# (main+0x3875f2c, main+0x388266c, main+0x632cbc) and read only by its return after the epilogue reloads it; nothing +# reads or writes the flags. +set owater NX2NAVI_WATER_STEPS 1 +site owater main 3876014 1e201900 1e201900 9458abc3 # fdiv s0, s8, s0 -> fdiv s0, s8, s0; bl page+0xf20 (InteractiveWaterMgr) +site owater main 38827f8 1e201900 1e201900 945879ca # the same at ResponsiveWaterMgr's +site owater main 632cfc 1e201900 1e201900 9521b889 # the same at the interactive floors' + # Movie-synchronised event timelines: the movie layer advances its counter at +760 on each draw of a ready # picture. main+0x3b3ec10 reads that counter for the event dispatcher, so rendering a picture twice also # advances voices and overlays twice. Both ready paths reach these sites only after main+0x3b5ce80 checks diff --git a/runtime/cfg.c b/runtime/cfg.c index ed7ff6bc..12d7879a 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -34,7 +34,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", "PICKUP_COUNT", "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", "PICKUP_COUNT", "WATER_STEPS", "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", @@ -102,7 +102,8 @@ static const struct { const char *key, *section; } sections[] = { {"GUEST_OPT", "performance"}, {"PASS_TIMES", "diagnostics"}, {"DUMP_KEY", "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"}, {"MOVIE_PTS", "frame rate"}, {"STEP_STEADY", "frame rate"}, + {"PHYSICS_STEPS", "frame rate"}, {"PICKUP_COUNT", "frame rate"}, {"WATER_STEPS", "frame rate"}, + {"MOVIE_PTS", "frame rate"}, {"STEP_STEADY", "frame rate"}, {"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"}, {"GPU_INDEX", "display"}, {"MONITOR", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"}, {"SHARPNESS", "display"}, {"STANDIN_FILTER", "display"}, {"SCENE_SHARE", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"}, @@ -242,6 +243,8 @@ static const CfgMeta metas[] = { {"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART}, {"MODS_REBUILD", "auto", "auto|off", RESTART}, {"HIGHFPS_FIX", "1", "0|1", RESTART}, + // the set owater of patches-v111.txt (frame_rate.c frame_rate_sim_host): water and floor surfaces' fixed steps + {"WATER_STEPS", "1", "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/frame_rate.c b/runtime/frame_rate.c index 40b607a1..96e21732 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -488,7 +488,8 @@ static double ocarina_step(double dt) { // above 60 fps, the cut loses the fraction every frame. whole_frames keeps it per counter (by its guest address) and // hands out the whole part, so each counter moves at the game's pace at any frame rate; at a step of exactly one it // gives what the game computes. A value that is not positive, or too large for a count, is cut as fcvtzs cuts it. -// The water's ripples keep their remainders per water object in a table of their own (frame_rate_water_host). +// The water's ripples keep their remainders per water object in a table of their own (frame_rate_water_host), and so do +// the fixed steps of the water and floor surfaces (frame_rate_sim_host). #define WHOLE_SLOTS 8 #define WATER_SLOTS 64 typedef struct { uint64_t at; float rem; uint64_t used; } WholeSlot; // used: whole_calls at the slot's last use @@ -573,6 +574,43 @@ void frame_rate_water_host(Cpu *c) { } } +// The fixed steps of the physics' height-field surfaces (patches-v111.txt owater): the water managers (main+0x3875f20, +// main+0x3882660) and the interactive floors' simulation (main+0x632cb0, the spider webs' sinking floor). Each update +// divides its seconds by 1/60 s into s0 and cuts that to the number of steps of exactly 1/60 s it runs (at most 3), +// dropping the fraction, so every frame shorter than 1/60 s ran none. The site calls this between the division and +// the game's own cut: s0 becomes the whole steps in the quotient plus the remainder kept for the object in x19, as a +// float the cut keeps exactly, and only s0 changes (its upper lanes stay zero, as the division left them). A quotient +// that is not positive, or too large for a count, stays for the cut. The remainders are in a table of their own: an +// object that shares slots with the counters above would lose its fraction to them. +// NX2NAVI_TRACE=perf: every 10 s, the steps the objects were given beside those the cut alone would have given. +#define SIM_SLOTS 8 +static WholeSlot sim_slot[SIM_SLOTS]; + +void frame_rate_sim_host(Cpu *c) { + uint32_t bits = (uint32_t)c->v[0]; + float steps; + memcpy(&steps, &bits, 4); + uint32_t whole = whole_take(sim_slot, SIM_SLOTS, c->x[19], steps); + if (steps > 0.0f && steps < 2147483648.0f) { // whole_take's own range: outside it the game's cut stands + float f = (float)whole; + memcpy(&bits, &f, 4); + c->v[0] = bits; + } + if (rt_trace & RT_TRACE_PERF) { + static uint64_t t0, asked, given, cut; + uint64_t now = plat_now_ns(), n = 0, g = 0, k = 0, t = 0; + plat_mutex_lock(&whole_lock); + asked++, given += whole, cut += cut_fcvtzs(steps); + if (!t0) t0 = now; + if (now - t0 >= 10000000000ull) n = asked, g = given, k = cut, t = now - t0, t0 = now, asked = given = cut = 0; + plat_mutex_unlock(&whole_lock); + if (n) + rt_log("frame rate: the water and floor surfaces got %llu fixed steps in %.1f s, asked %llu times (cut to " + "whole steps each time, as the game cuts them, %llu)", (unsigned long long)g, t / 1e9, + (unsigned long long)n, (unsigned long long)k); + } +} + // The 60 Hz frames the game steps at each update where the set ostep gives them (Ocarina); 1 elsewhere, the game's own // nominal frame. The mouse camera divides by it (mouse_direct.c): the game multiplies camera input by this step. float frame_step_frames(void) { diff --git a/runtime/host.h b/runtime/host.h index 6b444980..d3f08a6d 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -409,6 +409,7 @@ double frame_step_limit_hz(void); // the frame rate the double frame_step_game_hz(void); // the rate the game draws at under the limit; 0 unknown void frame_rate_day_host(Cpu *c); // the time of day's seconds a frame (set ostep, page +0xf60) void frame_rate_water_host(Cpu *c); // the water's ripple steps a frame (set ostep, page +0xf50) +void frame_rate_sim_host(Cpu *c); // water and floor surfaces' fixed steps (set owater, page +0xf20) float frame_step_frames(void); // the 60 Hz frames the game steps per update (set ostep), else 1 void frame_step_flight(float *page, float *game, uint64_t *presents); // flight.c: the step at a host present // rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs diff --git a/runtime/patches.c b/runtime/patches.c index 67216ae4..dca0d698 100644 --- a/runtime/patches.c +++ b/runtime/patches.c @@ -32,12 +32,13 @@ uint8_t rt_patch_on[RT_PATCH_COUNT > 0 ? RT_PATCH_COUNT : 1]; // read by the g #ifdef RT_PATCH_PAGE #include "debug_tools.h" __attribute__((constructor)) static void freecam_register(void) { rt_register_host(RT_PATCH_PAGE + 0xf70, dbg_freecam_host); } -// The time of day's site of the set ostep (patches-v111.txt) calls the page's +0xf60 and the water renderer's site -// its +0xf50 (frame_rate.c). +// The time of day's site of the set ostep (patches-v111.txt) calls the page's +0xf60, the water renderer's site its +// +0xf50, and the surfaces' fixed-step sites of the set owater its +0xf20 (frame_rate.c). __attribute__((constructor)) static void day_register(void) { if (!RT_V111) return; rt_register_host(RT_PATCH_PAGE + 0xf60, frame_rate_day_host); rt_register_host(RT_PATCH_PAGE + 0xf50, frame_rate_water_host); + rt_register_host(RT_PATCH_PAGE + 0xf20, frame_rate_sim_host); } #endif diff --git a/runtime/sprint.c b/runtime/sprint.c index 02364a6b..73b47993 100644 --- a/runtime/sprint.c +++ b/runtime/sprint.c @@ -12,8 +12,8 @@ // a load (no update of Link within GAMEPLAY_POLLS polls) start nothing: B closes the pause menu, and that press is the // menu's. Under the game's Swap Jump Controls B is the jump, which a B sprint button keeps from the game while Link // moves: L3 is the one to use then. -// Patch page slots: the set's sites call +0xf40 and +0xf30; +0xf50 to +0xff0 belong to other sets (runtime/patches.c, -// mouse_direct.c, ov_zstd.c). A slot registered twice ends the start (dispatch.c rt_register_host). +// Patch page slots: the set's sites call +0xf40 and +0xf30; +0xf20 and +0xf50 to +0xff0 belong to other sets +// (runtime/patches.c, mouse_direct.c, ov_zstd.c). A slot registered twice ends the start (dispatch.c rt_register_host). // // Link's state: the id at [[[[Link +0x18] +0x270] +0x188] +0x1c0] +0x28 (+0x4c when that reads 0x11a, a state that // wraps another), the same field main+0x4aed50 compares with 1 (Walk) before the game starts a sprint. diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index f4909c44..e67c59fe 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -87,7 +87,25 @@ static uint32_t day(uint64_t object, float secs) { // frame_rate_day_host: x19 frame_rate_day_host(&cpu); return (uint32_t)cpu.x[8]; } - +// frame_rate_sim_host as the surfaces' sites call it (set owater): after the site's division, the update's seconds over +// 1/60 s in s0 and the object in x19; then the game's own cut of s0 (fcvtzs) gives the steps. Only s0's low lane may +// change: every general register, the other lanes and vector registers and the flags stay as they were. +static uint32_t sim(uint64_t object, float seconds) { + Cpu c, before; + uint8_t *p = (uint8_t *)&c; + for (size_t i = 0; i < sizeof c; i++) p[i] = (uint8_t)(i * 37u + 11u); + float quotient = seconds / (1.0f / 60.0f); // the site's fdiv s0, s8, s0 (the literal is 1/60 as a float) + uint32_t bits; + memcpy(&bits, "ient, 4); + c.v[0] = bits, c.x[19] = object; // the division wrote s0 and zeroed the lanes above it + before = c; + frame_rate_sim_host(&c); + int same = c.nzcv == before.nzcv && c.sp == before.sp && !memcmp(&c.v[1], &before.v[1], sizeof c.v - sizeof c.v[0]) && + c.v[0] >> 32 == 0 && !memcmp(c.x, before.x, sizeof c.x); + CHECK(same, "the surfaces' site changed something besides s0's low lane"); + memcpy("ient, &c.v[0], 4); + return cut_fcvtzs(quotient); // the game's fcvtzs after the site +} int main(int argc, char **argv) { int mean = argc > 1 && !strcmp(argv[1], "mean"); if (mean) steady = "0"; @@ -221,6 +239,32 @@ int main(int argc, char **argv) { for (int i = 0; i < 30; i++) CHECK(water(pond, 2.0f) == 2, "two frames, call %d", i); CHECK(water(pond, 0.5f) == 1, "the half frame kept through the whole ones"); } + // The water managers and the floors (set owater): at 120, 144 and 240 fps each runs 60 fixed steps a second (the cut + // gave none above 60), beside 64 water objects; at 60 and 30 fps one and two steps an update, as the game counts them. + { + const uint64_t interactive = 0x7400000000ull, responsive = 0x7400001000ull, floor = 0x7400002000ull, + first = 0x7500000000ull; + const float rates[] = {144.0f, 240.0f, 120.0f}; + for (int r = 0; r < 3; r++) { + uint32_t a = 0, b = 0, c = 0, w = 0; + int frames = (int)(rates[r] * 10); + for (int f = 0; f < frames; f++) { + a += sim(interactive, 1.0f / rates[r]); + for (int k = 0; k < 64; k++) w += water(first + 0x1000 * (uint64_t)k, 60.0f / rates[r]); + b += sim(responsive, 1.0f / rates[r]); + c += sim(floor, 1.0f / rates[r]); + } + CHECK(a >= 599 && a <= 600 && b >= 599 && b <= 600 && c >= 599 && c <= 600, + "%.0f fps: the managers and the floor ran %u, %u and %u steps in 10 s", + rates[r], a, b, c); + CHECK(w >= 64 * 599, "%.0f fps: the water objects beside the managers ran %u steps", rates[r], w); + } + for (int i = 0; i < 120; i++) CHECK(sim(interactive, 1.0f / 60.0f) == 1, "60 fps, update %d", i); + for (int i = 0; i < 60; i++) CHECK(sim(responsive, 1.0f / 30.0f) == 2, "30 fps, update %d", i); + CHECK(sim(interactive, 0.0f) == 0 && sim(interactive, -1.0f / 60.0f) == 0xffffffffu && + sim(interactive, NAN) == 0, "no time, negative time and NaN are cut as fcvtzs cuts"); + CHECK(sim(interactive, 1.0f / 15.0f) == 4, "four steps for a 15 fps update (the game's clamp to 3 follows)"); + } printf("frame_step_check: %s\n", fails ? "FAILED" : "ok"); return fails != 0; } diff --git a/tests/frame_step_check.py b/tests/frame_step_check.py index d77ec53a..5afd4c3f 100644 --- a/tests/frame_step_check.py +++ b/tests/frame_step_check.py @@ -11,6 +11,16 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MSYS_BIN = Path(r'C:\msys64\ucrt64\bin') +sys.path[:0] = [str(ROOT / 'recomp')] +from patches import load # noqa: E402 + +PAGE = 0x4ea0000 # the patch page of 1.1.1, main+0x4ea0000 +# The sites that call frame_rate.c's hosts, as recomp/patches-v111.txt has them: set, module offset, the original word, +# the words before the bl (the original's own, where the host comes after it), the page offset the bl reaches. +FDIV = 0x1e201900 # fdiv s0, s8, s0 +HOST_SITES = [('ostep', 0x3cdfc0, 0x1e380008, [], 0xf60), ('ostep', 0x475f8, 0x1e380009, [], 0xf50), + ('owater', 0x3876014, FDIV, [FDIV], 0xf20), ('owater', 0x38827f8, FDIV, [FDIV], 0xf20), + ('owater', 0x632cfc, FDIV, [FDIV], 0xf20)] def compiler(): @@ -27,6 +37,22 @@ LAYOUT = '''// A stand-in for the generated layout.h (tests/frame_step_check.py) ''' +def check_sites(): + """Each host site is its listed words and one bl to its page slot, over its original word.""" + table = load(ROOT / 'recomp/patches-v111.txt', [('main', 0x7000000000, PAGE)]) + bad = 0 + for name, site, original, before, slot in HOST_SITES: + (key, words, have), = table.sites['main'][site] + bl = words[-1] if isinstance(words[-1], int) else 0 + target = site + (((bl & 0x3ffffff) ^ 0x2000000) - 0x2000000) * 4 + ok = (table.sets[key][0] == name and have == original and words[:-1] == before and bl >> 26 == 0x25 and + target == PAGE + slot) + if not ok: + print(f'FAIL: the site main+{site:#x} of set {name} is {words} over {have:#x}') + bad += 1 + return bad + + def main(): work = ROOT / 'build' / 'test' / 'frame_step' work.mkdir(parents=True, exist_ok=True) @@ -39,7 +65,7 @@ def main(): '-I', str(ROOT / 'runtime'), '-I', str(work), str(ROOT / 'tests' / 'frame_step_check.c'), '-o', str(exe), '-lm'], env=env) rc = subprocess.run([str(exe)], env=env).returncode - return subprocess.run([str(exe), 'mean'], env=env).returncode or rc + return subprocess.run([str(exe), 'mean'], env=env).returncode or rc or check_sites() if __name__ == '__main__':