mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
A domain's objects[32] only grew: CloseVirtualHandle returned without freeing anything, so a client that opens and closes sub-objects (the applet proxy getters, about 17 at boot) would get IPC_R_UNKNOWN_COMMAND for its 33rd, which the SDK aborts on. The handlers took the new object's id from nobjects, the table had no lock (two threads could race on nobjects), and a sub-object reached through its own handle (CopyFromCurrentDomain) kept a root pointer that the domain's last CloseHandle had freed. The domain table now has a lock in ipc.c (g_domains), taken only for sessions in a domain and never across a handler or a close callback. ipc_domain_add gives out the first free slot and returns the id the handlers write back; the table holds one reference on each object and each object one on its domain (held_root), counted in the domain's subs. CloseVirtualHandle clears the slot and drops the table's reference; it never closes the domain's own id. A request takes a reference on its target object under the lock for its length, and a cleared id is answered with IPC_R_BAD_HANDLE instead of being dereferenced; CopyFromCurrentDomain answers the same. When a domain's remaining references are all its objects', its client has closed it: the table lets the objects go, each object nothing else holds is freed and lets go of the domain, and the domain is freed with the last one. Domain objects now count in ipc_sessions_live from the moment they are added, since their last reference counts them out. Not reproduced before the change: no log here has TRACE=ipc, so the getters per return to the title are not counted. ipc_session_check gains a domain section: appletOE converted to a domain, 100 rounds of a getter and its CloseVirtualHandle each get id 3 with the table at 3 slots, a closed id gets a bad-handle result, the domain's own id survives a CloseVirtualHandle, a proxy with a handle of its own keeps the closed domain until that handle closes and then both are freed, and closing a domain frees its proxy and five getters. The compile check of every runtime file passes with no new warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1138 lines
53 KiB
C
1138 lines
53 KiB
C
// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
|
|
// Message framing per the HIPC, CMIF and TIPC wire layouts.
|
|
// 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 "ov.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 };
|
|
|
|
// The application's lifecycle (as the system's applet manager runs it): one state-changed
|
|
// event per applet, signalled while a message is waiting and cleared by the server once the queue drains (Horizon
|
|
// does not clear it on a wait). An *application* learns it has focus through FocusStateChanged (15) followed by
|
|
// GetCurrentFocusState == InFocus; ChangeIntoForeground (1) is the library-applet message and an application
|
|
// ignores it, which is what parked nn::oe::InitializeApplet on the event.
|
|
enum { AM_MSG_FOCUS_STATE_CHANGED = 15, AM_MSG_OPERATION_MODE_CHANGED = 30, AM_MSG_PERFORMANCE_MODE_CHANGED = 31 };
|
|
enum { AM_FOCUS_IN = 1, AM_FOCUS_NOT_IN = 2, AM_FOCUS_BACKGROUND = 3 };
|
|
#define AM_R_NO_MESSAGES 0x680u // the "no messages" result this SDK checks for
|
|
static uint32_t g_applet_event, g_applet_event_w;
|
|
static PlatMutex g_applet_lock = PLAT_MUTEX_INIT;
|
|
static uint32_t g_applet_msgs[32];
|
|
static unsigned g_applet_head, g_applet_count;
|
|
static uint8_t g_applet_focus = AM_FOCUS_IN;
|
|
|
|
void ipc_applet_push_message(uint32_t msg) {
|
|
plat_mutex_lock(&g_applet_lock);
|
|
if (g_applet_count < sizeof g_applet_msgs / sizeof *g_applet_msgs) {
|
|
g_applet_msgs[(g_applet_head + g_applet_count++) % (sizeof g_applet_msgs / sizeof *g_applet_msgs)] = msg;
|
|
} else {
|
|
rt_log("applet: message queue full, dropping %u", msg);
|
|
}
|
|
plat_mutex_unlock(&g_applet_lock);
|
|
if (g_applet_event_w) kern_event_signal(g_applet_event_w);
|
|
}
|
|
|
|
static int applet_pop_message(uint32_t *out) {
|
|
plat_mutex_lock(&g_applet_lock);
|
|
int ok = g_applet_count != 0;
|
|
if (ok) {
|
|
*out = g_applet_msgs[g_applet_head];
|
|
g_applet_head = (g_applet_head + 1) % (sizeof g_applet_msgs / sizeof *g_applet_msgs);
|
|
g_applet_count--;
|
|
}
|
|
int drained = g_applet_count == 0;
|
|
plat_mutex_unlock(&g_applet_lock);
|
|
if (drained && g_applet_event_w) kern_event_clear(g_applet_event_w);
|
|
return ok;
|
|
}
|
|
|
|
static IpcSession *stub_object(const char *name);
|
|
static void applet_stub_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs);
|
|
|
|
// A stub object the applet proxy commands are answered on: the interfaces exist, their calls succeed.
|
|
// The getters below each hand out one of those objects, which is what the SDK stores and dereferences.
|
|
static void applet_proxy_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
|
if (rt_trace & RT_TRACE_IPC) rt_log("applet proxy: command %u", rq->cmd);
|
|
static const struct { unsigned cmd; const char *name; } kinds[] = {
|
|
{0, "common state getter"}, {1, "self controller"}, {2, "window controller"},
|
|
{3, "audio controller"}, {4, "display controller"}, {10, "process winding controller"},
|
|
{11, "library applet creator"}, {20, "application functions"},
|
|
{40, "nx2 interface"}, {50, "nx2 interface"}, {60, "graphics scale controller"},
|
|
{70, "nx2 interface"}, {80, "nx2 interface"}, {90, "nx2 interface"},
|
|
{1000, "debug functions"},
|
|
};
|
|
for (unsigned k = 0; k < sizeof kinds / sizeof *kinds; k++) {
|
|
if (kinds[k].cmd != rq->cmd) continue;
|
|
IpcSession *root = s->root ? s->root : s; // object ids belong to the client's whole domain
|
|
IpcSession *o = stub_object(kinds[k].name);
|
|
if (!o) { rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
|
uint32_t id = ipc_domain_add(root, o);
|
|
if (!id) { free(o); rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
|
IPC_OUT(rs, id);
|
|
rs->domain = 1;
|
|
rs->domain_objects = 1;
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
rs->result = 0;
|
|
}
|
|
|
|
// The interfaces themselves. Only the calls the game makes while booting are modelled: the common state getter
|
|
// hands out the applet's state-changed event, and everything else reports success.
|
|
static void applet_stub_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
|
if (rt_trace & RT_TRACE_IPC) rt_log("%s: command %u", s->name, rq->cmd);
|
|
int common = !strcmp(s->name, "common state getter");
|
|
if (common && rq->cmd == 0) { // GetEventHandle: the applet state-changed event
|
|
if (!g_applet_event) ipc_applet_event_init();
|
|
s->event = g_applet_event;
|
|
rs->copy[rs->ncopy++] = s->event;
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
if (common && rq->cmd == 1) { // ReceiveMessage: the next queued applet message, else "no messages"
|
|
uint32_t msg;
|
|
if (applet_pop_message(&msg)) {
|
|
if (rt_trace & RT_TRACE_IPC) rt_log("applet: ReceiveMessage -> %u", msg);
|
|
IPC_OUT(rs, msg);
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
rs->result = AM_R_NO_MESSAGES;
|
|
return;
|
|
}
|
|
if (common && rq->cmd == 5) { // GetOperationMode (matches the oe override)
|
|
uint8_t mode = !ov_handheld();
|
|
IPC_OUT(rs, mode);
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
if (common && rq->cmd == 6) { // GetPerformanceMode: boost when docked
|
|
uint32_t mode = !ov_handheld();
|
|
IPC_OUT(rs, mode);
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
if (common && rq->cmd == 9) { // GetCurrentFocusState
|
|
uint8_t focus = g_applet_focus;
|
|
IPC_OUT(rs, focus);
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
if (!strcmp(s->name, "graphics scale controller") && rq->cmd == 10) { // GetCurrentGraphicsScale
|
|
uint32_t scale[3] = {10, 0, 0}; // see ov_oe_scaling_parameters
|
|
IPC_OUT(rs, scale);
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
if (rt_trace & RT_TRACE_IPC) rt_log("%s: command %u answered with success and no data", s->name, rq->cmd);
|
|
rs->result = 0;
|
|
}
|
|
|
|
static IpcSession *stub_object(const char *name) {
|
|
IpcSession *o = calloc(1, sizeof *o);
|
|
if (!o) return NULL;
|
|
o->name = name;
|
|
o->handler = applet_stub_handler;
|
|
o->refs = 1;
|
|
return o;
|
|
}
|
|
|
|
// Domain tables, their counts of their objects' references on them and their closed flags (IpcSession). Taken only for
|
|
// sessions in a domain; never held across a handler or a close callback.
|
|
static PlatMutex g_domains = PLAT_MUTEX_INIT;
|
|
static volatile int32_t g_live;
|
|
|
|
// The first free slot: a closed object's id is given out again, so a client that opens and closes objects in a loop
|
|
// never runs out of the 32.
|
|
unsigned ipc_domain_add(IpcSession *domain, IpcSession *object) {
|
|
unsigned id = 0;
|
|
plat_mutex_lock(&g_domains);
|
|
int i = 0, max = (int)(sizeof domain->objects / sizeof *domain->objects);
|
|
while (i < domain->nobjects && domain->objects[i]) i++;
|
|
if (!domain->dom_closed && i < max) {
|
|
domain->objects[i] = object;
|
|
if (i == domain->nobjects) domain->nobjects++;
|
|
object->root = domain;
|
|
object->held_root = domain;
|
|
plat_inc32(&domain->refs);
|
|
domain->subs++;
|
|
plat_inc32(&g_live); // a live server object from here; its last reference counts it out
|
|
id = (unsigned)i + 1;
|
|
}
|
|
plat_mutex_unlock(&g_domains);
|
|
return id;
|
|
}
|
|
|
|
// appletOE: the application-environment service. nn::oe::Initialize opens it; the calls that matter for
|
|
// boot answer through symbol overrides (focus, performance, language), so a permissive handler that
|
|
// reports success and writes nothing is enough until a command needs real data  then it is added here.
|
|
static void applet_oe_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
|
if (rt_trace & RT_TRACE_IPC) rt_log("appletOE: command %u", rq->cmd);
|
|
if (rq->cmd == 0 || rq->cmd == 10) { // OpenApplicationProxy: hand the client a domain object
|
|
IpcSession *proxy = calloc(1, sizeof *proxy);
|
|
if (!proxy) { rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
|
proxy->name = "applet proxy";
|
|
proxy->handler = applet_proxy_handler;
|
|
proxy->refs = 1;
|
|
uint32_t id = ipc_domain_add(s, proxy); // the object's id inside the domain, written into the reply data
|
|
if (!id) { free(proxy); rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
|
IPC_OUT(rs, id);
|
|
rs->domain = 1;
|
|
rs->domain_objects = 1;
|
|
}
|
|
rs->result = 0;
|
|
}
|
|
|
|
// aud:u: the Switch 2 audio entry point (nn::audio::CreateUserClientSessionByHipc). It hands out the Switch 1
|
|
// managers as sub-objects (command ids inferred from the calls the SDK makes):
|
|
// 2 -> IAudioRendererManager (the audren:u manager), 1 -> IAudioOutManager (not served by this host).
|
|
static void aud_user_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
|
(void)s;
|
|
if (rt_trace & RT_TRACE_IPC) rt_log("aud:u: command %u", rq->cmd);
|
|
if (rq->cmd == 2) {
|
|
rs->move[rs->nmove++] = ipc_session_new("IAudioRendererManager", audren_manager_handler, NULL, NULL);
|
|
rs->result = 0;
|
|
return;
|
|
}
|
|
rt_fatal("aud:u: command %u is not implemented (1 is GetAudioOutManager: no audout service here)", rq->cmd);
|
|
}
|
|
|
|
static void csrng_fill(uint64_t addr, uint64_t size) {
|
|
if (!size) return;
|
|
if (!plat_random(ipc_guest(addr, size), (size_t)size)) {
|
|
ipc_backtrace();
|
|
rt_fatal("csrng: no random bytes from the system");
|
|
}
|
|
}
|
|
|
|
// csrng: the SDK's cryptographic random source. GenerateRandomBytes (0) is its one command; the host's generator fills
|
|
// the output buffer. The SDK sends it as a B buffer (a contributor's trace: one B buffer, no others); a C buffer is
|
|
// filled too, with its reply entry, for a caller that lets the buffer type be chosen. No start or session logged on
|
|
// our own PCs opens the service, but the game imports the call, and without the service a request for it ended the
|
|
// game ("sm: service csrng is not implemented").
|
|
static void csrng_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
|
if (rt_trace & RT_TRACE_IPC)
|
|
rt_log("csrng: command %u (x%u a%u b%u w%u c%u)", rq->cmd, rq->nx, rq->na, rq->nb, rq->nw, rq->nc);
|
|
if (rq->cmd != 0) ipc_unimplemented(s, rq);
|
|
for (unsigned i = 0; i < rq->nb; i++) csrng_fill(rq->b[i].addr, rq->b[i].size);
|
|
for (unsigned i = 0; i < rq->nc; i++)
|
|
if (rq->c[i].size && (i >= rq->nb || !rq->b[i].size)) {
|
|
csrng_fill(rq->c[i].addr, rq->c[i].size);
|
|
rs->xout[rs->nxout++] = (IpcBuffer){rq->c[i].addr, rq->c[i].size, i};
|
|
}
|
|
rs->result = 0;
|
|
}
|
|
|
|
// 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},
|
|
{"appletOE", applet_oe_handler},
|
|
{"aud:u", aud_user_handler},
|
|
{"auddmg:u", audio_dmg_handler},
|
|
{"audsmx:u", audio_sysmix_handler},
|
|
{"csrng", csrng_handler},
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
// kern_handle_hold's callback: a request's reference, dropped through ipc_handle_closed once the request is answered.
|
|
static void session_hold(void *o) { plat_inc32(&((IpcSession *)o)->refs); }
|
|
|
|
// Drops one reference on a session; sub: the one an object of a domain held on that domain. A session in no domain
|
|
// takes no lock here. A domain whose remaining references are all its objects' has lost its client: its table lets the
|
|
// objects go, and each object that nothing else holds is closed and lets go of the domain in turn.
|
|
static void session_release(IpcSession *s, int sub) {
|
|
IpcSession *drop[32];
|
|
int ndrop = 0;
|
|
int32_t n;
|
|
if (!sub && !s->root) {
|
|
n = plat_dec32(&s->refs);
|
|
} else {
|
|
plat_mutex_lock(&g_domains);
|
|
if (sub) s->subs--;
|
|
n = plat_dec32(&s->refs);
|
|
if (n && n == s->subs && s->root == s && !s->dom_closed) {
|
|
s->dom_closed = 1;
|
|
for (int i = 0; i < s->nobjects; i++) {
|
|
if (s->objects[i] && s->objects[i] != s) drop[ndrop++] = s->objects[i];
|
|
s->objects[i] = NULL;
|
|
}
|
|
}
|
|
plat_mutex_unlock(&g_domains);
|
|
}
|
|
for (int i = 0; i < ndrop; i++) session_release(drop[i], 0);
|
|
if (n) return;
|
|
if (rt_trace & RT_TRACE_IPC) rt_log("ipc: %s closed", s->name);
|
|
if (s->close) s->close(s->state);
|
|
IpcSession *root = s->held_root;
|
|
free(s);
|
|
plat_dec32(&g_live);
|
|
plat_inc64(&g_released);
|
|
if (root) session_release(root, 1);
|
|
}
|
|
|
|
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);
|
|
session_release(obj, 0);
|
|
}
|
|
|
|
// ---- helpers for services
|
|
static int readable(uint64_t a, uint64_t n) { return mem_host_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);
|
|
if (rs->domain && !tipc) { // CMIF domain reply: 16-byte header with the objects appended, then SFCO
|
|
uint32_t dh[4] = {rs->domain_objects, 0, 0, 0};
|
|
memcpy(payload, dh, 16);
|
|
payload += 16;
|
|
}
|
|
unsigned used = (unsigned)(payload - words) + header + rs->data_size;
|
|
unsigned nwords = tipc ? (used + 3) / 4 : (used + 16 + 3) / 4; // CMIF keeps alignment slack
|
|
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 || rs->domain) ? 1u << 31 : 0);
|
|
memcpy(m, out, IPC_MSG_BYTES);
|
|
}
|
|
|
|
// CMIF control requests.
|
|
static void control(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
|
switch (rq->cmd) {
|
|
case 0: // ConvertCurrentObjectToDomain: the session answers domain-tagged requests from now on
|
|
plat_mutex_lock(&g_domains);
|
|
s->is_domain = 1;
|
|
s->root = s;
|
|
if (s->nobjects == 0) { s->objects[0] = s; s->nobjects = 1; }
|
|
plat_mutex_unlock(&g_domains);
|
|
return;
|
|
case 1: { // CopyFromCurrentDomain(object id): a handle to that object of the domain (a closed id is a bad handle)
|
|
uint32_t id = rq->data_size >= 4 ? *(const uint32_t *)(const void *)rq->data : 0;
|
|
if (!id || id > (uint32_t)s->nobjects) { rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
|
plat_mutex_lock(&g_domains);
|
|
IpcSession *o = s->objects[id - 1];
|
|
if (o) plat_inc32(&o->refs);
|
|
plat_mutex_unlock(&g_domains);
|
|
if (!o) { rs->result = IPC_R_BAD_HANDLE; return; }
|
|
rs->move[rs->nmove++] = kern_handle_new(KERN_SESSION, o);
|
|
return;
|
|
}
|
|
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;
|
|
IpcSession *target = s, *held = NULL; // held: a domain object the request holds a reference on
|
|
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);
|
|
IpcSession *dom = s->root ? s->root : s; // ids belong to the client's domain, not to this object
|
|
if ((s->is_domain || dom->is_domain) && (rq.type == T_REQUEST || rq.type == T_REQUEST_CTX)) {
|
|
// Domain request: a 16-byte domain header names the object (1-based), then the usual SFCI payload.
|
|
uint32_t dh[4];
|
|
memcpy(dh, payload, 16);
|
|
payload += 16;
|
|
rs.domain = 1;
|
|
uint32_t dcmd = dh[0] & 0xff, obj = dh[1];
|
|
if (dcmd == 2) { // CloseVirtualHandle: the object leaves the domain (never the domain's own); no answer
|
|
IpcSession *o = NULL;
|
|
plat_mutex_lock(&g_domains);
|
|
if (obj >= 2 && obj <= (uint32_t)dom->nobjects && dom->objects[obj - 1] != dom) {
|
|
o = dom->objects[obj - 1];
|
|
dom->objects[obj - 1] = NULL;
|
|
}
|
|
plat_mutex_unlock(&g_domains);
|
|
if (o) session_release(o, 0);
|
|
return;
|
|
}
|
|
if (obj == 0) obj = 1; // this SDK addresses the domain's own object as 0
|
|
if (obj > (uint32_t)dom->nobjects) {
|
|
ipc_backtrace();
|
|
rt_fatal("ipc: %s domain object %u (of %d) does not exist [hdr %08x %08x %08x %08x] [msg %08x %08x %08x %08x]", s->name, obj, dom->nobjects, dh[0], dh[1], dh[2], dh[3], m[0], m[1], m[2], m[3]);
|
|
}
|
|
plat_mutex_lock(&g_domains);
|
|
target = held = dom->objects[obj - 1];
|
|
if (held) plat_inc32(&held->refs);
|
|
plat_mutex_unlock(&g_domains);
|
|
if (!target) { // an id the client closed
|
|
rs.result = IPC_R_BAD_HANDLE;
|
|
respond(m, 0, &rs);
|
|
return;
|
|
}
|
|
}
|
|
uint32_t h[4];
|
|
memcpy(h, payload, 16);
|
|
if (h[0] != CMIF_IN_MAGIC) {
|
|
ipc_backtrace();
|
|
rt_fatal("ipc: %s: CMIF magic %#x", 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 target->handler(target, &rq, &rs);
|
|
if (held) session_release(held, 0);
|
|
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]);
|
|
// The request holds a reference on the session until it is answered: a CloseHandle of the session on another
|
|
// thread meanwhile leaves the object, and its close callback (renderer_close frees the audio renderer the
|
|
// handler uses), to whichever drops the last reference.
|
|
IpcSession *s = kern_handle_hold(h, KERN_SESSION, session_hold);
|
|
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;
|
|
ipc_handle_closed(KERN_SESSION, s); // the request's reference
|
|
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 "NX2NAVI_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; // the 16-byte alignment pad is reserved 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 NX2navi-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 = {0}; // the client leaves the id of an unused slot zero
|
|
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, ®, &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);
|
|
// csrng: GenerateRandomBytes into a B buffer, as the SDK sends it; two requests give different bytes
|
|
uint32_t csrng = get_service(c, sm, "csrng", 0);
|
|
uint8_t *rnd = mem_kernel_alloc(64, 16);
|
|
memset(rnd, 0, 64);
|
|
for (int i = 0; csrng && i < 2; i++) {
|
|
Msg q = {.type = T_REQUEST, .cmd = 0, .nb = 1, .b[0] = {(uint64_t)(uintptr_t)(rnd + 32 * i), 32, 0}};
|
|
k = call(c, csrng, &q, &r);
|
|
CHECK(!k && !r.result, "csrng GenerateRandomBytes: kernel %#x result %#x", k, r.result);
|
|
}
|
|
static const uint8_t zero[32];
|
|
CHECK(memcmp(rnd, zero, 32) && memcmp(rnd + 32, zero, 32) && memcmp(rnd, rnd + 32, 32),
|
|
"csrng: the buffers were not filled with random bytes");
|
|
printf("csrng: GenerateRandomBytes filled two buffers of 32 bytes, each differently\n");
|
|
mem_kernel_free(rnd, 64);
|
|
CHECK(!csrng || !close_handle(c, csrng), "close csrng");
|
|
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 (NX2navi 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;
|
|
}
|
|
|
|
// Create the applet state-changed event at boot and queue the launch message: the application starts in focus
|
|
// (the applet manager: SetFocusState(InFocus) -> FocusStateChanged for an application).
|
|
void ipc_applet_event_init(void) {
|
|
if (!g_applet_event) {
|
|
g_applet_event_w = kern_event_new();
|
|
g_applet_event = kern_event_readable(g_applet_event_w);
|
|
g_applet_focus = AM_FOCUS_IN;
|
|
ipc_applet_push_message(AM_MSG_FOCUS_STATE_CHANGED);
|
|
}
|
|
}
|