diff --git a/docs/building.md b/docs/building.md index ac602db5..c1671e1a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -469,7 +469,9 @@ frame, uneven frame times one steady step, and game time follows the clock. It c 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. +`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. 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 1623ae3e..6abeee94 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -42,7 +42,9 @@ # highfps_fix = 1 # Above 60 fps the game's physics solved contacts and friction in fewer steps a second than at 60, and in none at all # in a frame shorter than 1/240 s, so things standing on moving platforms were left behind. 1 rounds the step count -# up instead; 0 is the game's own count. +# up instead; 0 is the game's own count. 2 also gives every frame at least the 4 steps of a 60 fps frame, which costs +# more physics time above 60 fps (576 steps a second at 144 fps, 960 at 240, against about 288 and 240 with 1) and is +# a candidate fix for Link left behind on the Deku Tree's floating platform. Read at start. # physics_steps = 1 # The number in the window that shows a pick-up (a rupee) counts up by whole steps a frame, and above 60 fps the game # rounded that step down to nothing: the window stayed until the area was left and its counting sound went on. 1 lets diff --git a/recomp/patches-v111.txt b/recomp/patches-v111.txt index 6054d4c4..dd993f5f 100644 --- a/recomp/patches-v111.txt +++ b/recomp/patches-v111.txt @@ -60,6 +60,26 @@ site ostep main 3cdfc0 1e380008 952b4be8 # fcvtzs w8, s0 -> bl page+0xf60 # parent keeps it across its call), and x30 is the function's own saved one. site ostep main 475f8 1e380009 95396656 # fcvtzs w9, s0 -> bl page+0xf50 +# Physics solver steps, at least a 60 fps frame's (physics_steps = 2, runtime/frame_rate.c frame_step_init switches +# this set on and osolver off): the step setup below with the count of osolver, ceil(frames per update * count - 0.25), +# raised to the count itself, the steps of one 60 Hz frame. Above 60 fps the rounded-up count still gives fewer +# solver steps an update (2 at 120 to 160 fps, 1 at 240) while their number a second stays near 240; with this set +# every update gets the 60 Hz frame's 4, each covering a quarter of the update's time, so a run at 240 fps solves 960 +# steps a second, 576 at 144. Link stood on the Deku Tree's floating platform (B1, Obj_Deku_MoveFloor_A_01, drifting +# under GroundControlActor_WaveLoop) is carried by it fully at 60 fps, about a tenth at 160 and not at all at 240, +# which follows the steps an update and not those a second; whether this mends the carry needs a run. At 60 and 30 +# fps the count is the game's (4 and 8). The first two sites are osolver's; at the third the cut becomes fmax s0, s0, +# s1 (s1 the count, made a float by the scvtf before the fmadd) and the round up, so w1 = max(ceil(s0), count). s0 +# and s1 are not read before the call's callee (the world's main+0x3a24c20) and the code after it write them, and +# none of the words reads or writes the flags. +set osolverfull NX2NAVI_PHYSICS_STEPS_FULL 0 +site osolverfull main 38aebe4 bd440d02 1e2a1002 # ldr s2, [x8, #0x40c] (0.001) -> fmov s2, #0.25 +site osolverfull main 38aebf0 1f010800 1f218800 # fmadd s0, s0, s1, s2 -> fnmsub s0, s0, s1, s2 +site osolverfull main 38aebf4 1e380001 1e214800 1e280001 # fcvtzs w1, s0 -> fmax s0, s0, s1; fcvtps w1, s0 +site osolverfull main 38afd10 bd440d02 1e2a1002 # the same three in the second copy +site osolverfull main 38afd1c 1f010800 1f218800 +site osolverfull main 38afd20 1e380001 1e214800 1e280001 + # Physics solver steps: the physics system's step setup (main+0x38aeb80, and the same code in main+0x38afc10) gives the # rigid body world its solver step count for the frame, trunc(frames per update * count + 0.001) with the count at # system +0x244 (4, set by the constructor main+0x38ae800), and the world keeps the result's inverse beside it (world diff --git a/runtime/cfg.c b/runtime/cfg.c index 18c58782..8e7bcc6d 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", "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", "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", diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 70c2ab62..30a6f79f 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -96,6 +96,20 @@ 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)"); } + // 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. + int solver = rt_patch_set("osolver"), solver_full = rt_patch_set("osolverfull"); + if (solver_full >= 0) { + const char *e = cfg_get("NX2NAVI_PHYSICS_STEPS"); + int full = (e && !strcmp(e, "2")) || rt_patch_enabled(solver_full); + rt_patch_switch(solver_full, full); + if (full && solver >= 0) rt_patch_disable(solver); + rt_log("frame rate: physics solver steps an update: %s", + full ? "rounded up and at least a 60 fps frame's 4 (physics_steps = 2)" + : solver >= 0 && rt_patch_enabled(solver) ? "rounded up (physics_steps = 1)" + : "the game's own count (physics_steps = 0)"); + } if (realtime < 0) { rt_log("frame rate: no patch table, shipping 30 fps whole-vsync steps"); return; diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index 4ea47c6c..fd866fd1 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -129,6 +129,25 @@ static uint32_t sim(uint64_t object, float seconds) { memcpy("ient, &c.v[0], 4); return cut_fcvtzs(quotient); // the game's fcvtzs after the site } +// The physics solver step count (patches-v111.txt osolver and osolverfull, lifted by frame_step_check.py): the step +// setup's words from the literal load to the cut, run on the frames per update in s0 and the count (4) in s1 as the +// game loads them, x8 pointing at the literal 0.001 the original loads. Returns w1, the steps. +void solver_game(Cpu *c); +void solver_osolver(Cpu *c); +void solver_osolverfull(Cpu *c); +static uint32_t solver_steps(void (*fn)(Cpu *), float frames) { + static float literal[0x410 / 4]; + literal[0x40c / 4] = 0.001f; + Cpu c; + memset(&c, 0, sizeof c); + uint32_t bits; + memcpy(&bits, &frames, 4); + c.v[0] = bits, c.v[1] = 4, c.x[8] = (uint64_t)(uintptr_t)literal, c.x[1] = ~0ull; + fn(&c); + CHECK(c.x[1] >> 32 == 0, "the solver count leaves x1's upper half %#llx", (unsigned long long)c.x[1]); + return (uint32_t)c.x[1]; +} + int main(int argc, char **argv) { int mean = argc > 1 && !strcmp(argv[1], "mean"); if (mean) steady = "0"; @@ -341,6 +360,25 @@ int main(int argc, char **argv) { sound_gate_take(&g, 0, 0.0f), "NaN and 0 run as the game has them and clear what is owed"); fake_manager = 0; } + // The solver step count: at 60 and 30 fps all three give the game's 4 and 8 (also with the steady step a little + // off them); above 60 fps the game's count falls to 1 and to 0, osolver's rounds up (2 at 144 fps, 1 at 240), and + // osolverfull's keeps every update at the 60 Hz frame's 4. + { + const struct { float frames; uint32_t game, up, full; } want[] = { + {1.0f, 4, 4, 4}, {2.0f, 8, 8, 8}, {0.98f, 3, 4, 4}, {1.02f, 4, 4, 4}, {60.0f / 144.0f, 1, 2, 4}, + {60.0f / 160.0f, 1, 2, 4}, {0.25f, 1, 1, 4}, {60.0f / 300.0f, 0, 1, 4}, {1.5f, 6, 6, 6}}; + for (size_t i = 0; i < sizeof want / sizeof want[0]; i++) { + uint32_t g = solver_steps(solver_game, want[i].frames), u = solver_steps(solver_osolver, want[i].frames), + f = solver_steps(solver_osolverfull, want[i].frames); + CHECK(g == want[i].game && u == want[i].up && f == want[i].full, + "%.4f frames an update: solver steps %u / %u / %u, not %u / %u / %u", want[i].frames, g, u, f, + want[i].game, want[i].up, want[i].full); + } + // steps a second at 144 and 240 fps: osolver 288 and 240, osolverfull 576 and 960 + printf("frame_step_check: solver steps a second at 144 / 240 fps: physics_steps 1 %u / %u, 2 %u / %u\n", + 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)); + } 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 7f6fa691..5e132cc3 100644 --- a/tests/frame_step_check.py +++ b/tests/frame_step_check.py @@ -11,8 +11,10 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MSYS_BIN = Path(r'C:\msys64\ucrt64\bin') -sys.path[:0] = [str(ROOT / 'recomp')] +sys.path[:0] = [str(ROOT), str(ROOT / 'recomp')] +from lifter import Lifter # noqa: E402 from patches import load # noqa: E402 +from tools.check_content import GENERATED_MARKER # 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, @@ -54,17 +56,55 @@ def check_sites(): return bad +# The solver step count's words at the step setup's first copy (main+0x38aebe4..0x38aebf4; the second copy at +# main+0x38afd10 has the same words): the original's, the set osolver's and the set osolverfull's, lifted as the +# generator lifts them into one C function each, with the scvtf between them (main+0x38aebec) that every variant keeps. +SOLVER_SITES = (0x38aebe4, 0x38aebf0, 0x38aebf4) +SOLVER_COPY = (0x38afd10, 0x38afd1c, 0x38afd20) +SCVTF = 0x5e21d821 # scvtf s1, s1 + + +def solver_functions(): + table = load(ROOT / 'recomp/patches-v111.txt', [('main', 0x7000000000, PAGE)]) + names = {i: t[0] for i, t in enumerate(table.sets)} + variants = {'game': [], 'osolver': [], 'osolverfull': []} + for site, copy in zip(SOLVER_SITES, SOLVER_COPY): + alts = {names[key]: (words, have) for key, words, have in table.sites['main'][site]} + copies = {names[key]: (words, have) for key, words, have in table.sites['main'][copy]} + assert set(alts) == {'osolver', 'osolverfull'} and alts == copies, (hex(site), alts, copies) + original = alts['osolver'][1] + variants['game'].append([original]) + for name in ('osolver', 'osolverfull'): + variants[name].append(alts[name][0]) + lift = Lifter(0, simd='c') + out = [f'// {GENERATED_MARKER}: the solver step count words lifted by tests/frame_step_check.py.', + '#include "cpu.h"', '#include "simd.h"', ''] + for name, sites in variants.items(): + body = [] + for k, words in enumerate(sites): + for w in words: + insn = lift.lift(w, 0x7000000000 + SOLVER_SITES[k]) + assert insn.kind == 'op', (name, hex(w), insn.kind) + body.append(' ' + insn.code) + if k == 0: + body.append(' ' + lift.lift(SCVTF, 0x70038aebec).code) + out.append(f'void solver_{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()) env = dict(os.environ) if os.name == 'nt' and MSYS_BIN.exists(): env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') subprocess.check_call([compiler(), '-O1', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing', '-I', str(ROOT / 'runtime'), '-I', str(work), - str(ROOT / 'tests' / 'frame_step_check.c'), '-o', str(exe), '-lm'], env=env) + 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()