Boot 1.4.3 to its first NVN call with the 1.4.3 patch table, loader values and SDK 20.5 swkbd layout

recomp/patches-v143.txt ports MacNativeGames' 1.4.3 fps60, real-time step, cloth and bounce sites; the real-time
producer reads the nominal interval from patch page +8, which frame_rate.c now writes as seconds * 60.
make_romfs_patches.py takes --version (Common.Product.140 on 1.4.3).

SDK 20.5 grew KeyboardConfig to 0x518 bytes, so ShowKeyboardArg's work buffer is at +0x518; swkbd_win.c and the
services probe pick the offset by version. 1.4.3's movie::Decoder vtable names the complete-object destructor D1,
which gets the D2 override. The loader probe has the measured 1.4.3 import and relocation counts, and probe-ipc
expects REV15 from the 1.4.3 SDK and skips the 1.2.1 sf heap constructors (1.4.3 creates the audio heap lazily).
A non-1.2.1 build defaults to its own profile, build/profile-<version>: 1.4.3 rewrites the saves it loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-10-26 17:50:40 +03:00
1 parent c8105294ed
commit cd60d20a7f
10 files changed
+101 -34

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
# Code patches for TotK 1.4.3 main (recomp/patches.py). Ported from MacNativeGames
# games/totk/runtime/game_patches_v143.c (MIT, Copyright (c) 2026 MacNativeGames contributors) at upstream 73c4821,
# each original word checked against the 1.4.3 main. MacNativeGames branches to veneers after the text; here a
# veneer's body is the site's replacement. The patch page holds three floats the runtime writes at every present
# (frame_rate.c): +0 the 30 Hz ratio (seconds * 30), +4 the frame step in seconds, +8 the step in 60 Hz ticks.
set fps60 TOTK_FPS60 1
set realtime TOTK_REALTIME 1
set cloth TOTK_CLOTH_REALTIME 1
set bounce TOTK_HIGHFPS_FIX 1
# 60 fps: the adaptive present-interval controller (main+0x2db174, 1.2.1 0x8f26c8) gets the minimum interval 1.
site fps60 main 2db204 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
site fps60 main 2db208 1a89b10a 1a9fa52a # clamp: csinc w10, w9, wzr, ge (desired = max(desired, 1))
site fps60 main 2db22c b940468a d503201f # lower trigger: nop the stored-candidate load
site fps60 main 2db360 6b0801bf 710005bf # raise path: cmp w13, #1
site fps60 main 2db36c 1a88c1ac 1a9fc5ac # raise path: csinc w12, w13, wzr, gt (next lower candidate floors at 1)
site fps60 main 2db340 b9400288 52800028 # lower path: mov w8, #1 (minimum interval 1)
site fps60 main 2db34c 2a0a03f5 1a9fa555 # lower path: csinc w21, w10, wzr, ge (result never below 1)
# Real-time step. The frame-step producer feeds both FrameRateMgr call sites with s8 = nominal interval * a 0.5/1.0
# coefficient (sys+0x1da bit 0, fcsel above the site); the nominal interval comes from the patch page, the coefficient
# and FrameRateMgr's own arithmetic stay. x8 is free: the ldxr after the site loads it. The global ratio after the
# controller call (1.2.1 0x8f2394) reads the ratio from the page instead of 60 / interval * 0.5.
site realtime main 3e5658 1e210808 @page:x8 bd400900 1e210808 # fmul s8, s0, s1: s0 = [page+8] first
site realtime main 2daa54 bd425260 @page:x9 # ldr s0, [x19, #0x250] -> adrp x9, page
site realtime main 2daa58 52a84e08 bd400120 # mov w8, #60.0 -> ldr s0, [x9] (global ratio)
site realtime main 2daa6c 5e21d800 d503201f # scvtf s0, s0 -> nop
site realtime main 2daa70 1e201820 d503201f # fdiv s0, s1, s0 -> nop
site realtime main 2daa74 1e2c1001 d503201f # fmov s1, #0.5 -> nop
site realtime main 2daa78 1e210800 d503201f # fmul s0, s0, s1 -> nop
# Hair and cloth: the actor animation update builds a second clock {seconds = factor / 30, ratio = factor} per
# rendered frame. 1.4.3 inlines it in two actor paths, where the factor was just stored to the ratio slot: the
# replacement stores measured ratio * factor there and computes measured seconds * factor. Two other callers share the
# out-of-line update (main+0x2a0e40), whose s2 (seconds) and s3 (ratio) are scaled by the measured ratio; its third
# caller already passes measured time and stays. x8 and s4 are free at all four sites.
site cloth main 246eac 1e200940 @page:x8 bd400100 1e200940 bd0367e0 bd400500 1e200940 # fmul s0, s10, s0
site cloth main 345904 1e210801 @page:x8 bd400101 1e210801 bd0117e1 bd400501 1e210801 # fmul s1, s0, s1
site cloth main 24944c 94015e7d @page:x8 bd400104 1e240842 1e240863 94015e7d # bl 0x2a0e40
site cloth main 28bc98 9400546a @page:x8 bd400104 1e240842 1e240863 9400546a # bl 0x2a0e40
# Trampolines: the contact code scales the bounce vector by 0.1 when time object +0x28 (30 / (ratio * time scale)) is
# over 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
# s1 is set again by the fmov two instructions on.
site bounce main 562a64 bd402920 bd402920 bd403921 1e210800 # ldr s0, [x9, #0x28]; ldr s1, [x9, #0x38]; fmul s0, s0, s1
+15 -10
View File
@@ -2,22 +2,23 @@
// contributors): runtime_frame_step_update (2338bb0: no 1/120 s floor) and the speed survey's player chain
// (world_survey.c player_actor).
// Frame rate: 60 fps and the real-time frame step, the host half of the patch sets fps60, realtime and cloth
// (build table patches-v121.txt; docs/research/macnativegames.md section 6.2).
// (build tables patches-v121.txt, patches-v143.txt; docs/research/macnativegames.md section 6.2).
//
// TotK's adaptive present-interval controller (main+0x8f26c8) turns the main thread's measured work into a number of
// 60 Hz ticks clamped to [minimum, maximum]; shipping data sets the minimum to 2 (30 fps), the set fps60 lowers it to
// 1. The controller still raises the interval when a frame does not fit. Shipping code then steps the game by whole
// vsyncs (VFR step main+0x7ede6c: ratio = interval * 0.5, seconds = interval / 60; global ratio at main+0x8f2394), so
// game speed is right only when frames take exactly their interval. With the set realtime both read the patch page
// instead: +0 ratio = seconds * 30, +4 seconds, the moving average of the last 5 display intervals, at most 1/15 s
// instead: +0 ratio = seconds * 30, +4 seconds (1.4.3: +8 interval = seconds * 60, the producer's nominal interval
// before its half-step coefficient), the moving average of the last 5 display intervals, at most 1/15 s
// (slower frames slow the game down, as on the console), no lower bound, so a higher frame-rate cap only changes the
// pacer (nvn_api_obj.c VSYNC_HZ). The set cloth gives hair and cloth the same clock and needs realtime.
//
// The sample is the display time the pacer gives each present (nvnQueuePresentTexture), so the step follows the
// frames as shown, pacing waits included. With the step on, the pacer ignores the game's interval and holds a frame
// only to the frame-rate cap (nvn_api_obj.c present_period): the controller's 2 or 3 vsyncs would only add latency.
// One 64-bit store per present; the guest reads the two floats on its own
// threads, and both stay finite and in range whichever store they see.
// One 64-bit and one 32-bit store per present; the guest reads the floats on its own threads, and each stays finite
// and in range whichever store it sees.
//
// TOTK_TRACE=perf adds a line per 120 presents: fps, the game seconds stepped against wall seconds, and with
// TOTK_SPEED_PROBE=1 the player's position and ground speed (an independent check: Link runs at the same speed at
@@ -40,16 +41,19 @@
#define STEP_FIRST_S (1.0 / 60.0)
static _Atomic uint64_t *step_word; // patch page +0: ratio (low float), seconds (high float)
static _Atomic uint32_t *step_interval; // patch page +8: seconds * 60 (patches-v143.txt realtime)
static uint64_t last_ns;
static double samples[STEP_SAMPLES];
static unsigned sample_count, sample_next;
static uint64_t step_bits(float seconds) {
float ratio = seconds * 30.0f;
uint32_t lo, hi;
static void step_store(float seconds) {
float ratio = seconds * 30.0f, interval = seconds * 60.0f;
uint32_t lo, hi, iv;
memcpy(&lo, &ratio, 4);
memcpy(&hi, &seconds, 4);
return (uint64_t)lo | (uint64_t)hi << 32;
memcpy(&iv, &interval, 4);
atomic_store_explicit(step_word, (uint64_t)lo | (uint64_t)hi << 32, memory_order_release);
atomic_store_explicit(step_interval, iv, memory_order_release);
}
void frame_step_init(void) {
@@ -64,7 +68,8 @@ void frame_step_init(void) {
}
if (rt_patch_enabled(realtime)) {
step_word = (_Atomic uint64_t *)rt_patch_page();
atomic_store(step_word, step_bits((float)STEP_FIRST_S));
step_interval = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 8);
step_store((float)STEP_FIRST_S);
}
rt_log("frame rate: present interval %s, %s", rt_patch_enabled(fps60) ? "1..3 (60 fps)" : "2..3 (shipping 30 fps)",
step_word ? "real-time step (5-frame average of display intervals, at most 1/15 s)" : "whole-vsync steps");
@@ -177,7 +182,7 @@ void frame_step_present(uint64_t display_ns, int interval) {
double sum = 0;
for (unsigned i = 0; i < sample_count; i++) sum += samples[i];
step = sum / sample_count;
atomic_store_explicit(step_word, step_bits((float)step), memory_order_release);
step_store((float)step);
}
}
last_ns = display_ns;
+3 -3
View File
@@ -721,7 +721,7 @@ static void sdk_prepare(Cpu *c) {
ST32(LD64(c->tpidrro + 0x1F8) + 0x1B0, kern_main_thread(&again));
const RtLoaded *sdk = ldr_module(2);
static const uint64_t heap_ctors[] = {0x1dcbd0, 0x1de8a0};
for (unsigned k = 0; k < sizeof heap_ctors / sizeof *heap_ctors; k++) {
for (unsigned k = 0; !RT_V143 && k < sizeof heap_ctors / sizeof *heap_ctors; k++) {
int found = 0;
for (uint64_t e = 0; e < sdk->init_array_size; e += 8)
found |= LD64(sdk->init_array + e) == sdk->base + heap_ctors[k];
@@ -740,8 +740,8 @@ static void probe_sdk_client(Cpu *c) {
rt_call(c, stub(SYM_INIT_PARAM), 1, (uint64_t)(uintptr_t)param);
printf("sdk: InitializeAudioRendererParameter -> %d Hz x%d, mix buffers %d, revision word %#x (REV%d)\n", param[0],
param[1], param[2], (uint32_t)param[13], revision_number_of((uint32_t)param[13]));
CHECK(param[0] == 48000 && param[1] == 240 && (uint32_t)param[13] == g_param.revision,
"SDK default parameter (TotK 1.2.1 builds REV11 requests)");
CHECK(param[0] == 48000 && param[1] == 240 && (uint32_t)param[13] == AUDREN_REV(RT_V143 ? 15 : 11),
"SDK default parameter (TotK 1.2.1 builds REV11 requests, 1.4.3 REV15)");
param[2] = g_param.mix_buffer_count, param[3] = g_param.submix_count, param[4] = g_param.voice_count;
param[5] = g_param.sink_count, param[6] = g_param.effect_count;
uint64_t work = rt_call(c, stub(SYM_WORK_SIZE), 1, (uint64_t)(uintptr_t)param);
+3 -3
View File
@@ -578,9 +578,9 @@ static const Expect expect_121[] = {
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
};
static const Expect expect_143[] = {
{"main", {499078, 1393, 28, 650}, 501149, 0, 0, 0, {0, 0, 0}, 0, 0x10b4f10, 10717},
{"subsdk0", {3424, 10927, 905, 3817}, 19073, 0, 0, 0, {0, 0, 0}, 0, 0x20, 45},
{"sdk", {15013, 10135, 1079, 7832}, 34059, 0, 0, 0, {0, 0, 0}, 0, 0x400, 83},
{"main", {499078, 1393, 28, 650}, 501149, 2071, 0, 680, {0, 16, 656}, 8, 0x10b4f10, 10717},
{"subsdk0", {3424, 10927, 905, 3817}, 19073, 6557, 9092, 663, {9, 0, 654}, 0, 0x20, 45},
{"sdk", {15013, 10135, 1079, 7832}, 34059, 10, 19036, 8, {8, 0, 0}, 0, 0x400, 83},
};
#define expect (RT_V143 ? expect_143 : expect_121)
+2
View File
@@ -854,6 +854,8 @@ static void ov_get_output_md(Cpu *c) {
HOST_OVERRIDE("_ZN5movie7DecoderC1ENS_11DecoderTypeENS_19DecoderOutputFormatENS_11DecoderModeEi", ov_ctor);
HOST_OVERRIDE("_ZN5movie7DecoderD2Ev", ov_d2);
// 1.4.3's vtable and a PLT call name the complete-object destructor; without virtual bases it is D2.
HOST_OVERRIDE("_ZN5movie7DecoderD1Ev", ov_d2);
HOST_OVERRIDE("_ZN5movie7DecoderD0Ev", ov_d0);
HOST_OVERRIDE("_ZN5movie7Decoder9ConfigureEPNS_9MediaDataE", ov_configure);
HOST_OVERRIDE("_ZN5movie7Decoder20RegisterClientEventsEPNS_13DecoderEventsE", ov_register_events);
+5 -3
View File
@@ -628,10 +628,12 @@ static void probe_media_gfx(void) {
"vsync event %s\n", display, layer, window, vsync ? "bound (auto-clear)" : "FAILED");
}
// ---- software keyboard (swkbd_win.c): ShowKeyboardArg built by the 1.2.1 SDK's own setters, as main+0x1b2b3a0 does
// ---- software keyboard (swkbd_win.c): ShowKeyboardArg built by the SDK's own setters, as the horse naming does
enum { KB_TYPE = 0x00, KB_DISABLED = 0x1c, KB_HEADER = 0x24, KB_GUIDE = 0x1a8, KB_MAX_LEN = 0x3ac, KB_MIN_LEN = 0x3b0,
KB_UTF8 = 0x3bd, KB_INIT_LEN = 0x3c4, ARG_WORK = 0x4d0, ARG_WORK_SIZE = 0x4d8, ARG_SIZE = 0x500,
STRING_SIZE = 0x7d4, SWKBD_CANCELED = 0x29f };
KB_UTF8 = 0x3bd, KB_INIT_LEN = 0x3c4, STRING_SIZE = 0x7d4, SWKBD_CANCELED = 0x29f };
#define ARG_WORK (RT_V143 ? 0x518 : 0x4d0) // SDK 20.5 grew KeyboardConfig (swkbd_win.c)
#define ARG_WORK_SIZE (ARG_WORK + 8)
#define ARG_SIZE (RT_V143 ? 0x548 : 0x500)
static uint64_t sdk(const char *name) {
uint64_t a = ldr_symbol(name);
+2 -1
View File
@@ -6,7 +6,8 @@
//
// The patch page is the first 4 KiB page after main's image, inside the module window memory.c reserved: replacement
// code reaches it with one adrp, as MacNativeGames' data page after main (games/totk/runtime/game_patches.c). Its
// layout belongs to the sets that use it (frame_rate.c: +0 the 30 Hz ratio, +4 the frame step in seconds).
// layout belongs to the sets that use it (frame_rate.c: +0 the 30 Hz ratio, +4 the frame step in seconds, +8 the
// step in 60 Hz ticks).
// Probe modes never call rt_patches_init: every set stays off.
#include "cfg.h"
#include "platform/platform.h"
+5 -3
View File
@@ -30,12 +30,14 @@
#include "ui_text.h"
#include "vk.h"
// KeyboardConfig (0x4d0 bytes) then the work buffer {pointer, size}: offsets read from the SDK setters (MNG) and
// checked against the 1.2.1 SDK by --probe-services.
// KeyboardConfig then the work buffer {pointer, size}: offsets read from the SDK setters (MNG) and checked against
// the SDK by --probe-services. SDK 20.5 (1.4.3) put a text-check callback at +0x4c8 and grew KeyboardConfig from
// 0x4d0 to 0x518 bytes (SetInitialText, SetUserWordList read the work buffer there); the fields below +0x3d8 stay.
#define ARG_WORK (RT_V143 ? 0x518 : 0x4d0)
#define ARG_WORK_SIZE (ARG_WORK + 8)
enum {
KB_TYPE = 0x00, KB_OK_TEXT = 0x04, KB_DISABLED = 0x1c, KB_HEADER = 0x24, KB_SUB = 0xa6, KB_GUIDE = 0x1a8,
KB_MAX_LEN = 0x3ac, KB_MIN_LEN = 0x3b0, KB_PASSWORD = 0x3b4, KB_UTF8 = 0x3bd, KB_INIT_OFF = 0x3c0, KB_INIT_LEN = 0x3c4,
ARG_WORK = 0x4d0, ARG_WORK_SIZE = 0x4d8,
KB_TYPE_NUMPAD = 1, KB_LIMIT = 500, SWKBD_CANCELED = 0x29f,
};
enum { B_A = 0, B_B = 1, B_PLUS = 10 }; // input.h bits
+7 -3
View File
@@ -213,7 +213,8 @@ def module_glue(layout, linked, hashes, paths, version):
out += ['};', f'const int rt_exefs_sha256_count = {len(hashes)};', '']
for name in ('exefs', 'romfs', 'profile'):
out.append(f'const char rt_default_{name}[] = {c_string(paths[name].as_posix())};')
out += [f'const int rt_game_version = {int(DISPLAY_VERSIONS[version].replace(".", "")) if version in DISPLAY_VERSIONS else 0};', '']
number = int(DISPLAY_VERSIONS[version].replace('.', '')) if version in DISPLAY_VERSIONS else 0 # 1.4.3 -> 143
out += [f'const int rt_game_version = {number};', '']
return '\n'.join(out)
@@ -584,7 +585,9 @@ def main():
help="totk.exe's default ExeFS directory (default build/extracted/<version>/exefs)")
ap.add_argument('--romfs', type=Path, help="totk.exe's default RomFS directory with romfs.bin and index.json "
'(default build/extracted/<version>/romfs)')
ap.add_argument('--profile', type=Path, help="totk.exe's default profile directory (default build/profile)")
ap.add_argument('--profile', type=Path, help="totk.exe's default profile directory (default build/profile for v121, "
"build/profile-<version> otherwise: a newer game version rewrites the "
"saves it loads, and an older one cannot read them back)")
ap.add_argument('--toolchain', type=Path, help='llvm-mingw directory (with bin/clang.exe) to build with instead of '
'MSYS2 UCRT64 clang')
ap.add_argument('--ninja', type=Path, help='ninja.exe to build with (default: MSYS2 UCRT64, else bin/ninja.exe of the '
@@ -604,7 +607,8 @@ def main():
extracted = ROOT / 'build' / 'extracted' / args.version
paths = {'exefs': (args.exefs or extracted / 'exefs').resolve(),
'romfs': (args.romfs or extracted / 'romfs').resolve(),
'profile': (args.profile or ROOT / 'build' / 'profile').resolve()}
'profile': (args.profile or ROOT / 'build' / ('profile' if args.version == 'v121'
else f'profile-{args.version}')).resolve()}
expected = args.expected
if not expected:
if args.version not in DISPLAY_VERSIONS:
+13 -8
View File
@@ -1,17 +1,18 @@
# Adapted from MacNativeGames games/totk/scripts/make_romfs_patches.py @ ebe621d (MIT, Copyright (c) 2026
# MacNativeGames contributors).
"""Build the 60 fps RomFS overlay for TotK 1.2.1 (runtime/romfs.c, TOTK_ROMFS_PATCHES).
"""Build the 60 fps RomFS overlay for TotK 1.2.1 or 1.4.3 (runtime/romfs.c, TOTK_ROMFS_PATCHES).
UI/LayoutArchive/Common.Product.110.Nin_NX_NVN.blarc.zs: the weapon / shield / material quick menus capture the game
UI/LayoutArchive/Common.Product.<110 or 140>.Nin_NX_NVN.blarc.zs: the weapon / shield / material quick menus capture the game
picture for their background when the panes C_CaptureBG_00, C_CaptureClearBG_00 and P_CaptureClearBG_00 become
visible at animation frame 1.0 (anim/ShortCutBG_00_In.bflan), AutoBuilder the same with C_CaptureBG_00
(anim/AutoBuilder_00_In.bflan). UI animations advance 0.5 frames per game frame at 60 fps, so the capture lands after
the world stopped drawing and the background can be black. The visibility keys move to frame 0.0: the live world is
captured at once (upstream checked weapon, shield, material and ability wheel at 60 fps, and 30 fps).
The game-over flow fix of the same upstream script is 1.0.0 only (1.2.1 runs DmF_SY_GameOver.110) and is left out.
The game-over flow fix of the same upstream script is 1.0.0 only (1.2.1 and 1.4.3 run DmF_SY_GameOver.110) and is
left out.
Input: build/extracted/v121/romfs (romfs.bin and index.json, verified by tools/extract.py). Output: <out>/<path> and
<out>/manifest.tsv (path, size, SHA-256 of the overlay, SHA-256 of the original). Run with .venv-py314.
Input: build/extracted/<version>/romfs (romfs.bin and index.json, verified by tools/extract.py). Output:
build/<version>/romfs-patches/<path> by default, and <out>/manifest.tsv (path, size, SHA-256 of the overlay, SHA-256 of the original). Run with .venv-py314.
"""
import argparse
import compression.zstd as zstd
@@ -22,9 +23,12 @@ from pathlib import Path
root = Path(__file__).resolve().parents[1]
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--romfs', type=Path, default=root / 'build/extracted/v121/romfs')
ap.add_argument('--out', type=Path, default=root / 'build/v121/romfs-patches')
ap.add_argument('--version', choices=('v121', 'v143'), default='v121')
ap.add_argument('--romfs', type=Path, help='default build/extracted/<version>/romfs')
ap.add_argument('--out', type=Path, help='default build/<version>/romfs-patches')
args = ap.parse_args()
args.romfs = args.romfs or root / 'build/extracted' / args.version / 'romfs'
args.out = args.out or root / 'build' / args.version / 'romfs-patches'
index = json.loads((args.romfs / 'index.json').read_text())
assert index['status'] == 'pass'
files = {f['path']: f for f in index['files']}
@@ -73,7 +77,8 @@ def flvi_keys(data, base):
return out
ARCHIVE = 'UI/LayoutArchive/Common.Product.110.Nin_NX_NVN.blarc.zs'
COMMON = {'v121': '110', 'v143': '140'} # the version in the name of each game's Common archive
ARCHIVE = f'UI/LayoutArchive/Common.Product.{COMMON[args.version]}.Nin_NX_NVN.blarc.zs'
MEMBERS = {
'anim/ShortCutBG_00_In.bflan': ('C_CaptureBG_00', 'C_CaptureClearBG_00', 'P_CaptureClearBG_00'),
'anim/AutoBuilder_00_In.bflan': ('C_CaptureBG_00',),