Files
NX2navi/runtime/boot.c
chrg6 cd0b1a387d Do the whole warm on the boot screen at the first start after a driver change.
A start that finds the pipeline cache written by another device or driver has nothing built, and everything was
compiled beside the game: an 8-core integrated GPU drew fewer than 600 frames in its first 11 minutes. Such a start
now waits on the "Preparing shaders" screen, with counts, for every recorded pipeline, for the compute pipelines on
every core at normal priority, and, when a sample says the driver's own cache lacks the prepared stage libraries, for
all of them on every core. The memory gate still holds for the builds, and the library builds write the preparation's
journal while the start waits, so a variant the driver's compiler dies on is skipped from the next start on.

boot-warm-owed.txt beside the caches names the GPU and driver: a start closed during the warm is followed by one that
goes on with it, three at most, and "done" keeps later starts ordinary while the cache file still carries the earlier
driver's header. When the boot stops waiting (prewarm_wait_ms, or a minute without progress) the builders step down
to the background's thread counts and idle priority. prewarm_full = 0 turns the warm off.

With nothing in the driver's cache the probe took 425 s on an RTX 5090 with 16 CPU threads: 87 s for 5,775 recorded
pipelines, 19 s for 4,075 compute pipelines and 317 s for 42,231 prepared variants.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 20:03:12 -05:00

444 lines
23 KiB
C

// Contains code Copyright (c) 2026 MacNativeGames contributors, Copyright (c) 2026 Peiikai (MIT; see
// THIRD_PARTY_NOTICES.md).
// 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, at the return address of
// each in main (1.1.1; 1.1.0, 1.2.1, 1.4.3 in brackets):
// SetFocusHandlingMode main+0x1202d44 (+0xeb8c14, +0xe7a954, +0xbdb48c) in nnMain, every module's constructor
// flag reads 2. 1.1.1's SDK calls it first, from nn::oe::InitializeApplet before DT_INIT; a
// call from outside main is not the checkpoint.
// nvnBootstrapLoader main+0x346bf8c (+0xebcdec, +0xe7eb2c, +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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "cheats.h"
#include "coop.h"
#include "fs.h"
#include "kernel.h"
#include "ipc.h"
#include "gfx.h"
#include "mods.h"
#include "nvn_api.h"
#include "platform/platform.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
#define SYM_INIT_START "_ZN2nn4init5StartEmmPFvvES2_S2_"
// main: return addresses of nnMain's SetFocusHandlingMode call and of the first nvnBootstrapLoader call, per game
// version (from the analysis of each build's main; 1.1.1's are the x30 both host functions see in a normal start).
#define CHECKPOINT_RET (RT_V111 ? 0x1202d44ull : RT_V143 ? 0xbdb48cull : RT_V110 ? 0xeb8c14ull : 0xe7a954ull)
#define NVN_RET (RT_V111 ? 0x346bf8cull : RT_V143 ? 0xbe0360ull : RT_V110 ? 0xebcdecull : 0xe7eb2cull)
#define CTOR_DONE 2 // a module's constructor flag after its DT_INIT ran the init_array
// The init_array sizes (1.1.0 and 1.2.1: counted in the modules; 1.1.1 and 1.4.3: DT_INIT_ARRAYSZ).
// The four tables carry the same rows, so cb_module_inits walks them by the size of one.
typedef struct { const char *name; uint64_t entries; } InitCount;
static const InitCount inits_110[] = {{"main", 10668}, {"subsdk0", 45}, {"sdk", 116}};
static const InitCount inits_111[] = {{"main", 9783}, {"subsdk0", 0}, {"sdk", 96}};
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_V111 ? inits_111 : RT_V143 ? inits_143 : RT_V110 ? inits_110 : inits_121)
static volatile int32_t 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);
}
// The constructor flag is a NX2navi symbol the loader resolves; a title without it (or with a different
// name) reports "done" rather than dereferencing a null address.
static uint32_t ctor_state(const RtLoaded *m) { return m->ctor_flag ? LD32(m->ctor_flag) : CTOR_DONE; }
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.
static void cb_module_inits(Cpu *c) {
kern_seed_tls(); // the SDK clears the thread-local region during init; put the TLS back where it reads it
{ // where the sdk's lock thunks actually land once the loader has resolved them
static const unsigned long long off[4] = {0xe8fd20ull, 0xe8dff8ull, 0xe8f538ull, 0xe91200ull};
const unsigned char *sdkimg = (const unsigned char *)(uintptr_t)0x7005200000ull;
for (int k = 0; k < 4; k++)
rt_logf("boot: sdk GOT[%#llx] = %#llx\n", off[k],
*(const unsigned long long *)(const void *)(sdkimg + off[k]));
}
if (plat_cas32(&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_logf("boot: %s has %llu init_array entries (this title, not the %llu of the expected version)\n",
m->name, (unsigned long long)entries, (unsigned long long)expected_inits[k].entries);
}
plat_xchg32(&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 >= main_mod->size) { // 1.1.1's SDK sets the mode in nn::oe::InitializeApplet
rt_logf("boot: %s from %s, outside main: not the checkpoint\n", detail, at);
return;
}
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 (!plat_cas32(&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)));
plat_xchg32(&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 (!plat_cas32(&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);
plat_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 NX2NAVI_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);
}
// NX2NAVI_SHOTS=lo-hi:step [NX2NAVI_SHOTS_DIR=dir]: every step-th present in [lo, hi] saved as <dir>/f<n>.png (n the
// present), box-filtered to 480x270; NX2NAVI_SHOTS=w<lo>-<hi>:<step> takes them by world-clock seconds instead, the first
// present at or after each step (full size with NX2NAVI_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("NX2NAVI_SHOTS_FULL") != NULL;
const char *e = cfg_get("NX2NAVI_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("NX2NAVI_SHOTS_DIR") ? cfg_get("NX2NAVI_SHOTS_DIR") : ".";
if (hi) plat_file_mkdir_p(dir);
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 NX2NAVI_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[PLAT_PATH_MAX];
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);
coop_shutdown(); // LEAVE and BYE to the room, before the process ends
fs_save_flush();
vk_pipeline_prewarm_marker_clear(); // an orderly end, not a driver crash in the boot's builds (vk_pipeline.c pm_skipped)
vk_pipeline_save_exit(); // no-op without the Vulkan back end
prof_write();
rtss_limit_restore();
fflush(stderr);
plat_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 void prewarm_main(void *arg) {
(void)arg;
vk_pipeline_prewarm();
const char *prep = cfg_get("NX2NAVI_TEST_PREPARE_SHADERS"); // Task 2.3 test hook; Task 2.6 builds the preparation
if (prep && !strcmp(prep, "1")) {
int cpus = (int)plat_cpu_usable();
gfx_shader_prepare_recorded(cpus, cpus / 2 > 16 ? 16 : cpus / 2 < 1 ? 1 : cpus / 2);
}
// Translations of a previous translator are made again in the background from here on, after the boot's own
// translations. Not for a --shot image: which translation a shader gets would then depend on the pass's timing.
if (!rt_config()->shot) gfx_shader_carry_start();
return;
}
// rt_boot's wait for the prewarm, polled every 50 ms. A warm start is done in about half a second and shows nothing.
// When the prewarm translates shaders (a new translator, GPU or driver, or an emptied cache) or runs past
// BOOT_SCREEN_MS, the window shows the boot screen with its progress at 10 frames a second, and a black frame once it is
// done. cap_ms (NX2NAVI_PREWARM_WAIT_MS) bounds the wait, 0 none. Without progress for BOOT_STALL_MS (a translator or
// driver that hangs) the guest starts anyway; the prewarm then goes on in the background.
#define BOOT_SCREEN_MS 1000
#define BOOT_STALL_MS 60000
static void boot_wait(PlatThread *warm, uint64_t cap_ms) {
uint64_t t0 = plat_now_ms(), moved = t0, shown = 0;
VkPrewarmProgress p, last = {.phase = -1};
int screen = 0;
while (!plat_thread_join_ns(warm, 50000000ull)) {
uint64_t now = plat_now_ms();
vk_pipeline_prewarm_progress(&p);
if (p.phase != last.phase || p.sh_done != last.sh_done || p.pipe_done != last.pipe_done) last = p, moved = now;
if ((cap_ms && now - t0 >= cap_ms) || now - moved >= BOOT_STALL_MS) {
plat_thread_detach(warm);
vk_pipeline_prewarm_marker_clear();
vk_pipeline_prewarm_detach();
rt_log("boot: the prewarm goes on in the background after %llu ms (%s)", (unsigned long long)(now - t0),
cap_ms && now - t0 >= cap_ms ? "NX2NAVI_PREWARM_WAIT_MS" : "no progress for 60 s");
break;
}
if (!screen && (p.sh_translated > 0 || now - t0 >= BOOT_SCREEN_MS)) {
screen = 1;
rt_log("boot: preparing shaders on the boot screen (%lld of %lld shaders done, %lld translated)",
(long long)p.sh_done, (long long)p.sh_total, (long long)p.sh_translated);
}
if (screen && now - shown >= 100) {
int pipes = p.phase >= 2;
menu_boot_set(p.phase, pipes ? p.pipe_done : p.sh_done, pipes ? p.pipe_total : p.sh_total);
gfx_present_boot();
shown = now;
}
}
if (screen) { // the window stays black, not on the last count, until the game's first frame
menu_boot_set(-1, 0, 0);
gfx_present_boot();
}
}
void rt_boot(void) {
check_npdm();
fs_host_lock_profile(); // a second copy of the game stops here, before the prewarm below
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.
PlatThread *warm = plat_thread_new(prewarm_main, NULL, "prewarm");
ldr_load(rt_config()->exefs);
rt_patches_init();
mods_exefs_switch(); // after the patch sets' settings: an enabled mod's ExeFS patches on
frame_step_init();
res_init();
wide_init(); // Ocarina's widescreen sets (recomp/patches-v111.txt)
cheats_init(); // after the patch sets: a cheat's set follows its setting
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);
ipc_applet_event_init();
hostfn_seal();
uint64_t held = plat_now_ms();
// The guest waits for all of the prewarm (boot_wait): a warm start is ready in about half a second; a cold one (a new
// translator, GPU or driver) translated 2,987 shaders in about 11 s and then builds the recorded pipelines, with the
// boot screen showing its progress; after a driver change also the compute pipelines and the driver's cache of
// stage libraries (vk_pipeline.c bf_decide). Started early instead, the game would meet those shaders in play and
// wait for each one there. NX2NAVI_PREWARM_WAIT_MS caps the wait (0, the default: none); past it the prewarm goes
// on in the background. A --shot image waits for all of it without the screen, so that it does not depend on build
// timing.
const char *pw = cfg_get("NX2NAVI_PREWARM_WAIT_MS");
uint64_t wait_ms = pw && *pw ? strtoull(pw, NULL, 10) : 0;
if (warm && rt_config()->shot) plat_thread_join(warm);
else if (warm) boot_wait(warm, wait_ms);
else vk_pipeline_prewarm();
held = plat_now_ms() - held;
rt_log("boot: guest starts %llu ms after process start (%llu ms of it waiting for the prewarm)",
(unsigned long long)(plat_process_uptime_ns() / 1000000), (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");
}