Files
psxrecomp/runtime/include/overlay_sljit.h
T
Matthew Stanley 9ed788f125 sljit persist-cache Stage 2: serialize / reload / re-validate to disk
Persist validated sljit shards across launches (SLJIT_PERSIST_CACHE.md). On JIT,
serialize the (position-independent, Stage 1) LIR and write it to
<cache>/<game_id>/sljit/<arch-abi>/cg<N>/<entry8>_<crc8>.sljit with a small header
(magic, format/helper-order versions, entry/lo/len/crc/size). At init, scan that
dir, deserialize + sljit_generate_code for THIS process, and register each as a
dll<0 candidate carrying the blob's crc; dispatch re-hashes live RAM against it so
a reloaded shard re-earns trust exactly like a fresh JIT. Priority gcc > sljit >
interp is unchanged (a gcc DLL still supersedes a reloaded shard via the obsolete
path). Persist is write-on-generate (measured negligible; crash-safe).

Threads the serialized blob through OverlaySljitResult -> CompiledFragment;
register_sljit_candidate gains a crc_override (reloaded shards use the header crc
since the overlay RAM may not be loaded at init). sljit_status exposes
reloaded / persist / reload-diagnostic counters.

Two gotchas found + fixed in testing (vs the original design notes):
- serialize MUST precede generate_code (generate sets compiler->error =
  SLJIT_ERR_COMPILED, after which serialize returns NULL);
- serialize with options=0, not SLJIT_SERIALIZE_IGNORE_DEBUG, so an
  argument-checks build can deserialize its own blobs.

Validated: two-launch test reloaded 12 shards (deserfail=0), ran them at a fresh
process base with no crash, gameplay renders correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:52:18 -07:00

143 lines
7.4 KiB
C

#ifndef OVERLAY_SLJIT_H
#define OVERLAY_SLJIT_H
/* overlay_sljit — Tier-2 self-contained in-process JIT backend (sljit).
*
* Architecture (locked, see SLJIT.md):
* Tier 1 (highest) — statically recompiled native (main EXE + discovered
* overlay DLLs). Fastest. Unchanged.
* Tier 2 (this) — the "shard": a fragment recompiled ON THE FLY the first
* time execution would otherwise fall to the interpreter.
* Built in RAM (sljit, sub-ms, no toolchain), registered as
* a native candidate so the NEXT encounter is Tier 2 not
* Tier 3, then persisted so later sessions reload it instead
* of re-discovering through the slow interpreter. This is
* structurally identical to today's gcc capture->compile->
* cache->load path; sljit is just an in-process producer.
* Tier 3 (lowest) — dirty-RAM interpreter (dirty_ram_interp.c). Correctness
* floor for not-yet-discovered code.
*
* SAFETY CONTRACT (precision over recall): the emitter compiles only what it can
* prove it emits identically to the interpreter. The moment it meets any
* instruction/shape it does not yet support, it ABORTS the whole fragment and
* returns failure — the caller then runs Tier 3 (interpreter). A partial emitter
* is therefore always safe: it can never ship a mis-compiled shard, only decline
* to produce one. gcc remains the default backend; sljit slots in UNDER the
* proven path and is gated behind backend selection (see overlay_backend.h-style
* policy below) until it passes the same-state differential.
*/
#include "cpu_state.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* The native shard ABI — identical to overlay_loader's OverlayFn. */
typedef void (*OverlaySljitFn)(CPUState *);
/* Backend selection policy (config + PSX_OVERLAY_BACKEND env override). */
typedef enum {
OVERLAY_BACKEND_AUTO = 0, /* gcc if a toolchain is configured, else sljit */
OVERLAY_BACKEND_GCC = 1, /* force the spawn-gcc->DLL producer (dev) */
OVERLAY_BACKEND_SLJIT = 2 /* force the in-process sljit producer */
} OverlayBackend;
/* Resolve the active backend once at startup. autocompile_configured indicates
* whether a gcc compile command is wired (game.toml overlay_autocompile_cmd).
* Reads PSX_OVERLAY_BACKEND (auto|gcc|sljit) env; falls back to `cfg` (from
* [runtime] overlay_backend) then AUTO. Result is logged and cached. */
OverlayBackend overlay_backend_resolve(const char *cfg, int autocompile_configured);
/* The cached resolution (OVERLAY_BACKEND_AUTO until resolve() runs). */
OverlayBackend overlay_backend_active(void);
const char *overlay_backend_name(OverlayBackend b);
/* sljit availability — always 1 once the lib is linked (no external dep). */
int overlay_sljit_available(void);
/* Smoke test: JITs a trivial leaf in-process and runs it, returning 1 iff the
* produced machine code computes the expected value. Proves the sljit codegen +
* executable-allocator path works in THIS build/host before any real fragment is
* trusted. Surfaced via the debug server. */
int overlay_sljit_selftest(void);
/* Result of a fragment compile. fn == NULL means the emitter declined the whole
* fragment to the interpreter (Tier 3) — the always-safe outcome. On success,
* [code_lo, code_lo+code_len) is the PHYS byte-range the shard was compiled from
* (the caller registers a candidate over it for per-call live-byte validation,
* exactly like the gcc .ranges manifest). */
typedef struct {
OverlaySljitFn fn; /* NULL = declined */
uint32_t code_lo; /* phys start of compiled code range */
uint32_t code_len; /* byte length of compiled code range */
uint32_t insns; /* MIPS instructions compiled (diagnostics) */
/* Serialized (position-independent) LIR of this shard for the persisted
* cache (Stage 2). Allocated by sljit_serialize_compiler; the caller writes
* it to disk then frees it via overlay_sljit_free_serialized(). NULL if not
* serialized. */
void *serialized;
unsigned long serialized_size;
} OverlaySljitResult;
/* Attempt to JIT the leaf function at `entry` (guest vram) by decoding from
* `bytes` (an image whose byte[0] maps to vram `image_base_vram`) of `size`
* bytes. Fills *out. On ANY unsupported instruction/shape sets out->fn = NULL
* (caller falls through to Tier 3) — see the SAFETY CONTRACT above. First slice:
* compiles only single-block leaf functions terminating in `jr $ra`; everything
* else (internal branches, jal/jalr, jr to non-$ra, opcodes outside the slice)
* declines the whole fragment. */
void overlay_sljit_try_compile(uint32_t entry,
const uint8_t *bytes, uint32_t size,
uint32_t image_base_vram,
OverlaySljitResult *out);
/* Diagnostics counters for the debug server. */
void overlay_sljit_get_status(int *available, int *selftest_ok,
uint64_t *compiles, uint64_t *declines,
uint64_t *bytes_emitted);
/* Call helper invoked by JIT'd shards at a jal/jalr site. Mirrors the dirty-RAM
* interpreter's call path EXACTLY: dispatch the callee as a unit (compiled /
* native-overlay / interp), then apply the (ra,sp) call contract. Returns 1 if
* the shard must return immediately (a transfer/bail unwind is in progress —
* cpu->pc / g_psx_call_bail carry it up to the dispatch loop), 0 if the callee
* returned normally and the shard should continue at the next instruction.
* check_contract: 1 for jal and jalr whose link reg is $ra (the contract checks
* $ra==return_pc); 0 for jalr to a non-$ra link (the callee won't set $ra to
* return_pc, so the contract doesn't apply) — matches the interpreter. Defined
* in overlay_loader.c (has the dispatch machinery). */
int psx_sljit_call(CPUState *cpu, uint32_t target, uint32_t return_pc,
int check_contract);
/* Per-instruction helpers a shard calls for op-classes that are emitted as a
* call rather than inlined — each reproduces the dirty-RAM interpreter's exact
* behaviour for that instruction (parity by construction). Defined in
* overlay_loader.c.
* psx_sljit_cop2 — COP2/GTE: MFC2/CFC2/MTC2/CTC2/cmd (op 0x12), LWC2 (0x32),
* SWC2 (0x3A).
* psx_sljit_memx — unaligned mem: LWL/LWR/SWL/SWR (op 0x22/0x26/0x2A/0x2E). */
void psx_sljit_cop2(CPUState *cpu, uint32_t insn);
void psx_sljit_memx(CPUState *cpu, uint32_t insn);
/* Populate cpu->sljit_helpers[] with the host-helper addresses the JIT calls
* through (cpu-relative, so shard LIR is position-independent — see Stage 1 in
* SLJIT_PERSIST_CACHE.md). Call once at startup after CPUState is wired. */
void overlay_sljit_init_helpers(CPUState *cpu);
/* Reload a serialized shard blob (persisted cache, Stage 2) into a runnable
* native fn for THIS process. Returns NULL on failure. The blob's LIR is
* position-independent (Stage 1), so the regenerated code reaches host helpers
* via the cpu-relative table — correct at this process's ASLR base. */
OverlaySljitFn overlay_sljit_deserialize(const void *blob, unsigned long blob_size);
/* Free a buffer returned in OverlaySljitResult.serialized (sljit allocator). */
void overlay_sljit_free_serialized(void *p);
#ifdef __cplusplus
}
#endif
#endif /* OVERLAY_SLJIT_H */