Files
totk-pc-recomp/runtime/ipc.c
T

798 lines
37 KiB
C

// Adapted from MacNativeGames games/totk/runtime/ipc.c @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames
// contributors). Message framing per libnx sf/hipc.h, cmif.h, tipc.h (ISC), read for the layout only.
// In-process Horizon IPC: SVCs 0x15, 0x1F, 0x21, 0x22, the sm: service manager
// and the service table. The SDK client code runs unchanged: requests are read from the calling thread's message
// buffer (TLR, or the user buffer of 0x22) and answered in place. Unknown ports, services and commands stop with their
// name and id instead of returning a fake success. Also --probe-ipc.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "audren.h"
#include "host.h"
#include "ipc.h"
#include "nfp.h"
#include "platform/platform.h"
#define CMIF_IN_MAGIC 0x49434653u // "SFCI"
#define CMIF_OUT_MAGIC 0x4f434653u // "SFCO"
#define POINTER_BUFFER_SIZE 0x8000
#define R_INVALID_SIZE 0xCA01u
#define R_INVALID_ADDRESS 0xCC01u
enum { T_CLOSE = 2, T_REQUEST = 4, T_CONTROL = 5, T_REQUEST_CTX = 6, T_CONTROL_CTX = 7, T_TIPC_CLOSE = 15,
T_TIPC_BASE = 16 };
// Services sm: hands out. Anything else is a named fatal.
static const struct { const char *name; IpcHandler handler; } services[] = {
{"audren:u", audren_manager_handler},
{"nfp:user", nfp_user_manager_handler},
};
static volatile int32_t g_live;
static volatile int64_t g_released, g_requests;
static _Thread_local const Cpu *t_req; // guest context of the request being served
long ipc_sessions_live(void) { return plat_cas32(&g_live, 0, 0); }
uint64_t ipc_sessions_released(void) { return (uint64_t)plat_cas64(&g_released, 0, 0); }
// ---- sessions
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)) {
IpcSession *s = calloc(1, sizeof *s);
if (!s) rt_fatal("ipc: session allocation");
*s = (IpcSession){name, handler, state, close, 1};
plat_inc32(&g_live);
return kern_handle_new(KERN_SESSION, s);
}
static uint32_t session_clone(IpcSession *s) {
plat_inc32(&s->refs);
return kern_handle_new(KERN_SESSION, s);
}
void ipc_handle_closed(int kind, void *obj) {
if (kind == KERN_TMEM) {
free(obj);
return;
}
if (kind != KERN_SESSION) rt_fatal("ipc_handle_closed: handle kind %d", kind);
IpcSession *s = obj;
if (plat_dec32(&s->refs)) return;
if (rt_trace & RT_TRACE_IPC) rt_log("ipc: %s closed", s->name);
if (s->close) s->close(s->state);
free(s);
plat_dec32(&g_live);
plat_inc64(&g_released);
}
// ---- helpers for services
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
void ipc_backtrace(void) {
const Cpu *c = t_req;
if (!c) return;
char d[160];
rt_logf(" ipc request from %s\n", ldr_describe(c->x[30], d, sizeof d));
uint64_t fp = c->x[29];
for (int n = 0; n < 24 && 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;
}
}
void *ipc_guest(uint64_t addr, uint64_t size) {
if ((!addr && size) || addr + size < addr || addr + size > RT_ADDRESS_LIMIT) {
ipc_backtrace();
rt_fatal("ipc: client buffer %#llx+%#llx", (unsigned long long)addr, (unsigned long long)size);
}
return (void *)(uintptr_t)addr;
}
const void *ipc_arg(const IpcRequest *rq, uint32_t off, uint32_t n) {
if (off + n > rq->data_size) {
ipc_backtrace();
rt_fatal("ipc: command %u argument %u+%u beyond %u bytes", rq->cmd, off, n, rq->data_size);
}
return rq->data + off;
}
void ipc_out(IpcResponse *rs, const void *p, uint32_t n) {
if (rs->data_size + n > sizeof rs->data) rt_fatal("ipc: response larger than %u bytes", (unsigned)sizeof rs->data);
memcpy(rs->data + rs->data_size, p, n);
rs->data_size += n;
}
void ipc_unimplemented(const IpcSession *s, const IpcRequest *rq) {
fflush(stdout);
rt_logf("ipc: %s command %u (type %u, data %u bytes, x%u a%u b%u w%u c%u, copy %u move %u)\n", s->name,
rq->cmd, rq->type, rq->data_size, rq->nx, rq->na, rq->nb, rq->nw, rq->nc, rq->ncopy, rq->nmove);
ipc_backtrace();
rt_fatal("ipc: %s command %u is not implemented", s->name, rq->cmd);
}
// ---- HIPC framing
static uint64_t static_addr(const uint32_t *d) {
return d[1] | ((uint64_t)((d[0] >> 12) & 0xf) << 32) | ((uint64_t)((d[0] >> 6) & 0x3f) << 36);
}
static uint64_t buffer_addr(const uint32_t *d) {
return d[1] | ((uint64_t)((d[2] >> 28) & 0xf) << 32) | ((uint64_t)((d[2] >> 2) & 0x3fffff) << 36);
}
static uint64_t buffer_size(const uint32_t *d) { return d[0] | ((uint64_t)((d[2] >> 24) & 0xf) << 32); }
static void parse(const uint32_t *m, IpcRequest *rq, const uint32_t **data_words, unsigned *ndata) {
memset(rq, 0, sizeof *rq);
rq->type = m[0] & 0xffff;
rq->nx = (m[0] >> 16) & 0xf;
rq->na = (m[0] >> 20) & 0xf;
rq->nb = (m[0] >> 24) & 0xf;
rq->nw = (m[0] >> 28) & 0xf;
*ndata = m[1] & 0x3ff;
unsigned recv_mode = (m[1] >> 10) & 0xf, special = m[1] >> 31;
if (rq->nx > IPC_MAX_BUF || rq->na > IPC_MAX_BUF || rq->nb > IPC_MAX_BUF || rq->nw > IPC_MAX_BUF)
rt_fatal("ipc: too many buffers (x%u a%u b%u w%u)", rq->nx, rq->na, rq->nb, rq->nw);
const uint32_t *p = m + 2;
if (special) {
uint32_t h = *p++;
rq->has_pid = h & 1;
rq->ncopy = (h >> 1) & 0xf;
rq->nmove = (h >> 5) & 0xf;
if (rq->ncopy > 8 || rq->nmove > 8) rt_fatal("ipc: too many handles (copy %u move %u)", rq->ncopy, rq->nmove);
if (rq->has_pid) {
memcpy(&rq->pid, p, 8);
p += 2;
}
for (unsigned i = 0; i < rq->ncopy; i++) rq->copy[i] = *p++;
for (unsigned i = 0; i < rq->nmove; i++) rq->move[i] = *p++;
}
for (unsigned i = 0; i < rq->nx; i++, p += 2) rq->x[i] = (IpcBuffer){static_addr(p), p[0] >> 16, p[0] & 0x3f};
IpcBuffer *lists[3] = {rq->a, rq->b, rq->w};
unsigned counts[3] = {rq->na, rq->nb, rq->nw};
for (unsigned k = 0; k < 3; k++)
for (unsigned i = 0; i < counts[k]; i++, p += 3) lists[k][i] = (IpcBuffer){buffer_addr(p), buffer_size(p), i};
*data_words = p;
const uint32_t *recv = p + *ndata;
rq->nc = recv_mode == 2 ? 1 : recv_mode > 2 ? recv_mode - 2 : 0;
if (rq->nc > IPC_MAX_BUF) rt_fatal("ipc: %u receive buffers", rq->nc);
for (unsigned i = 0; i < rq->nc; i++, recv += 2)
rq->c[i] = (IpcBuffer){recv[0] | ((uint64_t)(recv[1] & 0xffff) << 32), recv[1] >> 16, i};
if ((const uint8_t *)recv - (const uint8_t *)m > IPC_MSG_BYTES) rt_fatal("ipc: request exceeds the message buffer");
}
static void respond(uint32_t *m, int tipc, const IpcResponse *rs) {
uint32_t out[IPC_MSG_BYTES / 4];
memset(out, 0, sizeof out);
unsigned special = rs->ncopy || rs->nmove;
uint32_t *p = out + 2;
if (special) {
*p++ = (rs->ncopy << 1) | (rs->nmove << 5);
for (unsigned i = 0; i < rs->ncopy; i++) *p++ = rs->copy[i];
for (unsigned i = 0; i < rs->nmove; i++) *p++ = rs->move[i];
}
for (unsigned i = 0; i < rs->nxout; i++, p += 2) {
uint64_t a = rs->xout[i].addr;
p[0] = (rs->xout[i].index & 0x3f) | (uint32_t)(((a >> 36) & 0x3f) << 6) | (uint32_t)(((a >> 32) & 0xf) << 12) |
(uint32_t)(rs->xout[i].size << 16);
p[1] = (uint32_t)a;
}
uint8_t *words = (uint8_t *)p, *payload;
unsigned header = tipc ? 4 : 16;
payload = tipc ? words : (uint8_t *)out + (((words - (uint8_t *)out) + 15) & ~15);
unsigned used = (unsigned)(payload - words) + header + rs->data_size;
unsigned nwords = tipc ? (used + 3) / 4 : (used + 16 + 3) / 4; // CMIF keeps alignment slack like libnx
if ((size_t)(words - (uint8_t *)out) + nwords * 4 > IPC_MSG_BYTES) rt_fatal("ipc: response exceeds the message buffer");
if (tipc) memcpy(payload, &rs->result, 4);
else {
uint32_t h[4] = {CMIF_OUT_MAGIC, 0, rs->result, 0};
memcpy(payload, h, 16);
}
memcpy(payload + header, rs->data, rs->data_size);
out[0] = (rs->nxout & 0xf) << 16;
out[1] = (nwords & 0x3ff) | (special ? 1u << 31 : 0);
memcpy(m, out, IPC_MSG_BYTES);
}
// CMIF control requests. Domains are not implemented: MNG boots 1.2.1 without them.
static void control(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
switch (rq->cmd) {
case 3: { uint16_t n = POINTER_BUFFER_SIZE; IPC_OUT(rs, n); return; } // QueryPointerBufferSize
case 2: case 4: rs->move[rs->nmove++] = session_clone(s); return; // CloneCurrentObject(Ex)
default:
ipc_backtrace();
rt_fatal("ipc: %s control command %u is not implemented (domains are not served)", s->name, rq->cmd);
}
}
static void dispatch(IpcSession *s, uint32_t *m) {
IpcRequest rq;
const uint32_t *words;
unsigned ndata;
parse(m, &rq, &words, &ndata);
if (rq.type == T_CLOSE || rq.type == T_TIPC_CLOSE) return;
plat_inc64(&g_requests);
IpcResponse rs;
memset(&rs, 0, sizeof rs);
int tipc = rq.type >= T_TIPC_BASE;
const uint8_t *bytes = (const uint8_t *)words, *end = bytes + ndata * 4;
if (tipc) {
rq.cmd = rq.type - T_TIPC_BASE;
rq.data = bytes;
rq.data_size = (uint32_t)(end - bytes);
} else if (rq.type == T_REQUEST || rq.type == T_REQUEST_CTX || rq.type == T_CONTROL || rq.type == T_CONTROL_CTX) {
const uint8_t *payload = (const uint8_t *)m + (((bytes - (const uint8_t *)m) + 15) & ~15);
uint32_t h[4];
memcpy(h, payload, 16);
if (h[0] != CMIF_IN_MAGIC) {
ipc_backtrace();
rt_fatal("ipc: %s: CMIF magic %#x (domain sessions are not served)", s->name, h[0]);
}
rq.cmd = h[2];
rq.data = payload + 16;
rq.data_size = payload + 16 <= end ? (uint32_t)(end - payload - 16) : 0;
} else {
fflush(stdout);
rt_logf("ipc: %s message type %u:", s->name, rq.type);
for (unsigned i = 0; i < 16; i++) rt_logf(" %08x", m[i]);
rt_log_raw("\n", 1);
ipc_backtrace();
rt_fatal("ipc: %s message type %u is not implemented", s->name, rq.type);
}
if (rq.type == T_CONTROL || rq.type == T_CONTROL_CTX) control(s, &rq, &rs);
else s->handler(s, &rq, &rs);
if (rt_trace & RT_TRACE_IPC) {
char d[160];
rt_log("ipc: %s %s %u -> %#x (copy %u move %u) from %s", s->name,
tipc ? "tipc" : rq.type == T_CONTROL || rq.type == T_CONTROL_CTX ? "control" : "cmd", rq.cmd,
rs.result, rs.ncopy, rs.nmove, t_req ? ldr_describe(t_req->x[30], d, sizeof d) : "host");
}
respond(m, tipc, &rs);
}
// ---- sm: service manager (CMIF, and TIPC as the SDK's sm client speaks it since 12.0.0)
static void sm_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
switch (rq->cmd) {
case 0: return; // RegisterClient / Initialize (pid)
case 1: { // GetServiceHandle(name)
char name[9] = {0};
memcpy(name, ipc_arg(rq, 0, 8), 8);
for (unsigned i = 0; i < sizeof services / sizeof *services; i++)
if (!strcmp(services[i].name, name)) {
rs->move[rs->nmove++] = ipc_session_new(services[i].name, services[i].handler, NULL, NULL);
rt_log("ipc: opened service %s", services[i].name);
return;
}
fflush(stdout);
ipc_backtrace();
rt_fatal("sm: service \"%s\" is not implemented", name);
}
default: ipc_unimplemented(s, rq);
}
}
// ---- SVCs
int ipc_svc(Cpu *c, uint32_t imm) {
uint64_t *x = c->x;
switch (imm) {
case 0x1F: { // ConnectToNamedPort(x1 name -> w1 handle)
char name[13] = {0};
const char *p = (const char *)(uintptr_t)x[1];
if (!p || !readable(x[1], 1)) rt_fatal("ConnectToNamedPort: name at %#llx", (unsigned long long)x[1]);
for (int i = 0; i < 12 && p[i]; i++) name[i] = p[i];
if (strcmp(name, "sm:")) {
t_req = c;
ipc_backtrace();
rt_fatal("ConnectToNamedPort(\"%s\"): only sm: is served", name);
}
x[0] = 0;
x[1] = ipc_session_new("sm:", sm_handler, NULL, NULL);
return 1;
}
case 0x21: case 0x22: { // SendSyncRequest(w0 handle) / WithUserBuffer(x0 buffer, x1 size, w2 handle)
uint32_t h = (uint32_t)(imm == 0x21 ? x[0] : x[2]);
IpcSession *s = kern_handle_get(h, KERN_SESSION);
if (!s) {
rt_log("ipc: svc %#x on %#x, not a session handle", imm, h);
x[0] = IPC_R_BAD_HANDLE;
return 1;
}
uint32_t *m = (uint32_t *)(uintptr_t)(imm == 0x21 ? c->tpidrro : x[0]);
if (!m || (imm == 0x22 && x[1] < IPC_MSG_BYTES))
rt_fatal("ipc: svc %#x message buffer %p size %#llx", imm, (void *)m, (unsigned long long)x[1]);
const Cpu *outer = t_req;
t_req = c;
dispatch(s, m);
t_req = outer;
x[0] = 0;
return 1;
}
case 0x15: { // CreateTransferMemory(x1 address, x2 size, w3 permission -> w1 handle)
if ((x[1] & 0xfff) || x[1] + x[2] < x[1] || x[1] + x[2] > RT_ADDRESS_LIMIT) {
x[0] = R_INVALID_ADDRESS;
return 1;
}
if (!x[2] || (x[2] & 0xfff)) {
x[0] = R_INVALID_SIZE;
return 1;
}
IpcTransferMemory *t = malloc(sizeof *t);
if (!t) rt_fatal("ipc: transfer memory allocation");
*t = (IpcTransferMemory){x[1], x[2], (uint32_t)x[3]};
x[0] = 0;
x[1] = kern_handle_new(KERN_TMEM, t);
return 1;
}
}
return 0;
}
// ---- --probe-ipc
// Hand-built CMIF/TIPC requests in the guest main thread's TLR go through rt_svc, the entry the lifted SVC wrappers
// use: sm: -> audren:u -> GetWorkBufferSize -> an audio renderer with the null mixer, whose frame event the probe
// times while it sends RequestUpdate every frame. A child process checks the unknown-service fatal.
#define PROBE_FRAMES 200
#define PROBE_CHILD_ENV "TOTK_PROBE_IPC_CHILD"
#define PROBE_UNKNOWN_SERVICE "lm"
#define H_CURRENT_PROCESS 0xFFFF8001u
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
typedef struct {
unsigned type, cmd, pid, ncopy, na, nb;
uint32_t copy[4];
IpcBuffer a[2], b[2];
const void *data;
uint32_t size;
} Msg;
typedef struct {
uint32_t result, copy[8], move[8];
unsigned ncopy, nmove;
const uint8_t *data;
} Reply;
static uint32_t sv(Cpu *c, uint32_t imm, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3) {
c->x[0] = a0;
c->x[1] = a1;
c->x[2] = a2;
c->x[3] = a3;
rt_svc(c, imm, 0);
return (uint32_t)c->x[0];
}
static uint32_t *put_buffer(uint32_t *p, IpcBuffer b) {
p[0] = (uint32_t)b.size;
p[1] = (uint32_t)b.addr;
p[2] = (uint32_t)(((b.addr >> 36) & 0x3fffff) << 2) | (uint32_t)(((b.size >> 32) & 0xf) << 24) |
(uint32_t)(((b.addr >> 32) & 0xf) << 28);
return p + 3;
}
static void msg_build(uint32_t *m, const Msg *q) {
memset(m, 0, IPC_MSG_BYTES);
int tipc = q->type >= T_TIPC_BASE, special = q->pid || q->ncopy;
m[0] = q->type | q->na << 20 | q->nb << 24;
uint32_t *p = m + 2;
if (special) {
*p++ = (q->pid ? 1u : 0u) | q->ncopy << 1;
if (q->pid) p += 2; // the kernel fills the process id in
for (unsigned i = 0; i < q->ncopy; i++) *p++ = q->copy[i];
}
for (unsigned i = 0; i < q->na; i++) p = put_buffer(p, q->a[i]);
for (unsigned i = 0; i < q->nb; i++) p = put_buffer(p, q->b[i]);
uint8_t *words = (uint8_t *)p;
unsigned ndata;
if (tipc) {
memcpy(words, q->data, q->size);
ndata = (q->size + 3) / 4;
} else {
uint8_t *payload = (uint8_t *)m + (((words - (uint8_t *)m) + 15) & ~15);
uint32_t h[4] = {CMIF_IN_MAGIC, 0, q->cmd, 0};
memcpy(payload, h, 16);
if (q->size) memcpy(payload + 16, q->data, q->size);
ndata = (16 + 16 + q->size + 3) / 4; // libnx reserves the 16-byte alignment pad in the data size
}
m[1] = ndata | (special ? 1u << 31 : 0);
}
static void msg_parse(const uint32_t *m, int tipc, Reply *r) {
memset(r, 0, sizeof *r);
unsigned nx = (m[0] >> 16) & 0xf;
const uint32_t *p = m + 2;
if (m[1] >> 31) {
uint32_t h = *p++;
if (h & 1) p += 2;
r->ncopy = (h >> 1) & 0xf;
r->nmove = (h >> 5) & 0xf;
for (unsigned i = 0; i < r->ncopy; i++) r->copy[i] = *p++;
for (unsigned i = 0; i < r->nmove; i++) r->move[i] = *p++;
}
p += 2 * nx;
if (tipc) {
r->result = p[0];
r->data = (const uint8_t *)(p + 1);
return;
}
const uint8_t *payload = (const uint8_t *)m + ((((const uint8_t *)p - (const uint8_t *)m) + 15) & ~15);
uint32_t h[4];
memcpy(h, payload, 16);
r->result = h[0] == CMIF_OUT_MAGIC ? h[2] : 0xFFFFFFFFu;
r->data = payload + 16;
}
// One request on the TLR message buffer; the kernel result of SendSyncRequest, the service result in r->result.
static uint32_t call(Cpu *c, uint32_t h, const Msg *q, Reply *r) {
uint32_t *m = (uint32_t *)(uintptr_t)c->tpidrro;
msg_build(m, q);
uint32_t k = sv(c, 0x21, h, 0, 0, 0);
if (k) return k;
msg_parse(m, q->type >= T_TIPC_BASE, r);
return 0;
}
static uint32_t cmd(Cpu *c, uint32_t h, unsigned id, const void *data, uint32_t size, Reply *r) {
Msg q = {.type = T_REQUEST, .cmd = id, .data = data, .size = size};
uint32_t k = call(c, h, &q, r);
return k ? k : r->result;
}
static uint32_t close_handle(Cpu *c, uint32_t h) { return sv(c, 0x16, h, 0, 0, 0); }
static uint32_t connect_sm(Cpu *c) {
static const char port[] = "sm:";
uint32_t r = sv(c, 0x1F, 0, (uint64_t)(uintptr_t)port, 0, 0);
CHECK(r == 0 && c->x[1], "ConnectToNamedPort(sm:) %#x", r);
return (uint32_t)c->x[1];
}
static uint32_t get_service(Cpu *c, uint32_t sm, const char *name, int tipc) {
char n[8] = {0};
memcpy(n, name, strlen(name));
Msg q = {.type = tipc ? T_TIPC_BASE + 1 : T_REQUEST, .cmd = 1, .data = n, .size = 8};
Reply r;
uint32_t k = call(c, sm, &q, &r);
CHECK(!k && !r.result && r.nmove == 1, "%s GetServiceHandle(%s): kernel %#x result %#x move %u",
tipc ? "TIPC" : "CMIF", name, k, r.result, r.nmove);
return r.nmove ? r.move[0] : 0;
}
// A TotK-like renderer: 48 kHz, 240-sample (5 ms) frames, final mix plus submixes, two sinks, REV11.
static const AudrenParameter g_param = {
.sample_rate = 48000, .sample_count = 240, .mix_buffer_count = 24, .submix_count = 8, .voice_count = 192,
.sink_count = 2, .effect_count = 12, .revision = AUDREN_REV(11)};
typedef struct {
uint8_t *in, *out, *perf;
uint32_t in_size, out_total, voices_out;
int16_t *pcm;
} Update;
static void update_build(Update *u, int first) {
const AudrenParameter *p = &g_param;
uint32_t npools = (uint32_t)p->effect_count + 4u * (uint32_t)p->voice_count, mc = (uint32_t)p->submix_count + 1;
AudrenHeader h = {.revision = p->revision, .behavior = 0x10, .memory_pools = npools * sizeof(AudrenPoolIn),
.voices = (uint32_t)p->voice_count * sizeof(AudrenVoiceIn),
.effects = (uint32_t)p->effect_count * sizeof(AudrenEffectIn), .mixes = mc * sizeof(AudrenMixIn),
.sinks = (uint32_t)p->sink_count * sizeof(AudrenSinkIn), .performance = 0x10};
h.total = sizeof h + h.behavior + h.memory_pools + h.voice_resources + h.voices + h.effects + h.splitters +
h.mixes + h.sinks + h.performance + h.render_info;
u->in_size = h.total;
memset(u->in, 0, h.total);
uint8_t *w = u->in;
memcpy(w, &h, sizeof h);
w += sizeof h + h.behavior + h.memory_pools + h.voice_resources;
for (int i = 0; i < p->voice_count; i++) {
AudrenVoiceIn v = {.id = (uint32_t)i};
if (i == 0) { // one PCM16 voice with a 100 ms buffer: consumed after 20 frames
v = (AudrenVoiceIn){.id = 0, .is_new = (uint8_t)first, .in_use = 1, .play_state = PLAY_STARTED,
.sample_format = FMT_PCM16, .sample_rate = 48000, .channel_count = 1, .pitch = 1,
.volume = 1, .mix_id = 0, .splitter_id = 0xffffffffu, .wave_buffer_count = 1};
v.wave_buffers[0] = (AudrenWaveBuffer){.address = (uint64_t)(uintptr_t)u->pcm, .size = 4800 * 2,
.end = 4800, .sent = (uint8_t)!first};
for (int k = 1; k < 4; k++) v.wave_buffers[k].sent = 1;
}
memcpy(w, &v, sizeof v);
w += sizeof v;
}
w += h.effects + h.splitters;
AudrenMixIn mix = {.volume = 1, .sample_rate = 48000, .buffer_count = 6, .in_use = 1, .dest_mix_id = 0x7fffffff,
.dest_splitter_id = 0xffffffffu};
memcpy(w, &mix, sizeof mix);
w += h.mixes;
AudrenSinkIn sink = {.type = SINK_DEVICE, .in_use = 1, .input_count = 2, .inputs = {0, 1}};
memcpy(w, &sink, sizeof sink);
u->out_total = (uint32_t)sizeof h + npools * sizeof(AudrenPoolOut) + (uint32_t)p->voice_count * sizeof(AudrenVoiceOut) +
(uint32_t)p->effect_count * 0x90u + (uint32_t)p->sink_count * sizeof(AudrenSinkOut) + 0x10 +
AUDREN_BEHAVIOR_OUT + AUDREN_RENDER_INFO_OUT;
u->voices_out = (uint32_t)sizeof h + npools * sizeof(AudrenPoolOut);
}
static uint32_t request_update(Cpu *c, uint32_t ren, Update *u, uint64_t *frames, AudrenVoiceOut *voice0) {
Msg q = {.type = T_REQUEST, .cmd = 4, .na = 1, .nb = 2};
q.a[0] = (IpcBuffer){(uint64_t)(uintptr_t)u->in, u->in_size, 0};
q.b[0] = (IpcBuffer){(uint64_t)(uintptr_t)u->out, 0x8000, 0};
q.b[1] = (IpcBuffer){(uint64_t)(uintptr_t)u->perf, 0x100, 1};
Reply r;
uint32_t k = call(c, ren, &q, &r);
if (k || r.result) return k ? k : r.result;
AudrenHeader o;
memcpy(&o, u->out, sizeof o);
if (o.revision != g_param.revision || o.total != u->out_total) return 0xFFFFFFFFu;
memcpy(frames, u->out + o.total - AUDREN_RENDER_INFO_OUT, 8);
memcpy(voice0, u->out + u->voices_out, sizeof *voice0);
return 0;
}
static void probe_renderer(Cpu *c, uint32_t audren) {
Reply r;
// GetWorkBufferSize(AudioRendererParameterInternal) -> u64
uint32_t res = cmd(c, audren, 1, &g_param, sizeof g_param, &r);
uint64_t work = 0;
if (!res) memcpy(&work, r.data, 8);
printf("audren:u GetWorkBufferSize(48000 Hz x240, %d voices, %d submixes, %d effects, REV11) -> result %#x, "
"%#llx bytes\n", g_param.voice_count, g_param.submix_count, g_param.effect_count, res,
(unsigned long long)work);
CHECK(!res && work >= 0x1000 && work < (256u << 20) && !(work & 0xfff), "GetWorkBufferSize");
if (res || !work) return;
// OpenAudioRenderer(param, pad, work size, aruid; pid; copy transfer memory, current process)
uint8_t *wb = mem_kernel_alloc(work, 0x1000);
uint32_t tm = 0;
res = sv(c, 0x15, 0, (uint64_t)(uintptr_t)wb, work, 0);
tm = (uint32_t)c->x[1];
CHECK(!res, "CreateTransferMemory %#x", res);
struct { AudrenParameter p; uint32_t pad; uint64_t work, aruid; } open = {g_param, 0, work, 0};
Msg q = {.type = T_REQUEST, .cmd = 0, .pid = 1, .ncopy = 2, .copy = {tm, H_CURRENT_PROCESS}, .data = &open,
.size = sizeof open};
long live0 = ipc_sessions_live();
uint64_t released0 = ipc_sessions_released();
uint32_t k = call(c, audren, &q, &r);
CHECK(!k && !r.result && r.nmove == 1 && audren_renderers_live() == 1, "OpenAudioRenderer: kernel %#x result %#x",
k, r.result);
if (k || r.result || !r.nmove) return;
uint32_t ren = r.move[0];
int32_t rate = 0, count = 0;
if (!cmd(c, ren, 0, NULL, 0, &r)) memcpy(&rate, r.data, 4);
if (!cmd(c, ren, 1, NULL, 0, &r)) memcpy(&count, r.data, 4);
CHECK(rate == 48000 && count == 240, "GetSampleRate %d, GetSampleCount %d", rate, count);
res = cmd(c, ren, 7, NULL, 0, &r); // QuerySystemEvent
uint32_t ev = !res && r.ncopy == 1 ? r.copy[0] : 0;
CHECK(ev, "QuerySystemEvent: result %#x, %u copy handles", res, r.ncopy);
Update u = {mem_kernel_alloc(0x40000, 64), mem_kernel_alloc(0x8000, 64), mem_kernel_alloc(0x100, 64), 0, 0, 0,
mem_kernel_alloc(4800 * 2, 64)};
for (int i = 0; i < 4800; i++) u.pcm[i] = (int16_t)(i * 7);
uint64_t frames = ~0ull;
AudrenVoiceOut v0 = {0};
update_build(&u, 1);
res = request_update(c, ren, &u, &frames, &v0);
CHECK(!res && frames == 0, "RequestUpdate before Start: %#x, %llu frames", res, (unsigned long long)frames);
update_build(&u, 0);
CHECK(!cmd(c, ren, 5, NULL, 0, &r), "Start");
uint32_t state = 1;
if (!cmd(c, ren, 3, NULL, 0, &r)) memcpy(&state, r.data, 4);
CHECK(state == 0, "GetState after Start: %u", state);
// Every frame, as the game's audio thread does: wait for the event, clear it, RequestUpdate.
uint32_t hs[1] = {ev};
sv(c, 0x17, ev, 0, 0, 0);
uint64_t t0 = 0, t1 = 0, f0 = 0, f1 = 0, worst = 0, prev = 0;
int waits_ok = 0, updates_ok = 0;
for (int i = 0; i < PROBE_FRAMES; i++) {
uint32_t w = sv(c, 0x18, 0, (uint64_t)(uintptr_t)hs, 1, 1000000000ull);
uint64_t now = kern_now_ns();
waits_ok += w == 0;
sv(c, 0x17, ev, 0, 0, 0);
uint64_t f = 0;
updates_ok += request_update(c, ren, &u, &f, &v0) == 0;
if (i == 0) t0 = now, f0 = f;
if (i && now - prev > worst) worst = now - prev;
prev = t1 = now;
f1 = f;
}
double period = (double)(t1 - t0) / (PROBE_FRAMES - 1) / 1e6, expect = 1e3 * g_param.sample_count / g_param.sample_rate;
double rendered = (double)(f1 - f0), elapsed = (double)(t1 - t0) / 1e6 / expect;
printf("render tick: %d waits on the frame event (%d ok), %d RequestUpdate (%d ok); mean period %.3f ms (expect "
"%.3f), worst gap %.3f ms; frames rendered %llu over %.1f periods\n", PROBE_FRAMES, waits_ok, PROBE_FRAMES,
updates_ok, period, expect, (double)worst / 1e6, (unsigned long long)(f1 - f0), elapsed);
CHECK(waits_ok == PROBE_FRAMES && updates_ok == PROBE_FRAMES, "event waits / updates");
CHECK(period > expect * 0.9 && period < expect * 1.1, "frame event period %.3f ms", period);
CHECK(rendered > elapsed - 3 && rendered < elapsed + 3, "frames rendered %.0f vs elapsed %.1f", rendered, elapsed);
printf("voice 0 (PCM16, 4800 samples): played %llu, wave buffers consumed %u\n",
(unsigned long long)v0.played_samples, v0.wave_buffers_consumed);
CHECK(v0.played_samples == 4800 && v0.wave_buffers_consumed == 1, "voice clock with the null mixer");
CHECK(!cmd(c, ren, 6, NULL, 0, &r), "Stop");
if (!cmd(c, ren, 3, NULL, 0, &r)) memcpy(&state, r.data, 4);
CHECK(state == 1, "GetState after Stop: %u", state);
res = close_handle(c, ren);
long live1 = ipc_sessions_live();
uint64_t released1 = ipc_sessions_released();
printf("CloseHandle(IAudioRenderer) -> %#x: sessions %ld -> %ld, released +%llu, renderers running %ld\n", res,
live0 + 1, live1, (unsigned long long)(released1 - released0), audren_renderers_live());
CHECK(!res && live1 == live0 && released1 == released0 + 1 && audren_renderers_live() == 0, "renderer close");
CHECK(!close_handle(c, ev) && !close_handle(c, tm), "close event and transfer memory");
mem_kernel_free(u.in, 0x40000);
mem_kernel_free(u.out, 0x8000);
mem_kernel_free(u.perf, 0x100);
mem_kernel_free(u.pcm, 4800 * 2);
mem_kernel_free(wb, work);
}
static void probe_main(Cpu *c) {
Reply r;
uint32_t sm = connect_sm(c);
Msg reg = {.type = T_REQUEST, .cmd = 0, .pid = 1, .data = (const uint64_t[]){0}, .size = 8};
uint32_t k = call(c, sm, &reg, &r);
CHECK(!k && !r.result, "sm: RegisterClient: kernel %#x result %#x", k, r.result);
uint32_t audren = get_service(c, sm, "audren:u", 0);
uint32_t dup = get_service(c, sm, "audren:u", 1);
printf("sm: connected, RegisterClient ok, GetServiceHandle(audren:u) -> %#x (CMIF), %#x (TIPC)\n", audren, dup);
uint64_t released0 = ipc_sessions_released();
CHECK(!close_handle(c, dup) && ipc_sessions_released() == released0 + 1, "CloseHandle(TIPC audren:u)");
Msg qpb = {.type = T_CONTROL, .cmd = 3};
k = call(c, audren, &qpb, &r);
uint16_t pbs = 0;
if (!k && !r.result) memcpy(&pbs, r.data, 2);
CHECK(pbs == POINTER_BUFFER_SIZE, "QueryPointerBufferSize %#x", pbs);
CHECK(sv(c, 0x21, 0x0fff, 0, 0, 0) == IPC_R_BAD_HANDLE, "SendSyncRequest on a bad handle");
if (audren) probe_renderer(c, audren);
CHECK(!close_handle(c, audren) && !close_handle(c, sm), "close audren:u and sm:");
printf("sessions alive at the end: %ld, released %llu\n", ipc_sessions_live(),
(unsigned long long)ipc_sessions_released());
CHECK(ipc_sessions_live() == 0, "sessions leaked");
}
static void child_main(Cpu *c) {
uint32_t sm = connect_sm(c);
get_service(c, sm, PROBE_UNKNOWN_SERVICE, 0);
printf("child: GetServiceHandle(%s) returned\n", PROBE_UNKNOWN_SERVICE);
}
// Re-run this executable with the same arguments and PROBE_CHILD_ENV set; it must stop with the named fatal.
static void probe_unknown_service(void) {
static char out[65536];
int code = plat_run_self(PROBE_CHILD_ENV, "1", out, sizeof out, 30000);
if (code < 0) rt_fatal("probe-ipc: cannot start the child");
const char *want = "FATAL: sm: service \"" PROBE_UNKNOWN_SERVICE "\" is not implemented";
const char *line = strstr(out, want);
printf("child GetServiceHandle(%s): exit code %d, %s\n", PROBE_UNKNOWN_SERVICE, code,
line ? "named fatal seen" : "named fatal MISSING");
if (line) printf(" child: %.*s\n", (int)strcspn(line, "\r\n"), line);
CHECK(code == 70 && line, "unknown service fatal (child output: %s)", out);
}
// ---- the game's own nn::audio client: main's PLT stubs -> lifted sdk -> SVCs -> this file
#define SYM_INIT_PARAM "_ZN2nn5audio32InitializeAudioRendererParameterEPNS0_22AudioRendererParameterE"
#define SYM_WORK_SIZE "_ZN2nn5audio30GetAudioRendererWorkBufferSizeERKNS0_22AudioRendererParameterE"
#define SYM_OPEN "_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleEPNS_2os11SystemEventERKNS0_22AudioRendererParameterEPvm"
#define SYM_CONFIG_SIZE "_ZN2nn5audio36GetAudioRendererConfigWorkBufferSizeERKNS0_22AudioRendererParameterE"
#define SYM_CONFIG_INIT "_ZN2nn5audio29InitializeAudioRendererConfigEPNS0_19AudioRendererConfigERKNS0_22AudioRendererParameterEPvm"
#define SYM_START "_ZN2nn5audio18StartAudioRendererENS0_19AudioRendererHandleE"
#define SYM_STOP "_ZN2nn5audio17StopAudioRendererENS0_19AudioRendererHandleE"
#define SYM_UPDATE "_ZN2nn5audio26RequestUpdateAudioRendererENS0_19AudioRendererHandleEPKNS0_19AudioRendererConfigE"
#define SYM_FRAMES "_ZN2nn5audio33GetAudioRendererElapsedFrameCountEPKNS0_19AudioRendererConfigE"
#define SYM_CLOSE "_ZN2nn5audio18CloseAudioRendererENS0_19AudioRendererHandleE"
#define SYM_WAIT_EVENT "_ZN2nn2os15WaitSystemEventEPNS0_15SystemEventTypeE"
static int revision_number_of(uint32_t rev) { return (int)(rev >> 24) - '0'; }
static uint64_t stub(const char *sym) {
uint64_t a = ldr_plt_stub("main", sym);
if (!a) rt_fatal("probe-ipc: no PLT stub for %s in main", sym);
return a;
}
// Without nn::init::Start the SDK's own initialisation has not run; the probe sets up the two things the audio client
// path reads (found by running it):
// - the "current thread object" at TLR+0x1F8 is the kernel's zeroed placeholder. SdkMutex compares its lock tag with
// ThreadType+0x1B0 (sdk+0x13186c), which nnosInitialize sets to the thread's handle; a zero there makes every free
// mutex look owned by the caller (__cxa_guard_acquire in nn::sm::Initialize aborts);
// - sdk's full DT_INIT needs nn::os initialised (its TLS slot constructor reaches ThreadManager), which is the boot
// sequence's job. The audio client only needs its two sf heap constructors: 16 KiB ExpHeaps on static buffers,
// stored at sdk+0xd37e90 and sdk+0xd3bec0 (CreateAudioRendererManagerByHipc reads the second). The 1.4.3 SDK creates
// that heap lazily; its OpenAudioRenderer takes nn::audio::detail::g_RendererResourceSemaphore first, which the
// constructor at sdk+0x221fb0 initializes (two renderers).
static void sdk_prepare(Cpu *c) {
Cpu *again;
ST32(LD64(c->tpidrro + 0x1F8) + 0x1B0, kern_main_thread(&again));
const RtLoaded *sdk = ldr_module(2);
static const uint64_t ctors_121[] = {0x1dcbd0, 0x1de8a0}, ctors_143[] = {0x221fb0};
const uint64_t *heap_ctors = RT_V143 ? ctors_143 : ctors_121;
unsigned nctors = RT_V143 ? sizeof ctors_143 / sizeof *ctors_143 : sizeof ctors_121 / sizeof *ctors_121;
for (unsigned k = 0; k < nctors; 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];
if (!found) rt_fatal("probe-ipc: sdk+%#llx is not an sdk constructor", (unsigned long long)heap_ctors[k]);
rt_call(c, sdk->base + heap_ctors[k], 0);
}
}
static void probe_sdk_client(Cpu *c) {
unsigned trace = rt_trace;
rt_trace |= RT_TRACE_IPC; // log the requests the SDK sends during set-up
sdk_prepare(c);
uint64_t requests0 = (uint64_t)plat_cas64(&g_requests, 0, 0);
uint64_t released0 = ipc_sessions_released();
int32_t *param = mem_kernel_alloc(0x100, 64);
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] == 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);
printf("sdk: GetAudioRendererWorkBufferSize -> %#llx (service %#x + client part)\n", (unsigned long long)work,
AUDREN_WORK_BUFFER_BYTES);
CHECK(work >= AUDREN_WORK_BUFFER_BYTES && work < (64u << 20), "SDK GetAudioRendererWorkBufferSize %#llx",
(unsigned long long)work);
uint8_t *wb = mem_kernel_alloc(work, 0x1000), *event = mem_kernel_alloc(0x100, 64);
uint64_t *handle = mem_kernel_alloc(0x40, 64); // AudioRendererHandle: 16 bytes, passed in x0 and x1
uint32_t r = (uint32_t)rt_call(c, stub(SYM_OPEN), 5, (uint64_t)(uintptr_t)handle, (uint64_t)(uintptr_t)event,
(uint64_t)(uintptr_t)param, (uint64_t)(uintptr_t)wb, work);
printf("sdk: OpenAudioRenderer -> %#x, renderers running %ld\n", r, audren_renderers_live());
CHECK(!r && handle[0] && audren_renderers_live() == 1, "SDK OpenAudioRenderer %#x", r);
if (r) return;
uint64_t csize = rt_call(c, stub(SYM_CONFIG_SIZE), 1, (uint64_t)(uintptr_t)param);
uint8_t *cbuf = mem_kernel_alloc(csize, 0x1000), *config = mem_kernel_alloc(0x200, 64);
rt_call(c, stub(SYM_CONFIG_INIT), 4, (uint64_t)(uintptr_t)config, (uint64_t)(uintptr_t)param,
(uint64_t)(uintptr_t)cbuf, csize);
rt_call(c, stub(SYM_START), 2, handle[0], handle[1]);
uint64_t t0 = 0, t1 = 0;
int bad = 0, f0 = 0, f1 = 0;
for (int i = 0; i < PROBE_FRAMES; i++) {
rt_call(c, stub(SYM_WAIT_EVENT), 1, (uint64_t)(uintptr_t)event);
uint64_t now = kern_now_ns();
bad += rt_call(c, stub(SYM_UPDATE), 3, handle[0], handle[1], (uint64_t)(uintptr_t)config) != 0;
int f = (int)rt_call(c, stub(SYM_FRAMES), 1, (uint64_t)(uintptr_t)config);
if (!i) t0 = now, f0 = f;
t1 = now, f1 = f;
rt_trace = trace; // the first update is logged, the rest is quiet
}
double period = (double)(t1 - t0) / (PROBE_FRAMES - 1) / 1e6;
printf("sdk: %d x WaitSystemEvent + RequestUpdateAudioRenderer: %d failed, mean period %.3f ms, elapsed frames "
"%d -> %d\n", PROBE_FRAMES, bad, period, f0, f1);
CHECK(!bad && period > 4.5 && period < 5.5 && f1 - f0 >= PROBE_FRAMES - 4 && f1 - f0 <= PROBE_FRAMES + 2,
"SDK render loop");
rt_call(c, stub(SYM_STOP), 2, handle[0], handle[1]);
rt_call(c, stub(SYM_CLOSE), 2, handle[0], handle[1]);
uint64_t requests = (uint64_t)plat_cas64(&g_requests, 0, 0) - requests0;
printf("sdk: CloseAudioRenderer: renderers running %ld, sessions released +%llu; %llu requests served for the "
"lifted client (sm: kept open by the SDK: %ld session)\n", audren_renderers_live(),
(unsigned long long)(ipc_sessions_released() - released0), (unsigned long long)requests,
ipc_sessions_live());
// The 1.2.1 SDK closes the renderer, device and manager sessions and keeps sm; the 1.4.3 SDK keeps the
// audren:u manager it created lazily, beside sm.
CHECK(audren_renderers_live() == 0 && ipc_sessions_released() - released0 >= (RT_V143 ? 1u : 3u) &&
ipc_sessions_live() == (RT_V143 ? 2 : 1),
"SDK CloseAudioRenderer");
rt_trace = trace;
}
static void guest_main(Cpu *c) {
probe_main(c);
probe_sdk_client(c);
}
int probe_ipc(void) {
if (getenv(PROBE_CHILD_ENV)) {
kern_run_on_guest_main(child_main);
return 0;
}
ldr_load(rt_config()->exefs);
kern_run_on_guest_main(guest_main);
probe_unknown_service();
RtStats s = rt_stats();
printf("stats: %llu SVCs, %llu threads, %llu host calls\n", (unsigned long long)s.svcs,
(unsigned long long)s.threads, (unsigned long long)s.host_calls);
if (failures) {
printf("probe-ipc: FAILED (%d failures)\n", failures);
return 1;
}
printf("probe-ipc: passed\n");
return 0;
}