Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
55 changes: 53 additions & 2 deletions games/totk/runtime/game_patches_ns2.c
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -9,10 +9,58 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <TargetConditionals.h>
#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) */
Expand Down Expand Up @@ -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");
Expand Down
18 changes: 18 additions & 0 deletions games/totk/runtime/oot_frame_step.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <math.h>
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);
}
46 changes: 46 additions & 0 deletions tests/oot_frame_step_probe.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <sys/mman.h>
#include <libkern/OSCacheControl.h>
#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)":"");
}
24 changes: 24 additions & 0 deletions tests/test_oot_frame_step.py
Original file line number Diff line number Diff line change
@@ -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)