HAL Laboratory's engine on NintendoWare, SDK 11.4.0, NVN. Plays on the Mac at 30 fps, as on the Switch: online and difficulty prompts, the 1080p opening movie, Point of Arrival with its guide movies, the city reveal, the Sword ability and fights, title and Continue; a 15-minute soak with no crash. No game code is cut; the profile is the memory layout and the shader groups. All 27,297 shaders build for Mac and iOS; the IPA (6.4 GB) is untested on a device. - Threads (general): a guest thread waiting for a host lock that HLE code holds while it runs guest code is blocked, not runnable (dm_host_lock, a native HLE wait). The movie decoder held its decode lock across SignalEvent; the signal's same-core handoff waited for the movie input thread, which waited for that lock, and the game froze at its first guide movie. t_preempt has the case. Docs: the game note, games table, README gallery, threads.md, shaders.md, symptom index; AGENTS count 73 -> 74. Checked: host tests, preempt, tool tests (253, 15 skips), docs tests; a Mac run on chain 5cddc47253ab through the first guide movie.
250 lines
13 KiB
C
250 lines
13 KiB
C
// Start/wake-to-run ordering through real thread SVCs. Suspend the child pthread
|
|
// to model a host dispatch delay: it is guest-runnable as soon as StartThread or
|
|
// a signal makes it ready, even though Mach still reports it stopped. Its payload
|
|
// must precede the lower-priority caller's return, including delays over 5 ms.
|
|
#include "../hle_thread.c"
|
|
#include <assert.h>
|
|
#include <unistd.h>
|
|
|
|
static thr_t worker;
|
|
static uint32_t event_read, event_write;
|
|
static _Atomic int payload;
|
|
static unsigned dispatch_us;
|
|
static _Atomic uint32_t mutex_word, condition_key;
|
|
static uint32_t main_handle;
|
|
static int wait_kind;
|
|
static _Atomic int worker_host_work;
|
|
static int worker_ready;
|
|
|
|
// FFT's SDK auto-clear event wakes one condition waiter. Its private worker
|
|
// queues cannot influence the kernel's priority/FIFO selection.
|
|
static thr_t fifo_workers[3];
|
|
static _Atomic int fifo_done;
|
|
static void *fifo_wait(void *arg) {
|
|
thr_t *t = arg;
|
|
pthread_setspecific(g_keythr, t);
|
|
atomic_store(&mutex_word, t->handle);
|
|
uint64_t r[32] = {(uint64_t)&mutex_word, (uint64_t)&condition_key, t->handle, (uint64_t)-1};
|
|
svc_thread(0x1C, r);
|
|
assert(r[0] == 0 && atomic_load(&mutex_word) == t->handle);
|
|
r[0] = (uint64_t)&mutex_word; svc_thread(0x1B, r);
|
|
atomic_fetch_add(&fifo_done, 1);
|
|
thread_exit(t);
|
|
return NULL;
|
|
}
|
|
static void condition_order(void) {
|
|
pthread_t threads[3];
|
|
atomic_store(&mutex_word, 0); atomic_store(&condition_key, 0); atomic_store(&fifo_done, 0);
|
|
for (int i = 0; i < 3; i++) {
|
|
thr_t *t = &fifo_workers[i]; memset(t, 0, sizeof *t);
|
|
pthread_cond_init(&t->cv, NULL); pthread_cond_init(&t->pcv, NULL);
|
|
t->started = 1; t->prio = i == 2 ? 45 : 46; t->core = 0; t->mask = 7;
|
|
pthread_mutex_lock(&G);
|
|
t->handle = h_new(O_THREAD, t); t->all = g_all; g_all = t;
|
|
pthread_mutex_unlock(&G);
|
|
assert(!pthread_create(&threads[i], NULL, fifo_wait, t));
|
|
for (;;) {
|
|
pthread_mutex_lock(&G); int waiting = t->wkind == 2; pthread_mutex_unlock(&G);
|
|
if (waiting) break;
|
|
usleep(100);
|
|
}
|
|
}
|
|
int expected[] = {2, 0, 1}; // priority first, then registration order
|
|
for (int i = 0; i < 3; i++) {
|
|
atomic_store(&mutex_word, main_handle);
|
|
uint64_t r[32] = {(uint64_t)&condition_key, 1}; svc_thread(0x1D, r);
|
|
pthread_mutex_lock(&G);
|
|
assert(fifo_workers[expected[i]].wkind == 1);
|
|
assert(atomic_load(&mutex_word) == (main_handle | WAITERS_BIT));
|
|
pthread_mutex_unlock(&G);
|
|
r[0] = (uint64_t)&mutex_word; svc_thread(0x1B, r);
|
|
while (atomic_load(&fifo_done) != i + 1) usleep(100);
|
|
}
|
|
assert(atomic_load(&condition_key) == 0);
|
|
for (int i = 2; i >= 0; i--) {
|
|
thr_t *t = &fifo_workers[i]; pthread_join(threads[i], NULL);
|
|
pthread_mutex_lock(&G); assert(g_all == t); g_all = t->all;
|
|
g_h[t->handle - 0x1000].type = O_FREE; g_h[t->handle - 0x1000].obj = NULL;
|
|
pthread_mutex_unlock(&G);
|
|
pthread_cond_destroy(&t->cv); pthread_cond_destroy(&t->pcv);
|
|
}
|
|
puts("PASS: condition waiters selected by priority, then FIFO; mutex reacquired before return");
|
|
}
|
|
|
|
static void *work(void *arg) {
|
|
(void)arg;
|
|
pthread_setspecific(g_keythr, &worker);
|
|
worker.mport = pthread_mach_thread_np(pthread_self());
|
|
if (wait_kind == 0) {
|
|
pthread_mutex_lock(&G);
|
|
worker_ready = 1;
|
|
while (!worker.started) pthread_cond_wait(&worker.cv, &G);
|
|
pthread_mutex_unlock(&G);
|
|
}
|
|
uint32_t handle = event_read; uint64_t r[32] = {0};
|
|
if (wait_kind == 4) {
|
|
r[1] = (uint64_t)&handle; r[2] = 1; r[3] = (uint64_t)-1;
|
|
svc_thread(0x18, r);
|
|
} else if (wait_kind == 2) {
|
|
atomic_store(&mutex_word, worker.handle);
|
|
r[0] = (uint64_t)&mutex_word; r[1] = (uint64_t)&condition_key;
|
|
r[2] = worker.handle; r[3] = (uint64_t)-1;
|
|
svc_thread(0x1C, r);
|
|
} else if (wait_kind == 1) {
|
|
r[0] = main_handle; r[1] = (uint64_t)&mutex_word; r[2] = worker.handle;
|
|
svc_thread(0x1A, r);
|
|
}
|
|
assert(r[0] == 0);
|
|
atomic_store(&payload, 1);
|
|
usleep(1000); // dispatching the work is insufficient: its teardown must finish too
|
|
atomic_store(&payload, 2);
|
|
thread_exit(&worker);
|
|
return NULL;
|
|
}
|
|
static void *dispatch(void *arg) {
|
|
(void)arg;
|
|
usleep(dispatch_us);
|
|
assert(thread_resume(worker.mport) == KERN_SUCCESS);
|
|
return NULL;
|
|
}
|
|
static int delayed_wake(unsigned delay, int kind, int exclusion) {
|
|
uint64_t r[32] = {0};
|
|
svc_thread(0x45, r); assert(r[0] == 0);
|
|
event_write = r[1]; event_read = r[2];
|
|
memset(&worker, 0, sizeof worker);
|
|
pthread_cond_init(&worker.cv, NULL); pthread_cond_init(&worker.pcv, NULL);
|
|
worker.started = kind != 0; worker.prio = 30; worker.core = 0; worker.mask = 1;
|
|
if (exclusion == 1) worker.prio = 44; // equal priority
|
|
if (exclusion == 2) worker.core = 1, worker.mask = 2; // another guest core
|
|
if (exclusion == 3) worker.mask = 7; // can run on another guest core
|
|
if (exclusion == 4) worker.host_owned = 1; // host helper, no guest core
|
|
if (exclusion == 5) worker.pause_req = 1; // guest suspension
|
|
if (exclusion == 6) { worker_host_work = 1; worker.host_work = &worker_host_work; } // host GPU work
|
|
pthread_mutex_lock(&G);
|
|
worker.handle = h_new(O_THREAD, &worker); worker.all = g_all; g_all = &worker;
|
|
pthread_mutex_unlock(&G);
|
|
atomic_store(&payload, 0);
|
|
atomic_store(&mutex_word, main_handle | WAITERS_BIT); atomic_store(&condition_key, 0);
|
|
wait_kind = kind;
|
|
worker_ready = 0;
|
|
pthread_t w, d; assert(!pthread_create(&w, NULL, work, NULL));
|
|
for (;;) {
|
|
pthread_mutex_lock(&G); int waiting = kind == 0 ? worker_ready : worker.wkind == kind; pthread_mutex_unlock(&G);
|
|
if (waiting) break;
|
|
usleep(100);
|
|
}
|
|
assert(thread_suspend(worker.mport) == KERN_SUCCESS);
|
|
dispatch_us = delay; assert(!pthread_create(&d, NULL, dispatch, NULL));
|
|
if (kind == 0) { r[0] = worker.handle; svc_thread(0x09, r); }
|
|
else if (kind == 4) { r[0] = event_write; svc_thread(0x11, r); }
|
|
else if (kind == 2) { r[0] = (uint64_t)&condition_key; r[1] = 1; svc_thread(0x1D, r); }
|
|
else { r[0] = (uint64_t)&mutex_word; svc_thread(0x1B, r); }
|
|
assert(r[0] == 0);
|
|
pthread_mutex_lock(&G); int ordered = atomic_load(&payload) == 2 && worker.exited; pthread_mutex_unlock(&G);
|
|
pthread_join(d, NULL); pthread_join(w, NULL);
|
|
pthread_mutex_lock(&G); g_all = worker.all;
|
|
unsigned slot = worker.handle - 0x1000; g_h[slot].type = O_FREE; g_h[slot].obj = NULL;
|
|
pthread_mutex_unlock(&G);
|
|
pthread_cond_destroy(&worker.cv); pthread_cond_destroy(&worker.pcv);
|
|
r[0] = event_read; svc_thread(0x16, r);
|
|
r[0] = event_write; svc_thread(0x16, r);
|
|
int ok = exclusion ? !ordered : ordered;
|
|
printf("%s: wait kind %d, dispatch delay %u us, exclusion %d, payload before signaller returns %d\n", ok ? "PASS" : "FAIL", kind, delay, exclusion, ordered);
|
|
return ok;
|
|
}
|
|
// The scope of a handoff: the threads the signaller's wake made runnable, and those they wake in turn, each until it blocks.
|
|
// A busy higher-priority peer of the core that the signaller never woke must not hold it (issue #247: Tears of the Kingdom's
|
|
// boot thread waited for its core's busy loaders at every wake; issue #259: Tropical Freeze's job thread for pollers).
|
|
static thr_t scope_peer[3];
|
|
static uint32_t scope_ev[2][2]; // [event][write, read]
|
|
static _Atomic int scope_payload, scope_release;
|
|
static thr_t *scope_new(thr_t *t, int prio) {
|
|
memset(t, 0, sizeof *t);
|
|
pthread_cond_init(&t->cv, NULL); pthread_cond_init(&t->pcv, NULL);
|
|
t->started = 1; t->prio = prio; t->core = 0; t->mask = 1;
|
|
pthread_mutex_lock(&G); t->handle = h_new(O_THREAD, t); t->all = g_all; g_all = t; pthread_mutex_unlock(&G);
|
|
return t;
|
|
}
|
|
static void scope_free(thr_t *t) {
|
|
pthread_mutex_lock(&G);
|
|
for (thr_t **p = &g_all; *p; p = &(*p)->all) if (*p == t) { *p = t->all; break; }
|
|
g_h[t->handle - 0x1000].type = O_FREE; g_h[t->handle - 0x1000].obj = NULL;
|
|
pthread_mutex_unlock(&G);
|
|
pthread_cond_destroy(&t->cv); pthread_cond_destroy(&t->pcv);
|
|
}
|
|
static void scope_wait(int ev) { uint32_t h = scope_ev[ev][1]; uint64_t r[32] = {0, (uint64_t)&h, 1, (uint64_t)-1}; svc_thread(0x18, r); assert(r[0] == 0); }
|
|
static void scope_signal(int ev) { uint64_t r[32] = {scope_ev[ev][0]}; svc_thread(0x11, r); assert(r[0] == 0); }
|
|
static void *scope_worker(void *arg) { // waits on event 1, then finishes its payload (1 ms of host work) and exits
|
|
thr_t *t = arg; pthread_setspecific(g_keythr, t);
|
|
scope_wait(1);
|
|
atomic_store(&scope_payload, 1); usleep(1000); atomic_store(&scope_payload, 2);
|
|
thread_exit(t); return NULL;
|
|
}
|
|
static void *scope_dispatcher(void *arg) { // waits on event 0, wakes the worker (its equal priority: no handoff of its own) and exits
|
|
thr_t *t = arg; pthread_setspecific(g_keythr, t);
|
|
scope_wait(0); scope_signal(1);
|
|
thread_exit(t); return NULL;
|
|
}
|
|
static void *scope_any_yield(void *arg) { // waits on event 1, then yields to any thread (-2) until the signaller has returned
|
|
thr_t *t = arg; pthread_setspecific(g_keythr, t);
|
|
scope_wait(1);
|
|
atomic_store(&scope_payload, 1);
|
|
while (!atomic_load(&scope_release)) { uint64_t r[32] = {(uint64_t)-2}; svc_thread(0x0B, r); }
|
|
thread_exit(t); return NULL;
|
|
}
|
|
static pthread_mutex_t scope_mu = PTHREAD_MUTEX_INITIALIZER; // a host lock the signaller holds across its signal (hle_movie.c: the decoder's dmu)
|
|
static void *scope_host_lock(void *arg) { // waits on event 1, then waits for the host lock, finishes its payload and exits
|
|
thr_t *t = arg; pthread_setspecific(g_keythr, t);
|
|
scope_wait(1);
|
|
atomic_store(&scope_payload, 1);
|
|
dm_host_lock(&scope_mu); atomic_store(&scope_payload, 2); pthread_mutex_unlock(&scope_mu);
|
|
thread_exit(t); return NULL;
|
|
}
|
|
static void scope_until_waiting(thr_t *t) {
|
|
for (;;) { pthread_mutex_lock(&G); int w = t->wkind == 4; pthread_mutex_unlock(&G); if (w) break; usleep(100); }
|
|
}
|
|
// kind 0: a busy unrelated peer and one woken worker; 1: a dispatcher that wakes the worker; 2: a woken thread that yields to any thread;
|
|
// 3: a woken thread that waits for a host lock the signaller holds (it never returned before that wait counted as blocked)
|
|
static int handoff_scope(int kind) {
|
|
for (int e = 0; e < 2; e++) { uint64_t r[32] = {0}; svc_thread(0x45, r); assert(r[0] == 0); scope_ev[e][0] = r[1]; scope_ev[e][1] = r[2]; }
|
|
atomic_store(&scope_payload, 0); atomic_store(&scope_release, 0);
|
|
thr_t *busy = scope_new(&scope_peer[0], 30); // runnable in guest code the whole time, woken by nobody
|
|
thr_t *w = scope_new(&scope_peer[1], 30), *d = kind == 1 ? scope_new(&scope_peer[2], 30) : NULL;
|
|
pthread_t pw, pd;
|
|
assert(!pthread_create(&pw, NULL, kind == 2 ? scope_any_yield : kind == 3 ? scope_host_lock : scope_worker, w)); scope_until_waiting(w);
|
|
if (d) { assert(!pthread_create(&pd, NULL, scope_dispatcher, d)); scope_until_waiting(d); }
|
|
if (kind == 3) pthread_mutex_lock(&scope_mu);
|
|
scope_signal(d ? 0 : 1);
|
|
pthread_mutex_lock(&G); int payload = atomic_load(&scope_payload), done = w->exited; pthread_mutex_unlock(&G);
|
|
if (kind == 3) pthread_mutex_unlock(&scope_mu);
|
|
atomic_store(&scope_release, 1);
|
|
pthread_join(pw, NULL); if (d) pthread_join(pd, NULL);
|
|
if (d) scope_free(d);
|
|
scope_free(w); scope_free(busy);
|
|
for (int e = 0; e < 2; e++) for (int i = 0; i < 2; i++) { uint64_t r[32] = {scope_ev[e][i]}; svc_thread(0x16, r); }
|
|
int ok = kind >= 2 ? payload == 1 : payload == 2 && done;
|
|
static const char *what[] = {"woken worker beside a busy unrelated peer", "worker woken by the woken dispatcher", "woken thread yielding to any thread",
|
|
"woken thread waiting for the signaller's host lock"};
|
|
printf("%s: handoff scope, %s: signaller returned, payload %d, worker exited %d\n", ok ? "PASS" : "FAIL", what[kind], payload, done);
|
|
return ok;
|
|
}
|
|
int main(int argc, char **argv) {
|
|
(void)argv;
|
|
alarm(10);
|
|
setenv("DM_PREEMPT", argc > 1 ? "1" : "0", 1);
|
|
setenv("DM_YIELD_BACKOFF", "0", 1); // plain yields only: a back-off sleep would end the yielding thread's handoff on its own
|
|
main_handle = svc_thread_main_init();
|
|
condition_order();
|
|
int ok = 1;
|
|
if (argc == 1) for (int kind = 0; kind <= 3; kind++) ok &= handoff_scope(kind); // with every runnable peer checked (core_preempt), the busy peer holds the signaller by design
|
|
for (int kind = 0; kind <= 4; kind++) if (kind != 3) {
|
|
ok &= delayed_wake(1000, kind, 0);
|
|
ok &= delayed_wake(20000, kind, 0);
|
|
}
|
|
for (int exclusion = 1; exclusion <= 6; exclusion++) {
|
|
ok &= delayed_wake(20000, 0, exclusion);
|
|
ok &= delayed_wake(20000, 4, exclusion);
|
|
}
|
|
return ok ? 0 : 1;
|
|
}
|