feat(widescreen,sljit): shared cull helper across backends, cache versioning, sljit default-on

Three coupled landings on top of the sljit merge:

1. Shared widescreen render-cull helper. auto_screen_x now emits a call to one
   runtime helper psx_ws_cull_sltiu(sx,imm) (gpu.c) instead of an inline expr, and
   the sljit JIT (overlay_sljit.c) + dirty-RAM interpreter (dirty_ram_interp.c)
   route flagged render-funnel sltiu 0x140/0x141 through the SAME helper. So an
   overlay's GTE render funnel widens identically whether it runs as a gcc DLL, a
   sljit shard, or interpreted. Helper is byte-identical to vanilla at 4:3
   (margin 0), so widescreen-off stays bit-exact. psx_ws_func_has_screen_cull
   gates the widen to real render funnels (sltiu 0x140 + sltiu 0xE0 signature).

2. Versioned overlay cache path. gcc cache moves to <game_id>/gcc/<arch-abi>/cg<N>/
   where N = PSX_OVERLAY_CODEGEN_VER (overlay_api.h), parsed identically by the
   runtime (overlay_loader.c) and compile_overlays.py so they can't drift. A build
   whose emitter output changed (e.g. this auto_screen_x change) reads+writes a
   FRESH dir and never reuses a stale DLL; old versions coexist (no auto-delete).
   Pre-1.0: legacy flat/unversioned fallbacks dropped.

3. sljit default-on (validated). gcc and sljit are complementary, not exclusive:
   gcc fills the cache via prebuilt DLLs + async batch (compile_fragment NULL),
   sljit JITs gcc-absent regions synchronously on-miss. New code_provider_sljit()
   exposes sljit for the JIT-on-miss gap-fill independent of the "active" backend;
   the dispatch gate fires whenever sljit is available (head<0 == gcc-absent), and
   apply_live_policy defaults live ON when sljit is available. Priority gcc > sljit
   > interp; shards still route through the same-state diff gate (device-touch +
   diverging shards pinned to interp) so default-on does NOT reintroduce the
   save-load wedge; a declined/failing shard falls to interp (never a crash).
   Verified live: live=1, 62 shards compiled, 2 declines->interp, native:interp
   ratio ~31%->~87%, gcc still primary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-15 19:36:43 -07:00
parent 52ddde7808
commit 0c1f005c35
10 changed files with 198 additions and 43 deletions
+7 -10
View File
@@ -730,16 +730,12 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
uint16_t uimm = get_imm16_u(instr);
if (uimm == 0x140 || uimm == 0x141) {
uint32_t rs = get_rs(instr), rt = get_rt(instr);
// SX is a 16-bit GTE screen coord loaded via lhu, so an off-LEFT
// vertex is a large unsigned value (e.g. -30 -> 0xFFE2). A plain
// `SX <u (imm+2m)` widen therefore reveals only the RIGHT margin —
// the left stays culled. Sign-extend SX and shift by +margin so the
// verdict is the wide window (-margin <= SX < imm+margin): both the
// left AND right 16:9 margins pass, and geometry past the wide
// surface on either side is still rejected. At margin=0 this reduces
// exactly to the vanilla `SX <u imm` verdict (byte-identical).
return fmt::format("{} = ((uint32_t)((int32_t)(int16_t){} + psx_ws_x_margin()) "
"< (uint32_t)((int32_t){} + 2*psx_ws_x_margin())) ? 1 : 0;"
// Route through the shared runtime helper psx_ws_cull_sltiu so the
// gcc emit, the sljit JIT, and the interpreter all widen identically.
// It sign-extends SX and shifts by +margin (wide window
// -margin <= SX < imm+margin, both edges); at margin=0 it reduces
// bit-for-bit to the vanilla `SX <u imm` verdict (4:3 byte-identical).
return fmt::format("{} = psx_ws_cull_sltiu({}, {});"
" /* ws auto screen-x cull (both edges) */{}",
reg_name(rt), reg_name(rs), (int)uimm, comment);
}
@@ -1760,6 +1756,7 @@ std::string CodeGenerator::generate_file(
ss << "extern void debug_server_log_call_entry(uint32_t func_addr);\n";
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n";
ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n";
ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (gpu.c) */\n";
ss << "extern void gte_ws_set_suppress(int on); /* widescreen far-backdrop un-squash (gte.cpp) */\n\n";
+11 -2
View File
@@ -77,10 +77,19 @@ typedef struct CodeProvider {
* at overlay-cache init, on the emu thread, before the run loop starts. */
void code_provider_init(const char *cfg_backend, int gcc_configured);
/* The active provider. Never NULL — defaults to the gcc provider before
* code_provider_init() runs, so any early caller is safe. */
/* The active/primary provider (owns the batch path + default selection). Never
* NULL — defaults to the gcc provider before code_provider_init() runs. */
const CodeProvider *code_provider_active(void);
/* The sljit provider IF it is available, else NULL. gcc and sljit are
* COMPLEMENTARY, not exclusive: gcc fills the cache via the async batch path +
* prebuilt DLLs and never JITs synchronously (compile_fragment == NULL), while
* sljit is the synchronous JIT-on-miss that fills the gaps gcc has not (yet)
* covered. The dispatch path uses this — independent of which provider is
* "active" — so on a gcc dev box sljit still JITs gcc-absent regions
* (priority gcc > sljit > interp). NULL => sljit unavailable, gaps fall to interp. */
const CodeProvider *code_provider_sljit(void);
#ifdef __cplusplus
}
#endif
+10
View File
@@ -111,6 +111,16 @@ int gpu_ws_present_native_43(void);
* cull immediates by the recompiler ([widescreen.cull]); 0 unless stretching. */
int psx_ws_x_margin(void);
/* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x):
* the gcc emit, the sljit JIT, and the interpreter all route a flagged
* `sltiu rt, sx, 0x140/0x141` through this one helper so every overlay execution
* path widens identically. Returns the sltiu verdict (1 = keep). 0 at 4:3. */
int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm);
/* True if a run of instruction words carries the screen-extent reject signature
* (a sltiu 0x140/0x141 AND a sltiu 0xE0/0xF1). Used by sljit/interp to gate the
* widening to real render funnels. */
int psx_ws_func_has_screen_cull(const uint32_t *words, int n);
/* Backdrop screen-X correction ([widescreen.backdrop] x_sites). The parallax
* 2D backdrop layer computes screen-X without the GTE, so it misses the
* widescreen squash; the recompiler emits this on each backdrop handler's
+13
View File
@@ -48,6 +48,19 @@
#define PSX_OVERLAY_ABI_TAG \
((int)((PSX_OVERLAY_ABI_VERSION & 0xFFFF) | ((PSX_OVERLAY_FLAVOR & 0xFFFF) << 16)))
/* Codegen version — bumped whenever the recompiler emits DIFFERENT bytes for the
* same guest overlay WITHOUT an ABI change (the load-time overlay_abi() gate only
* catches ABI/flavor changes; a pure-emitter change produces a different DLL the
* gate would happily accept). The cache is namespaced by this version
* (gcc/<arch-abi>/cg<N>/), so a build with new codegen writes + reads a FRESH
* directory and never reuses a stale DLL; old versions coexist on disk (no
* auto-delete — a user who downgrades still finds their matching cache). Python
* (compile_overlays.py) parses this same constant from this header, so the two
* sides can never drift. BUMP THIS when you change code_generator.cpp emit in a
* way that alters overlay output (e.g. the auto_screen_x widescreen widening).
* 1: baseline + auto_screen_x render-funnel cull via psx_ws_cull_sltiu. */
#define PSX_OVERLAY_CODEGEN_VER 1
typedef struct {
/* Core dispatch: routes call_by_address() and out-of-overlay jal */
void (*dispatch_call)(CPUState *cpu, uint32_t addr, uint32_t ra);
+6
View File
@@ -70,3 +70,9 @@ void code_provider_init(const char *cfg_backend, int gcc_configured) {
}
const CodeProvider *code_provider_active(void) { return s_active; }
/* sljit provider for the synchronous JIT-on-miss gap-fill, regardless of which
* provider is "active". NULL when sljit isn't available so callers fall to interp. */
const CodeProvider *code_provider_sljit(void) {
return (s_sljit.available && s_sljit.available()) ? &s_sljit : NULL;
}
+33 -1
View File
@@ -147,6 +147,31 @@ static inline uint32_t fetch_word(uint32_t phys) {
| ((uint32_t)ram[phys + 3] << 24);
}
/* Widescreen render-funnel cull detection for the interpreter ([widescreen.cull]
* auto_screen_x). A `sltiu …,0x140/0x141` is widened ONLY when its enclosing
* render function also carries a `sltiu …,0xE0/0xF1` (the GTE per-vertex trivial-
* reject signature) — a lone 0x140 elsewhere stays vanilla. The interp has no
* function boundaries, so we scan a +/-512-byte window around the site (clamped
* to main RAM) via the shared psx_ws_func_has_screen_cull, cached per-PC so a
* hot render loop pays the scan once. Single-threaded interp => static buffers ok. */
static int ws_cull_site(uint32_t pc) {
enum { WIN = 128 }; /* +/- 128 words = +/- 512 bytes */
static struct { uint32_t pc; int8_t flag; } cache[256];
uint32_t slot = (pc >> 2) & 255u;
if (cache[slot].pc == pc) return cache[slot].flag;
uint32_t phys = pc & 0x1FFFFFFFu;
uint32_t lo = (phys > (uint32_t)(WIN * 4)) ? phys - (uint32_t)(WIN * 4) : 0u;
uint32_t hi = phys + (uint32_t)(WIN * 4);
if (hi > 0x200000u) hi = 0x200000u; /* 2 MB main RAM */
static uint32_t words[2 * WIN + 1];
int n = 0;
for (uint32_t a = lo; a + 4u <= hi && n < (int)(2 * WIN + 1); a += 4u)
words[n++] = fetch_word(a);
int flag = psx_ws_func_has_screen_cull(words, n);
cache[slot].pc = pc; cache[slot].flag = (int8_t)flag;
return flag;
}
static void dirty_ram_log_instruction(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t before_s0, uint32_t next_pc,
uint32_t target, int transferred) {
@@ -684,7 +709,14 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
cpu->gpr[0] = 0;
return 0;
case 0x0B: /* SLTIU */
cpu->gpr[rt] = (cpu->gpr[rs] < (uint32_t)simm) ? 1u : 0u;
/* Widescreen render-funnel cull widening (auto_screen_x): always apply
* the shared helper for a flagged render-cull site — it is byte-identical
* to the vanilla compare at 4:3 (margin 0) and widens at 16:9, so the one
* code path serves both aspects (no widescreen-specific caching). */
if ((imm == 0x140 || imm == 0x141) && ws_cull_site(pc))
cpu->gpr[rt] = (uint32_t)psx_ws_cull_sltiu(cpu->gpr[rs], imm);
else
cpu->gpr[rt] = (cpu->gpr[rs] < (uint32_t)simm) ? 1u : 0u;
cpu->gpr[0] = 0;
return 0;
case 0x0C: /* ANDI */
+31
View File
@@ -182,6 +182,37 @@ int psx_ws_x_margin(void) {
return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum;
}
/* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x),
* called identically by the gcc emit, the sljit JIT, and the interpreter so every
* overlay execution path widens the same way. sx = the GTE screen-X as loaded by
* the guest's lhu (low 16 bits significant); imm = the original bound (0x140 /
* 0x141). Returns the sltiu verdict (1 = on-screen/keep). Sign-extends sx and
* shifts by +margin so BOTH 16:9 margins pass; at 4:3 margin==0 so it reduces
* bit-for-bit to the vanilla `(uint16)sx < imm`. */
int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm) {
int m = psx_ws_x_margin();
return ((uint32_t)((int32_t)(int16_t)(uint16_t)sx + m)
< (uint32_t)((int32_t)imm + 2 * m)) ? 1 : 0;
}
/* Detect the GTE per-vertex trivial-reject signature in a run of instruction
* words: at least one `sltiu …,0x140/0x141` (width) AND one `sltiu …,0xE0/0xF1`
* (height). Lets the sljit JIT + interpreter gate the cull-widening to real
* render funnels (a lone sltiu 0x140 elsewhere must stay vanilla). Mirrors the
* recompiler's func_has_screen_extent_cull. */
int psx_ws_func_has_screen_cull(const uint32_t *words, int n) {
int has_x = 0, has_y = 0;
for (int i = 0; i < n; i++) {
uint32_t w = words[i];
if ((w & 0xFC000000u) != 0x2C000000u) continue; /* sltiu */
uint32_t imm = w & 0xFFFFu;
if (imm == 0x140 || imm == 0x141) has_x = 1;
else if (imm == 0xE0 || imm == 0xF1) has_y = 1;
if (has_x && has_y) return 1;
}
return 0;
}
/* Widescreen backdrop screen-X correction ([widescreen.backdrop] x_sites).
* The parallax 2D backdrop layer (ocean/cloud/mountain/grass — overlay actor
* handlers e.g. 0x801216BC) computes its screen-X in pure integer math
+28 -15
View File
@@ -379,8 +379,11 @@ static void sljit_mark_tried(uint32_t phys) {
* hook. Decodes from live RAM exactly as the interpreter does. */
static void try_sljit_region(uint32_t addr) {
uint32_t phys = addr & 0x1FFFFFFFu;
const CodeProvider *cp = code_provider_active();
if (!cp->compile_fragment) return;
/* Use the sljit provider specifically (not the active one): gcc is the
* primary/batch provider with compile_fragment == NULL, so the synchronous
* JIT-on-miss gap-fill must go through sljit even when gcc is "active". */
const CodeProvider *cp = code_provider_sljit();
if (!cp || !cp->compile_fragment) return;
if (sljit_already_tried(phys)) return;
extern int dirty_ram_is_dirty(uint32_t phys);
if (!dirty_ram_is_dirty(phys)) return; /* only JIT real runtime code */
@@ -490,17 +493,16 @@ static void scan_one_cache_dir(const char *dir) {
#endif
}
/* Scan the namespaced gcc cache (gcc/<arch-abi>/) AND the legacy flat layout
* (<game_id>/*.dll) for back-compat — existing caches keep loading with no
* migration. The sljit/<arch-abi>/ namespace is reserved (no on-disk blobs in
* v1: sljit re-JITs from the coverage manifest; see SLJIT.md §5.3). */
/* Scan the namespaced gcc cache: gcc/<arch-abi>/cg<codegen-ver>/. The codegen
* version segment means a build with new emitter output reads a FRESH directory
* and never picks up a stale DLL (old versions coexist on disk, no migration).
* The sljit/<arch-abi>/ namespace is reserved (no on-disk blobs: sljit re-JITs
* from the coverage manifest; see SLJIT.md §5.3). (Pre-1.0: no legacy fallback —
* older flat / unversioned caches are simply ignored and regenerated.) */
static void scan_cache_dir(void) {
char dir[768];
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s",
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI);
scan_one_cache_dir(dir);
/* Legacy flat layout (pre-namespacing). */
snprintf(dir, sizeof(dir), "%s/%s", s_cache_dir, s_game_id);
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d",
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER);
scan_one_cache_dir(dir);
}
@@ -717,13 +719,19 @@ void overlay_loader_init(const char *cache_dir, const char *game_id) {
* forces on) — a dev escape hatch. NOTE: "live" here is VALIDATED-live, not the
* old blind path; see overlay_loader_dispatch's diff gate. */
void overlay_loader_apply_live_policy(void) {
int live = (overlay_backend_active() == OVERLAY_BACKEND_SLJIT) ? 1 : 0;
/* DEFAULT-ON (validated): sljit is live whenever it is AVAILABLE, so it fills
* gcc-absent overlay regions even on a gcc dev box (gcc > sljit > interp).
* "Live" is VALIDATED-live — shards still route through the same-state diff
* gate (see overlay_loader_dispatch) and device-touching / diverging shards
* stay on the interpreter, so default-on cannot reintroduce the save-load
* wedge. The PSX_OVERLAY_SLJIT_LIVE env var still overrides (0 forces off). */
int live = (code_provider_sljit() != NULL) ? 1 : 0;
const char *e = getenv("PSX_OVERLAY_SLJIT_LIVE");
if (e && *e) live = (*e != '0') ? 1 : 0;
s_sljit_live = live;
loader_log("sljit live policy: backend=%s env=%s -> live=%d",
loader_log("sljit live policy: active_backend=%s sljit_avail=%d env=%s -> live=%d",
overlay_backend_name(overlay_backend_active()),
e && *e ? e : "(unset)", live);
code_provider_sljit() != NULL, e && *e ? e : "(unset)", live);
}
void overlay_loader_check_cache(uint32_t load_addr, uint32_t size,
@@ -844,7 +852,12 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
* 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 &&
overlay_backend_active() == OVERLAY_BACKEND_SLJIT) {
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);
}
+38 -3
View File
@@ -171,6 +171,12 @@ typedef struct { int reg; int dirty; int pinned; uint32_t lru; } GprSlot;
static GprSlot s_gpr[GPR_SLOTS_MAX];
static int s_gpr_slots; /* active slots this compile (<= GPR_SLOTS_MAX)*/
static uint32_t s_gpr_lru;
/* Set per-fragment (PASS 1) when this fragment carries the GTE render-funnel
* screen-extent reject signature (sltiu 0x140/0x141 + sltiu 0xE0/0xF1). When
* set, SLTIU 0x140/0x141 is routed through psx_ws_cull_sltiu so a sljit-JIT'd
* overlay widens identically to the gcc cache + interp (widescreen FOV). */
static int s_frag_ws_cull;
extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* gpu.c — shared widen */
static inline sljit_s32 slot_reg(int k) { return SLJIT_S(k + 2); } /* S2.. */
@@ -345,6 +351,23 @@ static void emit_helper2(struct sljit_compiler *C, void *fn, uint32_t insn) {
gpr_reset();
}
/* Emit `rt = psx_ws_cull_sltiu(gpr[rs], imm)` for a flagged auto_screen_x cull
* site, so a sljit-JIT'd overlay widens the render-funnel screen-X reject the
* same way the gcc backend does. The helper reads only its two args (+ the
* runtime widescreen margin); it does NOT touch cpu->gpr[], so the GPR cache
* survives — same icall discipline as emit_load (materialise the source into a
* scratch before the call, write the result into rt after). */
static void emit_ws_cull(struct sljit_compiler *C, uint32_t rt, uint32_t rs, uint32_t imm) {
sljit_s32 a; sljit_sw aw;
gpr_src(C, rs, &a, &aw);
sljit_emit_op1(C, SLJIT_MOV32, SLJIT_R0, 0, a, aw); /* sx -> R0 (arg0) */
sljit_emit_op1(C, SLJIT_MOV32, SLJIT_R1, 0, SLJIT_IMM, (sljit_sw)imm); /* imm -> R1 (arg1) */
sljit_emit_op1(C, SLJIT_MOV, SLJIT_R2, 0, SLJIT_IMM, (sljit_sw)(uintptr_t)psx_ws_cull_sltiu);
sljit_emit_icall(C, SLJIT_CALL, SLJIT_ARGS2(32, 32, 32), SLJIT_R2, 0); /* R0 = verdict */
sljit_emit_op1(C, SLJIT_MOV32, gpr_dst(C, rt), 0, SLJIT_R0, 0);
gpr_unpin();
}
enum { EMIT_OK = 0, EMIT_TERM = 1, EMIT_ABORT = 2 };
/* Emit ONE non-control instruction. Returns EMIT_OK, or EMIT_TERM iff `insn`
@@ -507,7 +530,10 @@ static int emit_one(struct sljit_compiler *C, uint32_t insn) {
emit_slt(C, SLJIT_SUB32 | SLJIT_SET_SIG_LESS, SLJIT_SIG_LESS, rt, rs, 0, 0, simm);
return EMIT_OK;
case 0x0B: /* SLTIU (unsigned, simm sign-extended then compared unsigned) */
emit_slt(C, SLJIT_SUB32 | SLJIT_SET_LESS, SLJIT_LESS, rt, rs, 0, 0, simm);
if (s_frag_ws_cull && (imm == 0x140 || imm == 0x141))
emit_ws_cull(C, rt, rs, imm); /* widescreen render-funnel cull widen (auto_screen_x) */
else
emit_slt(C, SLJIT_SUB32 | SLJIT_SET_LESS, SLJIT_LESS, rt, rs, 0, 0, simm);
return EMIT_OK;
case 0x0C: case 0x0D: case 0x0E: { /* ANDI / ORI / XORI (zero-extended imm) */
sljit_s32 a, d; sljit_sw aw;
@@ -742,8 +768,17 @@ void overlay_sljit_try_compile(uint32_t entry,
* see gpr_ref_mask). distinct >= the max regs in any single instruction, so
* gpr_alloc always finds a slot. */
uint32_t used_mask = 0;
for (uint32_t i = 0; i < frag_words; i++)
used_mask |= gpr_ref_mask(img_word(bytes, off0 + i * 4u));
int ws_hx = 0, ws_hy = 0; /* render-funnel screen-cull signature (auto_screen_x) */
for (uint32_t i = 0; i < frag_words; i++) {
uint32_t w = img_word(bytes, off0 + i * 4u);
used_mask |= gpr_ref_mask(w);
if ((w & 0xFC000000u) == 0x2C000000u) { /* sltiu */
uint32_t im = w & 0xFFFFu;
if (im == 0x140 || im == 0x141) ws_hx = 1;
else if (im == 0xE0 || im == 0xF1) ws_hy = 1;
}
}
s_frag_ws_cull = ws_hx && ws_hy;
int distinct = 0;
for (uint32_t m = used_mask; m; m &= m - 1u) distinct++;
s_gpr_slots = (distinct < GPR_SLOTS_MAX) ? distinct : GPR_SLOTS_MAX;
+21 -12
View File
@@ -42,6 +42,20 @@ except ImportError:
import json
import platform
import re
def codegen_ver(runtime_include: str) -> int:
"""Parse PSX_OVERLAY_CODEGEN_VER from overlay_api.h so the cache path version
is the SAME value the runtime (overlay_loader.c) uses — the two can't drift.
The cache is namespaced gcc/<arch-abi>/cg<N>/, so a build with new emitter
output reads+writes a FRESH dir and never reuses a stale DLL."""
hdr = os.path.join(runtime_include, 'overlay_api.h')
with open(hdr) as f:
m = re.search(r'#define\s+PSX_OVERLAY_CODEGEN_VER\s+(\d+)', f.read())
if not m:
raise SystemExit(f'PSX_OVERLAY_CODEGEN_VER not found in {hdr}')
return int(m.group(1))
def cache_arch_abi() -> str:
@@ -1126,13 +1140,14 @@ def main():
return
os.makedirs(args.out_dir, exist_ok=True)
else:
# Namespaced gcc cache (SLJIT.md §4 — no comingling). Legacy flat layout
# (<out-dir>/<game_id>/) is still READ by the loader and for prior-ranges
# coverage merge below, so existing caches keep working with no migration.
legacy_cache_dir = os.path.join(args.out_dir, game_id)
cache_dir = os.path.join(legacy_cache_dir, 'gcc', cache_arch_abi())
# Namespaced + versioned gcc cache: <game_id>/gcc/<arch-abi>/cg<N>/
# (SLJIT.md §4 — no comingling; cg<N> = codegen version so a new emitter
# build never reuses a stale DLL, old versions coexist). MUST match
# overlay_loader.c scan_cache_dir(). Pre-1.0: no legacy fallback.
cg = codegen_ver(args.runtime_include)
cache_dir = os.path.join(args.out_dir, game_id, 'gcc', cache_arch_abi(), f'cg{cg}')
os.makedirs(cache_dir, exist_ok=True)
print(f'Cache dir: {cache_dir}')
print(f'Cache dir: {cache_dir} (codegen ver {cg})')
with open(args.captures) as f:
captures = json.load(f)
@@ -1160,12 +1175,6 @@ def main():
if not args.static:
ranges_name = f'{phys_addr:08X}_{crc32:08X}.ranges'
prior_ranges = os.path.join(cache_dir, ranges_name)
# Fall back to the legacy flat layout so coverage from pre-namespacing
# builds still merges forward (don't regress a fresh build).
if not os.path.exists(prior_ranges):
legacy_prior = os.path.join(legacy_cache_dir, ranges_name)
if os.path.exists(legacy_prior):
prior_ranges = legacy_prior
if os.path.exists(prior_ranges):
prior_entries = []
with open(prior_ranges) as pf: