diff --git a/docs/building.md b/docs/building.md index c1671e1a..34ea73cd 100644 --- a/docs/building.md +++ b/docs/building.md @@ -471,7 +471,10 @@ the water managers and floors of the set `owater` get 60 fixed steps a second wi 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, and runs the physics solver step count's words of the original and the sets `osolver` and `osolverfull` (`physics_steps` 0, 1 and 2), lifted as the generator lifts them: the game's 4 and 8 at 60 and 30 fps -for all three, rounded up by `osolver` and at least 4 every update by `osolverfull` above 60 fps. No game files. +for all three, rounded up by `osolver` and at least 4 every update by `osolverfull` above 60 fps. For the set `ofollow` +(`floor_carry`) it checks the two words of each of the three floor-carry gates and runs the gate's compare with the +game's words and the set's: the page's cut is 100 at 60 and 30 fps, and from 90 to 240 fps the set cuts a floor's +velocity at the time scales where the game cuts it at 60 fps and nowhere else. 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 6abeee94..723c251e 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -58,6 +58,10 @@ # held after an area unloaded until new sounds and then the music went silent. 1 runs the sound system 60 times a # second with whole frames while the frame rate is above 60; 0 is the game's own step. Read at start. # sound_steps = 1 +# Above 100 fps the game kept only a tenth of a moving floor's speed for whoever stood on it, so Link was left behind +# on moving platforms (the Deku Tree's floating one). 1 makes the floor carry him as at 60 fps at any frame rate; 0 is +# the game's own cut. Read at start; needs game code generated with this release. +# floor_carry = 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 dd993f5f..83674b4c 100644 --- a/recomp/patches-v111.txt +++ b/recomp/patches-v111.txt @@ -99,6 +99,36 @@ 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 +# Floors that carry a character: the character controller's update (main+0x3890a80, run from the physics step's jobs) +# calls each of the controller's update modules (vtable +0x50). Three of them make the character follow what it stands +# on: CharacterUpdateFollowSurface (vtable main+0x4c35730, update main+0x13d490), CharacterUpdateFollowSurfaceExtension +# (main+0x4b9c008, main+0x139dc0) and CharacterUpdateDynamicFollowSurface (main+0x4c357b0, main+0x389aa40). Each finds +# the supporting body by its ID (the word at [[controller +8] +0x28] +0x2c, looked up in the physics system's body +# table), takes the body's velocity at the character (main+0x139c20: the linear part from the body's last and previous +# transforms times the inverse of the step, main+0x138940, plus the angular part) and adds it to the character's move. +# Before it adds it, each compares the physics system's inverse step ([[main+0x4e87ff0] +0xc0] +0x28, one over the +# frame's seconds times the time scale, main+0x3b69870) with 100.0 and, above it, keeps a tenth of the velocity +# (main+0x13dad4, main+0x13a39c, main+0x389af24; the 0.1 at main+0x3e40fb4). At the game's 60 and 30 fps the inverse +# step is 60 or 30, so that happens only in a slow motion under 0.6 of the game's speed; above 100 fps it happened every +# update. Link on the Deku Tree's floating platform (B1, Obj_Deku_MoveFloor_A_01) was carried at about a tenth of its +# speed at 117 and 160 fps, and the platform drifted from under him (at 240 fps not at all, which the tenth alone does +# not explain). The set compares with the page's +260 instead (runtime/frame_rate.c floor_carry_store, written with +# +256): 100 / frames per update under one frame per update, else 100. The inverse step times frames per update is 60 +# over the time scale at any frame rate, so above 60 fps the cut comes where it came at 60 fps, and at 60 fps and below +# (100 there, and until the first present) the words give what the game computes. At each site mov w8, #100.0 and +# fmov s1, w8 become adrp x8, page and ldr s1, [x8, #260]; the fcmp +# s0, s1 and b.gt after them are the game's. x8 is written again before any read on every path from the sites; s1 is +# written again before any read, or reaches only calls that take no float argument (getters such as main+0xacd430 and +# those behind vtable +0x68 and +0x210, nn::os::LockMutex and UnlockMutex, GetTlsValue, memset) and returns whose +# caller (main+0x3890a80) reads no float result. No word reads or writes the flags. +set ofollow NX2NAVI_FLOOR_CARRY 1 +site ofollow main 13d704 52a85908 @page:x8 # CharacterUpdateFollowSurface: mov w8, #0x42c80000 (100.0) -> adrp x8, page +site ofollow main 13d708 1e270101 bd410501 # fmov s1, w8 -> ldr s1, [x8, #260] +site ofollow main 13a064 52a85908 @page:x8 # the same two in CharacterUpdateFollowSurfaceExtension +site ofollow main 13a068 1e270101 bd410501 +site ofollow main 389ac20 52a85908 @page:x8 # and in CharacterUpdateDynamicFollowSurface +site ofollow main 389ac24 1e270101 bd410501 + # 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 diff --git a/runtime/cfg.c b/runtime/cfg.c index 8e7bcc6d..ac73e1f2 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", "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_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", @@ -104,7 +104,7 @@ static const struct { const char *key, *section; } sections[] = { {"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"}, - {"SOUND_STEPS", "frame rate"}, + {"SOUND_STEPS", "frame rate"}, {"FLOOR_CARRY", "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"}, @@ -243,6 +243,9 @@ static const CfgMeta metas[] = { {"WATER_STEPS", "1", "0|1", RESTART}, // the set osound of patches-v111.txt (frame_rate.c frame_rate_sound_gate_host): the sound job at 60 Hz above 60 fps {"SOUND_STEPS", "1", "0|1", RESTART}, + // the set ofollow of patches-v111.txt (frame_rate.c floor_carry_store): a moving floor carries a character above + // 100 fps as at 60 + {"FLOOR_CARRY", "1", "0|1", RESTART}, // frame_rate.c floor_probe: lines about Link on a moving floor (0 off, 1 = 4000 lines, or a count) {"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) diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 30a6f79f..c0561407 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -68,6 +68,20 @@ static _Atomic uint64_t *step_word; // patch page +0: ratio (low float), secon static double step_now = STEP_FIRST_S; // the step the patch page holds: what the game steps each frame static _Atomic uint32_t *step_interval; // patch page +8: seconds * 60 (patches-v143.txt realtime) static _Atomic uint32_t *ostep_page; // patch page +256 with the set ostep: frames per update (patches-v111.txt) +static _Atomic uint32_t *carry_page; // patch page +260 with the set ofollow: 100 / frames per update + +// The set ofollow (patches-v111.txt): the three follow-surface modules of the character controller keep only a tenth +// of the supporting floor's velocity when the physics' inverse step is over the number at page +260. The game compares +// with 100, which the inverse step passed at every update above 100 fps; under one frame per update 100 / frames per +// update makes the cut come where it came at 60 fps. At one frame or more it stays 100, so 60 and 30 fps keep the +// game's own cut. Written with frames per update (ocarina_step_write). +static void floor_carry_store(float frames) { + if (!carry_page || !(frames > 0)) return; + float cut = frames < 1.0f ? 100.0f / frames : 100.0f; + uint32_t bits; + memcpy(&bits, &cut, 4); + atomic_store_explicit(carry_page, bits, memory_order_release); +} static uint64_t last_ns; static double samples[STEP_SAMPLES]; static unsigned sample_count, sample_next; @@ -96,6 +110,13 @@ void frame_step_init(void) { atomic_store_explicit(ostep_page, bits, memory_order_release); rt_log("frame rate: the game's frames per update follow the display interval (set ostep)"); } + int follow = rt_patch_set("ofollow"); + if (follow >= 0) { + carry_page = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 260); + floor_carry_store(1.0f); + rt_log("frame rate: a moving floor carries a character as at 60 fps: %s", + rt_patch_enabled(follow) ? "on (set ofollow)" : "off, the game's cut above 100 fps (floor_carry = 0)"); + } // physics_steps (patches-v111.txt osolver and osolverfull): 0 the game's solver step count, trunc(frames per update // * 4 + 0.001); 1 (the default) that count rounded up; 2 rounded up and at least the 4 of a 60 Hz frame, every // update. The two sets patch the same words; this start keeps one of them on. @@ -725,6 +746,7 @@ static void ocarina_step_write(double dt) { uint32_t bits; memcpy(&bits, &f, 4); atomic_store_explicit(ostep_page, bits, memory_order_release); + floor_carry_store(f); ostep_last = f; return; } diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index fd866fd1..e80a2260 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -21,8 +21,8 @@ const char *cfg_get(const char *key) { return NULL; } unsigned cfg_version(void) { return version; } -int rt_patch_set(const char *name) { return !strcmp(name, "ostep") ? 0 : -1; } -int rt_patch_enabled(int set) { return set == 0; } +int rt_patch_set(const char *name) { return !strcmp(name, "ostep") ? 0 : !strcmp(name, "ofollow") ? 1 : -1; } +int rt_patch_enabled(int set) { return set == 0 || set == 1; } void rt_patch_disable(int set) { (void)set; } void rt_patch_switch(int set, int on) { (void)set, (void)on; } void *rt_patch_page(void) { return page; } @@ -148,13 +148,29 @@ static uint32_t solver_steps(void (*fn)(Cpu *), float frames) { return (uint32_t)c.x[1]; } +// The floor-carry gate (set ofollow): 1 when its b.gt is taken, which keeps a tenth of the floor's velocity. s0 is the +// physics' inverse step; the set's load reads the page through x8. +void follow_game(Cpu *c); +void follow_ofollow(Cpu *c); +static int follow_cut(void (*fn)(Cpu *), float inverse) { + Cpu c; + memset(&c, 0, sizeof c); + uint32_t bits; + memcpy(&bits, &inverse, 4); + c.v[0] = bits, c.x[8] = (uint64_t)(uintptr_t)page; + fn(&c); + return !FLAG_Z(&c) && FLAG_N(&c) == FLAG_V(&c); // gt +} + int main(int argc, char **argv) { int mean = argc > 1 && !strcmp(argv[1], "mean"); if (mean) steady = "0"; frame_step_init(); - float one; + float one, cut0; memcpy(&one, page + 256, 4); + memcpy(&cut0, page + 260, 4); CHECK(one == 1.0f, "the page holds one frame before the first present, not %g", one); + CHECK(cut0 == 100.0f, "the floor carry's cut is the game's 100 before the first present, not %g", cut0); const double p60 = 16666667e-9, p240 = 4166667e-9; if (mean) { // the comparison setting: 5 and 30 ms in turn give 15 and 20 ms in turn @@ -379,6 +395,32 @@ int main(int argc, char **argv) { 144 * solver_steps(solver_osolver, 60.0f / 144.0f), 240 * solver_steps(solver_osolver, 0.25f), 144 * solver_steps(solver_osolverfull, 60.0f / 144.0f), 240 * solver_steps(solver_osolverfull, 0.25f)); } + // The floor carry (set ofollow). The physics' inverse step is 60 / (frames per update * time scale); the game cuts + // a floor's velocity to a tenth above 100, which at 60 fps is a time scale under 0.6 and above 100 fps every + // update. With the page's 100 / frames per update the set cuts exactly where the game cuts at 60 fps at any rate + // above 60; at 60 and 30 fps the page holds 100 and the set cuts where the game's own words do. + { + const char *limits[] = {"60", "30", "90", "120", "144", "240"}; + const int hz[] = {60, 30, 90, 120, 144, 240}; + const float scales[] = {1.0f, 0.8f, 0.62f, 0.58f, 0.5f, 0.25f}; + for (int r = 0; r < 6; r++) { + set_limit(limits[r]); + float frames = 0, cut; + for (int i = 0; i < 60; i++) frames = present(1.0 / hz[r]); + memcpy(&cut, page + 260, 4); + CHECK(cut == (frames < 1 ? 100.0f / frames : 100.0f), "%d fps (%g frames): the floor carry's cut is %g", + hz[r], frames, cut); + for (int s = 0; s < 6; s++) { + float inverse = 60.0f / (frames * scales[s]); + int set = follow_cut(follow_ofollow, inverse), + want = follow_cut(follow_game, hz[r] > 60 ? 60.0f / scales[s] : inverse); + CHECK(set == want, "%d fps, time scale %.2f: the set %s, the game %s %s", hz[r], scales[s], + set ? "cuts" : "carries", hz[r] > 60 ? "at 60 fps" : "here", want ? "cuts" : "carries"); + } + CHECK(follow_cut(follow_game, 60.0f / frames) == (hz[r] > 100), "%d fps: the game's own words %s", hz[r], + hz[r] > 100 ? "do not cut" : "cut"); + } + } 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 5e132cc3..5fd37fb0 100644 --- a/tests/frame_step_check.py +++ b/tests/frame_step_check.py @@ -92,13 +92,50 @@ def solver_functions(): return '\n'.join(out) +# The floor-carry gate of the three follow-surface modules (set ofollow): mov w8, #100.0 and fmov s1, w8 before the +# game's fcmp s0, s1 (s0 the physics' inverse step) and the b.gt to the tenth of the floor's velocity. The set makes the +# two words adrp x8, page and ldr s1, [x8, #260]; the test lifts the load and the fcmp with x8 set to its page. +FOLLOW_SITES = (0x13d704, 0x13a064, 0x389ac20) +FOLLOW_GAME = (0x52a85908, 0x1e270101) # mov w8, #0x42c80000; fmov s1, w8 +FOLLOW_LOAD = 0xbd410501 # ldr s1, [x8, #260] +FCMP = 0x1e212000 # fcmp s0, s1 + + +def check_follow(): + """Each gate's two words are the set ofollow's adrp to the page in x8 and its load of +260, over the game's.""" + table = load(ROOT / 'recomp/patches-v111.txt', [('main', 0x7000000000, PAGE)]) + names = {i: t[0] for i, t in enumerate(table.sets)} + bad = 0 + for site in FOLLOW_SITES: + first = {names[k]: (words, have) for k, words, have in table.sites['main'].get(site, [])} + second = {names[k]: (words, have) for k, words, have in table.sites['main'].get(site + 4, [])} + if (first != {'ofollow': ([('page', 8)], FOLLOW_GAME[0])} or + second != {'ofollow': ([FOLLOW_LOAD], FOLLOW_GAME[1])}): + print(f'FAIL: the floor-carry gate at main+{site:#x} is {first} / {second}') + bad += 1 + return bad + + +def follow_functions(): + lift = Lifter(0, simd='c') + out = [] + for name, words in (('game', FOLLOW_GAME + (FCMP,)), ('ofollow', (FOLLOW_LOAD, FCMP))): + body = [] + for k, w in enumerate(words): + insn = lift.lift(w, 0x7000000000 + FOLLOW_SITES[0] + 4 * k + (4 if name == 'ofollow' else 0)) + assert insn.kind == 'op', (name, hex(w), insn.kind) + body.append(' ' + insn.code) + out.append(f'void follow_{name}(Cpu *c) {{\n' + '\n'.join(body) + '\n}\n') + return '\n'.join(out) + + def main(): work = ROOT / 'build' / 'test' / 'frame_step' work.mkdir(parents=True, exist_ok=True) (work / 'layout.h').write_text(LAYOUT) exe = work / ('frame_step_check.exe' if os.name == 'nt' else 'frame_step_check') solver = work / 'solver_sites.c' - solver.write_text(solver_functions()) + solver.write_text(solver_functions() + '\n' + follow_functions()) env = dict(os.environ) if os.name == 'nt' and MSYS_BIN.exists(): env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') @@ -106,7 +143,7 @@ def main(): '-I', str(ROOT / 'runtime'), '-I', str(work), str(ROOT / 'tests' / 'frame_step_check.c'), str(solver), '-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 or check_sites() + return subprocess.run([str(exe), 'mean'], env=env).returncode or rc or check_sites() + check_follow() if __name__ == '__main__':