Files
totk-pc-recomp/runtime/boot.c
T
Valiantsin DzerakhandClaude Opus 5.5 ce150130df Make code comments say what they mean instead of pointing at things a reader cannot follow
Comments cited codes from local notes (port-lessons P1, backlog), task labels (T6c1-T6c3, M4), bare upstream commit
hashes and issue numbers, a file that no longer exists (nvn_api.c, now nvn_api_*.c), a generated file (main_0103.c)
and line numbers into upstream files that refs/ has moved past, so they pointed at unrelated code. Those now name the
file, function or project and say what the cited code does. Attribution of copied code keeps its pinned commits.

Corrected facts: TOTK_REPLAY_ASYNC defaults to 0, and the shader cache, pipeline cache and pipeline keys live next to
totk.exe, not in build/v121.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:58:01 +03:00

363 lines
18 KiB
C

// Adapted from MacNativeGames games/totk/runtime/loader.c (lines 456-477) and services.c (lines 219-235) @ b75be8cf
// (MIT, Copyright (c) 2026 MacNativeGames contributors) and LAS Recomp runtime/main.c (lines 16-35, 141-159) @
// da35b83 (MIT, Copyright (c) 2026 Peiikai).
// The boot driver: the guest main thread runs the game's own nn::init::Start
// with three host callbacks, the second of which runs DT_INIT of main, subsdk0 and sdk; Start then calls nnMain.
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call:
// SetFocusHandlingMode called from main+0xe7a954 (1.4.3: +0xbdb48c) in nnMain, every module's constructor flag
// reads 2
// nvnBootstrapLoader called from main+0xe7eb2c (1.4.3: +0xbe0360) in the graphics bring-up; logs the run
// statistics, exits 0
// With --seconds S the run continues past that call; rt_frame_presented ends it once the world clock passed S.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "fs.h"
#include "gfx.h"
#include "nvn_api.h"
#include "vk.h"
#define SYM_INIT_START "_ZN2nn4init5StartEmmPFvvES2_S2_"
// main: return addresses of nnMain's SetFocusHandlingMode call (1.2.1 from this port's analysis of main; 1.4.3 from
// MacNativeGames' scripts/verify_boot.py) and of the first nvnBootstrapLoader call, per game version.
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : 0xe7a954ull)
#define NVN_RET (RT_V143 ? 0xbe0360ull : 0xe7eb2cull)
#define CTOR_DONE 2 // a module's constructor flag after its DT_INIT ran the init_array
// The init_array sizes (1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check expects
// the same.
typedef struct { const char *name; uint64_t entries; } InitCount;
static const InitCount inits_121[] = {{"main", 10673}, {"subsdk0", 45}, {"sdk", 116}};
static const InitCount inits_143[] = {{"main", 10717}, {"subsdk0", 45}, {"sdk", 83}};
#define expected_inits (RT_V143 ? inits_143 : inits_121)
static volatile LONG inits_done, checkpoint_seen;
uint64_t rt_guest_start_ns;
static const RtLoaded *module_named(const char *name) {
for (int i = 0; i < ldr_count(); i++)
if (!strcmp(ldr_module(i)->name, name)) return ldr_module(i);
rt_fatal("boot: module %s is not loaded", name);
}
static uint32_t ctor_state(const RtLoaded *m) { return LD32(m->ctor_flag); }
static const char *caller(const Cpu *c, char *buf, int n) { return ldr_describe(c->x[30] - 4, buf, n); }
// main.npdm META: the main thread's priority (+0xE), core (+0xF) and stack size (+0x1C). kernel.c builds the main
// thread with the 1.2.1 values; a different npdm means a different game build.
static void check_npdm(void) {
size_t n;
uint8_t *npdm = ldr_read_verified(rt_config()->exefs, "main.npdm", &n);
if (n < 0x80 || memcmp(npdm, "META", 4)) rt_fatal("boot: main.npdm has no META header");
uint32_t stack;
memcpy(&stack, npdm + 0x1C, 4);
int prio = npdm[0xE], core = npdm[0xF];
free(npdm);
if (prio != 44 || core != 0 || stack != 0x100000)
rt_fatal("boot: main.npdm asks for priority %d, core %d, stack %#x; kernel.c builds 44, 0, 0x100000", prio,
core, stack);
}
// ---- the three callbacks of nn::init::Start (rtld's job on hardware)
static void cb_loader_ready(Cpu *c) {
(void)c;
rt_log("boot: rtld callback: modules linked");
}
// DT_INIT registers the module with musl (__nnmusl_init_dso) and runs its init_array; the constructor flag reads 2
// afterwards (MacNativeGames loader.c:458-462).
static void cb_module_inits(Cpu *c) {
if (InterlockedCompareExchange(&inits_done, 0, 0)) rt_fatal("boot: module constructors requested twice");
for (unsigned k = 0; k < sizeof inits_121 / sizeof *inits_121; k++) {
const RtLoaded *m = module_named(expected_inits[k].name);
uint64_t entries = m->init_array_size / 8;
if (!m->init) rt_fatal("boot: %s has no DT_INIT", m->name);
rt_log("boot: ENTER %s DT_INIT (%llu init_array entries)", m->name, (unsigned long long)entries);
rt_call(c, m->init, 0);
uint32_t state = ctor_state(m);
rt_logf("boot: DT_INIT %s state=%u (%llu)\n", m->name, state, (unsigned long long)entries);
if (state != CTOR_DONE) rt_fatal("boot: %s DT_INIT left its constructor flag at %u, not 2", m->name, state);
if (entries != expected_inits[k].entries)
rt_fatal("boot: %s has %llu init_array entries, this game version has %llu", m->name, (unsigned long long)entries,
(unsigned long long)expected_inits[k].entries);
}
InterlockedExchange(&inits_done, 1);
}
static void cb_module_finis(Cpu *c) {
char lr[128];
rt_fatal("boot: the game asked for module finalizers (from %s): the guest exited", caller(c, lr, sizeof lr));
}
// ---- checkpoints (host.h)
static void stats_line(void) {
RtStats s = rt_stats();
rt_logf("boot: passed (%llu SVCs, %llu threads, %llu host calls)\n", (unsigned long long)s.svcs,
(unsigned long long)s.threads, (unsigned long long)s.host_calls);
}
void rt_boot_event(const char *what, Cpu *c, const char *detail) {
char at[128];
const RtLoaded *main_mod = module_named("main");
uint64_t ret = c->x[30];
caller(c, at, sizeof at);
if (!strcmp(what, "checkpoint")) {
if (ret != main_mod->base + CHECKPOINT_RET)
rt_fatal("boot: checkpoint %s reached from %s, expected main+%#llx", detail, at,
(unsigned long long)CHECKPOINT_RET);
if (!InterlockedCompareExchange(&inits_done, 0, 0)) rt_fatal("boot: checkpoint %s before DT_INIT", detail);
for (int i = 0; i < ldr_count(); i++)
if (ctor_state(ldr_module(i)) != CTOR_DONE)
rt_fatal("boot: checkpoint %s: %s constructor flag reads %u", detail, ldr_module(i)->name,
ctor_state(ldr_module(i)));
InterlockedExchange(&checkpoint_seen, 1);
rt_logf("boot: checkpoint %s from main+%#llx\n", detail, (unsigned long long)(ret - main_mod->base));
return;
}
if (!strcmp(what, "nvn")) {
if (ret != main_mod->base + NVN_RET)
rt_fatal("boot: first NVN call %s(\"%s\") from %s, expected main+%#llx", "nvnBootstrapLoader", detail, at,
(unsigned long long)NVN_RET);
if (!InterlockedCompareExchange(&checkpoint_seen, 0, 0))
rt_fatal("boot: first NVN call before the SetFocusHandlingMode checkpoint");
rt_logf("boot: first NVN call nvnBootstrapLoader(\"%s\") from main+%#llx\n", detail,
(unsigned long long)(ret - main_mod->base));
if (rt_config()->seconds) return; // --seconds: the run goes on into the world
if (rt_trace & RT_TRACE_HOSTCALL) hostfn_report();
stats_line();
fflush(stdout);
fflush(stderr);
_exit(0); // other guest threads are still running; nothing of theirs needs an orderly end
}
rt_fatal("boot: unknown boot event %s (%s) from %s", what, detail ? detail : "", at);
}
// ---- --seconds S
// A PNG with stored (uncompressed) deflate blocks: no compression library, and the shot is only read by tools.
// rt_write_png (declared in vk.h) is shared with the TOTK_DUMP frame dump (vk_dump.c).
static uint32_t crc32_update(uint32_t crc, const uint8_t *p, size_t n) {
static uint32_t table[256];
if (!table[1])
for (uint32_t i = 0; i < 256; i++) {
uint32_t c = i;
for (int k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
table[i] = c;
}
for (size_t i = 0; i < n; i++) crc = table[(crc ^ p[i]) & 0xFF] ^ (crc >> 8);
return crc;
}
static void png_chunk(FILE *f, const char *type, const uint8_t *data, uint32_t n) {
uint8_t hdr[8] = {(uint8_t)(n >> 24), (uint8_t)(n >> 16), (uint8_t)(n >> 8), (uint8_t)n};
memcpy(hdr + 4, type, 4);
uint32_t crc = crc32_update(crc32_update(~0u, hdr + 4, 4), data, n) ^ ~0u;
uint8_t tail[4] = {(uint8_t)(crc >> 24), (uint8_t)(crc >> 16), (uint8_t)(crc >> 8), (uint8_t)crc};
fwrite(hdr, 1, 8, f);
if (n) fwrite(data, 1, n, f);
fwrite(tail, 1, 4, f);
}
void rt_write_png(const char *path, const uint8_t *rgba, uint32_t w, uint32_t h) {
uint64_t row = 1 + 4ull * w, raw_n = row * h;
uint8_t *raw = malloc(raw_n);
if (!raw) rt_fatal("boot: out of memory for the shot");
for (uint32_t y = 0; y < h; y++) {
raw[y * row] = 0; // filter: none
memcpy(raw + y * row + 1, rgba + 4ull * w * y, 4ull * w);
}
uint64_t blocks = (raw_n + 65534) / 65535;
uint8_t *z = malloc(2 + raw_n + 5 * blocks + 4);
if (!z) rt_fatal("boot: out of memory for the shot");
uint64_t zn = 0;
z[zn++] = 0x78, z[zn++] = 0x01;
uint32_t a = 1, b = 0;
for (uint64_t off = 0; off < raw_n; off += 65535) {
uint32_t k = raw_n - off > 65535 ? 65535 : (uint32_t)(raw_n - off);
z[zn++] = off + k >= raw_n;
z[zn++] = (uint8_t)k, z[zn++] = (uint8_t)(k >> 8), z[zn++] = (uint8_t)~k, z[zn++] = (uint8_t)(~k >> 8);
memcpy(z + zn, raw + off, k);
zn += k;
for (uint32_t i = 0; i < k; i++) a = (a + raw[off + i]) % 65521, b = (b + a) % 65521;
}
uint32_t ad = b << 16 | a;
z[zn++] = (uint8_t)(ad >> 24), z[zn++] = (uint8_t)(ad >> 16), z[zn++] = (uint8_t)(ad >> 8), z[zn++] = (uint8_t)ad;
FILE *f = fopen(path, "wb");
if (!f) rt_fatal("boot: cannot write %s", path);
static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', 13, 10, 26, 10};
uint8_t ihdr[13] = {(uint8_t)(w >> 24), (uint8_t)(w >> 16), (uint8_t)(w >> 8), (uint8_t)w,
(uint8_t)(h >> 24), (uint8_t)(h >> 16), (uint8_t)(h >> 8), (uint8_t)h, 8, 6, 0, 0, 0};
fwrite(sig, 1, 8, f);
png_chunk(f, "IHDR", ihdr, 13);
png_chunk(f, "IDAT", z, (uint32_t)zn);
png_chunk(f, "IEND", NULL, 0);
if (fclose(f)) rt_fatal("boot: cannot write %s", path);
free(z);
free(raw);
}
static void save_shot(const tex_t *t, const char *path) {
int fmt = t->d.format;
int bgra = fmt == NVN_FORMAT_BGRA8 || fmt == NVN_FORMAT_BGRA8_SRGB;
if (!bgra && fmt != NVN_FORMAT_RGBA8 && fmt != NVN_FORMAT_RGBA8_SRGB)
rt_fatal("boot: --shot supports 8-bit RGBA and BGRA targets, the presented texture has NVN format %#x", fmt);
uint32_t w = t->d.width, h = t->d.height;
uint8_t *px = malloc(4ull * w * h);
if (!px) rt_fatal("boot: out of memory for the shot");
view_t view;
memset(&view, 0, sizeof view);
NVNcopyRegion r = {.width = (int)w, .height = (int)h, .depth = 1};
if (gfx_replay_async()) gfx_backend()->finish(); // the presented frame may still be queued for the replay
gfx_backend()->read_texels(t, &view, &r, px, 4ull * w, 4ull * w * h);
if (bgra)
for (uint64_t i = 0; i < 4ull * w * h; i += 4) {
uint8_t x = px[i];
px[i] = px[i + 2];
px[i + 2] = x;
}
rt_write_png(path, px, w, h);
free(px);
rt_logf("boot: shot %s (%ux%u)\n", path, w, h);
}
// TOTK_SHOTS=lo-hi:step [TOTK_SHOTS_DIR=dir]: every step-th present in [lo, hi] saved as <dir>/f<n>.png (n the
// present), box-filtered to 480x270; TOTK_SHOTS=w<lo>-<hi>:<step> takes them by world-clock seconds instead, the first
// present at or after each step (full size with TOTK_SHOTS_FULL=1), for finding short glitches. Pipeline builds stay asynchronous, so the
// pictures show what a player sees.
static void series_shot(const tex_t *t, uint64_t n) {
static int parsed;
static unsigned long long lo, hi, step, next;
static const char *dir;
static int full, world;
if (!parsed) {
full = cfg_get("TOTK_SHOTS_FULL") != NULL;
const char *e = cfg_get("TOTK_SHOTS");
double wl, wh, ws;
if (e && e[0] == 'w' && sscanf(e + 1, "%lf-%lf:%lf", &wl, &wh, &ws) == 3 && ws > 0) // world-clock seconds
world = 1, lo = (unsigned long long)(wl * 1e6), hi = (unsigned long long)(wh * 1e6),
step = (unsigned long long)(ws * 1e6), next = lo;
else if (!e || sscanf(e, "%llu-%llu:%llu", &lo, &hi, &step) != 3 || !step) hi = 0;
dir = cfg_get("TOTK_SHOTS_DIR") ? cfg_get("TOTK_SHOTS_DIR") : ".";
if (hi) CreateDirectoryA(dir, NULL);
parsed = 1;
}
if (world) { // the first present at or after each step of the world clock
int64_t w = frame_step_world_us();
if (w < 0 || (unsigned long long)w < next || next > hi) return;
while (next <= (unsigned long long)w) next += step;
} else if (n < lo || n > hi || (n - lo) % step) {
return;
}
int fmt = t->d.format;
int bgra = fmt == NVN_FORMAT_BGRA8 || fmt == NVN_FORMAT_BGRA8_SRGB;
if (!bgra && fmt != NVN_FORMAT_RGBA8 && fmt != NVN_FORMAT_RGBA8_SRGB) return;
uint32_t w = t->d.width, h = t->d.height, ow = full ? w : 480, oh = full ? h : 270;
uint8_t *px = malloc(4ull * w * h), *out = malloc(4ull * ow * oh);
if (!px || !out) rt_fatal("boot: out of memory for a TOTK_SHOTS frame");
view_t view;
memset(&view, 0, sizeof view);
NVNcopyRegion r = {.width = (int)w, .height = (int)h, .depth = 1};
if (gfx_replay_async()) gfx_backend()->finish(); // the presented frame may still be queued for the replay
gfx_backend()->read_texels(t, &view, &r, px, 4ull * w, 4ull * w * h);
for (uint32_t y = 0; y < oh; y++)
for (uint32_t x = 0; x < ow; x++) {
uint32_t x0 = x * w / ow, x1 = (x + 1) * w / ow, y0 = y * h / oh, y1 = (y + 1) * h / oh;
if (x1 <= x0) x1 = x0 + 1;
if (y1 <= y0) y1 = y0 + 1;
uint32_t sum[4] = {0}, k = 0;
for (uint32_t sy = y0; sy < y1; sy++)
for (uint32_t sx = x0; sx < x1; sx++, k++)
for (int c = 0; c < 4; c++) sum[c] += px[4ull * (sy * (uint64_t)w + sx) + (uint32_t)c];
uint8_t *o = out + 4ull * (y * ow + x);
for (int c = 0; c < 4; c++) o[c] = (uint8_t)(sum[bgra && c != 3 ? 2 - c : c] / k);
o[3] = 255;
}
char path[MAX_PATH];
snprintf(path, sizeof path, "%s/f%05llu.png", dir, (unsigned long long)n);
rt_write_png(path, out, ow, oh);
free(out);
free(px);
}
// A --seconds run ends at the first present after the world clock (game time since the save's first frame,
// frame_step_world_us) passed S seconds, so a run covers the same game time at any frame rate; any other frame is
// counted by the back end only.
void rt_frame_presented(const tex_t *t, uint64_t n) {
const RtConfig *cfg = rt_config();
series_shot(t, n);
int64_t w = cfg->seconds ? frame_step_world_us() : -1;
if (w < 0 || (double)w < cfg->seconds * 1e6) return;
if (cfg->shot) save_shot(t, cfg->shot);
GfxStats g = gfx_stats();
rt_logf("boot: frames %llu presented (nvn calls %llu, distinct %llu, unimplemented %llu, validation errors %llu, "
"shader failures %llu, draws skipped %llu)\n", (unsigned long long)g.frames,
(unsigned long long)g.nvn_calls, (unsigned long long)g.nvn_distinct, (unsigned long long)g.unimplemented,
(unsigned long long)g.validation_errors, (unsigned long long)g.shader_failures,
(unsigned long long)g.draws_skipped);
stats_line();
fflush(stdout);
fflush(stderr);
fs_save_flush();
vk_pipeline_save(); // no-op without the Vulkan back end
prof_write();
fflush(stderr);
_exit(0);
}
// ---- steps 9-14
static uint64_t cb_addr[3];
static void guest_main(Cpu *c) {
uint32_t handle = kern_main_thread(&c);
uint64_t start = ldr_symbol(SYM_INIT_START);
if (!start) rt_fatal("boot: %s is not exported", SYM_INIT_START);
char d[128];
rt_log("boot: ENTER nn::init::Start at %s, main thread %#x", ldr_describe(start, d, sizeof d), handle);
// nn::init::Start(main thread handle, arguments region, loader_ready, module_inits, module_finis)
rt_call(c, start, 5, (uint64_t)handle, (uint64_t)0, cb_addr[0], cb_addr[1], cb_addr[2]);
rt_fatal("boot: nn::init::Start returned");
}
static DWORD WINAPI prewarm_main(void *arg) {
(void)arg;
vk_pipeline_prewarm();
const char *prep = cfg_get("TOTK_TEST_PREPARE_SHADERS"); // Task 2.3 test hook; Task 2.6 builds the preparation
if (prep && !strcmp(prep, "1")) {
int cpus = (int)GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
gfx_shader_prepare_recorded(cpus, cpus / 2 > 16 ? 16 : cpus / 2 < 1 ? 1 : cpus / 2);
}
return 0;
}
void rt_boot(void) {
check_npdm();
if (!rt_config()->shot) vk_pipeline_async_enable(); // a --shot image must not depend on build timing
// Shaders and pipelines recorded in earlier runs are made ready while the modules load; the guest waits for them.
HANDLE warm = CreateThread(NULL, 0, prewarm_main, NULL, 0, NULL);
ldr_load(rt_config()->exefs);
rt_patches_init();
frame_step_init();
res_init();
cb_addr[0] = hostfn_register("<rtld loader_ready>", cb_loader_ready);
cb_addr[1] = hostfn_register("<rtld module_inits>", cb_module_inits);
cb_addr[2] = hostfn_register("<rtld module_finis>", cb_module_finis);
hostfn_seal();
ULONGLONG held = GetTickCount64();
if (warm) WaitForSingleObject(warm, INFINITE), CloseHandle(warm);
else vk_pipeline_prewarm();
held = GetTickCount64() - held;
FILETIME created, x0, x1, x2, now;
GetProcessTimes(GetCurrentProcess(), &created, &x0, &x1, &x2);
GetSystemTimeAsFileTime(&now);
ULARGE_INTEGER c = {.LowPart = created.dwLowDateTime, .HighPart = created.dwHighDateTime},
n = {.LowPart = now.dwLowDateTime, .HighPart = now.dwHighDateTime};
rt_log("boot: guest starts %llu ms after process start (%llu ms of it waiting for the prewarm)",
(unsigned long long)((n.QuadPart - c.QuadPart) / 10000), (unsigned long long)held);
rt_guest_start_ns = kern_now_ns();
kern_run_on_guest_main(guest_main);
rt_fatal("boot: the guest main thread ended");
}