sljit: refresh stale docs/comments + ISSUES #7

Documentation only. Prior sessions kept re-deriving state from breadcrumbs that
were no longer true:
- overlay_sljit.c / code_provider.{h,c}: corrected "declines every fragment" and
  "compile_fragment not yet called from dispatch" — the MIPS->sljit emitter is
  fully implemented and wired (then validated via the same-state diff).
- ISSUES.md: add Issue #7 (sljit live execution was unvalidated; the pure-live
  save-load wedge), root cause and fix recorded.

(SLJIT.md — the §11 forward plan and architecture clarification — lives at the
workspace root F:/Projects/psxrecomp/SLJIT.md, outside this repo, updated
separately.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-15 17:12:39 -07:00
parent 5972164b50
commit 4c33e8dade
4 changed files with 81 additions and 9 deletions
+67
View File
@@ -559,3 +559,70 @@ Tooling: `tools/crop_launcher_assets.ps1` (crop + knockout),
`tools/shot_launcher.ps1` (screenshot the GL launcher window). Mockup
source: `C:\Users\Matthew\Desktop\ef772e04-a7db-4ecd-98bb-eb75a01de0a6.png`
(1448×1086). Pure-cosmetic; does not block Phase 4/5 wiring.
---
## Issue #7 — sljit live execution is unvalidated (pure-live save-load wedge)
**Status:** open, root-caused — fix in progress (branch `feat/sljit-backend`)
**Date opened:** 2026-06-15
**Area:** overlay Tier-2 sljit backend (`runtime/src/overlay_loader.c`,
`overlay_sljit.c`, `code_provider.c`, `overlay_sljit.c` resolution)
### Symptom
With `PSX_OVERLAY_SLJIT_LIVE=1` (the prototype toolchain-less production path),
save/load **wedges the guest**: `reason=atexit, pc=0`, dispatch looping a
BIOS/kernel address (`0xB0`/`0x650`). Reproduces only in live mode — the
dev differential path (`overlay_diff_on`) is clean (0 divergences / 395 shadow
calls). Flagged in commit `81cf21b` as Known Bug #1.
### Root cause (NOT the emitter)
The MIPS→sljit emitter + block-local register allocator are **proven correct**
this session: 66/66 emitter unit, 22/22 `$sp`-balance probe, and byte-identical to
the pre-regalloc emitter across 313 overlay functions
(`runtime/tests/sljit_{emit_test,sp_probe,offline_diff}.c`). The crash is in the
**live-execution wiring**, which bypasses the validation harness:
- `run_shadow_diff` (the device-touch detector + diff gate) is gated on
`s_diff_mode`. Live mode sets `s_sljit_live`, not `s_diff_mode`, so in live mode
**the diff never runs.** Therefore:
- `device_touch` is **never computed** for a live shard → a device-touching
function runs its shard and **double-executes** its SIO/memory-card/DMA I/O
against real hardware state, corrupting the kernel byte-handler path → the
`atexit, pc=0`, kernel-`0xB0/0x650` livelock. *(Known Bug #3 — leading cause.)*
- Shards run with `diff_passes == 0`, i.e. **completely unvalidated**.
*(Known Bug #2.)*
- Separately, the verify budget counts **cumulative** (not consecutive) clean
passes — `diff_passes` never resets on a divergence — so even in dev mode an
intermittently-wrong shard could reach budget on lucky passes.
The historical "`$sp` off by 0x18 at 0x12E478" crash was a **different, already-fixed
bug** (diff over-scoping the call tree, fixed by per-function isolation in
`81cf21b`) — not this one, and not a codegen bug.
### Fix (SLJIT.md §11 Phase A) — runtime-only, no regen
Collapse blind-live into validated-live: route live-mode shards through the diff
gate (`(s_diff_mode || s_sljit_live)` at the dispatch gate), so `device_touch` is
computed and a shard runs native live ONLY after a clean verify budget; and reset
`diff_passes` to 0 on any divergence (consecutive-clean = "0 divergences"). After
this there is no blind path: device + diverging shards stay on the interpreter.
### Related gaps (same area, tracked in SLJIT.md §11 Phases B–C)
- **gcc>sljit precedence not enforced** (LIFO chain order can put an sljit shard
ahead of a gcc shard for the same region). → Phase B.
- **Non-dev machine never triggers sljit generation in normal play** (on-miss hook
gated behind a dead env-only flag; `auto` resolves to gcc on every machine because
`autocompile_configured()` tests for a config *string*, not a real toolchain). →
Phase C.
### End-to-end success criteria (per DEBUG.md end-to-end rule)
`PSX_OVERLAY_SLJIT_LIVE=1` (or `auto`→sljit on a toolchain-less box): play through
save-load, FMV, menus, and combat with **0 divergences, 0 wedges**, shards
validated-then-promoted, device functions correctly pinned to the interpreter.
"Diff is clean" or "shard validated" is NOT success until the full save-load loop
closes live.
+3 -2
View File
@@ -15,8 +15,9 @@
* rescan on the emu thread. Seconds of latency; many fragments per run.
* - SYNC / ON-MISS (compile_fragment): the sljit model. JIT a single fragment
* in-process at a dispatch miss, sub-ms, return a native fn the caller
* registers immediately. (Wired into the dispatch path in SLJIT.md step 5;
* defined here so the abstraction is honest about both modes, not gcc-only.)
* registers immediately. Wired into the dispatch path (overlay_loader.c
* try_sljit_region); the sljit shard is then VALIDATED via the same-state
* diff before it runs live (see SLJIT.md §11 / overlay_loader_dispatch).
*
* A provider sets the hooks it supports and leaves the rest NULL; callers
* null-check before invoking. The gcc provider's batch hooks are thin pass-
+4 -3
View File
@@ -28,9 +28,10 @@ static const CodeProvider s_gcc = {
/* ---- sljit provider: in-process JIT (sync on-miss only, v1) ---------------- */
/* No batch/async path: sljit produces synchronously at a dispatch miss, so
* request/busy/poll_main are inert. compile_fragment delegates to the emitter,
* which currently declines every fragment (the emitter is SLJIT.md step 4) —
* safe by construction (precision over recall). */
* request/busy/poll_main are inert. compile_fragment delegates to the MIPS->sljit
* emitter (overlay_sljit_try_compile), which compiles the supported instruction
* classes and declines the rest (fn=NULL) — safe by construction (precision over
* recall). The returned shard is validated via the same-state diff before live. */
static int sljit_request(void) { return 0; }
static int sljit_busy(void) { return 0; }
static void sljit_poll_main(void) { }
+7 -4
View File
@@ -1,9 +1,12 @@
/* overlay_sljit.c — Tier-2 self-contained in-process JIT backend (sljit).
* See overlay_sljit.h for the tier model and the precision-over-recall SAFETY
* CONTRACT. This file currently provides: backend-selection policy, sljit
* availability + a real codegen smoke test, and the try_compile entry point
* that (until the MIPS->sljit emitter lands) safely declines every fragment to
* the interpreter. The validated gcc path is untouched. */
* CONTRACT. Provides: backend-selection policy, sljit availability + a real
* codegen smoke test, and the MIPS->sljit emitter (overlay_sljit_try_compile)
* that parallels dirty_ram_interp.c — leaf+multi-block functions with branches,
* mult/div, jal/jalr calls, jr-tables, GTE/COP2, unaligned mem, and block-local
* GPR register allocation. Unsupported instructions/shapes abort the WHOLE
* fragment (return fn=NULL → caller runs the interpreter), so the emitter can
* decline but never mis-compile. The validated gcc path is untouched. */
#include "overlay_sljit.h"