mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Add physics_steps = 2: at least a 60 fps frame's solver steps in every update on 1.1.1.
Link is carried by the Deku Tree's floating platform fully at 60 fps, about a tenth at 160 and not at all at 240, which follows the solver steps an update (4, 2, 1 with physics_steps = 1) rather than those a second. The new set osolverfull raises the rounded-up count to the 60 Hz frame's 4 in every update; physics_steps = 2 switches it on and osolver off at start, and the mode is logged. 60 and 30 fps keep the game's 4 and 8. It costs more physics time above 60 fps: 576 solver steps a second at 144 fps and 960 at 240, against 288 and 240 with 1, the default, which stays until a run confirms the carry. frame_step_check runs the lifted words of all three counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user