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)