From 22bacba42760b7144c7b018fcc37977c16da942c Mon Sep 17 00:00:00 2001 From: baromas Date: Sat, 7 Nov 2026 23:00:17 +0300 Subject: [PATCH] fix: account for measured frame time in OoT game stepping Use the existing render timestamp callback and guard the original ARM64 instruction and stub padding. Keep the original frame step available through DM_FRAME_DELTA=0. --- THIRD_PARTY_NOTICES.md | 2 + games/totk/runtime/game_patches_ns2.c | 55 ++++++++++++++++++++++++++- games/totk/runtime/oot_frame_step.h | 18 +++++++++ tests/oot_frame_step_probe.c | 46 ++++++++++++++++++++++ tests/test_oot_frame_step.py | 24 ++++++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 games/totk/runtime/oot_frame_step.h create mode 100644 tests/oot_frame_step_probe.c create mode 100644 tests/test_oot_frame_step.py diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 935f6d207..92f008a8e 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -36,3 +36,5 @@ Fetched or linked at build time (not in this repository): | [ALVR](https://github.com/alvr-org/ALVR) client v20.14.0, downloaded (SHA-256 checked) and installed on the headset by `zeldaVR/install_vr.sh` | shows the VR picture on the Meta Quest 3 | MIT | | [Noto Sans CJK SC](https://github.com/notofonts/noto-cjk) (Source Han Sans) DemiLight at `165c01b`, downloaded and checked by `windows_games/ra2/scripts/languages.py` | the Simplified Chinese glyphs it draws into the player's game font | SIL OFL 1.1 | | [OpenCC](https://github.com/BYVoid/OpenCC), run (not included) by `windows_games/ra2/scripts/languages.py` | Traditional to Simplified Chinese | Apache-2.0 | + +OoT measured frame-step producer follows MIT [nxnative PR #1543](https://github.com/seedds/nxnative/pull/1543), `c254ba960d3b8eaf1eaf6827cab5e307fe77bc2e`. The checked native veneer and ARM64 fixture are implemented for this runtime. diff --git a/games/totk/runtime/game_patches_ns2.c b/games/totk/runtime/game_patches_ns2.c index 11d82aca7..4e53a5dbd 100644 --- a/games/totk/runtime/game_patches_ns2.c +++ b/games/totk/runtime/game_patches_ns2.c @@ -1,5 +1,5 @@ /* Switch 2 games (GAME_NS2 in runtime/game_version.h; first Donkey Kong Bananza, GAME_DKB, docs/ns2-port.md): the patch file - the Makefile builds in place of TOTK's game_patches.c. No game-code site is patched: the game runs its own frame pacing + the Makefile builds in place of TOTK's game_patches.c. Only OoT has a guarded measured-time producer; the other games run their own frame pacing and output size, and the output-size and aspect hooks the frontend calls are inert. Exclusive loads take acquire, as on Breath of the Wild and Super Mario Bros. Wonder (games/wonder/runtime/game_patches_wonder.c): sead's spin locks, which Bananza's engine (sead / agl / gsys) uses too, take the lock with a plain ldxr. */ @@ -9,10 +9,58 @@ #include #include #include +#include +#ifdef GAME_OOT +#include "oot_frame_step.h" +static _Atomic uint32_t *oot_seconds; +static OotFrameStep oot_clock; +static void oot_realtime_patch(Module *m) { + /* v131072 main e0f3ccfaf50065ae4ea01eb5aeb13b1a only (game_version.h). + nxnative PR1543: the original fdiv constructs 60/target without a clock. + Keep it, then take max(original, s1 * measured seconds). x16/x17/s0 are + dead after this site. The veneer does not change NZCV. + Use the loader's private page and text padding, like TOTK's producer. */ + const uint64_t site = 0x6e6cc4, stub = (m->text_end + 15) & ~15ULL; + const uint64_t data = (m->file_size + 0x3fff) & ~0x3fffULL; + if (site + 4 > m->text_end || data + 0x4000 != m->size || + stub + 24 > ((m->text_end + 0x3fff) & ~0x3fffULL)) runtime_fail("OoT frame-step layout"); + int64_t pages = (int64_t)(data >> 12) - (int64_t)((stub + 4) >> 12); + int64_t jump = (int64_t)stub - site, back = (int64_t)(site + 4) - (int64_t)(stub + 20); + if (pages < -(1 << 20) || pages >= (1 << 20) || + jump < -(1 << 27) || jump >= (1 << 27) || back < -(1 << 27) || back >= (1 << 27)) runtime_fail("OoT frame-step branch range"); + uint32_t imm = (uint32_t)pages & 0x1fffff; + uint32_t code[] = {0x1e201828, /* fdiv s8,s1,s0 */ + 0x90000010 | ((imm & 3) << 29) | ((imm >> 2) << 5), /* adrp x16,data */ + 0xbd400200, 0x1e210800, 0x1e204908, /* ldr s0,[x16]; fmul s0,s0,s1; fmax s8,s8,s0 */ + 0x14000000 | ((uint32_t)(back >> 2) & 0x3ffffff)}; + uint32_t branch = 0x14000000 | ((uint32_t)(jump >> 2) & 0x3ffffff), old; + memcpy(&old, m->base + site, 4); +#if TARGET_OS_IPHONE + if (old != branch || memcmp(m->base + stub, code, sizeof code)) runtime_fail("OoT embedded frame-step mismatch"); +#else + if (old != 0x1e201828) runtime_fail("OoT frame-step original instruction mismatch"); + for (unsigned i = 0; i < sizeof code; i++) if (m->base[stub + i]) runtime_fail("OoT frame-step padding not empty"); + memcpy(m->base + stub, code, sizeof code); memcpy(m->base + site, &branch, 4); +#endif + oot_seconds = (_Atomic uint32_t *)(void *)(m->base + data); + atomic_store(oot_seconds, 0); + fprintf(stderr, "[oot] checked measured frame step enabled (DM_FRAME_DELTA=0: original fixed step)\n"); +} +#endif /* No frame-step patch: these are what the frontend and the frame lock call. */ void runtime_frame_step_pin(double ms) { (void)ms; } -void runtime_frame_step_update(uint64_t now_ns) { (void)now_ns; } +void runtime_frame_step_update(uint64_t now_ns) { +#ifdef GAME_OOT + if (!oot_seconds) return; + const char *enabled = getenv("DM_FRAME_DELTA"); + float seconds = enabled && !strcmp(enabled, "0") ? 0 : oot_frame_seconds(&oot_clock, now_ns); + uint32_t bits; memcpy(&bits, &seconds, 4); + atomic_store_explicit(oot_seconds, bits, memory_order_release); +#else + (void)now_ns; +#endif +} int runtime_output_res(unsigned *w, unsigned *h) { (void)w; (void)h; return 0; } int runtime_render_res(unsigned *w, unsigned *h) { (void)w; (void)h; return 0; } /* no larger scene size for the heaps (romfs.c) */ @@ -50,6 +98,9 @@ static unsigned excl_acquire(uint8_t *code, size_t n) { void runtime_game_patches(Module *m) { if (strcmp(m->name, "main")) runtime_fail("game patches expect the main module"); +#ifdef GAME_OOT + oot_realtime_patch(m); +#endif if (getenv("TOTK_PCHTXT") || getenv("TOTK_ASPECT") || (getenv("TOTK_OUTPUT_RES") && strcmp(getenv("TOTK_OUTPUT_RES"), "0"))) fprintf(stderr, "[totk] %s: TOTK_PCHTXT, TOTK_ASPECT and TOTK_OUTPUT_RES are not supported; ignored\n", GAME_NAME); const char *a = getenv("TOTK_EXCL_ACQUIRE"); diff --git a/games/totk/runtime/oot_frame_step.h b/games/totk/runtime/oot_frame_step.h new file mode 100644 index 000000000..cf1d25950 --- /dev/null +++ b/games/totk/runtime/oot_frame_step.h @@ -0,0 +1,18 @@ +/* Measured step, adapted from seedds/nxnative PR1543, c254ba96 (MIT). + Only OoT's checked 60/target producer consumes it; this is not a host FPS cap. */ +#pragma once +#include +#include +enum { OOT_FRAME_SAMPLES = 8 }; +typedef struct { uint64_t last; double samples[OOT_FRAME_SAMPLES]; unsigned n, i; } OotFrameStep; +static inline float oot_frame_seconds(OotFrameStep *s, uint64_t now) { + uint64_t previous = s->last; s->last = now; + if (!previous || now <= previous) { s->n = s->i = 0; return 0; } + double dt = (double)(now - previous) * 1e-9, total = 0; + s->samples[s->i] = fmin(dt, 0.1); + s->i = (s->i + 1) % OOT_FRAME_SAMPLES; + if (s->n < OOT_FRAME_SAMPLES) s->n++; + for (unsigned i = 0; i < s->n; i++) total += s->samples[i]; + double mean = total / s->n; + return mean * 60 < 1.1 ? 0 : (float)fmin(mean, 4.0 / 60); +} diff --git a/tests/oot_frame_step_probe.c b/tests/oot_frame_step_probe.c new file mode 100644 index 000000000..ca56c1fbb --- /dev/null +++ b/tests/oot_frame_step_probe.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include +#include +#include "../games/totk/runtime/game_patches_ns2.c" +static int checks; +_Noreturn void runtime_fail(const char *s) { fprintf(stderr,"%s\n",s); exit(2); } +#define check(ok) do { if (!(ok)) { fprintf(stderr,"check line %d failed\n",__LINE__); runtime_fail("frame step assertion"); } checks++; } while(0) +extern float execute_step(void *, float, float); +__asm__(".text\n.align 2\n_execute_step:\n" + "stp x29,x30,[sp,#-32]!\nstr d8,[sp,#16]\nblr x0\n" + "ldr d8,[sp,#16]\nldp x29,x30,[sp],#32\nret\n"); +int main(int argc,char **argv) { + const size_t size=0x804000, site=0x6e6cc4; + uint8_t *base=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_ANON|MAP_PRIVATE,-1,0); + check(base!=MAP_FAILED); + Module m={.name="main",.base=base,.text_end=0x6fff00,.file_size=0x800000,.size=size,.patch_off=0x700000}; + uint32_t original=0x1e201828, tail[]={0x1e204100,0xd65f03c0}; /* fmov s0,s8; ret */ + memcpy(base+site,&original,4);memcpy(base+site+4,tail,sizeof tail); + if(argc>1&&!strcmp(argv[1],"bad-site"))base[site]^=1; + if(argc>1&&!strcmp(argv[1],"bad-padding"))base[m.text_end]=1; + runtime_game_patches(&m); + check(!mprotect(base,0x704000,PROT_READ|PROT_EXEC));sys_icache_invalidate(base,0x704000); + check(execute_step(base+site,60,60)==1); + uint64_t now=1000000000;runtime_frame_step_update(now); + for(int i=0;i<40;i++){now+=50000000;runtime_frame_step_update(now);} + int original_mode=getenv("DM_FRAME_DELTA")&&!strcmp(getenv("DM_FRAME_DELTA"),"0"); + check(fabsf(execute_step(base+site,60,60)-(original_mode?1:3))<1e-5); + check(fabsf(execute_step(base+site,30,30)-(original_mode?1:1.5f))<1e-5); + for(int i=0;i<40;i++){now+=16666667;runtime_frame_step_update(now);} + check(execute_step(base+site,60,60)==1); + check(execute_step(base+site,60,30)==0.5f); + if(!original_mode){ + for(int i=0;i<40;i++){now+=1000000000;runtime_frame_step_update(now);} + check(fabsf(execute_step(base+site,60,60)-4)<1e-5); + now-=1000000000;runtime_frame_step_update(now); + check(execute_step(base+site,60,60)==1); + } + OotFrameStep clock={0};now=1000000000;check(oot_frame_seconds(&clock,now)==0); + for(int fps=15;fps<=120;fps+=15){clock=(OotFrameStep){0};oot_frame_seconds(&clock,now); + for(int i=0;i<1000;i++){now+=1000000000/fps;float step=oot_frame_seconds(&clock,now); + double want=fps>=60?0:fmin(1.0/fps,4.0/60);check(fabs(step-want)<1e-6);}} + munmap(base,size);printf("%d native frame-step checks passed%s\n",checks,original_mode?" (original-step control)":""); +} diff --git a/tests/test_oot_frame_step.py b/tests/test_oot_frame_step.py new file mode 100644 index 000000000..00775003f --- /dev/null +++ b/tests/test_oot_frame_step.py @@ -0,0 +1,24 @@ +"""Native ARM64 execution of the production OoT timing veneer; no game files.""" +import os +from pathlib import Path +import platform +import subprocess +import tempfile +import unittest +ROOT=Path(__file__).resolve().parents[1] +@unittest.skipUnless(platform.system()=='Darwin' and platform.machine()=='arm64','native Apple silicon required') +class OotFrameStep(unittest.TestCase): + def test_measured_time_original_control_and_instruction_guards(self): + with tempfile.TemporaryDirectory() as td: + exe=Path(td)/'probe' + built=subprocess.run(['xcrun','clang','-O2','-DGAME_OOT=1','-DGAME_NS2=1',str(ROOT/'tests/oot_frame_step_probe.c'),'-o',str(exe)],capture_output=True,text=True,timeout=60) + self.assertEqual(built.returncode,0,built.stderr) + env={k:v for k,v in os.environ.items() if not k.startswith(('TOTK_','DM_'))} + for extra in ({},{'DM_FRAME_DELTA':'0'}): + ran=subprocess.run([str(exe)],env=env|extra,capture_output=True,text=True,timeout=20) + self.assertEqual(ran.returncode,0,ran.stdout+ran.stderr) + self.assertIn('native frame-step checks passed',ran.stdout) + for mode,reason in [('bad-site','original instruction mismatch'),('bad-padding','padding not empty')]: + ran=subprocess.run([str(exe),mode],env=env,capture_output=True,text=True,timeout=20) + self.assertEqual(ran.returncode,2,ran.stderr) + self.assertIn(reason,ran.stderr)