Files
NX2navi/runtime/svc.c
chrg6 cd3f015732 Log the guest's ExitProcess line after the exit guard, and bring the comments on gfx_window_closed's return up to date.
The review of the exit sequence found that svc.c case 0x07 logged "guest ExitProcess at ...; exiting" before
rt_exit_begin. When a close had already begun, the guest thread that the guard then parks still claimed to exit, and
past the log cap the line fell outside the end's reserve, which rt_exit_begin opens. It also found that host.h and
gfx_wsys.h still said gfx_window_closed does not return, though it returns for its own thread coming back, and that
rt_exit_begin's comment spoke of the window thread handling a message inside the sequence, which since the close
runs on a thread of its own happens only in the fallback.

svc.c logs the line after rt_exit_begin returns 1. The two header comments say gfx_window_closed returns only for its
own thread coming back (gfx_present.c rt_exit_begin), and rt_exit_begin's comment says the window thread runs the
sequence itself only when gfx_window_win.c exit_start could make no thread.

tests/exit_order_check.py checks statically that 0x07's rt_log comes after rt_exit_begin; it fails on the code
before this change and passes after it. exit_order_check and the compile check of every runtime file (a header
changed) pass, with no new -Wall warnings in svc.c and gfx_present.c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:24 -05:00

257 lines
11 KiB
C

// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
// The SVC entry of the lifted code: preemption checkpoint, trace, then the thread and
// synchronization SVCs (kernel.c), IPC (ipc.c), memory (memory.c), Break, OutputDebugString, GetInfo and ExitProcess. The 21 SVCs
// NX2navi has but never reaches on its boot path stop with a named fatal, the caller and the guest backtrace.
#include <stdio.h>
#include <string.h>
#include "fs.h"
#include "host.h"
#include "kernel.h"
#include "vk.h"
#include "platform/platform.h"
#define R_OK 0u
#define ST_FREE 0u
#define ST_STACK 0xBu
#define PERM_RW 3u
#define PROGRAM_ID 0x0400486024dec000ull // the game's title id (GetInfo 18); fs_host.c NX2NAVI_APPLICATION_ID is the same
static uint64_t g_svcs;
void (*volatile svc_test_exit_hook)(unsigned code);
RtStats rt_stats(void) {
return (RtStats){__atomic_load_n(&g_svcs, __ATOMIC_RELAXED), kern_threads_created(), hostfn_calls()};
}
static int readable(uint64_t a, uint64_t n) { return mem_host_readable(a, n); }
void kern_backtrace(const Cpu *c) {
char d[160];
rt_logf(" lr %s\n", ldr_describe(c->x[30], d, sizeof d));
uint64_t fp = c->x[29];
for (int n = 0; n < 32 && fp && !(fp & 7) && readable(fp, 16); n++) {
uint64_t next = LD64(fp), lr = LD64(fp + 8);
rt_logf(" guest frame %2d: %s\n", n, ldr_describe(lr, d, sizeof d));
if (next <= fp || next - fp > (64u << 20)) break;
fp = next;
}
}
static const struct { uint32_t imm; const char *name; } unimplemented[] = {
{0x13, "MapSharedMemory"}, {0x14, "UnmapSharedMemory"},
{0x23, "SendAsyncRequestWithUserBuffer"}, {0x28, "ReturnFromException"}, {0x2A, "FlushEntireDataCache"},
{0x2F, "GetLastThreadInfo"}, {0x32, "SetThreadActivity"}, {0x33, "GetThreadContext3"},
{0x36, "SynchronizePreemptionState"}, {0x40, "CreateSession"}, {0x41, "AcceptSession"},
{0x43, "ReplyAndReceive"}, {0x44, "ReplyAndReceiveWithUserBuffer"}, {0x48, "MapPhysicalMemoryUnsafe"},
{0x49, "UnmapPhysicalMemoryUnsafe"}, {0x4B, "CreateCodeMemory"}, {0x50, "CreateSharedMemory"},
{0x51, "MapTransferMemory"}, {0x52, "UnmapTransferMemory"}, {0x70, "CreatePort"}, {0x72, "ConnectToPort"},
};
static __attribute__((noreturn)) void svc_fatal(Cpu *c, uint32_t imm, uint64_t pc, const char *why) {
const char *name = "unknown";
for (unsigned i = 0; i < sizeof unimplemented / sizeof *unimplemented; i++)
if (unimplemented[i].imm == imm) name = unimplemented[i].name;
char at[160];
fflush(stdout);
rt_logf("FATAL: svc #%#x %s at %s: %s\n x0 %#llx x1 %#llx x2 %#llx x3 %#llx\n", imm, name,
ldr_describe(pc, at, sizeof at), why, (unsigned long long)c->x[0], (unsigned long long)c->x[1],
(unsigned long long)c->x[2], (unsigned long long)c->x[3]);
kern_backtrace(c);
rt_fatal("svc #%#x %s is not implemented", imm, name);
}
// QueryMemory sees the guest main thread's stack, which memory.c does not know: Stack inside it, free ranges
// around it clipped (the way memory.c clips moved stacks).
static void main_stack_query(uint64_t a, RtMemoryInfo *r) {
uint64_t mb, ms;
kern_main_stack(&mb, &ms);
if (!ms) return;
uint64_t me = mb + ms;
if (a >= mb && a < me) {
*r = (RtMemoryInfo){.base = mb, .size = ms, .state = ST_STACK, .perm = PERM_RW};
return;
}
if (r->state != ST_FREE) return;
uint64_t lo = r->base, hi = r->base + r->size;
if (me <= a && me > lo) lo = me;
if (mb > a && mb < hi) hi = mb;
r->base = lo;
r->size = hi - lo;
}
static int on_main_stack(uint64_t a, uint64_t n) {
uint64_t mb, ms;
kern_main_stack(&mb, &ms);
return ms && a < mb + ms && mb < a + n;
}
static uint64_t user_exception_context(void) {
static void *page;
void *p = __atomic_load_n(&page, __ATOMIC_ACQUIRE);
if (!p) {
void *mine = mem_kernel_alloc(0x1000, 0x1000), *expected = NULL;
if (__atomic_compare_exchange_n(&page, &expected, mine, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) p = mine;
else {
mem_kernel_free(mine, 0x1000);
p = expected;
}
}
return (uint64_t)(uintptr_t)p;
}
// GetInfo(w1 id, w2 handle, x3 sub id -> x1)
static void get_info(Cpu *c, uint64_t pc) {
uint32_t id = (uint32_t)c->x[1];
uint64_t v = 0;
if (mem_info(id, &v)) {
c->x[0] = R_OK;
c->x[1] = v;
return;
}
switch (id) {
case 0: v = 0x3f; break; // core mask: cores 0-5 (Switch 2 application cores; see kernel.c NCORE)
case 1: v = 0x0ffffffff0000000ull; break; // priority mask
case 8: v = 0; break; // debugger attached
case 10: v = 0; break; // idle tick count
case 11: // random entropy
if (!plat_random(&v, sizeof v)) rt_fatal("GetInfo 11: no random bytes from the system");
break;
case 18: v = PROGRAM_ID; break;
case 20: v = user_exception_context(); break; // the SDK keeps e.g. the dying message at +0x1C0
case 23: v = 1; break;
case 29: // region the SDK records at nnosInitialize (address, then
case 30: v = 0; break;
case 33: v = 0x1000; break; // VAMM control region size: mapped at 0x300000000 and made into a unit heap
default: {
char why[64];
snprintf(why, sizeof why, "GetInfo id %u (handle %#x, sub id %#llx)", id, (uint32_t)c->x[2],
(unsigned long long)c->x[3]);
svc_fatal(c, 0x29, pc, why);
}
}
c->x[0] = R_OK;
c->x[1] = v;
}
static void svc_body(Cpu *c, uint32_t imm, uint64_t pc);
void rt_svc(Cpu *c, uint32_t imm, uint64_t pc) {
kern_checkpoint();
if (__builtin_expect(!kern_host_track || !c->thread, 1)) {
svc_body(c, imm, pc);
return;
}
void *t = c->thread; // kern_host_track: the SVC counted in the thread's host depth (hostfn.c hostfn_tracked)
int *d = kern_host_depth(t);
__atomic_store_n(d, *d + 1, __ATOMIC_RELAXED);
svc_body(c, imm, pc);
int n = *d - 1;
__atomic_store_n(d, n, __ATOMIC_RELAXED);
if (!n && kern_preempt_pending(t)) kern_host_left(t);
}
static void svc_body(Cpu *c, uint32_t imm, uint64_t pc) {
__atomic_fetch_add(&g_svcs, 1, __ATOMIC_RELAXED);
if (rt_trace & RT_TRACE_SVC) {
char at[160];
rt_log("svc %#x at %s x0=%#llx x1=%#llx x2=%#llx x3=%#llx", imm, ldr_describe(pc, at, sizeof at),
(unsigned long long)c->x[0], (unsigned long long)c->x[1], (unsigned long long)c->x[2],
(unsigned long long)c->x[3]);
}
if (kern_svc(c, imm)) return;
uint64_t *x = c->x;
switch (imm) {
case 0x15: case 0x1F: case 0x21: case 0x22:
if (ipc_svc(c, imm)) return;
svc_fatal(c, imm, pc, "not handled by ipc.c");
case 0x01: { // SetHeapSize(x1 size -> x1 address)
uint64_t a = 0;
uint32_t r = mem_set_heap_size(x[1], &a);
x[0] = r;
if (!r) x[1] = a;
return;
}
case 0x02: x[0] = mem_set_permission(x[0], x[1], (uint32_t)x[2]); return;
case 0x03: x[0] = mem_set_attribute(x[0], x[1], (uint32_t)x[2], (uint32_t)x[3]); return;
case 0x04: // MapMemory(x0 dst, x1 src, x2 size)
x[0] = on_main_stack(x[0], x[2]) ? KERN_R_INVALID_STATE_MEM : mem_map(x[0], x[1], x[2]);
return;
case 0x05: x[0] = mem_unmap(x[0], x[1], x[2]); return;
case 0x06: { // QueryMemory(x0 MemoryInfo*, x2 address -> w1 page info)
RtMemoryInfo mi;
uint32_t r = mem_query(x[2], &mi);
main_stack_query(x[2], &mi);
if (!r && x[0]) memcpy((void *)(uintptr_t)x[0], &mi, sizeof mi);
x[0] = r;
x[1] = 0;
return;
}
case 0x2C: x[0] = mem_map_physical(x[0], x[1]); return;
case 0x2D: x[0] = mem_unmap_physical(x[0], x[1]); return;
case 0x26: { // Break(x0 reason, x1 info, x2 info size); bit 31 of the reason: notification only
uint32_t result = 0;
if (x[1] && x[2] >= 4 && readable(x[1], 4)) memcpy(&result, (const void *)(uintptr_t)x[1], 4);
char at[160];
rt_log("guest Break reason %#llx result %#x at %s", (unsigned long long)x[0], result,
ldr_describe(pc, at, sizeof at));
if (x[0] & 0x80000000ull) {
x[0] = R_OK;
return;
}
{ // the SDK keeps the abort text in the user exception context: show any readable run
const char *ctx = (const char *)(uintptr_t)user_exception_context();
char run[161];
int n = 0;
for (int i = 0; i < 0x400; i++) {
unsigned char ch = (unsigned char)ctx[i];
if (ch >= 0x20 && ch < 0x7f) {
if (n < 160) run[n++] = (char)ch;
} else if (n >= 4) {
run[n] = 0;
rt_log("guest abort text at ctx+%#x: %s", i - n, run);
n = 0;
} else {
n = 0;
}
}
}
{ // the object the aborting code was working on: its first bytes carry the state machine's byte
uint64_t obj = c->x[19];
if (obj && readable(obj, 32)) {
const unsigned char *p = (const unsigned char *)(uintptr_t)obj;
rt_log("guest abort object %#llx: %02x %02x %02x %02x %02x %02x %02x %02x "
"%02x %02x %02x %02x %02x %02x %02x %02x", (unsigned long long)obj,
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7],
p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15]);
}
}
svc_fatal(c, imm, pc, "Break (the SDK aborted)");
}
case 0x27: { // OutputDebugString(x0 text, x1 size)
uint64_t n = x[1];
if (n > 65536) svc_fatal(c, imm, pc, "debug string longer than 64 KiB");
if (n) {
fflush(stdout);
rt_log_raw((const char *)(uintptr_t)x[0], n);
}
x[0] = R_OK;
return;
}
case 0x29: get_info(c, pc); return;
case 0x07: { // ExitProcess, as closing the window: the orderly end (gfx_present.c rt_exit_steps), exit code 0
// once, shared with the window's close: a close already ending the process keeps this thread there, and the
// exit line is that close's alone; this thread coming back would resume guest code after its ExitProcess, so it
// ends the process instead
if (!rt_exit_begin()) plat_exit(0);
char at[160]; // after the guard, which also lets the line reach the log file past its cap (main.c log_sink)
rt_log("guest ExitProcess at %s; exiting", ldr_describe(pc, at, sizeof at));
rt_exit_steps(RT_EXIT_SETTINGS);
fflush(NULL);
if (svc_test_exit_hook) return svc_test_exit_hook(0);
plat_exit(0);
}
}
svc_fatal(c, imm, pc, "not implemented");
}