mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Keep the fraction of a frame the ocarina recorder's clock was losing above 60 fps, in a new patch set orecord.
The ocarina manager counts the length of each note and rest in whole 60 Hz frames and adds the time manager's frames per update to that clock at four sites (main+0x32926c8, main+0x3293410, main+0x3296b08, main+0x3296da4) through fcvtzu, which cuts the step toward zero. Above 60 fps the step is under one frame, so the clock never moved: a recording kept its notes without their lengths (the recorder leaves a length of 0 out), the 360 and 120 frame limits never came, and a recording ended after a fixed 60 frames of its timer instead of following the clock; at the 40 and 45 fps limits it counted one a frame. The set replaces each fcvtzu with nine words that add the fraction carried on the patch page's +264, store what is left after the cut (NaN and negatives as 0) and convert the sum. Steps of one and two frames give the game's count with the fraction left at 0, so 30 and 60 fps are unchanged. The words call nothing and take no lock (one of the functions returns through an unsaved x30), and use only v16-v18, which the three functions never name. The set is appended so that no existing set's index moves; ocarina_timing = 0 gives the game's words. Not reproduced before the change (read from the code). The generator's check of the original words passed against the 1.1.1 main, the new tests/ocarina_timing_check.py passed (.venv, Unicorn), as did frame_step_check, text_speed_check, sprint_check, movie_timeline_check, cfg_parse_check, launcher_test and cfg_rows_check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -550,6 +550,17 @@ words each setting writes and the set it switches, the skip site's decide bit an
|
|||||||
player may skip and one the game keeps from skipping, the speed site's factor for that typewriter and no other, the
|
player may skip and one the game keeps from skipping, the speed site's factor for that typewriter and no other, the
|
||||||
registers and flags both sites leave alone, and a start that kept the set off. No game files.
|
registers and flags both sites leave alone, and a start that kept the set off. No game files.
|
||||||
|
|
||||||
|
A change to the ocarina recorder's clock (the set `orecord` of `recomp/patches-v111.txt`, `ocarina_timing`) passes
|
||||||
|
`python tests\ocarina_timing_check.py --cc <clang>` (`.venv`, Unicorn): it checks the set's four sites, their original
|
||||||
|
word and nine replacement words, the page word resolved to the patch page at each site and no other set's word on the
|
||||||
|
page's +264; it lifts the words and runs them against the game's word and a float32 model: steps of one and two frames
|
||||||
|
give the game's count with the fraction left at 0, the steps of the 40 to 240 fps limits count 3600 frames a minute
|
||||||
|
(0 and 1 in turn at 120, 5 in every 12 updates at 144, 1 in every 4 at 240), NaN, negative, infinite and huge steps
|
||||||
|
give the game's count, and only x8, v16-v18 and FPSR change; and it runs the nine words in Unicorn at each site against
|
||||||
|
the lifted C over 10,000 random steps and fractions, comparing registers and the page bit for bit. It does not cover
|
||||||
|
the sites inside their functions: a regenerate of the game code does, and its check of the original words. No game
|
||||||
|
files.
|
||||||
|
|
||||||
A change to the mouse camera (`runtime/mouse_direct.c`, the mouse path of `runtime/input.c`, the set `mouse_direct` of
|
A change to the mouse camera (`runtime/mouse_direct.c`, the mouse path of `runtime/input.c`, the set `mouse_direct` of
|
||||||
`recomp/patches-v111.txt` or `patches-v143.txt`) passes `python tests\mouse_direct_check.py --cc <clang> --deps
|
`recomp/patches-v111.txt` or `patches-v143.txt`) passes `python tests\mouse_direct_check.py --cc <clang> --deps
|
||||||
<ucrt64>\include`, which builds `mouse_direct.c`, and `input.c` with `--deps`, with the rest of the runtime stubbed and
|
<ucrt64>\include`, which builds `mouse_direct.c`, and `input.c` with `--deps`, with the rest of the runtime stubbed and
|
||||||
|
|||||||
@@ -69,6 +69,11 @@
|
|||||||
# on moving platforms (the Deku Tree's floating one). 1 makes the floor carry him as at 60 fps at any frame rate; 0 is
|
# on moving platforms (the Deku Tree's floating one). 1 makes the floor carry him as at 60 fps at any frame rate; 0 is
|
||||||
# the game's own cut. Read at start; needs game code generated with this release.
|
# the game's own cut. Read at start; needs game code generated with this release.
|
||||||
# floor_carry = 1
|
# floor_carry = 1
|
||||||
|
# The ocarina's recorder counts how long each note and rest lasts in 60 Hz frames, and the game cut each frame's step
|
||||||
|
# to a whole number: above 60 fps that was 0, so a recorded performance lost its rests and the recorder's time limits
|
||||||
|
# never came, and at the 40 and 45 fps limits it counted slow. 1 keeps the fraction for the next frame, 60 a second at
|
||||||
|
# any frame rate; 0 is the game's own count. Read at start; needs game code generated with this release.
|
||||||
|
# ocarina_timing = 1
|
||||||
# A movie's narration and fades follow the picture on screen: 1 counts their time from the shown picture's
|
# A movie's narration and fades follow the picture on screen: 1 counts their time from the shown picture's
|
||||||
# timestamp, so they stay with it at any frame rate (with movie_fps = 0 too) and wait when the picture stalls; 0 counts
|
# timestamp, so they stay with it at any frame rate (with movie_fps = 0 too) and wait when the picture stalls; 0 counts
|
||||||
# the game's redraws, as before, which keeps them with the music when the picture stalls. Read at start; needs game
|
# the game's redraws, as before, which keeps them with the music when the picture stalls. Read at start; needs game
|
||||||
|
|||||||
@@ -363,3 +363,26 @@ site sprint main c07f3c 39400108 39400108 950a63fd # ldrb w8, [x8] -> ldrb w8,
|
|||||||
set text_speed NX2NAVI_TEXT_SPEED_PATCH 1
|
set text_speed NX2NAVI_TEXT_SPEED_PATCH 1
|
||||||
site text_speed main 774b74 36000074 @page:x9 394d2a6a d100054a 8a0a010a f901012a 39483129 2a140129 36000069 # tbz w20, #0, #0x774b80 -> adrp x9, page; ldrb w10, [x19, #0x34a]; sub x10, x10, #1; and x10, x8, x10; str x10, [x9, #0x200]; ldrb w9, [x9, #0x20c]; orr w9, w9, w20; tbz w9, #0, #0x774b80
|
site text_speed main 774b74 36000074 @page:x9 394d2a6a d100054a 8a0a010a f901012a 39483129 2a140129 36000069 # tbz w20, #0, #0x774b80 -> adrp x9, page; ldrb w10, [x19, #0x34a]; sub x10, x10, #1; and x10, x8, x10; str x10, [x9, #0x200]; ldrb w9, [x9, #0x20c]; orr w9, w9, w20; tbz w9, #0, #0x774b80
|
||||||
site text_speed main afe1d8 bd404e61 bd404e61 @page:x22 f94102db cb13037b dac0137b d346ff7b 9e220362 bd420ac8 1e280842 1f020421 # ldr s1, [x19, #0x4c]; adrp x22, page; ldr x27, [x22, #0x200]; sub x27, x27, x19; clz x27, x27; lsr x27, x27, #6; scvtf s2, x27; ldr s8, [x22, #0x208]; fmul s2, s2, s8; fmadd s1, s1, s2, s1
|
site text_speed main afe1d8 bd404e61 bd404e61 @page:x22 f94102db cb13037b dac0137b d346ff7b 9e220362 bd420ac8 1e280842 1f020421 # ldr s1, [x19, #0x4c]; adrp x22, page; ldr x27, [x22, #0x200]; sub x27, x27, x19; clz x27, x27; lsr x27, x27, #6; scvtf s2, x27; ldr s8, [x22, #0x208]; fmul s2, s2, s8; fmadd s1, s1, s2, s1
|
||||||
|
|
||||||
|
# The ocarina recorder's clock: the ocarina manager (its update main+0x17bad0, the vtable slot at main+0x4ba19a8)
|
||||||
|
# keeps at +0x265fc of its object the length of the current note or rest in 60 Hz frames, and adds trunc(frames per
|
||||||
|
# update), the time manager's +0x10, to it at four sites (fcvtzu w8, s0): main+0x32926c8 and main+0x3293410 in two of
|
||||||
|
# the manager's update states (main+0x3292690, main+0x32932b0), main+0x3296b08 and main+0x3296da4 in its recording
|
||||||
|
# function (main+0x3296840, modes 10 and 13 at +0x230). At each recorded note the length goes into the recording as a
|
||||||
|
# word of its own (twice the length, into +0x300 or +0x498, left out when it is 0) and the clock starts again; the
|
||||||
|
# clock stops at 360 frames, and at 120 while +0x271 is set, and a recording ends once the float timer at +0x244
|
||||||
|
# passes min(clock + 60, 420) frames (main+0x329285c). Below one frame per update the cut is 0: above 60 fps the clock
|
||||||
|
# stood still, so recordings lost their rests, the clock's limits never came and the end of a recording came at a
|
||||||
|
# fixed 60 frames of that timer; at the 40 and 45 fps limits it counted one a frame. Each site carries the fraction to
|
||||||
|
# the next on the patch page's +264: s16 = step + fraction, the fraction becomes max(s16 - trunc(s16), 0) and
|
||||||
|
# w8 = trunc(s16), so the clock counts 60 a second at any frame rate, and a step of exactly one or two frames gives
|
||||||
|
# what the game computes with the fraction left at 0. A step that is NaN, negative or infinite gives the game's cut and
|
||||||
|
# leaves the fraction 0. Only the manager's update runs the four sites, so the page word needs no lock.
|
||||||
|
# main+0x3296840 returns through x30 without saving it, so these sites cannot call the page. x8 is each site's own
|
||||||
|
# result; s16, s17 and s18 are used nowhere in the three functions, s0 keeps the step, and no word reads or writes the
|
||||||
|
# flags.
|
||||||
|
set orecord NX2NAVI_OCARINA_TIMING 1
|
||||||
|
site orecord main 32926c8 1e390008 @page:x8 bd410910 1e302810 1e25c211 1e313a11 2f00e412 1e326a31 bd010911 1e390208 # fcvtzu w8, s0 -> adrp x8, page; ldr s16, [x8, #264]; fadd s16, s0, s16; frintz s17, s16; fsub s17, s16, s17; movi d18, #0; fmaxnm s17, s17, s18; str s17, [x8, #264]; fcvtzu w8, s16
|
||||||
|
site orecord main 3293410 1e390008 @page:x8 bd410910 1e302810 1e25c211 1e313a11 2f00e412 1e326a31 bd010911 1e390208 # the same at the other three sites
|
||||||
|
site orecord main 3296b08 1e390008 @page:x8 bd410910 1e302810 1e25c211 1e313a11 2f00e412 1e326a31 bd010911 1e390208
|
||||||
|
site orecord main 3296da4 1e390008 @page:x8 bd410910 1e302810 1e25c211 1e313a11 2f00e412 1e326a31 bd010911 1e390208
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ static int save_ok = 1; // the last cfg_save_to wrote (o
|
|||||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||||
static const char *const known[] = {
|
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", "PHYSICS_STEPS_FULL", "PICKUP_COUNT", "WATER_STEPS", "SOUND_STEPS", "FLOOR_PROBE", "FLOOR_SCAN", "FLOOR_WATCH", "FLOOR_CARRY", "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", "FLOOR_WATCH", "FLOOR_CARRY", "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)
|
||||||
|
"OCARINA_TIMING",
|
||||||
"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",
|
"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",
|
||||||
"AUDIO_STRICT",
|
"AUDIO_STRICT",
|
||||||
"BARRIER_GAME", "MESH_AHEAD", "MESH_FALLBACK", "MESH_FALLBACK_MB", "MESH_FALLBACK_WORKERS", "PROBE_MESHFB_DIR",
|
"BARRIER_GAME", "MESH_AHEAD", "MESH_FALLBACK", "MESH_FALLBACK_MB", "MESH_FALLBACK_WORKERS", "PROBE_MESHFB_DIR",
|
||||||
@@ -119,7 +120,7 @@ static const struct { const char *key, *section; } sections[] = {
|
|||||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"RTSS", "frame rate"},
|
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"RTSS", "frame rate"},
|
||||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||||
{"PHYSICS_STEPS", "frame rate"}, {"PICKUP_COUNT", "frame rate"}, {"WATER_STEPS", "frame rate"},
|
{"PHYSICS_STEPS", "frame rate"}, {"PICKUP_COUNT", "frame rate"}, {"WATER_STEPS", "frame rate"},
|
||||||
{"SOUND_STEPS", "frame rate"}, {"FLOOR_CARRY", "frame rate"},
|
{"SOUND_STEPS", "frame rate"}, {"FLOOR_CARRY", "frame rate"}, {"OCARINA_TIMING", "frame rate"},
|
||||||
{"MOVIE_PTS", "frame rate"}, {"STEP_STEADY", "frame rate"},
|
{"MOVIE_PTS", "frame rate"}, {"STEP_STEADY", "frame rate"},
|
||||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||||
{"GPU_INDEX", "display"}, {"MONITOR", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
{"GPU_INDEX", "display"}, {"MONITOR", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||||
@@ -279,6 +280,8 @@ static const CfgMeta metas[] = {
|
|||||||
// the set ofollow of patches-v111.txt (frame_rate.c floor_carry_store): a moving floor carries a character above
|
// the set ofollow of patches-v111.txt (frame_rate.c floor_carry_store): a moving floor carries a character above
|
||||||
// 100 fps as at 60
|
// 100 fps as at 60
|
||||||
{"FLOOR_CARRY", "1", "0|1", RESTART},
|
{"FLOOR_CARRY", "1", "0|1", RESTART},
|
||||||
|
// the set orecord of patches-v111.txt: the ocarina recorder's clock keeps each frame's fraction above 60 fps
|
||||||
|
{"OCARINA_TIMING", "1", "0|1", RESTART},
|
||||||
// frame_rate.c floor_probe: lines about Link on a moving floor (0 off, 1 = 4000 lines, or a count)
|
// frame_rate.c floor_probe: lines about Link on a moving floor (0 off, 1 = 4000 lines, or a count)
|
||||||
{"FLOOR_PROBE", "0", NULL, RESTART},
|
{"FLOOR_PROBE", "0", NULL, RESTART},
|
||||||
// frame_rate.c floor_scan: finds the moving floor under Link in guest memory (0 off, 1 = 20000 lines, or a count)
|
// frame_rate.c floor_scan: finds the moving floor under Link in guest memory (0 off, 1 = 20000 lines, or a count)
|
||||||
|
|||||||
350
tests/ocarina_timing_check.py
Normal file
350
tests/ocarina_timing_check.py
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
"""Check the set orecord of recomp/patches-v111.txt: the ocarina recorder's clock carries each update's fraction of a
|
||||||
|
60 Hz frame on the patch page's +264 instead of cutting it.
|
||||||
|
|
||||||
|
1. The table: the set's switch and default, its four sites, their original word and the nine replacement words, the
|
||||||
|
page word resolved to adrp x8 of the patch page at each site, and no other set's word loading or storing +264.
|
||||||
|
2. The lifted words (recomp/lifter.py, as the generator lifts them) against the game's word and a float32 model:
|
||||||
|
steps of one and two frames give the game's count with the fraction left at 0; the steps of the 40 to 240 fps
|
||||||
|
limits count 3600 frames a minute; NaN, negative, infinite and huge steps give the game's count; only x8, v16-v18
|
||||||
|
and FPSR change.
|
||||||
|
3. Unicorn: the nine words, the adrp included, run at each site against the lifted C over random steps and fractions:
|
||||||
|
registers and the page agree bit for bit.
|
||||||
|
|
||||||
|
Requires Unicorn, numpy and a C compiler (--cc or CC). No game files. Harness files stay in build/test/ocarina_timing.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import unicorn as uc
|
||||||
|
from unicorn import arm64_const as arm
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
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
|
||||||
|
|
||||||
|
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||||
|
BASE = 0x7000000000
|
||||||
|
SIZE = 0x4ea0000 # a stand-in main size: the patch page lands at main+0x4ea0000, as on 1.1.1
|
||||||
|
SITES = (0x32926c8, 0x3293410, 0x3296b08, 0x3296da4)
|
||||||
|
GAME = 0x1e390008 # fcvtzu w8, s0
|
||||||
|
WORDS = [('page', 8), 0xbd410910, 0x1e302810, 0x1e25c211, 0x1e313a11, 0x2f00e412, 0x1e326a31, 0xbd010911,
|
||||||
|
0x1e390208]
|
||||||
|
FRACTION = 264 # the page byte the words keep the fraction in
|
||||||
|
CPU_SIZE = 1024
|
||||||
|
X8, NZCV, FPCR, FPSR, V = 8 * 8, 272, 320, 328, 512
|
||||||
|
|
||||||
|
|
||||||
|
def compiler(cc):
|
||||||
|
if cc:
|
||||||
|
return cc
|
||||||
|
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
||||||
|
return str(MSYS_BIN / 'clang.exe')
|
||||||
|
return shutil.which('clang') or 'clang'
|
||||||
|
|
||||||
|
|
||||||
|
def f32(bits):
|
||||||
|
return np.frombuffer(struct.pack('<I', bits), dtype='<f4')[0]
|
||||||
|
|
||||||
|
|
||||||
|
def bits(value):
|
||||||
|
return struct.unpack('<I', np.float32(value).tobytes())[0]
|
||||||
|
|
||||||
|
|
||||||
|
def fcvtzu(value):
|
||||||
|
"""fcvtzu w, s: toward zero, NaN and negatives to 0, saturated at 2^32 - 1."""
|
||||||
|
if np.isnan(value) or value <= 0:
|
||||||
|
return 0
|
||||||
|
if value >= np.float32(4294967296.0):
|
||||||
|
return 0xffffffff
|
||||||
|
return int(np.trunc(value))
|
||||||
|
|
||||||
|
|
||||||
|
def model(step, fraction):
|
||||||
|
"""The words in float32 with round to nearest: (w8, the new fraction's bits)."""
|
||||||
|
with np.errstate(all='ignore'):
|
||||||
|
total = np.float32(step) + np.float32(fraction)
|
||||||
|
rest = total - np.trunc(total)
|
||||||
|
if np.isnan(rest):
|
||||||
|
rest = np.float32(0.0)
|
||||||
|
elif rest < 0 or (rest == 0 and np.signbit(rest)):
|
||||||
|
rest = np.float32(0.0)
|
||||||
|
return fcvtzu(total), bits(rest)
|
||||||
|
|
||||||
|
|
||||||
|
def check_table():
|
||||||
|
table = load(ROOT / 'recomp/patches-v111.txt', [('main', BASE, SIZE)])
|
||||||
|
names = [s[0] for s in table.sets]
|
||||||
|
assert table.sets[-1] == ('orecord', 'NX2NAVI_OCARINA_TIMING', 1), table.sets[-1]
|
||||||
|
key = names.index('orecord')
|
||||||
|
mine = sorted(off for off, alts in table.sites['main'].items() if any(k == key for k, _, _ in alts))
|
||||||
|
assert mine == list(SITES), [hex(s) for s in mine]
|
||||||
|
resolved = table.for_module('main', BASE)
|
||||||
|
for site in SITES:
|
||||||
|
alts = table.sites['main'][site]
|
||||||
|
assert len(alts) == 1, (hex(site), alts)
|
||||||
|
k, words, original = alts[0]
|
||||||
|
assert k == key and original == GAME and words == WORDS, (hex(site), words, hex(original))
|
||||||
|
(_, real), = resolved[BASE + site]
|
||||||
|
adrp = real[0]
|
||||||
|
imm = ((adrp >> 29) & 3) | ((adrp >> 5) & 0x7ffff) << 2
|
||||||
|
imm -= (imm & 1 << 20) << 1
|
||||||
|
target = ((BASE + site) & ~0xfff) + (imm << 12)
|
||||||
|
assert adrp & 0x9f00001f == 0x90000008 and target == table.page, (hex(site), hex(adrp), hex(target))
|
||||||
|
# No other set's word loads or stores the fraction's bytes through an unsigned offset.
|
||||||
|
for off, alts in table.sites['main'].items():
|
||||||
|
for k, words, _ in alts:
|
||||||
|
if k == key or not any(isinstance(w, tuple) for w in words):
|
||||||
|
continue
|
||||||
|
for w in words:
|
||||||
|
if isinstance(w, tuple) or w & 0x3b000000 != 0x39000000:
|
||||||
|
continue
|
||||||
|
scale = w >> 30
|
||||||
|
if (w >> 26) & 1 and (w >> 22) & 2:
|
||||||
|
scale = 4
|
||||||
|
start = ((w >> 10) & 0xfff) << scale
|
||||||
|
overlap = start < FRACTION + 4 and start + (1 << scale) > FRACTION
|
||||||
|
assert not overlap, (names[k], hex(off), hex(w))
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def build(cc, work):
|
||||||
|
lift = Lifter(0, simd='c')
|
||||||
|
export = '__declspec(dllexport) ' if os.name == 'nt' else ''
|
||||||
|
# Each function sets the host's floating-point mode from c->fpcr as the runtime does on entry to guest code
|
||||||
|
# (runtime/cpu.h rt_fpcr_sync) and puts Python's back after the words.
|
||||||
|
out = [f'// {GENERATED_MARKER}: the set orecord\'s words lifted by tests/ocarina_timing_check.py.',
|
||||||
|
'#include "cpu.h"', '',
|
||||||
|
'#if defined(__x86_64__)',
|
||||||
|
'#define ENTER unsigned saved = __builtin_ia32_stmxcsr(); rt_fpcr_sync(c);',
|
||||||
|
'#define LEAVE __builtin_ia32_ldmxcsr(saved);',
|
||||||
|
'#else',
|
||||||
|
'#define ENTER',
|
||||||
|
'#define LEAVE',
|
||||||
|
'#endif', '']
|
||||||
|
for i, site in enumerate(SITES):
|
||||||
|
body = []
|
||||||
|
for j, w in enumerate(WORDS[1:], 1): # x8 is the page: the caller sets it as the adrp would
|
||||||
|
insn = lift.lift(w, BASE + site + 4 * j)
|
||||||
|
assert insn.kind == 'op', (hex(w), insn.kind)
|
||||||
|
body.append(' ' + insn.code)
|
||||||
|
out.append(f'{export}void orecord_{i}(Cpu *c) {{\n ENTER\n' + '\n'.join(body) + '\n LEAVE\n}\n')
|
||||||
|
insn = lift.lift(GAME, BASE + SITES[0])
|
||||||
|
out.append(f'{export}void game_word(Cpu *c) {{\n ENTER\n {insn.code}\n LEAVE\n}}\n')
|
||||||
|
source = work / 'orecord.c'
|
||||||
|
library = work / ('orecord.dll' if os.name == 'nt' else 'orecord.so')
|
||||||
|
source.write_text('\n'.join(out))
|
||||||
|
env = dict(os.environ)
|
||||||
|
if os.name == 'nt' and MSYS_BIN.exists():
|
||||||
|
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||||
|
subprocess.run([cc, '-shared', '-O2', '-fno-strict-aliasing', '-I', str(ROOT / 'runtime'),
|
||||||
|
*([] if os.name == 'nt' else ['-fPIC']), str(source), '-o', str(library)], check=True, env=env)
|
||||||
|
if os.name == 'nt' and MSYS_BIN.exists() and hasattr(os, 'add_dll_directory'):
|
||||||
|
os.add_dll_directory(str(MSYS_BIN))
|
||||||
|
dll = ctypes.CDLL(str(library))
|
||||||
|
fns = []
|
||||||
|
for name in [f'orecord_{i}' for i in range(len(SITES))] + ['game_word']:
|
||||||
|
fn = getattr(dll, name)
|
||||||
|
fn.argtypes = [ctypes.c_void_p]
|
||||||
|
fn.restype = None
|
||||||
|
fns.append(fn)
|
||||||
|
return fns[:-1], fns[-1]
|
||||||
|
|
||||||
|
|
||||||
|
class Harness:
|
||||||
|
"""A Cpu and a page buffer for the lifted words; x8 is set to the page before each call."""
|
||||||
|
|
||||||
|
def __init__(self, sites, game):
|
||||||
|
self.sites, self.game = sites, game
|
||||||
|
self.raw = ctypes.create_string_buffer(CPU_SIZE + 16)
|
||||||
|
self.cpu = (ctypes.addressof(self.raw) + 15) & ~15
|
||||||
|
self.page = ctypes.create_string_buffer(4096)
|
||||||
|
|
||||||
|
def set_v(self, n, value):
|
||||||
|
ctypes.memmove(self.cpu + V + 16 * n, value.to_bytes(16, 'little'), 16)
|
||||||
|
|
||||||
|
def get_v(self, n):
|
||||||
|
return int.from_bytes(ctypes.string_at(self.cpu + V + 16 * n, 16), 'little')
|
||||||
|
|
||||||
|
def x8(self):
|
||||||
|
return struct.unpack('<Q', ctypes.string_at(self.cpu + X8, 8))[0]
|
||||||
|
|
||||||
|
def fraction(self):
|
||||||
|
return struct.unpack_from('<I', self.page.raw, FRACTION)[0]
|
||||||
|
|
||||||
|
def set_fraction(self, b):
|
||||||
|
struct.pack_into('<I', self.page, FRACTION, b)
|
||||||
|
|
||||||
|
def run(self, step_bits, site=0, game=False):
|
||||||
|
"""One call; returns w8. The Cpu changes only in x8, v16-v18 and FPSR, and the page only at +264."""
|
||||||
|
self.set_v(0, step_bits)
|
||||||
|
if not game:
|
||||||
|
ctypes.memmove(self.cpu + X8, struct.pack('<Q', ctypes.addressof(self.page)), 8)
|
||||||
|
before = ctypes.string_at(self.cpu, CPU_SIZE)
|
||||||
|
page = self.page.raw
|
||||||
|
(self.game if game else self.sites[site])(self.cpu)
|
||||||
|
after = ctypes.string_at(self.cpu, CPU_SIZE)
|
||||||
|
keep = [(0, X8), (X8 + 8, FPSR), (FPSR + 8, V + 16 * 16), (V + 19 * 16, CPU_SIZE)]
|
||||||
|
for lo, hi in keep:
|
||||||
|
assert before[lo:hi] == after[lo:hi], ('Cpu bytes changed', lo, hi)
|
||||||
|
if game:
|
||||||
|
assert self.page.raw == page
|
||||||
|
else:
|
||||||
|
assert self.page.raw[:FRACTION] == page[:FRACTION] and self.page.raw[FRACTION + 4:] == page[FRACTION + 4:]
|
||||||
|
w8 = self.x8()
|
||||||
|
assert w8 >> 32 == 0, hex(w8)
|
||||||
|
return w8
|
||||||
|
|
||||||
|
|
||||||
|
def check_lifted(h):
|
||||||
|
rng = random.Random(264)
|
||||||
|
# Whole steps: the game's count, and the fraction stays +0.0.
|
||||||
|
for step, want in ((1.0, 1), (2.0, 2)):
|
||||||
|
h.set_fraction(0)
|
||||||
|
for _ in range(1000):
|
||||||
|
h.set_v(16, rng.getrandbits(128))
|
||||||
|
assert h.run(bits(step)) == want and h.run(bits(step), game=True) == want
|
||||||
|
assert h.fraction() == 0, hex(h.fraction())
|
||||||
|
# The limits' steps: 3600 frames a minute, each update the floor or the ceiling of the step.
|
||||||
|
patterns = {120: [0, 1], 240: [0, 0, 0, 1]}
|
||||||
|
for hz in (40, 45, 90, 120, 144, 165, 240):
|
||||||
|
step = bits(np.float32(60.0 / hz))
|
||||||
|
h.set_fraction(0)
|
||||||
|
lo, hi = int(np.floor(f32(step))), int(np.ceil(f32(step)))
|
||||||
|
game = h.run(step, game=True)
|
||||||
|
assert game == (1 if hz < 60 else 0), (hz, game)
|
||||||
|
seq, fraction = [], 0
|
||||||
|
for k in range(60 * hz):
|
||||||
|
w = h.run(step, site=k % len(SITES))
|
||||||
|
want, fraction = model(f32(step), f32(fraction))
|
||||||
|
assert (w, h.fraction()) == (want, fraction), (hz, k, w, want, hex(h.fraction()), hex(fraction))
|
||||||
|
assert lo <= w <= hi, (hz, k, w)
|
||||||
|
seq.append(w)
|
||||||
|
assert abs(sum(seq) - 3600) <= 1, (hz, sum(seq))
|
||||||
|
if hz in patterns:
|
||||||
|
p = patterns[hz]
|
||||||
|
assert seq == p * (len(seq) // len(p)), (hz, seq[:16])
|
||||||
|
if hz == 144: # 5 in every 12 updates; the float step is a little under 5/12, so the first 12 count 4
|
||||||
|
blocks = [sum(seq[i:i + 12]) for i in range(0, len(seq), 12)]
|
||||||
|
assert blocks[0] in (4, 5) and all(b == 5 for b in blocks[1:]), (hz, sorted(set(blocks)))
|
||||||
|
# A carried fraction: 0.75 + 0.5 gives one frame and leaves 0.25.
|
||||||
|
h.set_fraction(bits(0.75))
|
||||||
|
assert h.run(bits(0.5)) == 1 and h.fraction() == bits(0.25)
|
||||||
|
# Steps the game never gives: the game's count, a fraction in [0, 1), and the next whole step counts one.
|
||||||
|
specials = [0x7fc00000, 0xffc00000, bits(-0.5), 0x80000000, 0, 0x7f800000, 0xff800000, bits(1e10), bits(-1e10)]
|
||||||
|
for start in (0, bits(0.75)):
|
||||||
|
for s in specials:
|
||||||
|
h.set_fraction(start)
|
||||||
|
game = h.run(s, game=True)
|
||||||
|
assert h.run(s) == game == fcvtzu(f32(s)), (hex(start), hex(s), game)
|
||||||
|
f = f32(h.fraction())
|
||||||
|
assert not np.isnan(f) and 0 <= f < 1 and not np.signbit(f), (hex(start), hex(s), hex(h.fraction()))
|
||||||
|
if start == 0 and s not in (0, 0x80000000, bits(1e10)):
|
||||||
|
assert h.fraction() == 0, (hex(s), hex(h.fraction()))
|
||||||
|
assert h.run(bits(1.0)) == 1
|
||||||
|
# v0 keeps the step, its upper lanes too.
|
||||||
|
h.set_fraction(0)
|
||||||
|
v0 = (rng.getrandbits(96) << 32) | bits(0.25)
|
||||||
|
h.set_v(0, v0)
|
||||||
|
ctypes.memmove(h.cpu + X8, struct.pack('<Q', ctypes.addressof(h.page)), 8)
|
||||||
|
h.sites[0](h.cpu)
|
||||||
|
assert h.get_v(0) == v0
|
||||||
|
return 60 * (40 + 45 + 90 + 120 + 144 + 165 + 240)
|
||||||
|
|
||||||
|
|
||||||
|
def check_unicorn(h, table, count):
|
||||||
|
page = table.page
|
||||||
|
resolved = table.for_module('main', BASE)
|
||||||
|
emu = uc.Uc(uc.UC_ARCH_ARM64, uc.UC_MODE_ARM)
|
||||||
|
emu.ctl_set_cpu_model(arm.UC_CPU_ARM64_A57)
|
||||||
|
emu.reg_write(arm.UC_ARM64_REG_CPACR_EL1, 0x300000) # FP and SIMD on
|
||||||
|
code_lo, code_hi = (BASE + min(SITES)) & ~0xfff, (BASE + max(SITES) + 0x1000) & ~0xfff
|
||||||
|
emu.mem_map(code_lo, code_hi - code_lo)
|
||||||
|
emu.mem_map(page, 0x1000)
|
||||||
|
for site in SITES:
|
||||||
|
(_, words), = resolved[BASE + site]
|
||||||
|
emu.mem_write(BASE + site, struct.pack('<9I', *words))
|
||||||
|
xregs = [getattr(arm, f'UC_ARM64_REG_X{i}') for i in range(31)]
|
||||||
|
qregs = [getattr(arm, f'UC_ARM64_REG_Q{i}') for i in range(32)]
|
||||||
|
rng = random.Random(0x265fc)
|
||||||
|
specials = [0x7fc00000, 0xffc00000, bits(-0.5), 0x80000000, 0, 0x7f800000, 0xff800000, bits(1e10), 0x00000001,
|
||||||
|
0x7f7fffff, bits(1.0), bits(2.0), bits(0.5), bits(np.float32(60.0 / 144))]
|
||||||
|
cases = [(s, f) for s in specials for f in (0, bits(0.75), bits(np.nextafter(np.float32(1), np.float32(0))))]
|
||||||
|
cases += [(bits(rng.uniform(0, 4)), bits(rng.uniform(0, 1))) for _ in range(count)]
|
||||||
|
for n, (step, fraction) in enumerate(cases):
|
||||||
|
if f32(fraction) >= 1:
|
||||||
|
fraction = 0
|
||||||
|
i = n % len(SITES)
|
||||||
|
x = [rng.getrandbits(64) for _ in range(31)]
|
||||||
|
v = [rng.getrandbits(128) for _ in range(32)]
|
||||||
|
v[0] = (v[0] & ~0xffffffff) | step
|
||||||
|
nzcv = rng.randrange(16) << 28
|
||||||
|
fpcr = rng.choice([0, 0, 0, 1 << 24, 1 << 25, 3 << 24])
|
||||||
|
image = bytearray(rng.getrandbits(8) for _ in range(16)) * 256
|
||||||
|
struct.pack_into('<I', image, FRACTION, fraction)
|
||||||
|
# Unicorn: the nine words from the site's address.
|
||||||
|
emu.mem_write(page, bytes(image))
|
||||||
|
for reg, value in zip(xregs, x):
|
||||||
|
emu.reg_write(reg, value)
|
||||||
|
for reg, value in zip(qregs, v):
|
||||||
|
emu.reg_write(reg, value)
|
||||||
|
emu.reg_write(arm.UC_ARM64_REG_NZCV, nzcv)
|
||||||
|
emu.reg_write(arm.UC_ARM64_REG_FPCR, fpcr)
|
||||||
|
emu.emu_start(BASE + SITES[i], BASE + SITES[i] + 36)
|
||||||
|
# The lifted C: x8 set to the page buffer as the adrp sets it to the page.
|
||||||
|
x_c = list(x)
|
||||||
|
x_c[8] = ctypes.addressof(h.page)
|
||||||
|
cpu = struct.pack('<31Q', *x_c) + bytes(8 + 8 + 8) + struct.pack('<Q', nzcv)
|
||||||
|
ctypes.memmove(h.cpu, cpu + bytes(CPU_SIZE - len(cpu)), CPU_SIZE)
|
||||||
|
ctypes.memmove(h.cpu + FPCR, struct.pack('<Q', fpcr), 8)
|
||||||
|
for k, value in enumerate(v):
|
||||||
|
h.set_v(k, value)
|
||||||
|
ctypes.memmove(ctypes.addressof(h.page), bytes(image), len(image))
|
||||||
|
h.sites[i](h.cpu)
|
||||||
|
want_x = [emu.reg_read(reg) for reg in xregs]
|
||||||
|
have_x = list(struct.unpack('<31Q', ctypes.string_at(h.cpu, 31 * 8)))
|
||||||
|
assert have_x == want_x, (hex(step), hex(fraction), [(k, hex(a), hex(b)) for k, (a, b) in
|
||||||
|
enumerate(zip(have_x, want_x)) if a != b])
|
||||||
|
for k, reg in enumerate(qregs):
|
||||||
|
assert h.get_v(k) == emu.reg_read(reg), (hex(step), hex(fraction), hex(fpcr), k, hex(h.get_v(k)),
|
||||||
|
hex(emu.reg_read(reg)))
|
||||||
|
for k in (16, 17, 18):
|
||||||
|
assert h.get_v(k) >> 32 == 0, (k, hex(h.get_v(k)))
|
||||||
|
assert emu.reg_read(arm.UC_ARM64_REG_NZCV) & 0xf0000000 == nzcv
|
||||||
|
assert struct.unpack('<Q', ctypes.string_at(h.cpu + NZCV, 8))[0] == nzcv
|
||||||
|
mem = bytes(emu.mem_read(page, 0x1000))
|
||||||
|
assert h.page.raw == mem, (hex(step), hex(fraction))
|
||||||
|
assert mem[:FRACTION] == bytes(image[:FRACTION]) and mem[FRACTION + 4:] == bytes(image[FRACTION + 4:])
|
||||||
|
if fpcr == 0:
|
||||||
|
assert (want_x[8], struct.unpack_from('<I', mem, FRACTION)[0]) == model(f32(step), f32(fraction)), \
|
||||||
|
(hex(step), hex(fraction))
|
||||||
|
return len(cases)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
parser.add_argument('--cc', default=os.environ.get('CC'))
|
||||||
|
parser.add_argument('--count', type=int, default=10000, help='random steps compared with Unicorn')
|
||||||
|
args = parser.parse_args()
|
||||||
|
table = check_table()
|
||||||
|
work = ROOT / 'build/test/ocarina_timing'
|
||||||
|
work.mkdir(parents=True, exist_ok=True)
|
||||||
|
sites, game = build(compiler(args.cc), work)
|
||||||
|
h = Harness(sites, game)
|
||||||
|
calls = check_lifted(h)
|
||||||
|
compared = check_unicorn(h, table, args.count)
|
||||||
|
print(f'Ocarina timing: the set orecord\'s four sites and words; {calls} lifted calls at the 40 to 240 fps steps '
|
||||||
|
f'count 3600 frames a minute; {compared} C/Unicorn comparisons agree')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user