diff --git a/dm/host/dm.h b/dm/host/dm.h index 12b3d91..4afbf96 100644 --- a/dm/host/dm.h +++ b/dm/host/dm.h @@ -100,6 +100,8 @@ void hle_signal_system_event(void *g); // hle_thread.c: signal a service-layer void hle_bind_system_event(void *g, int autoclear); // On a guest thread, bind a guest SystemEvent to a kernel event void hle_clear_system_event(void *g); // hle_thread.c: clear a bound service-layer SystemEvent (any thread) int hle_signal_bound_system_event(void *g); // hle_thread.c: signal a service-layer SystemEvent only if it is bound (any thread); 0 when it is not +uint64_t hle_system_event_binding(void *g); // Opaque lifetime ID of a binding, 0 if unbound; a rebind gets a new ID even if its address and handle are reused +int hle_signal_system_event_binding(uint64_t binding); // Signal only this binding lifetime (any thread); 0 after its write handle closes // Page fault hook / hook before the host writes guest memory (crash.c; used for write-protect dirty page tracking) void dm_fault_hook_register(int (*fn)(void *addr, int is_write, void *uc)); void dm_guest_write_prep_register(void (*fn)(void *addr, size_t len)); diff --git a/dm/host/hle_sdk16.c b/dm/host/hle_sdk16.c index 61ed9fe..74f2598 100644 --- a/dm/host/hle_sdk16.c +++ b/dm/host/hle_sdk16.c @@ -215,19 +215,36 @@ HLE_FORCE("_ZN2nn3nfp29StartNicknameAndOwnerSettingsEPNS0_12DeviceHandleEPbPNS0_ // ---------- vi: vsync event ---------- // The game passes a SystemEventType; after binding, a host thread fires it at 60 Hz (DM_VSYNC_HZ can change it). While the display link presents, the event follows its ticks, as a // Switch's follows its display: Super Mario Galaxy 2 runs the Wii's VI retrace (and with it each emulated frame) on this event, and on a clock of the host's own the frames drifted -// against the screen's vsyncs, two in some display-link ticks and none in others (iPhone: about one tick in seven, a stutter the frame-rate counter showed as 50 to 60) +// against the screen's vsyncs, two in some display-link ticks and none in others (iPhone: about one tick in seven, a stutter the frame-rate counter showed as 50 to 60). +// One thread per binding lifetime: repeated requests must not double its rate. +// A destroyed binding retires its worker even if another service reuses the same +// guest storage and handle number before the next tick (MacNativeGames dab728a). void dm_host_helper_qos(qos_class_t q); // hle_thread.c +typedef struct vsync_event { void *ev; uint64_t binding; struct vsync_event *next; } vsync_event_t; +static pthread_mutex_t g_vs_mu = PTHREAD_MUTEX_INITIALIZER; +static vsync_event_t *g_vs_events; +static int vsync_tick(vsync_event_t *v) { // 0: the event is gone and this thread has left the table + if (hle_signal_system_event_binding(v->binding)) return 1; + pthread_mutex_lock(&g_vs_mu); + vsync_event_t **p = &g_vs_events; while (*p && *p != v) p = &(*p)->next; + if (*p) *p = v->next; + pthread_mutex_unlock(&g_vs_mu); + LOG("vi: vsync event %p destroyed by the game, its thread ends", v->ev); + return 0; +} __attribute__((weak)) int mtl_link_wait_vsync(uint64_t *seen, uint64_t timeout_ns) { (void)seen; (void)timeout_ns; return 0; } // nvn_mtl.m -static void *vsync_thread(void *ev) { +static void *vsync_thread(void *arg) { + vsync_event_t *v = arg; dm_host_helper_qos(QOS_CLASS_USER_INTERACTIVE); // the game paces frames on this event const char *e = getenv("DM_VSYNC_HZ"); double hz = e ? atof(e) : 60.0; if (hz <= 0) hz = 60.0; uint64_t period = (uint64_t)(1e9 / hz), next = now_ns() + period, seen = 0; for (;;) { - if (!e && mtl_link_wait_vsync(&seen, 2 * period)) { hle_signal_system_event(ev); next = now_ns() + period; continue; } + if (!e && mtl_link_wait_vsync(&seen, 2 * period)) { if (!vsync_tick(v)) break; next = now_ns() + period; continue; } uint64_t t = now_ns(); if (next > t) { struct timespec ts = {(time_t)((next - t) / 1000000000ull), (long)((next - t) % 1000000000ull)}; nanosleep(&ts, NULL); } - hle_signal_system_event(ev); next += period; if (now_ns() > next + 4 * period) next = now_ns() + period; // do not try to catch up when far behind + if (!vsync_tick(v)) break; + next += period; if (now_ns() > next + 4 * period) next = now_ns() + period; // do not try to catch up when far behind } - return NULL; + free(v); return NULL; } // Image memory needed for an indirect layer (an applet's picture handed to the game to draw, such as the inline keyboard): on hardware it asks the vi service. Computed as linear RGBA8, 4 KB aligned (Super Mario 3D World + Bowser's Fury's inline keyboard asks for 1280x720) HLE_FORCE("_ZN2nn2vi33GetIndirectImageMemoryRequirementEPmS1_ii", "indirect layer image memory: RGBA8 linear, 4 KB aligned", Result, h_vi_IndirectReq, size_t *size, size_t *align, int w, int h) { @@ -235,8 +252,16 @@ HLE_FORCE("_ZN2nn2vi33GetIndirectImageMemoryRequirementEPmS1_ii", "indirect laye } HLE_FORCE("_ZN2nn2vi20GetDisplayVsyncEventEPNS_2os15SystemEventTypeEPNS0_7DisplayE", "vsync event: a host thread signals it at 60 Hz", Result, h_vi_VsyncEvent, void *ev, void *disp) { (void)disp; hle_bind_system_event(ev, 1); - pthread_t th; if (pthread_create(&th, NULL, vsync_thread, ev)) die("cannot start the vsync thread"); pthread_detach(th); - LOG("vi::GetDisplayVsyncEvent: %p signalled by the host at 60 Hz", ev); return RES_OK; + uint64_t binding = hle_system_event_binding(ev); + pthread_mutex_lock(&g_vs_mu); + int running = 0; for (vsync_event_t *v = g_vs_events; v; v = v->next) running |= v->binding == binding; + if (!running) { + vsync_event_t *v = calloc(1, sizeof *v); if (!v) die("cannot allocate a vsync event"); + v->ev = ev; v->binding = binding; v->next = g_vs_events; g_vs_events = v; + pthread_t th; if (pthread_create(&th, NULL, vsync_thread, v)) die("cannot start the vsync thread"); pthread_detach(th); + } + pthread_mutex_unlock(&g_vs_mu); + LOG("vi::GetDisplayVsyncEvent: %p signalled by the host at 60 Hz%s", ev, running ? " (its thread already runs)" : ""); return RES_OK; } // ---------- diag ---------- diff --git a/dm/host/hle_thread.c b/dm/host/hle_thread.c index 3cfc6b5..a51dcf5 100644 --- a/dm/host/hle_thread.c +++ b/dm/host/hle_thread.c @@ -33,8 +33,9 @@ enum { O_FREE, O_THREAD, O_EVENT_R, O_EVENT_W }; // An entry lives as long as that handle: CloseHandle on it removes the entry (se_unbind), so a SystemEvent destroyed and created again at the same address gets a new kernel event. // Entries used to stay forever: the second bind returned early, the new event was never created and signals went to the closed handle (or to whatever event reused its number). // Lock order: G, then g_se_lock (nothing takes G while holding g_se_lock) -static struct { void *ev; uint32_t wh; } g_se[512]; +static struct { void *ev; uint32_t wh; uint64_t id; } g_se[512]; static int g_nse; +static uint64_t g_next_se_id = 1; // g_se_lock: binding identity survives neither address nor handle reuse static pthread_mutex_t g_se_lock = PTHREAD_MUTEX_INITIALIZER; static uint32_t se_find(void *ev) { // caller holds g_se_lock for (int i = 0; i < g_nse; i++) if (g_se[i].ev == ev) return g_se[i].wh; @@ -1015,10 +1016,30 @@ void hle_bind_system_event(void *ev, int autoclear) { // Must be called on wh = getw(ev); pthread_mutex_lock(&g_se_lock); if (g_nse >= (int)(sizeof g_se / sizeof g_se[0])) die("SystemEvent binding table full"); - g_se[g_nse].ev = ev; g_se[g_nse++].wh = wh; + g_se[g_nse].ev = ev; g_se[g_nse].wh = wh; g_se[g_nse++].id = g_next_se_id++; pthread_mutex_unlock(&g_se_lock); } -void hle_signal_system_event(void *ev) { // An unbound one is bound on the spot as manual-clear (can only happen on a guest thread) +uint64_t hle_system_event_binding(void *ev) { + uint64_t id = 0; + pthread_mutex_lock(&g_se_lock); + for (int i = 0; i < g_nse; i++) if (g_se[i].ev == ev) { id = g_se[i].id; break; } + pthread_mutex_unlock(&g_se_lock); + return id; +} +int hle_signal_system_event_binding(uint64_t binding) { + // Match the lifetime and signal under G, just as CloseHandle removes it under + // G: a stale producer cannot signal a new event that reuses either number. + pthread_mutex_lock(&G); + uint32_t wh = 0; + pthread_mutex_lock(&g_se_lock); + for (int i = 0; i < g_nse; i++) if (g_se[i].id == binding) { wh = g_se[i].wh; break; } + pthread_mutex_unlock(&g_se_lock); + event_t *e = wh ? h_get(wh, O_EVENT_W) : NULL; + if (e) { e->signaled = 1; wake_sync_waiters(e); } + pthread_mutex_unlock(&G); + return e != NULL; +} +void hle_signal_system_event(void *ev) { // An unbound one is bound on the spot as manual-clear (only on a guest thread: off one it is skipped) // Lookup and signal under one hold of G: CloseHandle (which unbinds) also runs under G, so the handle cannot be closed and its number reused in between for (int bound = 0; ; bound = 1) { pthread_mutex_lock(&G); @@ -1032,6 +1053,9 @@ void hle_signal_system_event(void *ev) { // An unbound one is bound on the spo } pthread_mutex_unlock(&G); if (bound) die("SystemEvent %p still not found after binding", ev); + // A host thread (audio, vsync) signalling an event the game has destroyed: binding needs SDK code, which only a guest thread can run, and the event is gone anyway + // (MacNativeGames dab728a: skipped, not fatal) + if (!pthread_getspecific(g_keythr)) { static _Atomic int once; if (!atomic_exchange(&once, 1)) LOG("[RED] SystemEvent %p signalled off a guest thread after the game destroyed it: skipped", ev); return; } hle_bind_system_event(ev, 0); // guest code (SVCs take G): outside the lock } } diff --git a/dm/host/tests/Makefile b/dm/host/tests/Makefile index 9b5ba53..00c7349 100644 --- a/dm/host/tests/Makefile +++ b/dm/host/tests/Makefile @@ -25,6 +25,7 @@ run: $(B)/t_heap_size $(B)/t_touch $(B)/t_gesture $(B)/t_oe $(B)/t_fs $(B)/t_fs_ $(B)/t_pctl $(B)/t_time $(B)/t_shared_font + $(B)/t_system_event $(B)/t_vorbis $(B)/t_vorbis_divides $(B)/t_hid @@ -32,6 +33,12 @@ run: $(B)/t_heap_size $(B)/t_touch $(B)/t_gesture $(B)/t_oe $(B)/t_fs $(B)/t_fs_ $(B)/t_haptics_apple $(B)/t_nro run: $(B)/t_nro +run: $(B)/t_system_event +$(B)/t_system_event: t_system_event.c ../hle_thread.c ../hle_sdk16.c ../hle.h ../dm.h $(GAME_H) | $(B) + clang -O1 -g -Wall -Werror -Wno-unused-function -Wno-frame-address -I.. $(GINC) -o $@ t_system_event.c +.PHONY: system-event +system-event: $(B)/t_system_event + $(B)/t_system_event $(B)/t_nro: t_nro.c ../loader.c ../hle_ro.c ../hle.h ../dm.h $(GAME_H) | $(B) clang -O1 -g -Wall -Wno-unused-function -Wno-deprecated-declarations -I.. $(GINC) -o $@ t_nro.c .PHONY: nro diff --git a/dm/host/tests/t_shared_font.c b/dm/host/tests/t_shared_font.c index ff7af21..7fd9f7a 100644 --- a/dm/host/tests/t_shared_font.c +++ b/dm/host/tests/t_shared_font.c @@ -31,6 +31,8 @@ module_t *module_of(const void *p) { (void)p; return NULL; } void dm_backtrace(uint64_t fp) { (void)fp; } void hle_bind_system_event(void *ev, int autoclear) { (void)ev; (void)autoclear; } void hle_signal_system_event(void *ev) { (void)ev; } +uint64_t hle_system_event_binding(void *ev) { (void)ev; return 1; } +int hle_signal_system_event_binding(uint64_t binding) { (void)binding; return 1; } uint32_t hle_hid_connected_npad(void) { return 0; } void dm_host_helper_qos(qos_class_t q) { (void)q; } extern const struct mach_header_64 _mh_execute_header; diff --git a/dm/host/tests/t_system_event.c b/dm/host/tests/t_system_event.c new file mode 100644 index 0000000..1ec4289 --- /dev/null +++ b/dm/host/tests/t_system_event.c @@ -0,0 +1,135 @@ +// Service-event lifetimes through real binding / CloseHandle SVCs and the vi +// hook. Hold worker startup and advance its fallback clock explicitly so event +// destruction and address / handle reuse always happen before the first tick. +#include "../hle_thread.c" +#include +#include +#include +#include + +static void *(*jobs[32])(void *); +static void *args[32]; +static int njobs, sleeps, observed_signal; +static uint64_t clock_ns; +typedef struct { uint32_t wh, rh; } test_event; +static test_event *observe; +static void destroy_event(test_event *ev); +static int test_create(pthread_t *th, const pthread_attr_t *attr, void *(*fn)(void *), void *arg) { + (void)attr; assert(njobs < 32); *th = (pthread_t)(uintptr_t)(njobs + 1); + jobs[njobs] = fn; args[njobs++] = arg; return 0; +} +static int test_detach(pthread_t th) { (void)th; return 0; } +static int test_sleep(const struct timespec *ts, struct timespec *left) { + (void)left; + // Bound an old worker that incorrectly follows a replacement event: inspect + // its first tick, then destroy the replacement so even that worker can exit. + if (sleeps++ && observe && observe->wh) { + pthread_mutex_lock(&G); observed_signal |= ((event_t *)h_get(observe->wh, O_EVENT_W))->signaled; pthread_mutex_unlock(&G); + destroy_event(observe); + } + assert(sleeps < 4); + clock_ns += (uint64_t)ts->tv_sec * 1000000000ull + ts->tv_nsec; + return 0; +} +#define pthread_create test_create +#define pthread_detach test_detach +#define nanosleep test_sleep +#include "../hle_sdk16.c" +#undef pthread_create +#undef pthread_detach +#undef nanosleep + +struct dm_flags g_flag; +module_t g_mod[MAX_MOD]; +int g_nmod; +uint64_t g_t0_ns; +__thread _Atomic int dm_host_work; +pthread_key_t g_key18; +void crash_thread_init(void) {} +void guest_thread_init(void) {} +void dm_thread_enter(uint64_t entry, uint64_t arg, uint64_t sp) { (void)entry; (void)arg; (void)sp; abort(); } +uint64_t now_ns(void) { return clock_ns; } +uint64_t guest_ns(void) { return now_ns(); } +int dm_detclock_enabled(void) { return 0; } +int dm_sdk_major(void) { return 20; } +int dm_main_imports(const char *prefix) { (void)prefix; return 1; } +module_t *module_of(const void *p) { (void)p; return NULL; } +void dm_backtrace(uint64_t fp) { (void)fp; } +uint32_t hle_hid_connected_npad(void) { return 0; } +static uint32_t create_event(void *p, int mode, int interprocess) { + (void)mode; assert(interprocess); + uint64_t r[32] = {0}; assert(svc_thread(0x45, r) && !r[0]); + test_event *ev = p; ev->wh = r[1]; ev->rh = r[2]; return 0; +} +static uint32_t writable_event(const void *p) { return ((const test_event *)p)->wh; } +void *guest_sym(const char *name) { + if (!strcmp(name, "_ZN2nn2os17CreateSystemEventEPNS0_15SystemEventTypeENS0_14EventClearModeEb")) return create_event; + if (!strcmp(name, "_ZN2nn2os30GetWritableHandleOfSystemEventEPKNS0_15SystemEventTypeE")) return writable_event; + return NULL; +} +static void destroy_event(test_event *ev) { + uint64_t r[32] = {ev->wh}; assert(ev->wh && svc_thread(0x16, r) && !r[0]); + r[0] = ev->rh; assert(svc_thread(0x16, r) && !r[0]); + ev->wh = ev->rh = 0; +} +static int signalled(test_event *ev) { + pthread_mutex_lock(&G); int signalled = ((event_t *)h_get(ev->wh, O_EVENT_W))->signaled; pthread_mutex_unlock(&G); + return signalled; +} +static void run_job(int i) { + void *guest = pthread_getspecific(g_keythr); pthread_setspecific(g_keythr, NULL); + sleeps = 0; jobs[i](args[i]); + pthread_setspecific(g_keythr, guest); +} +static void duplicate(void) { + test_event ev = {0}; + assert(!h_vi_VsyncEvent(&ev, NULL)); assert(!h_vi_VsyncEvent(&ev, NULL)); + assert(njobs == 1); destroy_event(&ev); run_job(0); +} +static void capacity(void) { + test_event ev[9] = {0}; + for (unsigned i = 0; i < 9; i++) { assert(!h_vi_VsyncEvent(&ev[i], NULL)); destroy_event(&ev[i]); } + for (int i = 0; i < njobs; i++) run_job(i); + assert(g_nse == 0); +} +static void reuse(void) { + test_event ev = {0}; assert(!h_vi_VsyncEvent(&ev, NULL)); + uint32_t old_wh = ev.wh; destroy_event(&ev); hle_bind_system_event(&ev, 0); + assert(ev.wh == old_wh); // Force reuse of both guest storage and the kernel handle number. + observe = &ev; run_job(0); + assert(!observed_signal && (!ev.wh || !signalled(&ev))); + if (ev.wh) destroy_event(&ev); +} +static void rebind(void) { + test_event ev = {0}; assert(!h_vi_VsyncEvent(&ev, NULL)); destroy_event(&ev); + assert(!h_vi_VsyncEvent(&ev, NULL)); + // The replacement vi registration must still tick after the old lifetime + // retires, including when it uses exactly the same storage and handles. + observe = &ev; + for (int i = 0; i < njobs; i++) run_job(i); + assert(observed_signal); +} +static void host_signal(void) { + test_event ev = {0}; hle_bind_system_event(&ev, 0); + void *guest = pthread_getspecific(g_keythr); pthread_setspecific(g_keythr, NULL); + hle_signal_system_event(&ev); assert(signalled(&ev)); + destroy_event(&ev); hle_signal_system_event(&ev); hle_signal_system_event(&ev); + assert(g_nse == 0); pthread_setspecific(g_keythr, guest); + // Guest callers retain lazy creation for service events not yet bound. + hle_signal_system_event(&ev); assert(signalled(&ev)); destroy_event(&ev); +} +int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); + struct rlimit lim = {0, 0}; setrlimit(RLIMIT_CORE, &lim); + void (*tests[])(void) = {duplicate, capacity, reuse, rebind, host_signal}; + const char *names[] = {"duplicate vi registration", "rapid vi event replacement", "storage and handle reuse by another service", "vi rebind at reused storage", "host skip and guest lazy binding"}; + int failed = 0; + for (unsigned i = 0; i < sizeof tests / sizeof *tests; i++) { + pid_t pid = fork(); assert(pid >= 0); + if (!pid) { alarm(5); setenv("DM_VSYNC_HZ", "60", 1); svc_thread_main_init(); tests[i](); _exit(0); } + int status; assert(waitpid(pid, &status, 0) == pid); + int ok = WIFEXITED(status) && !WEXITSTATUS(status); failed |= !ok; + printf("%s: %s\n", ok ? "PASS" : "FAIL", names[i]); + } + return failed; +} diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 24e07e6..a5285ac 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -78,6 +78,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - a 30 Hz game thread runs at 27 Hz, and whatever it paces comes out slow (cutscenes at 36 ms per frame instead of 33.3; gameplay was fine at 60). → [Timer slack in guest sleeps (`sleep_precise`)](../reference/threads.md#timer-slack-in-guest-sleeps-sleep_precise) - a guest thread keeps a core at 100% with billions of `SleepThread` calls. → [Sleeps under 20 µs take the yield path](../reference/threads.md#sleeps-under-20-µs-take-the-yield-path) - a loading screen sits for a minute with the CPU nearly idle; one thread yields thousands of times a second, every yield a back-off sleep (the power line's top yielder, with as many back-off sleeps). → [Yields that make progress return at once (`thr_yield`)](../reference/threads.md#yields-that-make-progress-return-at-once-thr_yield) +- a host helper thread aborts when signalling a destroyed `SystemEvent`; repeated `vi::GetDisplayVsyncEvent` calls double the event's tick rate. Rapid replacement can abort with `too many vsync events`, or a retired vsync worker can signal another service's event at reused storage. → [Service-event producers follow the binding lifetime](../reference/threads.md#service-event-producers-follow-the-binding-lifetime) - the main thread waits forever on a job's done event at boot while every worker sleeps, two of them on one event (`main+0x1426c` waiting on `Job::m_doneEvent`); 4 of 6 Mac runs. → [One auto-clear event shared by a pool's workers](../reference/threads.md#one-auto-clear-event-shared-by-a-pools-workers) - abort in `os::WaitSystemEvent` under `oe::PopNotificationMessage` (The Legend of Zelda: Link's Awakening); one guest thread at about 1000 ms/s on the iPhone only, the phone hot, CPU energy 5–7 W (Monster Hunter Rise's `MessageSuperviseThread`, priority 32, in the `[nvn] threads` lines). → [The oe notification event and its queue](../reference/threads.md#the-oe-notification-event-and-its-queue) diff --git a/docs/reference/threads.md b/docs/reference/threads.md index a97a39f..8cde367 100644 --- a/docs/reference/threads.md +++ b/docs/reference/threads.md @@ -109,6 +109,14 @@ The yield back-off (`DM_YIELD_BACKOFF`) and the rewriter's spin waits (`patch_ns ## Events and notifications +### Service-event producers follow the binding lifetime + +- **Symptom:** a host helper thread aborts when signalling a destroyed `SystemEvent`; repeated `vi::GetDisplayVsyncEvent` calls double the event's tick rate. Rapid replacement can abort with `too many vsync events`, or a retired vsync worker can signal another service's event at reused storage. +- **Cause:** lazy binding calls the SDK's `CreateSystemEvent`, which requires a guest thread. Checking only the guest address also mistakes a replacement binding for the original event, even if its kernel handle number is checked: both can be reused before the next tick. An eight-slot worker table counts destroyed events until their workers next run. +- **Fix:** `hle_signal_system_event` retains lazy binding on guest threads; host threads skip an unbound event and log once. Each service-event binding gets a monotonically increasing identity. `hle_signal_system_event_binding` matches that identity and signals under the same lock as `CloseHandle`, so a stale producer cannot signal a replacement. The vi registry keeps one worker per binding identity in an allocated list; a worker whose binding closed removes only its own entry and exits. A new vi binding at reused storage gets its own worker, while repeated requests for a live binding share the existing one. +- **Found on:** synthetic host regression cases in [`t_system_event.c`](../../dm/host/tests/t_system_event.c), using real binding and event SVCs with worker startup held until after destruction and handle reuse; no game failure is claimed. +- **Check:** duplicate vi registration, nine rapid replacements before the first tick, storage and handle reuse by a different service, vi rebinding at reused storage, host signalling after destruction, and guest lazy binding. The capacity and unrelated-service cases fail on the original PR #202 and pass with lifetime identities and the allocated registry. + ### One auto-clear event shared by a pool's workers - **Symptom:** the main thread waits forever on a job's done event at boot while every worker sleeps, two of them on one event (`main+0x1426c` waiting on `Job::m_doneEvent`); 4 of 6 Mac runs.