Files
NX2navi/tests/frame_step_check.py
chrg6 328554e7c6 Run the sound manager's job at 60 Hz with whole steps above 60 fps.
The job (main+0x415300) hands the time manager's step to the sound system in its phase 0 and runs the sound objects'
and the system's updates in phases 1 and 2. With the set ostep that step is a fraction of a frame above 60 fps, and fed
fractions the game has been reported to keep sounds held after an area unloaded until no voice was free, new sounds
and then the music going silent until a restart. The new set osound (sound_steps, on by default) calls the page's
+0xf10 after the job's prologue: phase 0 adds the step to what the job is owed and runs once a whole step has built
up, phases 1 and 2 follow that choice, and a job that does not run gets phase 3, which returns through its own
epilogue. After phase 0's load of the step the page's +0xf00 makes a fraction one whole step. At 60 fps and below
the job runs every frame with the game's step. Both sites were checked against the module; frame_step_check covers
the gate at 60, 30, 144 and 240 fps. Needs the game code generated again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 05:54:58 -05:00

74 lines
3.2 KiB
Python

"""Build and run tests/frame_step_check.c: the step runtime/frame_rate.c gives Ocarina, with the rest of the runtime
stubbed. No game files: the module layout the runtime includes is a stand-in written to the test's build folder.
Usage: frame_step_check.py
"""
import os
import shutil
import subprocess
import sys
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),
('osound', 0x41531c, 0x910083fd, [0x910083fd], 0xf10), ('osound', 0x415334, 0xbd401100, [0xbd401100], 0xf00)]
def compiler():
if os.environ.get('CC'):
return os.environ['CC']
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
LAYOUT = '''// A stand-in for the generated layout.h (tests/frame_step_check.py).
#pragma once
#define MOD_MAIN_BASE 0x7000000000ull
#define MOD_MAIN_SIZE 0x4ea0000ull
'''
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)
(work / 'layout.h').write_text(LAYOUT)
exe = work / ('frame_step_check.exe' if os.name == 'nt' else 'frame_step_check')
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)
rc = subprocess.run([str(exe)], env=env).returncode
return subprocess.run([str(exe), 'mean'], env=env).returncode or rc or check_sites()
if __name__ == '__main__':
sys.exit(main())