From 9bd722c95d960275b9127a562059dbf48764f52f Mon Sep 17 00:00:00 2001 From: baromas Date: Sat, 7 Nov 2026 22:43:17 +0300 Subject: [PATCH] fix: honor offline event-counter contracts and release cores while waiting Isolated from 460ca742 for review on the OoT bring-up branch. --- THIRD_PARTY_NOTICES.md | 2 + docs/oot-offline-eventfd.md | 52 ++++++++ games/totk/runtime/offline_host.c | 194 +++++++++++++++++++----------- tests/offline_eventfd_probe.c | 95 +++++++++++++++ tests/test_offline_eventfd.py | 52 ++++++++ 5 files changed, 326 insertions(+), 69 deletions(-) create mode 100644 docs/oot-offline-eventfd.md create mode 100644 tests/offline_eventfd_probe.c create mode 100644 tests/test_offline_eventfd.py diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 935f6d207..5737bdd4c 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -36,3 +36,5 @@ Fetched or linked at build time (not in this repository): | [ALVR](https://github.com/alvr-org/ALVR) client v20.14.0, downloaded (SHA-256 checked) and installed on the headset by `zeldaVR/install_vr.sh` | shows the VR picture on the Meta Quest 3 | MIT | | [Noto Sans CJK SC](https://github.com/notofonts/noto-cjk) (Source Han Sans) DemiLight at `165c01b`, downloaded and checked by `windows_games/ra2/scripts/languages.py` | the Simplified Chinese glyphs it draws into the player's game font | SIL OFL 1.1 | | [OpenCC](https://github.com/BYVoid/OpenCC), run (not included) by `windows_games/ra2/scripts/languages.py` | Traditional to Simplified Chinese | Apache-2.0 | + +| Local event-counter contracts in `games/totk/runtime/offline_host.c`, adapted to virtual-core handoff and this host's symbol lookup | [nxnative PR #1299](https://github.com/seedds/nxnative/pull/1299), revision `ee9b7765d579c46e45a7297493565e098f3bda33`, `dm/host/hle_net.c` | MIT | `games/totk/third_party/licenses/nxnative.txt` | diff --git a/docs/oot-offline-eventfd.md b/docs/oot-offline-eventfd.md new file mode 100644 index 000000000..65d6a7b7e --- /dev/null +++ b/docs/oot-offline-eventfd.md @@ -0,0 +1,52 @@ +# Offline event counters and virtual-core waits + +This branch follows `fix/oot-image-write-usage`. It repairs the local wake-up service in the OoT bring-up base; +network connections remain refused. gRPC and the movie TaskQueue use these counters while offline. + +The contract was checked against nxnative's MIT implementation and tests at +[`ee9b7765`](https://github.com/seedds/nxnative/tree/ee9b7765d579c46e45a7297493565e098f3bda33/dm/host) +(`hle_net.c`, `tests/t_net_eventfd.c`). The implementation here uses this host's existing virtual-core API and +retains its small polling loop instead of importing nxnative's scheduler or whole service layer. + +## Corrections + +- EventFdRead/Write return **0 on success**, not a byte count. A non-semaphore read returns the counter before + clearing it; a semaphore read takes one. Previously an ordinary read returned the caller's previous buffer value. +- Socket EAGAIN is BSD **35**; guest C-library errno is Linux **11**. Set both, per thread. Fcntl's BSD O_NONBLOCK + is **4**, not Linux 2048. Close-on-exec is retained as metadata (this host has no exec). +- Zero writes succeed without advancing the counter. UINT64_MAX is rejected before subtraction, and a full counter + cannot overflow. Descriptor lookup/state/flags now stay under the mutex. A blocked read/write detects closure and + generation reuse instead of consuming a newly allocated descriptor's counter. +- Select preserves its requested sets while waiting, only publishes results when returning, does not invent an + exception event, and counts each ready bit. Poll also honors POLLRDNORM. Unknown offline descriptors still report + no poll event, preserving the bring-up host's policy; this is not a general network poll implementation. +- Finite timeouts use the monotonic deadline, including time spent waiting to regain a virtual core. Every blocking + sleep releases that core and regains it afterward, **outside** the event-counter mutex. A producer on the same + virtual core can therefore run. No priority, affinity, game state or guest instruction is changed. +- C++ Poll/Fcntl exports use the local event service as the C exports already do. Diagnostics remain opt-in. + +## Checks + +`python -m unittest discover -s tests -p test_offline_eventfd.py -v` + +The production source is compiled into a small host-only probe for both Donkey Kong Bananza and OoT configurations. +Fifteen tests cover contracts, arithmetic bounds, BSD flags, readiness, delayed Select, timeouts, descriptor reuse, +thread-local error reporting and exports. Four use a single simulated virtual-core mutex: the blocked service must +release it for the producer/consumer, and regain it without holding the event mutex. These are service tests, not a +claim of full Horizon scheduler conformance. No game files, real socket connections or windows are needed. + +All 30 configuration/case combinations pass. The 15 cases also pass with AddressSanitizer and UndefinedBehaviorSanitizer. +The unchanged production source fails five independent negative controls (counter, flags, Poll, Select, delayed Select). +All three Mac targets that build offline_host.c compile (DKB, Pokopia, OoT). +The complete root suite passes: 1,477 tests, 164 skipped, no failures/errors. The combined image branch also +builds all eight shared Mac runtime targets. + +An iOS syntax build first hits an existing unrelated `gethostuuid` call in `pseudo_device_id`, unavailable on iOS; +the original source has the same error. A local-only syntax copy with that call removed checks the event changes +for all three variants. That workaround is not committed. No complete iOS app or device test is claimed. + +## Limits + +A previous game run stopped before AV1 initialization; these are concrete platform defects, but that observation +alone does not establish their responsibility for the black screen. A new bounded game trial is needed. The separate +image-write fix addresses a different verified missing output operation. Installed TOTK is untouched. diff --git a/games/totk/runtime/offline_host.c b/games/totk/runtime/offline_host.c index 59b588892..8caae54e2 100644 --- a/games/totk/runtime/offline_host.c +++ b/games/totk/runtime/offline_host.c @@ -83,108 +83,164 @@ static uint32_t socket_htonl(uint32_t v) { return htonl(v); } static uint16_t socket_htons(uint16_t v) { return htons(v); } static const char *socket_ntop(int family, const void *in, char *out, uint32_t size) { return family == 2 ? inet_ntop(AF_INET, in, out, size) : NULL; } -/* ---------- nn::socket event counters (eventfd): gRPC's poller (NPLN, in main) and the movie TaskQueue (subsdk0) - wake through these; with them cut, gRPC finds no poll engine and aborts at boot (Ocarina of Time). Linux eventfd - semantics, small table. Poll readiness is a 1 ms sleep loop (ponytail: condvars if wakeup latency ever matters). ---------- */ +/* ---------- nn::socket event counters: local wakeups work even on the offline host. + Horizon's socket errno/flags are BSD values; nnmusl's errno is Linux. EventFdRead/Write return status 0, not + the byte count of a POSIX read/write. Poll keeps the existing offline-descriptor behavior (nothing ready). + Blocking host waits must hand off the virtual core with no host mutex held. */ #include #include +#include #define EVFDS 64 -#define EV_BASE 512 /* stay below select()'s 1024-bit set, away from host fds (as nxnative does) */ +#define EV_BASE 512 #define EV_SEMAPHORE 1u +#define EV_CLOEXEC 2u #define EV_NONBLOCK 4u -#define EV_MAX 0xfffffffffffffffeull -static struct { uint64_t count; unsigned flags; int used; } evfd[EVFDS]; +#define EV_MAX UINT64_C(0xfffffffffffffffe) +static struct { uint64_t count, generation; unsigned flags; int used; } evfd[EVFDS]; static pthread_mutex_t evfd_lock = PTHREAD_MUTEX_INITIALIZER; -static int ev_error(int e) { socket_errno = e; return -1; } /* Linux errnos: the game reads them via nnsocketGetLastErrno */ +static int ev_error(int e) { + /* gRPC tests the guest C library's errno as well as nnsocketGetLastErrno. */ + static _Thread_local int *(*location)(void); + socket_errno = e; + int libc_error = e == 35 ? 11 : e; + if (!location) location = runtime_symbol("__errno_location"); + if (location) *location() = libc_error; + errno = libc_error; + return -1; +} +/* Caller holds evfd_lock. */ static int ev_fd(int fd) { return fd >= EV_BASE && fd < EV_BASE + EVFDS && evfd[fd - EV_BASE].used ? fd - EV_BASE : -1; } +static void ev_sleep_us(long us) { + struct timespec ts = {us / 1000000, (us % 1000000) * 1000}; + int away = runtime_core_away(); + while (nanosleep(&ts, &ts) && errno == EINTR) { } + runtime_core_back(&away); +} +static uint64_t ev_deadline(uint64_t duration) { + uint64_t now = runtime_now_ns(); + return duration > UINT64_MAX - now ? UINT64_MAX : now + duration; +} +static void ev_pause(uint64_t deadline) { + uint64_t now = runtime_now_ns(); + if (now >= deadline) return; + uint64_t left = deadline - now; + ev_sleep_us(left >= 1000000 ? 1000 : (long)((left + 999) / 1000)); +} static int ev_event_fd(uint64_t init, unsigned flags) { - if (init == EV_MAX + 1) return ev_error(22); /* EINVAL, as eventfd(2) */ + if (init > EV_MAX || (flags & ~(EV_SEMAPHORE | EV_CLOEXEC | EV_NONBLOCK))) return ev_error(22); pthread_mutex_lock(&evfd_lock); - for (int i = 0; i < EVFDS; i++) if (!evfd[i].used) { evfd[i].count = init; evfd[i].flags = flags; evfd[i].used = 1; pthread_mutex_unlock(&evfd_lock); return EV_BASE + i; } - pthread_mutex_unlock(&evfd_lock); - return ev_error(24); /* EMFILE */ + for (int i = 0; i < EVFDS; i++) if (!evfd[i].used) { + evfd[i].count = init; evfd[i].flags = flags; evfd[i].used = 1; evfd[i].generation++; + pthread_mutex_unlock(&evfd_lock); return EV_BASE + i; + } + pthread_mutex_unlock(&evfd_lock); return ev_error(24); } static int ev_read(int fd, uint64_t *out) { - int i = ev_fd(fd); if (i < 0) return ev_error(9); /* EBADF */ + pthread_mutex_lock(&evfd_lock); + int i = ev_fd(fd); + if (i < 0) { pthread_mutex_unlock(&evfd_lock); return ev_error(9); } + uint64_t generation = evfd[i].generation; for (;;) { - pthread_mutex_lock(&evfd_lock); - if (evfd[i].count) { *out = evfd[i].flags & EV_SEMAPHORE ? (evfd[i].count--, 1) : (evfd[i].count = 0, *out); pthread_mutex_unlock(&evfd_lock); return 8; } + if (!evfd[i].used || evfd[i].generation != generation) { pthread_mutex_unlock(&evfd_lock); return ev_error(9); } + if (evfd[i].count) { + uint64_t take = evfd[i].flags & EV_SEMAPHORE ? 1 : evfd[i].count; + evfd[i].count -= take; if (out) *out = take; + pthread_mutex_unlock(&evfd_lock); return 0; + } + int nonblock = evfd[i].flags & EV_NONBLOCK; pthread_mutex_unlock(&evfd_lock); - if (evfd[i].flags & EV_NONBLOCK) return ev_error(11); /* EAGAIN */ - struct timespec ts = {0, 1000000}; nanosleep(&ts, NULL); /* blocking read: wait for a write */ + if (nonblock) return ev_error(35); + ev_sleep_us(1000); + pthread_mutex_lock(&evfd_lock); } } static int ev_write(int fd, uint64_t v) { - int i = ev_fd(fd); if (i < 0) return ev_error(9); - if (!v) return ev_error(22); + pthread_mutex_lock(&evfd_lock); + int i = ev_fd(fd); + if (i < 0) { pthread_mutex_unlock(&evfd_lock); return ev_error(9); } + if (v > EV_MAX) { pthread_mutex_unlock(&evfd_lock); return ev_error(22); } + uint64_t generation = evfd[i].generation; for (;;) { - pthread_mutex_lock(&evfd_lock); - if (evfd[i].count <= EV_MAX - v) { evfd[i].count += v; pthread_mutex_unlock(&evfd_lock); return 8; } + if (!evfd[i].used || evfd[i].generation != generation) { pthread_mutex_unlock(&evfd_lock); return ev_error(9); } + if (evfd[i].count <= EV_MAX - v) { evfd[i].count += v; pthread_mutex_unlock(&evfd_lock); return 0; } + int nonblock = evfd[i].flags & EV_NONBLOCK; pthread_mutex_unlock(&evfd_lock); - if (evfd[i].flags & EV_NONBLOCK) return ev_error(11); - struct timespec ts = {0, 1000000}; nanosleep(&ts, NULL); /* counter full: wait for a read */ + if (nonblock) return ev_error(35); + ev_sleep_us(1000); + pthread_mutex_lock(&evfd_lock); } } -static void ev_sleep_us(long us) { struct timespec ts = {us / 1000000, (us % 1000000) * 1000}; nanosleep(&ts, NULL); } -static int ev_poll(void *fds_, unsigned nfds, int timeout_ms) { /* struct pollfd {int fd; short events; short revents} */ +static int ev_poll(void *fds_, uint64_t nfds, int timeout_ms) { struct { int fd; short events, revents; } *fds = fds_; - /* TEMP probe (OoT boot): what the movie TaskQueue and gRPC poll for. */ - static int ep_count; - if (getenv("TOTK_WAIT_LOG") && ep_count < 10) { ep_count++; - fprintf(stderr, "[wait-probe] poll nfds=%u timeout=%d fds:", nfds, timeout_ms); - for (unsigned k = 0; k < nfds && k < 8; k++) fprintf(stderr, " %d/%x", fds[k].fd, fds[k].events); - fprintf(stderr, "\n"); } - long waited = 0; + if (nfds && !fds) return ev_error(14); + static unsigned ep_count; + if (getenv("TOTK_WAIT_LOG") && __atomic_fetch_add(&ep_count, 1, __ATOMIC_RELAXED) < 10) { + fprintf(stderr, "[wait-probe] poll nfds=%llu timeout=%d fds:", (unsigned long long)nfds, timeout_ms); + for (uint64_t k = 0; k < nfds && k < 8; k++) fprintf(stderr, " %d/%x", fds[k].fd, fds[k].events); + fprintf(stderr, "\n"); + } + uint64_t deadline = timeout_ms < 0 ? UINT64_MAX : ev_deadline((uint64_t)timeout_ms * 1000000); for (;;) { int ready = 0; pthread_mutex_lock(&evfd_lock); - for (unsigned k = 0; k < nfds; k++) { fds[k].revents = 0; int i = ev_fd(fds[k].fd); - if (i >= 0) { if ((fds[k].events & 1) && evfd[i].count) fds[k].revents |= 1; if ((fds[k].events & 4) && evfd[i].count < EV_MAX) fds[k].revents |= 4; if (fds[k].revents) ready++; } } + for (uint64_t k = 0; k < nfds; k++) { + fds[k].revents = 0; int i = ev_fd(fds[k].fd); + if (i < 0) continue; /* no network sockets exist on this offline host */ + if (evfd[i].count) fds[k].revents |= fds[k].events & (1 | 0x40); /* POLLIN | POLLRDNORM */ + if (evfd[i].count < EV_MAX) fds[k].revents |= fds[k].events & 4; /* POLLOUT / POLLWRNORM */ + ready += fds[k].revents != 0; + } pthread_mutex_unlock(&evfd_lock); - if (ready || (timeout_ms >= 0 && waited >= timeout_ms * 1000L)) return ready; - ev_sleep_us(1000); waited += 1000; + if (ready || runtime_now_ns() >= deadline) return ready; + ev_pause(deadline); } } -static int ev_select(int nfds, void *rin_, void *win_, void *ein_, void *tv_) { /* FdSet: 32 u32 words; TimeVal {sec,usec} */ - unsigned *rin = rin_, *win = win_, *ein = ein_; - /* TEMP probe (OoT boot): the movie TaskQueue selects on what? */ - static int es_count; - if (getenv("TOTK_WAIT_LOG") && es_count < 10) { es_count++; - fprintf(stderr, "[wait-probe] select nfds=%d bits r:", nfds); - for (int w = 0; w < 32; w++) if (rin && rin[w]) fprintf(stderr, " w%d=%x", w, rin[w]); - fprintf(stderr, " w:"); - for (int w = 0; w < 32; w++) if (win && win[w]) fprintf(stderr, " w%d=%x", w, win[w]); - fprintf(stderr, " tv=%lld.%09ld\n", tv_ ? (long long)((long *)tv_)[0] : -1, tv_ ? (long)((long *)tv_)[1] : 0L); } - long budget = -1; /* microseconds */ - if (tv_) { long long s = ((long *)tv_)[0], u = ((long *)tv_)[1]; budget = s * 1000000 + u; } - long waited = 0; +static int ev_select(int nfds, void *rin_, void *win_, void *ein_, void *tv_) { + unsigned *sets[3] = {rin_, win_, ein_}, want[3][32] = {{0}}; + if (nfds < 0 || nfds > 1024) return ev_error(22); + uint64_t deadline = UINT64_MAX; + if (tv_) { + const int64_t *tv = tv_; + if (tv[0] < 0 || tv[0] > INT64_MAX / 1000000000 || tv[1] < 0 || tv[1] >= 1000000) return ev_error(22); + deadline = ev_deadline((uint64_t)tv[0] * 1000000000 + (uint64_t)tv[1] * 1000); + } + for (int k = 0; k < 3; k++) if (sets[k]) memcpy(want[k], sets[k], sizeof want[k]); for (;;) { - int ready = 0; + unsigned result[3][32] = {{0}}; int ready = 0, bad = 0; pthread_mutex_lock(&evfd_lock); - for (int i = 0; i < EVFDS && EV_BASE + i < nfds; i++) { - int w = (EV_BASE + i) >> 5, b = 1u << ((EV_BASE + i) & 31); - int r = (rin && rin[w] & b) || (ein && ein[w] & b), wr = win && win[w] & b; - int hit = (r && evfd[i].count) || (wr && evfd[i].count < EV_MAX); - if (rin) rin[w] = (rin[w] & ~b) | (hit && r && evfd[i].count ? b : 0); - if (win) win[w] = (win[w] & ~b) | (hit && wr ? b : 0); - if (ein) ein[w] &= ~b; - if (hit) ready++; + for (int fd = 0; fd < nfds; fd++) { + unsigned b = 1u << (fd & 31); int w = fd >> 5; + if (!((want[0][w] | want[1][w] | want[2][w]) & b)) continue; + int i = ev_fd(fd); if (i < 0) { bad = 1; break; } + if ((want[0][w] & b) && evfd[i].count) { result[0][w] |= b; ready++; } + if ((want[1][w] & b) && evfd[i].count < EV_MAX) { result[1][w] |= b; ready++; } + /* No exceptional condition on a local event counter. */ } pthread_mutex_unlock(&evfd_lock); - if (ready || (budget >= 0 && waited >= budget)) return ready; - ev_sleep_us(1000); waited += 1000; + if (bad) return ev_error(9); + if (ready || runtime_now_ns() >= deadline) { + for (int k = 0; k < 3; k++) if (sets[k]) memcpy(sets[k], result[k], sizeof result[k]); + return ready; + } + ev_pause(deadline); } } static int ev_close(int fd) { - int i = ev_fd(fd); if (i < 0) return ev_error(9); - pthread_mutex_lock(&evfd_lock); evfd[i].used = 0; pthread_mutex_unlock(&evfd_lock); - return 0; + pthread_mutex_lock(&evfd_lock); int i = ev_fd(fd); + if (i >= 0) evfd[i].used = 0; + pthread_mutex_unlock(&evfd_lock); return i < 0 ? ev_error(9) : 0; } -static int ev_fcntl(int fd, unsigned cmd, unsigned long arg) { /* F_GETFL=3 F_SETFL=4; O_NONBLOCK=2048 */ - int i = ev_fd(fd); if (i < 0) return ev_error(9); - if (cmd == 3) return (int)((evfd[i].flags & EV_NONBLOCK) ? 2048 : 0); - if (cmd == 4) { pthread_mutex_lock(&evfd_lock); if (arg & 2048) evfd[i].flags |= EV_NONBLOCK; else evfd[i].flags &= ~EV_NONBLOCK; pthread_mutex_unlock(&evfd_lock); return 0; } - return 0; +static int ev_fcntl(int fd, unsigned cmd, unsigned long arg) { + pthread_mutex_lock(&evfd_lock); int i = ev_fd(fd), result = 0; + if (i >= 0) { + if (cmd == 3) result = evfd[i].flags & EV_NONBLOCK ? 4 : 0; /* BSD O_NONBLOCK */ + else if (cmd == 4) evfd[i].flags = (evfd[i].flags & ~EV_NONBLOCK) | (arg & 4 ? EV_NONBLOCK : 0); + else if (cmd == 1) result = !!(evfd[i].flags & EV_CLOEXEC); + else if (cmd == 2) evfd[i].flags = (evfd[i].flags & ~EV_CLOEXEC) | (arg & 1 ? EV_CLOEXEC : 0); + else result = -1; + } + pthread_mutex_unlock(&evfd_lock); + return i < 0 ? ev_error(9) : result < 0 ? ev_error(22) : result; } static int ev_set_error(int e) { socket_errno = e; return 0; } @@ -280,8 +336,8 @@ void *runtime_offline_override(const char *name) { {"_ZN2nn4bcat21DeliveryCacheProgressC1Ev", nothing}, {"_ZN2nn4bcat21DeliveryCacheProgressD1Ev", nothing}, {"_ZN2nn4bcat24RequestSyncDeliveryCacheEPNS0_21DeliveryCacheProgressE", nifm_unavailable}, {"_ZN2nn6socket10GetSockOptEiiiPvPj", socket_down}, {"_ZN2nn6socket10SetSockOptEiiiPKvj", socket_down}, - {"_ZN2nn6socket11GetSockNameEiP8sockaddrPj", socket_down}, {"_ZN2nn6socket4PollEP6pollfdmi", socket_down}, - {"_ZN2nn6socket4RecvEiPvmi", socket_down}, {"_ZN2nn6socket4SendEiPKvmi", socket_down}, {"_ZN2nn6socket5FcntlEiNS0_12FcntlCommandEz", socket_down}, + {"_ZN2nn6socket11GetSockNameEiP8sockaddrPj", socket_down}, {"_ZN2nn6socket4PollEP6pollfdmi", ev_poll}, + {"_ZN2nn6socket4RecvEiPvmi", socket_down}, {"_ZN2nn6socket4SendEiPKvmi", socket_down}, {"_ZN2nn6socket5FcntlEiNS0_12FcntlCommandEz", ev_fcntl}, {"_ZN2nn6socket6AcceptEiP8sockaddrPj", socket_down}, {"_ZN2nn6socket6ListenEii", socket_down}, {"_ZN2nn6socket6SelectEiPNS0_5FdSetES2_S2_PNS0_7TimeValE", socket_down}, {"_ZN2nn6socket6SendToEiPKvmiPK8sockaddrj", socket_down}, {"_ZN2nn6socket7ConnectEiPK8sockaddrj", socket_down}, {"_ZN2nn6socket8RecvFromEiPvmiP8sockaddrPj", socket_down}, diff --git a/tests/offline_eventfd_probe.c b/tests/offline_eventfd_probe.c new file mode 100644 index 000000000..daa65c3a4 --- /dev/null +++ b/tests/offline_eventfd_probe.c @@ -0,0 +1,95 @@ +/* Production offline services, synthetic counters and host threads only. No socket connection or game data. */ +#ifndef OFFLINE_RUNTIME +#define OFFLINE_RUNTIME "../games/totk/runtime/offline_host.c" +#endif +#include OFFLINE_RUNTIME +#include +#include +#include + +static _Thread_local int guest_errno, core_held; +static _Atomic int away_calls, back_calls; +static pthread_mutex_t test_core = PTHREAD_MUTEX_INITIALIZER; +static int *guest_errno_location(void) { return &guest_errno; } +void *runtime_symbol(const char *name) { return !strcmp(name,"__errno_location") ? (void *)guest_errno_location : NULL; } +_Noreturn void runtime_fail(const char *why) { fprintf(stderr,"%s\n",why);exit(2); } +void hle_bind_system_event(void *event,int autoclear) { (void)event;(void)autoclear; } +void hle_signal_system_event(void *event) { (void)event; } +uint64_t runtime_now_ns(void) { struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000000000+t.tv_nsec; } +int runtime_core_away(void) { + atomic_fetch_add(&away_calls,1); + int held=core_held; + if(held){assert(!pthread_mutex_trylock(&evfd_lock));assert(!pthread_mutex_unlock(&evfd_lock));core_held=0;assert(!pthread_mutex_unlock(&test_core));} + return held; +} +void runtime_core_back(int *away) { atomic_fetch_add(&back_calls,1);if(*away){assert(!pthread_mutex_lock(&test_core));core_held=1;*away=0;} } +static void sleep_ms(unsigned ms) { struct timespec t={ms/1000,(ms%1000)*1000000};nanosleep(&t,NULL); } +static void check_error(int result,int sk,int libc) { assert(result==-1);assert(socket_last_error()==sk);assert(guest_errno==libc); } +static void bit(unsigned set[32],int fd) { set[fd>>5]|=1u<<(fd&31); } +static int has(const unsigned set[32],int fd) { return !!(set[fd>>5]&(1u<<(fd&31))); } +typedef struct { int fd,read_it,require_core,close_it,reopen;uint64_t value; } action; +static void *later(void *arg) { + action *a=arg;sleep_ms(20); + if(a->require_core){assert(!pthread_mutex_lock(&test_core));core_held=1;} + if(a->close_it){assert(!ev_close(a->fd));if(a->reopen){assert(ev_event_fd(9,4)==a->fd);}} + else if(a->read_it){uint64_t out=0;assert(!ev_read(a->fd,&out));assert(out==EV_MAX);} + else assert(!ev_write(a->fd,a->value)); + if(a->require_core){core_held=0;assert(!pthread_mutex_unlock(&test_core));} + return NULL; +} +static void start(pthread_t *thread,action *a) { assert(!pthread_create(thread,NULL,later,a)); } +static void *errno_thread(void *arg) { (void)arg;check_error(ev_read(-1,NULL),9,9);return NULL; } +int main(int argc,char **argv) { + assert(argc==2);const char *test=argv[1];uint64_t out=0x123456789abcdef0ull; + if(!strcmp(test,"counter")){ + int fd=ev_event_fd(7,6);assert(fd>=512&&fd<1024);assert(!ev_read(fd,&out)&&out==7); + check_error(ev_read(fd,&out),35,11);assert(out==7); + assert(!ev_write(fd,2)&&!ev_write(fd,3));assert(!ev_read(fd,&out)&&out==5);assert(!ev_close(fd)); + }else if(!strcmp(test,"semaphore")){ + int fd=ev_event_fd(2,5);assert(!ev_read(fd,&out)&&out==1);assert(!ev_read(fd,&out)&&out==1);check_error(ev_read(fd,&out),35,11); + }else if(!strcmp(test,"bounds")){ + int fd=ev_event_fd(EV_MAX,4);assert(!ev_write(fd,0));check_error(ev_write(fd,UINT64_MAX),22,22);check_error(ev_write(fd,1),35,11); + assert(!ev_read(fd,&out)&&out==EV_MAX);check_error(ev_event_fd(UINT64_MAX,0),22,22); + }else if(!strcmp(test,"flags")){ + int fd=ev_event_fd(0,0);assert(ev_fcntl(fd,3,0)==0);assert(!ev_fcntl(fd,4,4));assert(ev_fcntl(fd,3,0)==4);check_error(ev_read(fd,&out),35,11); + assert(!ev_fcntl(fd,4,0));assert(!ev_fcntl(fd,1,0));assert(!ev_fcntl(fd,2,1));assert(ev_fcntl(fd,1,0)==1);check_error(ev_fcntl(fd,123,0),22,22); + }else if(!strcmp(test,"poll")){ + int fd=ev_event_fd(1,4);struct {int fd;short events,revents;} p[3]={{fd,0x41,0},{-1,1,-1},{7,1,-1}}; + assert(ev_poll(p,3,0)==1&&p[0].revents==0x41&&!p[1].revents&&!p[2].revents); + assert(!ev_read(fd,&out));p[0].events=1|4;assert(ev_poll(p,1,0)==1&&p[0].revents==4); + }else if(!strcmp(test,"select")){ + int fd=ev_event_fd(1,4);unsigned r[32]={0},w[32]={0},e[32]={0};long tv[2]={0,0};bit(r,fd);bit(w,fd);bit(e,fd); + assert(ev_select(1024,r,w,e,tv)==2&&has(r,fd)&&has(w,fd)&&!has(e,fd)); + bit(e,fd);assert(!ev_select(1024,NULL,NULL,e,tv)&&!has(e,fd)); + }else if(!strcmp(test,"select-late")){ + int fd=ev_event_fd(0,4);unsigned r[32]={0};bit(r,fd);long tv[2]={1,0};action a={.fd=fd,.value=3};pthread_t t;start(&t,&a); + uint64_t begin=runtime_now_ns();assert(ev_select(1024,r,NULL,NULL,tv)==1&&has(r,fd));assert(runtime_now_ns()-begin<500000000);assert(!pthread_join(t,NULL)); + }else if(!strcmp(test,"timeout")){ + int fd=ev_event_fd(0,4);struct {int fd;short events,revents;} p={fd,1,-1};uint64_t begin=runtime_now_ns(); + assert(!ev_poll(&p,1,20)&&!p.revents);uint64_t elapsed=runtime_now_ns()-begin;assert(elapsed>=15000000&&elapsed<500000000); + unsigned r[32]={0};bit(r,fd);long tv[2]={0,20000};begin=runtime_now_ns();assert(!ev_select(1024,r,NULL,NULL,tv)&&!has(r,fd));assert(runtime_now_ns()-begin>=15000000); + assert(away_calls>0&&away_calls==back_calls); + }else if(!strncmp(test,"core-",5)){ + int write_it=!strcmp(test,"core-write"),fd=ev_event_fd(write_it?EV_MAX:0,0); + action a={.fd=fd,.value=3,.read_it=write_it,.require_core=1};pthread_t t; + assert(!pthread_mutex_lock(&test_core));core_held=1;start(&t,&a); + if(!strcmp(test,"core-read")){assert(!ev_read(fd,&out)&&out==3);} + else if(write_it){assert(!ev_write(fd,1));assert(!ev_read(fd,&out)&&out==1);} + else if(!strcmp(test,"core-poll")){struct {int fd;short events,revents;} p={fd,1,0};assert(ev_poll(&p,1,1000)==1&&p.revents==1);} + else if(!strcmp(test,"core-select")){unsigned r[32]={0};bit(r,fd);long tv[2]={1,0};assert(ev_select(1024,r,NULL,NULL,tv)==1&&has(r,fd));} + else return 3; + core_held=0;assert(!pthread_mutex_unlock(&test_core));assert(!pthread_join(t,NULL));assert(away_calls>0&&away_calls==back_calls); + }else if(!strcmp(test,"closed-wait")){ + int fd=ev_event_fd(0,0);action a={.fd=fd,.close_it=1,.reopen=1};pthread_t t;start(&t,&a); + check_error(ev_read(fd,&out),9,9);assert(!pthread_join(t,NULL));assert(!ev_read(fd,&out)&&out==9); + }else if(!strcmp(test,"errors")){ + int fd=ev_event_fd(0,4);check_error(ev_read(fd,&out),35,11);pthread_t t;assert(!pthread_create(&t,NULL,errno_thread,NULL));assert(!pthread_join(t,NULL));assert(socket_last_error()==35&&guest_errno==11); + check_error(ev_select(-1,NULL,NULL,NULL,NULL),22,22);long tv[2]={0,1000000};check_error(ev_select(1024,NULL,NULL,NULL,tv),22,22); + assert(socket_down()==-1&&socket_last_error()==50); + }else if(!strcmp(test,"exports")){ + assert(runtime_offline_override("_ZN2nn6socket4PollEP6pollfdmi")==ev_poll); + assert(runtime_offline_override("_ZN2nn6socket5FcntlEiNS0_12FcntlCommandEz")==ev_fcntl); + assert(runtime_offline_override("_ZN2nn6socket6SelectEiPNS0_5FdSetES2_S2_PNS0_7TimeValE")==ev_select); + }else return 3; + printf("{\"status\":\"pass\",\"case\":\"%s\",\"away\":%d,\"back\":%d}\n",test,away_calls,back_calls);return 0; +} diff --git a/tests/test_offline_eventfd.py b/tests/test_offline_eventfd.py new file mode 100644 index 000000000..0b34762fb --- /dev/null +++ b/tests/test_offline_eventfd.py @@ -0,0 +1,52 @@ +"""Actual offline service implementation; generated counters and pthreads, no game/network/window.""" +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +@unittest.skipUnless(sys.platform == 'darwin', 'requires the macOS host runtime headers') +class OfflineEventFd(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.temp = tempfile.TemporaryDirectory() + cls.addClassCleanup(cls.temp.cleanup) + cls.programs = [] + cls.env = {k:v for k,v in os.environ.items() if not k.startswith(('TOTK_', 'DM_'))} + for variant, flags in [('dkb', ['-DGAME_DKB=1']), ('oot', ['-DGAME_NS2=1', '-DGAME_OOT=1'])]: + program = Path(cls.temp.name) / variant + run = subprocess.run(['xcrun','clang','-std=c11','-O2','-Wall','-Wextra','-Werror', + '-Wno-deprecated-declarations','-I'+str(ROOT/'games/totk/runtime'), + *flags,str(ROOT/'tests/offline_eventfd_probe.c'),'-o',str(program)], + capture_output=True,text=True,timeout=90) + if run.returncode: + raise AssertionError(run.stdout+run.stderr) + cls.programs.append(program) + + def case(self, name): + for program in self.programs: + with self.subTest(variant=program.name): + run = subprocess.run([str(program),name],env=self.env,capture_output=True,text=True,timeout=4) + self.assertEqual(run.returncode,0,run.stdout+run.stderr) + self.assertEqual(json.loads(run.stdout)['status'],'pass') + + def test_counter_result_value_sum_and_reset(self): self.case('counter') + def test_semaphore(self): self.case('semaphore') + def test_zero_maximum_and_overflow_writes(self): self.case('bounds') + def test_bsd_fcntl_flags(self): self.case('flags') + def test_poll_readiness_and_offline_descriptors(self): self.case('poll') + def test_select_returns_each_ready_bit_and_no_false_exceptions(self): self.case('select') + def test_select_retains_request_during_wait(self): self.case('select-late') + def test_monotonic_timeouts_and_balanced_handoff(self): self.case('timeout') + def test_blocking_read_releases_core_for_producer(self): self.case('core-read') + def test_full_write_releases_core_for_consumer(self): self.case('core-write') + def test_poll_releases_core_for_producer(self): self.case('core-poll') + def test_select_releases_core_for_producer(self): self.case('core-select') + def test_closed_wait_does_not_consume_reused_descriptor(self): self.case('closed-wait') + def test_thread_local_socket_and_guest_errno(self): self.case('errors') + def test_exports_reach_local_event_service(self): self.case('exports')