From 71d6550eefa113e0dd732f78eeff13cce4e61e3d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 25 Jun 2026 10:13:53 -0700 Subject: [PATCH] overlay: off-thread sljit compile worker + observability; disable sljit tier by default (emitter bug) Off-thread sljit compile worker (overlay_compile_worker.c/.h): on a dispatch gap-fill miss the dispatch thread snapshots the region bytes and ENQUEUES; a background worker JITs from the snapshot (never live RAM) and publishes the shard to the on-disk cache via an atomic temp+rename; dispatch picks it up on a later miss via an idempotent rescan, so the candidate table stays single-threaded. This removes the synchronous JIT frame hitch. Validated on Tomba + MMX6: compiles off-thread (one MMX6 compile was 26ms, paid entirely off the dispatch thread with no frame-pacing spike), shards persist + reload, queue never backs up. Observability (ring-buffer doctrine): always-on recent-compile ring + per-compile timing in the worker, the resolved on-disk cache dir, and a tier-enabled flag, all surfaced via a new sljit_async TCP debug command (documented in TCP_COMMANDS.md). DISABLE the sljit tier by default (g_sljit_tier_enabled = 0): the sljit EMITTER mistranslates some MMX6 overlay instruction(s) -- sljit-only save-load softlocks/ loopbacks while PURE INTERP works (discriminator), and it reproduces with the worker OFF in the synchronous path (PSX_SLJIT_SYNC=1), so it is the emitter, not the async worker. Until the emitter bug is found + fixed: no shard READ (scan_sljit_cache_dir skipped), no GENERATE (worker not started + sljit_live forced 0 + dispatch gap-fill gated), every overlay miss falls to the interpreter. gcc (DLL cache + autocompile) and overlay capture are untouched, so priority is gcc > interp and dispatch misses are still recorded for future gcc coverage. gcc shards are bundled in release zips, so shipped builds are unaffected. PSX_SLJIT_ENABLE=1 re-enables the tier for emitter-fix testing. Runtime-only, no regen. Co-Authored-By: Claude Opus 4.8 --- TCP_COMMANDS.md | 1 + runtime/include/overlay_compile_worker.h | 75 +++++++++ runtime/runtime.cmake | 1 + runtime/src/debug_server.c | 52 ++++++ runtime/src/main.cpp | 2 + runtime/src/overlay_compile_worker.c | 204 +++++++++++++++++++++++ runtime/src/overlay_loader.c | 143 ++++++++++++++-- 7 files changed, 468 insertions(+), 10 deletions(-) create mode 100644 runtime/include/overlay_compile_worker.h create mode 100644 runtime/src/overlay_compile_worker.c diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index ab743b29..6a268dc4 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -47,6 +47,7 @@ Columns: **N** = native, **D** = DuckStation oracle. | `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | | `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | | `overlay_state` | | ✓ | — | Current overlay info | +| `sljit_async` | ✓ | | — | Off-thread sljit compile-worker telemetry: `worker_running`, `async_on`, always-on counters (`enqueued`/`compiled`/`failed`/`queued_now`), `last_compile_ms`/`max_compile_ms` (off-thread compile time), resolved `cache_dir`, and a `recent[]` ring of the last 64 compiles (per-shard `phys`/`crc`/`code_len`/`ms`/`ok`). Query it to prove the worker compiles off the dispatch thread without a frame hitch | | `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | | `cdrom_sector_history` | ✓ | | `count`, optional `lba` | Dump newest CD-ROM sector history entries, including raw XA subheader fields, CPU/audio delivery flags, and the first 128 bytes | | `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring | diff --git a/runtime/include/overlay_compile_worker.h b/runtime/include/overlay_compile_worker.h new file mode 100644 index 00000000..0db86ca1 --- /dev/null +++ b/runtime/include/overlay_compile_worker.h @@ -0,0 +1,75 @@ +#ifndef PSX_OVERLAY_COMPILE_WORKER_H +#define PSX_OVERLAY_COMPILE_WORKER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Off-main-thread sljit compile worker. + * + * The dispatch (guest-fiber) thread, on an sljit gap-fill miss, snapshots the + * region bytes and ENQUEUES here instead of JIT'ing inline (which used to spike + * the frame). A single background worker pops the queue, JITs the fragment FROM + * THE SNAPSHOT (never live guest RAM), and writes the serialized shard into the + * on-disk cache via overlay_loader_async_publish(). The dispatch thread picks + * the shard up on a later miss (cache-dirty -> idempotent rescan -> register), + * so the candidate table stays single-threaded: the worker NEVER touches it. + * + * Safety invariants (see docs/ASYNC_OVERLAY_COMPILE.md §3/§5): + * - candidate tables / sljit registry: dispatch thread only. + * - guest RAM: worker reads only the enqueue-time snapshot copy. + * - the sljit emitter's file-static scratch is safe because the worker is the + * SOLE runtime caller of the compiler (dispatch enqueues; selftest is init-time). + */ + +/* Start/stop the worker. Idempotent. stop() signals + joins. */ +void overlay_compile_worker_start(void); +void overlay_compile_worker_stop(void); + +/* Enqueue a region for background JIT. `vaddr` is the VIRTUAL entry (carries + * KSEG bits the guest uses for return_pc/jal targets). `src` points at the + * live region bytes [phys .. phys+snap_len); enqueue COPIES them, so the caller + * keeps ownership of `src`. `crc` is a content hash of the snapshot window, + * used only for (phys,crc) dedup. Deduped: a QUEUED/COMPILING/DONE (phys,crc) + * is ignored. Cheap no-op if the worker isn't running. */ +void overlay_compile_worker_enqueue(uint32_t vaddr, uint32_t phys, + const uint8_t *src, uint32_t snap_len, + uint32_t crc); + +/* Telemetry for the debug server (any out-ptr may be NULL). */ +void overlay_compile_worker_stats(uint64_t *enqueued, uint64_t *compiled, + uint64_t *failed, uint32_t *queued_now); + +/* 1 if the worker thread is currently running. */ +int overlay_compile_worker_running(void); + +/* Last / worst single-compile wall time (ms, SDL_GetTicks resolution). The whole + * point of the worker is that this time is paid OFF the dispatch thread, so a + * nonzero max here alongside a flat dispatch-thread ce_profile is the proof. */ +void overlay_compile_worker_timing(uint32_t *last_ms, uint32_t *max_ms); + +/* Always-on ring of the most-recent compile results (one entry PER shard the + * worker has produced or rejected). Query this for the window of interest — + * never arm-then-capture. */ +typedef struct { + uint32_t vaddr; /* virtual entry enqueued */ + uint32_t phys; /* physical entry (dedup key) */ + uint32_t crc; /* content crc of the enqueue-time snapshot */ + uint32_t code_lo; /* compiled range start (phys) */ + uint32_t code_len; /* compiled range length in bytes (0 if fail) */ + uint32_t compile_ms; /* wall time for this compile (off-thread) */ + uint8_t ok; /* 1 = published a shard, 0 = emitter declined */ +} OverlayCompileEvent; + +/* Copy up to `cap` most-recent events into `out` in chronological order (oldest + * first), returning the count copied. Thread-safe. */ +int overlay_compile_worker_recent(OverlayCompileEvent *out, int cap); + +#ifdef __cplusplus +} +#endif + +#endif /* PSX_OVERLAY_COMPILE_WORKER_H */ diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index e9298c13..9f2e273c 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -153,6 +153,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/card_data_writes.c ${PSXRECOMP_ROOT}/runtime/src/overlay_capture.c ${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c + ${PSXRECOMP_ROOT}/runtime/src/overlay_compile_worker.c ${PSXRECOMP_ROOT}/runtime/src/overlay_sljit.c ${PSXRECOMP_ROOT}/runtime/src/autocompile.c ${PSXRECOMP_ROOT}/runtime/src/code_provider.c diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 9ddc71f1..424a7b0a 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -21,6 +21,7 @@ #include "overlay_capture.h" #include "code_provider.h" #include "overlay_sljit.h" +#include "overlay_compile_worker.h" #include "cpu_state.h" #include "dma.h" #include "gpu.h" @@ -8539,6 +8540,56 @@ static void handle_sljit_status(int id, const char *json) overlay_loader_sljit_persist_dbg()); } +/* sljit_async: off-thread compile-worker telemetry. Reports the always-on + * counters (enqueued/compiled/failed/queued_now), last/worst off-thread compile + * time, the resolved on-disk shard dir, and the recent-compile ring. This is the + * ring-buffer proof that the worker actually does work: a rising `compiled` with + * a populated `recent[]` (each with a phys/crc/code_len/ms) while the dispatch + * thread's ce_profile stays flat == JIT moved off the hot path. */ +static void handle_sljit_async(int id, const char *json) +{ + extern void overlay_loader_sljit_cache_dir(char *buf, int n); + extern int overlay_loader_sljit_async_on(void); + extern int overlay_loader_sljit_tier_enabled(void); + (void)json; + uint64_t enq = 0, done = 0, failed = 0; + uint32_t queued = 0, last_ms = 0, max_ms = 0; + overlay_compile_worker_stats(&enq, &done, &failed, &queued); + overlay_compile_worker_timing(&last_ms, &max_ms); + char dir[768] = {0}; + overlay_loader_sljit_cache_dir(dir, (int)sizeof(dir)); + /* escape backslashes in the Windows path for valid JSON */ + char dir_esc[1536]; int di = 0; + for (int i = 0; dir[i] && di < (int)sizeof(dir_esc) - 2; i++) { + if (dir[i] == '\\') dir_esc[di++] = '\\'; + dir_esc[di++] = dir[i]; + } + dir_esc[di] = '\0'; + + OverlayCompileEvent evs[64]; + int ne = overlay_compile_worker_recent(evs, 64); + + char out[16384]; + int n = snprintf(out, sizeof(out), + "{\"id\":%d,\"ok\":true,\"tier_enabled\":%d,\"worker_running\":%d,\"async_on\":%d," + "\"enqueued\":%llu,\"compiled\":%llu,\"failed\":%llu,\"queued_now\":%u," + "\"last_compile_ms\":%u,\"max_compile_ms\":%u,\"cache_dir\":\"%s\"," + "\"recent\":[", + id, overlay_loader_sljit_tier_enabled(), + overlay_compile_worker_running(), overlay_loader_sljit_async_on(), + (unsigned long long)enq, (unsigned long long)done, + (unsigned long long)failed, queued, last_ms, max_ms, dir_esc); + for (int i = 0; i < ne && n < (int)sizeof(out) - 160; i++) { + n += snprintf(out + n, sizeof(out) - n, + "%s{\"vaddr\":\"0x%08X\",\"phys\":\"0x%08X\",\"crc\":\"0x%08X\"," + "\"code_lo\":\"0x%08X\",\"code_len\":%u,\"ms\":%u,\"ok\":%d}", + i ? "," : "", evs[i].vaddr, evs[i].phys, evs[i].crc, + evs[i].code_lo, evs[i].code_len, evs[i].compile_ms, evs[i].ok); + } + n += snprintf(out + n, sizeof(out) - n, "]}\n"); + send_fmt("%s", out); +} + /* sljit_try : force a one-shot JIT of the leaf function at a phys * address from live RAM (bypasses the diff gate — a probe) and report whether * the emitter accepted it. Lets the leaf emitter be exercised against a known @@ -9578,6 +9629,7 @@ static const CmdEntry s_commands[] = { { "turbo_loads", handle_turbo_loads }, { "autocompile_status", handle_autocompile_status }, { "sljit_status", handle_sljit_status }, + { "sljit_async", handle_sljit_async }, { "sljit_try", handle_sljit_try }, { "autocompile_run", handle_autocompile_run }, { "overlay_rescan", handle_overlay_rescan }, diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c96eff51..3c59ba4a 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -580,7 +580,9 @@ static std::filesystem::path default_memcard_dir(const char* argv0) { static void close_controller(void); +extern "C" void overlay_compile_worker_stop(void); static void shutdown_runtime(void) { + overlay_compile_worker_stop(); /* signal + join the off-thread sljit worker */ memcard_flush_all(); overlay_capture_write_json(); if (sdl_audio_device) { diff --git a/runtime/src/overlay_compile_worker.c b/runtime/src/overlay_compile_worker.c new file mode 100644 index 00000000..57cc723e --- /dev/null +++ b/runtime/src/overlay_compile_worker.c @@ -0,0 +1,204 @@ +#include "overlay_compile_worker.h" +#include "code_provider.h" +#include "overlay_sljit.h" +#include "crc32.h" +#include +#include +#include + +/* Dispatch-side publish (overlay_loader.c): atomically write the serialized + * shard into the on-disk cache + flag the loader to rescan-on-miss. It touches + * only the filesystem and read-only-after-init cache-path globals, so it is safe + * to call from this worker thread (it never touches the candidate table). */ +extern void overlay_loader_async_publish(uint32_t entry_phys, uint32_t lo, + uint32_t len, uint32_t crc, + const void *blob, unsigned long blob_size); + +#define WQ_CAP 256 /* bounded work queue */ +#define STATE_CAP 4096 /* per-(phys,crc) dedup map */ +#define SNAP_MAX (8u * 1024u) /* max window we JIT (SLJIT_MAX_FRAG_INSNS*4) */ + +typedef struct { + uint32_t vaddr, phys, crc, snap_len; + uint8_t *snap; /* worker-owned copy of the region bytes */ +} WorkItem; + +enum { ST_QUEUED = 1, ST_COMPILING = 2, ST_DONE = 3, ST_FAILED = 4 }; +typedef struct { uint32_t phys, crc; uint8_t state; } StateEnt; + +static SDL_Thread *s_thread; +static SDL_mutex *s_mtx; +static SDL_cond *s_cv; +static int s_running; + +static WorkItem s_q[WQ_CAP]; +static int s_q_head, s_q_tail, s_q_n; + +static StateEnt s_state[STATE_CAP]; +static int s_state_n; + +static uint64_t s_enq, s_done, s_failed; + +/* Always-on recent-compile ring (CLAUDE.md ring-buffer doctrine): every compile, + * success or decline, lands here so a probe can QUERY what the worker did in the + * window of interest rather than time-and-capture. Guarded by s_mtx. */ +#define EVT_CAP 64 +static OverlayCompileEvent s_evt[EVT_CAP]; +static uint64_t s_evt_seq; /* total events recorded ever */ +static uint32_t s_last_ms, s_max_ms; + +/* dedup map — all accesses hold s_mtx. */ +static StateEnt *state_find(uint32_t phys, uint32_t crc) { + for (int i = 0; i < s_state_n; i++) + if (s_state[i].phys == phys && s_state[i].crc == crc) return &s_state[i]; + return NULL; +} +static StateEnt *state_add(uint32_t phys, uint32_t crc, uint8_t st) { + if (s_state_n >= STATE_CAP) { + /* Full: evict the first finished entry (the on-disk cache is the source + * of truth, so a later re-miss can re-enqueue if ever needed). COMPILING/ + * QUEUED entries are never evicted (in flight). */ + for (int i = 0; i < s_state_n; i++) + if (s_state[i].state == ST_DONE || s_state[i].state == ST_FAILED) { + s_state[i] = s_state[--s_state_n]; + break; + } + if (s_state_n >= STATE_CAP) return NULL; + } + StateEnt *e = &s_state[s_state_n++]; + e->phys = phys; e->crc = crc; e->state = st; + return e; +} + +static int worker_main(void *unused) { + (void)unused; + const CodeProvider *cp = code_provider_sljit(); + SDL_LockMutex(s_mtx); + for (;;) { + while (s_running && s_q_n == 0) SDL_CondWait(s_cv, s_mtx); + if (!s_running && s_q_n == 0) break; + WorkItem it = s_q[s_q_head]; + s_q_head = (s_q_head + 1) % WQ_CAP; + s_q_n--; + { StateEnt *e = state_find(it.phys, it.crc); if (e) e->state = ST_COMPILING; } + SDL_UnlockMutex(s_mtx); + + /* Compile off-thread FROM THE SNAPSHOT (base = phys, so the fragment's + * byte offset into the buffer is entry_phys - phys). Never live RAM. */ + int ok = 0; + uint32_t ev_lo = 0, ev_len = 0; + Uint32 t0 = SDL_GetTicks(); + if (cp && cp->compile_fragment && it.snap) { + CompiledFragment frag; + memset(&frag, 0, sizeof frag); + cp->compile_fragment(it.vaddr, it.snap, it.snap_len, it.phys, &frag); + if (frag.fn && frag.serialized && frag.serialized_size && frag.code_len && + frag.code_lo >= it.phys && + (frag.code_lo - it.phys) + frag.code_len <= it.snap_len) { + /* crc over the EXACT compiled range within the snapshot — this is + * what dispatch re-hashes live RAM against when it loads the shard + * (matches persist_sljit_shard's crc-over-code-range). */ + uint32_t off = frag.code_lo - it.phys; + uint32_t crc = crc32_update(0xFFFFFFFFu, it.snap + off, frag.code_len) + ^ 0xFFFFFFFFu; + overlay_loader_async_publish(it.phys, frag.code_lo, frag.code_len, + crc, frag.serialized, frag.serialized_size); + ev_lo = frag.code_lo; ev_len = frag.code_len; + ok = 1; + } + if (frag.serialized) overlay_sljit_free_serialized(frag.serialized); + } + uint32_t dur = (uint32_t)(SDL_GetTicks() - t0); + free(it.snap); + + SDL_LockMutex(s_mtx); + { StateEnt *e = state_find(it.phys, it.crc); if (e) e->state = ok ? ST_DONE : ST_FAILED; } + if (ok) s_done++; else s_failed++; + /* record into the recent-compile ring */ + OverlayCompileEvent *ev = &s_evt[s_evt_seq % EVT_CAP]; + ev->vaddr = it.vaddr; ev->phys = it.phys; ev->crc = it.crc; + ev->code_lo = ev_lo; ev->code_len = ev_len; + ev->compile_ms = dur; ev->ok = (uint8_t)ok; + s_evt_seq++; + s_last_ms = dur; if (dur > s_max_ms) s_max_ms = dur; + } + SDL_UnlockMutex(s_mtx); + return 0; +} + +void overlay_compile_worker_start(void) { + if (s_thread) return; + if (!s_mtx) s_mtx = SDL_CreateMutex(); + if (!s_cv) s_cv = SDL_CreateCond(); + if (!s_mtx || !s_cv) return; + s_running = 1; + s_thread = SDL_CreateThread(worker_main, "psx-sljit-compile", NULL); + if (!s_thread) s_running = 0; +} + +void overlay_compile_worker_stop(void) { + if (!s_thread) return; + SDL_LockMutex(s_mtx); + s_running = 0; + SDL_CondSignal(s_cv); + SDL_UnlockMutex(s_mtx); + SDL_WaitThread(s_thread, NULL); + s_thread = NULL; + SDL_LockMutex(s_mtx); + while (s_q_n) { free(s_q[s_q_head].snap); s_q_head = (s_q_head + 1) % WQ_CAP; s_q_n--; } + SDL_UnlockMutex(s_mtx); +} + +void overlay_compile_worker_enqueue(uint32_t vaddr, uint32_t phys, + const uint8_t *src, uint32_t snap_len, + uint32_t crc) { + if (!s_thread || !src || snap_len == 0) return; + if (snap_len > SNAP_MAX) snap_len = SNAP_MAX; + SDL_LockMutex(s_mtx); + if (state_find(phys, crc) || s_q_n >= WQ_CAP) { SDL_UnlockMutex(s_mtx); return; } + uint8_t *copy = (uint8_t *)malloc(snap_len); + if (!copy) { SDL_UnlockMutex(s_mtx); return; } + memcpy(copy, src, snap_len); + if (!state_add(phys, crc, ST_QUEUED)) { free(copy); SDL_UnlockMutex(s_mtx); return; } + WorkItem *it = &s_q[s_q_tail]; + it->vaddr = vaddr; it->phys = phys; it->crc = crc; it->snap_len = snap_len; it->snap = copy; + s_q_tail = (s_q_tail + 1) % WQ_CAP; + s_q_n++; + s_enq++; + SDL_CondSignal(s_cv); + SDL_UnlockMutex(s_mtx); +} + +void overlay_compile_worker_stats(uint64_t *enqueued, uint64_t *compiled, + uint64_t *failed, uint32_t *queued_now) { + if (s_mtx) SDL_LockMutex(s_mtx); + if (enqueued) *enqueued = s_enq; + if (compiled) *compiled = s_done; + if (failed) *failed = s_failed; + if (queued_now) *queued_now = (uint32_t)s_q_n; + if (s_mtx) SDL_UnlockMutex(s_mtx); +} + +int overlay_compile_worker_running(void) { return s_thread != NULL; } + +void overlay_compile_worker_timing(uint32_t *last_ms, uint32_t *max_ms) { + if (s_mtx) SDL_LockMutex(s_mtx); + if (last_ms) *last_ms = s_last_ms; + if (max_ms) *max_ms = s_max_ms; + if (s_mtx) SDL_UnlockMutex(s_mtx); +} + +int overlay_compile_worker_recent(OverlayCompileEvent *out, int cap) { + if (!out || cap <= 0) return 0; + if (s_mtx) SDL_LockMutex(s_mtx); + uint64_t total = s_evt_seq; + int avail = (total < (uint64_t)EVT_CAP) ? (int)total : EVT_CAP; + if (avail > cap) avail = cap; + /* chronological (oldest first): the oldest of the kept window is at + * (seq - avail). */ + uint64_t start = total - (uint64_t)avail; + for (int i = 0; i < avail; i++) + out[i] = s_evt[(start + (uint64_t)i) % EVT_CAP]; + if (s_mtx) SDL_UnlockMutex(s_mtx); + return avail; +} diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 0de94cfc..23ca0cc3 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -389,6 +389,27 @@ static void sljit_mark_tried(uint32_t phys) { static void persist_sljit_shard(uint32_t entry_phys, uint32_t lo, uint32_t len, const void *blob, unsigned long blob_size); +#include "overlay_compile_worker.h" +/* Off-main-thread sljit compile (docs/ASYNC_OVERLAY_COMPILE.md). The background + * worker JITs from a snapshot and PUBLISHES the shard to the on-disk cache + + * sets s_async_cache_dirty; the DISPATCH thread rescans-and-registers on its + * next miss (candidate table stays single-threaded). PSX_SLJIT_SYNC=1 forces + * the legacy synchronous JIT-on-miss path. */ +static volatile LONG s_async_cache_dirty = 0; +static int g_sljit_async = 1; + +/* MASTER SAFETY GATE (2026-06-25): the sljit EMITTER mistranslates some overlay + * instruction(s). Proven on MMX6: sljit-only save-load softlocks/loopbacks, PURE + * INTERP works (discriminator), and it reproduces in the SYNCHRONOUS path — so it + * is the emitter, not the async worker. Until the emitter bug is found + fixed the + * whole sljit tier is DISABLED by default: no shard READ (scan_sljit_cache_dir), + * no shard GENERATE (worker / sync try / live gap-fill), every overlay miss falls + * to the interpreter. gcc (DLL cache + autocompile) and overlay capture are + * untouched, so priority is gcc > interp and dispatch misses are still recorded + * for future gcc coverage. PSX_SLJIT_ENABLE=1 re-enables it for emitter-fix + * testing. See memory mmx6_sljit_shard_malformed. */ +static int g_sljit_tier_enabled = 0; + /* Attempt an in-process JIT of the leaf function at `phys` from live RAM, and * register the shard as a candidate on success. Gated by the caller to the * validation configuration only (backend==sljit + diff_mode); see the dispatch @@ -570,6 +591,14 @@ static void sljit_cache_dir(char *buf, size_t n) { (unsigned)PSX_OVERLAY_CODEGEN_HASH); } +/* Public: the resolved on-disk sljit shard dir, so the sljit_async debug command + * can report exactly where the worker writes (no guessing about cache location). */ +void overlay_loader_sljit_cache_dir(char *buf, int n) { + if (buf && n > 0) sljit_cache_dir(buf, (size_t)n); +} +int overlay_loader_sljit_async_on(void) { return g_sljit_async; } +int overlay_loader_sljit_tier_enabled(void) { return g_sljit_tier_enabled; } + #ifdef _WIN32 static void sljit_mkdir_p(const char *path) { char tmp[768]; @@ -617,6 +646,60 @@ static void persist_sljit_shard(uint32_t entry_phys, uint32_t lo, uint32_t len, #endif } +/* Worker-thread publish (overlay_compile_worker.c): write a freshly-JIT'd shard + * into the on-disk cache ATOMICALLY (temp + rename, so dispatch's scan never + * reads a partial file) with an EXPLICIT crc (computed by the worker over the + * snapshot's compiled range, NOT live RAM), then flag dispatch to rescan. Only + * the filesystem + read-only-after-init cache-path globals are touched — never + * the candidate table — so it is safe off the dispatch thread. */ +void overlay_loader_async_publish(uint32_t entry_phys, uint32_t lo, uint32_t len, + uint32_t crc, const void *blob, + unsigned long blob_size) { +#ifdef _WIN32 + if (!s_sljit_persist || !blob || blob_size == 0 || len == 0) return; + char dir[768]; sljit_cache_dir(dir, sizeof(dir)); + sljit_mkdir_p(dir); + char path[860], tmp[920]; + snprintf(path, sizeof(path), "%s/%08X_%08X.sljit", dir, entry_phys & 0x1FFFFFFFu, crc); + snprintf(tmp, sizeof(tmp), "%s.tmp%lu", path, (unsigned long)GetCurrentThreadId()); + FILE *f = fopen(tmp, "wb"); + if (!f) return; + SljitBlobHeader hh; + hh.magic = SLJIT_BLOB_MAGIC; hh.format_ver = SLJIT_BLOB_FORMAT_VER; + hh.helper_order_ver = SLJIT_HELPER_ORDER_VER; + hh.entry_phys = entry_phys & 0x1FFFFFFFu; hh.code_lo = lo & 0x1FFFFFFFu; + hh.code_len = len; hh.crc_code = crc; hh.blob_size = (uint32_t)blob_size; + int wok = (fwrite(&hh, sizeof hh, 1, f) == 1) && + (fwrite(blob, 1, blob_size, f) == blob_size); + fclose(f); + if (!wok) { remove(tmp); return; } + if (!MoveFileExA(tmp, path, MOVEFILE_REPLACE_EXISTING)) { remove(tmp); return; } + s_sljit_persist_writes++; /* telemetry-only; benign cross-thread incr */ + InterlockedExchange(&s_async_cache_dirty, 1); +#else + (void)entry_phys; (void)lo; (void)len; (void)crc; (void)blob; (void)blob_size; +#endif +} + +/* Idempotency for the on-miss rescan: a dispatch-thread-only set of .sljit + * filenames already deserialized+registered, so re-running scan_sljit_cache_dir + * after a worker publish never double-registers. */ +#define MAX_LOADED_BLOBS 8192 +static char s_loaded_blobs[MAX_LOADED_BLOBS][24]; /* "XXXXXXXX_XXXXXXXX.sljit" */ +static int s_loaded_blob_n; +static int blob_already_loaded(const char *name) { + for (int i = 0; i < s_loaded_blob_n; i++) + if (!strcmp(s_loaded_blobs[i], name)) return 1; + return 0; +} +static void blob_mark_loaded(const char *name) { + if (s_loaded_blob_n < MAX_LOADED_BLOBS) { + strncpy(s_loaded_blobs[s_loaded_blob_n], name, 23); + s_loaded_blobs[s_loaded_blob_n][23] = '\0'; + s_loaded_blob_n++; + } +} + /* Reload persisted shards at init: deserialize + regenerate + register. The crc * comes from the blob header (overlay RAM may not be loaded yet; dispatch * re-hashes live RAM against it before ever running the shard). */ @@ -630,6 +713,7 @@ static void scan_sljit_cache_dir(void) { if (fh == INVALID_HANDLE_VALUE) return; do { s_reload_seen++; + if (blob_already_loaded(fd.cFileName)) continue; /* idempotent rescan */ char full[900]; snprintf(full, sizeof(full), "%s/%s", dir, fd.cFileName); FILE *f = fopen(full, "rb"); @@ -650,6 +734,7 @@ static void scan_sljit_cache_dir(void) { register_sljit_candidate(hd.entry_phys, (OverlayFn)fn, hd.code_lo, hd.code_len, hd.crc_code); s_sljit_reloaded++; + blob_mark_loaded(fd.cFileName); } while (FindNextFileA(fh, &fd)); FindClose(fh); if (s_sljit_reloaded) loader_log("sljit cache: reloaded %u shard(s)", s_sljit_reloaded); @@ -861,8 +946,18 @@ void overlay_loader_init(const char *cache_dir, const char *game_id) { * runs when [runtime] overlay_cache is on), so persist JIT'd shards and reload * any from a prior session now. Reloaded shards register as dll<0 candidates * and re-earn trust via the normal per-dispatch crc + diff gate. */ - s_sljit_persist = 1; - scan_sljit_cache_dir(); + { const char *e = getenv("PSX_SLJIT_ENABLE"); if (e && *e && *e != '0') g_sljit_tier_enabled = 1; } + if (g_sljit_tier_enabled) { + s_sljit_persist = 1; + scan_sljit_cache_dir(); + { const char *e = getenv("PSX_SLJIT_SYNC"); if (e && *e && *e != '0') g_sljit_async = 0; } + if (g_sljit_async) overlay_compile_worker_start(); /* off-thread JIT */ + } else { + /* Tier disabled (default): no read, no generate. persist off so the worker + * publish + persist_sljit_shard are inert even if reached. gcc > interp. */ + s_sljit_persist = 0; + loader_log("sljit tier DISABLED (emitter bug; gcc>interp). PSX_SLJIT_ENABLE=1 to re-enable"); + } /* Live-mode policy is applied later via overlay_loader_apply_live_policy(), * AFTER code_provider_init resolves the backend (the default depends on it). */ s_active = 1; @@ -893,6 +988,9 @@ void overlay_loader_apply_live_policy(void) { if (overlay_backend_active() == OVERLAY_BACKEND_GCC) live = 0; const char *e = getenv("PSX_OVERLAY_SLJIT_LIVE"); if (e && *e) live = (*e != '0') ? 1 : 0; + /* MASTER GATE: the sljit tier is disabled by default (emitter bug) — never go + * live, regardless of backend/env, until PSX_SLJIT_ENABLE=1. */ + if (!g_sljit_tier_enabled) live = 0; s_sljit_live = live; loader_log("sljit live policy: active_backend=%s sljit_avail=%d env=%s -> live=%d", overlay_backend_name(overlay_backend_active()), @@ -1028,22 +1126,47 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) { uint32_t phys = addr & 0x1FFFFFFFu; int head = idx_head(phys); if (head < 0 && s_active && overlay_cache_window_contains(phys)) { - try_load_region(phys); - head = idx_head(phys); + /* Pick up shards the background compile worker just published (idempotent + * rescan; runs only when a publish has flipped the dirty flag). */ + if (g_sljit_tier_enabled && g_sljit_async && InterlockedExchange(&s_async_cache_dirty, 0)) { + scan_sljit_cache_dir(); + head = idx_head(phys); + } + if (head < 0) { + try_load_region(phys); + head = idx_head(phys); + } /* sljit JIT-on-miss (SLJIT.md §7 step 5). Fires when the active backend is * sljit and EITHER the same-state differential is armed (dev validation * path — shards created then diffed vs interp) OR live mode is on (the * production model — shards created and run live, no per-shard diff). * Never inside a shadow run. */ - if (head < 0 && (s_diff_mode || s_sljit_live) && !s_in_shadow && + if (head < 0 && g_sljit_tier_enabled && (s_diff_mode || s_sljit_live) && !s_in_shadow && code_provider_sljit() != NULL) { /* head < 0 == no gcc DLL (or any candidate) registered for this region * after the cache-load attempt above => gcc-absent => sljit fills the - * gap (priority gcc > sljit > interp). Fires whenever sljit is - * AVAILABLE, not only when it is the exclusive backend, so sljit and - * gcc run complementarily. */ - try_sljit_region(addr); /* virtual entry (carries KSEG bits) */ - head = idx_head(phys); + * gap (priority gcc > sljit > interp). */ + if (g_sljit_async) { + /* OFF-THREAD: snapshot the region bytes + enqueue for the worker; + * interpret THIS pass. The shard registers on a later miss via the + * rescan above — no synchronous JIT spike on the dispatch thread. */ + if (!sljit_already_tried(phys)) { + extern int dirty_ram_is_dirty(uint32_t phys); + uint8_t *ram = memory_get_ram_ptr(); + if (ram && dirty_ram_is_dirty(phys)) { /* only JIT runtime code */ + sljit_mark_tried(phys); + uint32_t snap = 8u * 1024u; /* SLJIT_MAX_FRAG_INSNS*4 */ + if (phys + snap > 2u * 1024u * 1024u) + snap = 2u * 1024u * 1024u - phys; + uint32_t crc = crc32_update(0xFFFFFFFFu, ram + phys, snap) + ^ 0xFFFFFFFFu; + overlay_compile_worker_enqueue(addr, phys, ram + phys, snap, crc); + } + } + } else { + try_sljit_region(addr); /* legacy synchronous path (PSX_SLJIT_SYNC=1) */ + head = idx_head(phys); + } } }