backport the pc=0 root-cause cluster + autocompile/overlay fixes

Continues backporting onto this branch rather than moving to feat/rbengine,
keeping the known-good race performance while recovering fixes that landed
after the branch point.

pc=0 cluster (the mid-race 'crash' was a silent top-level GUEST_EXIT):
- 6ead85c9 honor IRQ redirects at compiled leaders + pc0 escape journal. THE
  root cause: every emitted block-leader IRQ check overwrote whatever resume
  PC the exception epilogue published, so a guest RFE to a non-EPC target
  (card-ISR handlers, SetCustomExitFromException, thread switch) lost its
  continuation and a later jr through a null pointer surfaced as PC=0.
  Its own message names this title and the static bake.
- 21a8457e the IRQ-redirect guard must CONSUME the benign same-site resume.
- (2edce1b6 and the async-RFE latch hunks landed in the previous commit.)
- cpu_state.h pc0-journal declarations, which the cherry-picks reference.

Overlay/autocompile correctness:
- d31953fe missing cull-widen ABI symbols; PGXP hooks swallowed by an #endif.
- 45f9d0f8 pin the overlay flavor through the spawn env — the same
  --flavor-defaults-to-0 trap that silently fills a cache this PGXP build
  cannot load.
- 8b323ffe coverage_vault 8 MB dev-RAM aware bounds.

Deliberately NOT taken: 249057f9 / ada331a4 (exact-sign NCLIP + GTE clamp
rescue) — broadens PGXP arming and adds a per-vertex RTPS push, and is the
leading suspect for the race-FPS regression this branch exists to avoid; and
3b698682 / 95a56e23 (image-edge tail-transfer), which 4532ad2f later gated to
CPS-only — this game emits non-CPS, where the legacy behaviour this branch
already has is the correct one.

Emitters rebuilt and game+BIOS regenerated (56 shards changed). Verified: 8 MB
mod plan applies, 60 Hz NTSC at 1.00x, no PC=0 exit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSNLFj7kvarYHEV9PCZpV
This commit is contained in:
Alex Vanderveen
2026-08-21 19:42:40 -04:00
parent 42efb87b03
commit 2c8e853ae7
12 changed files with 198 additions and 9 deletions
+18 -2
View File
@@ -140,18 +140,34 @@ std::string CodeGenerator::emit_mid_block_cycle_charge(uint32_t addr,
return ss.str();
}
/* Both check emitters honor an exception REDIRECT: when the delivery's
* epilogue publishes a resume PC different from this site's static
* continuation (guest RFE to a non-EPC target — longjmp-style handlers,
* SetCustomExitFromException, thread switch), the compiled code must
* surface it to the trampoline instead of overwriting it with the static
* transfer. This is the compiled analogue of the dirty interpreter's
* "Handler resumed elsewhere — surface to dispatch" contract; without it a
* baked/compiled leader silently drops the guest's continuation (WipEout 3
* static bake: deterministic top-level "execution completed, PC=0" ~10 s
* into boot, root-caused to redirects dropped at compiled leaders). A
* published pc equal to the static continuation falls through (same-thread
* restore publishes 0; the depth-guard rescue publishes the site PC). */
std::string CodeGenerator::emit_interrupt_check(uint32_t resume_pc,
const std::string& indent) const {
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", resume_pc);
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", resume_pc) + indent +
fmt::format("if (cpu->pc != 0u) {{ if ((cpu->pc ^ 0x{:08X}u) & 0x1FFFFFFFu) return; cpu->pc = 0u; }} /* IRQ redirect / consume same-site resume */\n",
resume_pc);
}
std::string CodeGenerator::emit_interrupt_check_expr(const std::string& resume_pc_expr,
const std::string& indent) const {
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr);
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr) + indent +
fmt::format("if (cpu->pc != 0u) {{ if ((cpu->pc ^ ({})) & 0x1FFFFFFFu) return; cpu->pc = 0u; }} /* IRQ redirect / consume same-site resume */\n",
resume_pc_expr);
}
std::string CodeGenerator::reg_name(int reg_num) {
+13 -4
View File
@@ -171,17 +171,25 @@ bool FullFunctionEmitter::emit_function(
const char* e = std::getenv("PSX_CPS");
return e == nullptr || e[0] != '0';
}();
/* Redirect-honoring checks — see CodeGenerator::emit_interrupt_check:
* an exception epilogue that publishes a resume PC different from this
* site's static continuation must surface to the trampoline, never be
* overwritten by the static transfer (the dropped-continuation class
* behind the WipEout 3 static-bake top-level PC=0 exit). */
auto emit_irq_check = [](uint32_t resume_pc, const std::string& indent = " ") {
uint32_t rt = bios_runtime_pc(resume_pc);
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n",
bios_runtime_pc(resume_pc));
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", rt) + indent +
fmt::format("if (cpu->pc != 0u) {{ if ((cpu->pc ^ 0x{:08X}u) & 0x1FFFFFFFu) return; cpu->pc = 0u; }} /* IRQ redirect / consume same-site resume */\n", rt);
};
auto emit_irq_check_expr = [](const std::string& resume_pc_expr,
const std::string& indent = " ") {
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr);
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr) + indent +
fmt::format("if (cpu->pc != 0u) {{ if ((cpu->pc ^ ({})) & 0x1FFFFFFFu) return; cpu->pc = 0u; }} /* IRQ redirect / consume same-site resume */\n",
resume_pc_expr);
};
auto emit_cosim_instr = [](uint32_t pc, const std::string& indent = " ") {
return "#ifdef PSX_COSIM\n" + indent +
@@ -1202,9 +1210,10 @@ bool FullFunctionEmitter::emit_function(
" * back here as a registered continuation target. */\n"
" if (cpu->read_word(0x{:08X}u) != 0u) {{\n"
" psx_check_interrupts_at(cpu, 0x{:08X}u);\n"
" if (cpu->pc != 0u) {{ if ((cpu->pc ^ 0x{:08X}u) & 0x1FFFFFFFu) return; cpu->pc = 0u; }} /* IRQ redirect / consume */\n"
" cpu->pc = 0x{:08X}u; return;\n"
" }}\n",
addr, ram_pc, ram_pc + 0x10u, ram_pc, ram_pc, ram_pc);
addr, ram_pc, ram_pc + 0x10u, ram_pc, ram_pc, ram_pc, ram_pc);
}
// Non-terminator: emit normally — unless this load's successor reads
+7
View File
@@ -38,6 +38,13 @@ inline int16_t get_imm16(uint32_t i) { return (int16_t)(i & 0xFFFF); }
* after the directive — it only warns. That bug dropped 3268 hooks (1641
* MULDIV + 1627 ALU) across 54 shards of one game's AOT output. */
void append_pgxp_hooks(uint32_t instr, std::string& code) {
/* A translation ending in a preprocessor directive (the block-cycles
* muldiv wrappers end in `#endif`) would swallow an appended hook: the
* preprocessor discards tokens after #endif (the "extra tokens at end of
* #endif" warning class), so the MULT/DIV/MFLO/MFHI hooks silently never
* ran. Give the hook its own line. */
if (code.size() >= 6 && code.compare(code.size() - 6, 6, "#endif") == 0)
code += "\n ";
const uint32_t opcode = (instr >> 26) & 0x3F;
const uint32_t rs = get_rs(instr);
const uint32_t rt = get_rt(instr);
+43
View File
@@ -220,6 +220,49 @@ extern uint32_t gte_geometry_correction_hits(void);
extern void gte_geometry_correction_stats(uint32_t *lookups, uint32_t *hits,
uint32_t *miss_unrecorded,
uint32_t *miss_ambiguous);
/* Exact-sign NCLIP coverage: precise = all three SXY shadows live and
* word-validated; fallback = integer sign decided (the widescreen thin-face
* flicker regime); corrected = sign disagreements actually fixed. */
extern void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks,
uint64_t *corrected);
/* pc=0 escape journal (traps.c). Every runtime site that publishes a null PC
* (the "continue the native chain below" convention) or rescues one records
* an entry; when the convention breaks — a published 0 reaches the top-level
* trampoline and the run dies with "execution completed, PC=0" — the ring
* tail names the exact publisher and its context. Publishes are rare, so the
* always-on cost is a few stores; this works in play builds where the
* per-dispatch fntrace ring is compiled out (the exit trace was blind). */
enum {
PSX_PC0J_TRAP_CHANGE_SELF = 1, /* a: target_tcb */
PSX_PC0J_TRAP_FIBER_SWITCH = 2,
PSX_PC0J_TRAP_CRIT_ENTER = 3, /* a: sr */
PSX_PC0J_TRAP_CRIT_EXIT = 4, /* a: sr */
PSX_PC0J_TRAP_NULL_TARGET = 5, /* a: addr (0) */
PSX_PC0J_TRAP_SENTINEL = 6, /* a: addr; in-exception longjmp path */
PSX_PC0J_TRAP_ASYNC_RFE = 7, /* a: resume pc (rescue, not a 0) */
PSX_PC0J_TRAP_E10_NOOP = 8, /* a: addr */
PSX_PC0J_TRAP_STALE_MISS = 9, /* a: addr */
PSX_PC0J_IRQ_SAME_THREAD = 10, /* a: would-be resume; d: escape reason */
PSX_PC0J_IRQ_RESCUE = 11, /* a: published resume; d: reason */
PSX_PC0J_DIRTY_SENTINEL = 12, /* d: bit0 in_exception, bit1 async-armed */
PSX_PC0J_DIRTY_UNSUPPORTED = 13, /* a: unsupported pc; d: reason */
};
typedef struct Pc0JournalEntry {
uint64_t seq;
uint32_t site; /* PSX_PC0J_* */
uint32_t frame; /* present frame count at publish */
int32_t depth; /* g_psx_dispatch_depth at publish */
uint32_t a; /* site-specific (see enum) */
uint32_t b; /* $ra at publish */
uint32_t c; /* COP0 EPC at publish */
uint32_t d; /* site-specific extra */
} Pc0JournalEntry;
#define PSX_PC0J_CAP 64
extern Pc0JournalEntry g_pc0j_ring[PSX_PC0J_CAP];
extern uint64_t g_pc0j_seq;
extern void psx_pc0_journal_note(uint32_t site, CPUState *cpu,
uint32_t a, uint32_t d);
/* PGXP dataflow-shadowing hook macros (PGXP_LOAD/STORE/ALU/MULDIV/COP2).
* The emitter writes them unconditionally; they expand to real calls only
@@ -211,6 +211,23 @@ int psx_ws_cull_vxrange(uint32_t x, uint32_t imm) {
return (((x + (uint32_t)margin) & 0xFFFFu) <
(bound + 2u * (uint32_t)margin)) ? 1 : 0;
}
/* [[widescreen.cull.widen]] SLT-family helpers. The emitter writes these at
* explicitly configured cull sites, INCLUDING in overlay-resident code, so
* the DLL must define them or the shard fails to link (undefined reference ->
* no native shard -> that mod-patched page runs interpreted forever). Same
* contract as the funnel helpers above: self-contained on psx_ws_x_margin(),
* identity at margin 0 (4:3), MUST stay byte-for-byte identical to the gpu.c
* implementations. */
int psx_ws_cull_slti_lower(uint32_t sx, uint32_t imm) {
int32_t bound = (int32_t)(int16_t)(uint16_t)imm;
return ((int32_t)sx < bound - psx_ws_x_margin()) ? 1 : 0;
}
int psx_ws_cull_slt_widen(uint32_t rs, uint32_t rt, int bound_is_rt) {
const int32_t m = psx_ws_x_margin();
const int32_t a = (int32_t)rs, b = (int32_t)rt;
return bound_is_rt ? ((a < b + m) ? 1 : 0)
: ((a + m < b) ? 1 : 0);
}
void psx_ws_sprite_tag(CPUState *cpu) {
if (g_cbs.ws_sprite_tag) g_cbs.ws_sprite_tag(cpu);
}
+10
View File
@@ -855,6 +855,16 @@ int autocompile_request(void) {
if (s_cache_dir[0]) SetEnvironmentVariableA("PSX_OVERLAY_CACHE_DIR", s_cache_dir);
if (s_captures[0]) SetEnvironmentVariableA("PSX_OVERLAY_CAPTURES", s_captures);
SetEnvironmentVariableA("PSX_OVERLAY_LIVE_AUTOCOMPILE", "1");
/* Pin the FLAVOR the same way: the tool's --flavor defaults to 0 (play),
* so an instrumented (flavor 2) runtime spawned a compile whose shards it
* could never load — the loader's flavor guard rejected them and EVERY
* overlay ran interpreted, forever, on debug builds. The child must
* always write the flavor this runtime reads. */
{
char flavor_buf[16];
snprintf(flavor_buf, sizeof flavor_buf, "%d", (int)PSX_OVERLAY_FLAVOR);
SetEnvironmentVariableA("PSX_OVERLAY_FLAVOR", flavor_buf);
}
/* cmd.exe /C resolves the command via PATH and supports the relative
* paths in the configured line (cwd = project root). The WHOLE command is
+5
View File
@@ -2686,6 +2686,9 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
g_sentinel_reach_dirty++;
if (!psx_get_in_exception()) g_sentinel_reach_traps++; /* reuse: in_exc==0 dirty reaches */
g_sentinel_reach_async = g_async_rfe_resume_pc;
psx_pc0_journal_note(PSX_PC0J_DIRTY_SENTINEL, cpu, addr,
(psx_get_in_exception() ? 1u : 0u) |
(g_async_rfe_resume_pc ? 2u : 0u));
cpu->pc = 0;
if (psx_get_in_exception()) {
psx_exception_longjmp(); /* does not return */
@@ -3034,6 +3037,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
g_dirty_ram_last_unsupported_pc = g_unsupported_pc;
g_dirty_ram_last_unsupported_insn = g_unsupported_insn;
g_dirty_ram_last_unsupported_reason = g_unsupported_reason;
psx_pc0_journal_note(PSX_PC0J_DIRTY_UNSUPPORTED, cpu,
g_unsupported_pc, g_unsupported_insn);
cpu->pc = 0;
OV_FPLOG_RET1();
}
+16
View File
@@ -1522,6 +1522,8 @@ irq_deliver_eval:
* disarms the RFE-flag escape). */
g_rfe_escape_pending = 0;
{
extern uint32_t g_async_rfe_resume_pc; /* dirty_ram_interp.c */
extern uint64_t g_async_rfe_set_count;
uint32_t real_pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc
: s_compiled_interrupt_resume_pc;
/* Top-level flush_resume / savestate: resync cleared the latches and
@@ -1565,6 +1567,15 @@ irq_deliver_eval:
cpu->cop0[COP0_EPC] = real_pc; /* architectural: the real resume PC */
g_exception_real_epc = real_pc;
g_exc_escape_reason = PSX_EXC_ESCAPE_NONE; /* set at the actual RFE/SYSCALL return */
/* Async-RFE latch (Tomba 2 frame-1997 fix): persist the most
* recent real interruption PC so a game-driven asynchronous
* ReturnFromException (sentinel RFE with in_exception==0, e.g. a
* card-ISR longjmp installed via HookEntryInt) resumes the guest
* here instead of resolving to pc=0 (abnormal top-level exit).
* The latch was documented but never assigned — the whole rescue
* in the traps/dirty sentinel gates was inert. */
g_async_rfe_resume_pc = real_pc;
g_async_rfe_set_count++;
} else {
uint32_t sentinel = PSX_EXC_SENTINEL_PC;
cpu->write_word(sentinel, 0x00000000u); /* NOP, read by the handler's BD check */
@@ -1913,7 +1924,12 @@ irq_deliver_eval:
if (resume != 0u)
cpu->pc = resume;
/* else keep post-RFE cpu->pc — never publish 0 */
psx_pc0_journal_note(PSX_PC0J_IRQ_RESCUE, cpu, resume,
g_exc_escape_reason);
} else {
psx_pc0_journal_note(PSX_PC0J_IRQ_SAME_THREAD, cpu,
s_compiled_interrupt_resume_pc,
g_exc_escape_reason);
cpu->pc = 0; /* continue the interrupted live chain */
}
}
+18
View File
@@ -16520,6 +16520,24 @@ session_reboot:
(unsigned long long)g_dirty_ram_insns_run, g_slice_exit_pc, g_slice_exit_reason,
g_slice_exit_iter, g_slice_exit_dispatchable, g_slice_exit_dirty,
g_slice_exit_in_text, g_slice_exit_want); }
/* pc=0 escape journal tail: names the exact runtime site that published
* (or rescued) each recent null PC, with frame/depth/ra/epc context —
* the play-build replacement for the debug-only fntrace ring, which is
* compiled out here (the exit trace JSON was blind: fntrace_seq=0). */
{
uint64_t n = g_pc0j_seq < PSX_PC0J_CAP ? g_pc0j_seq : PSX_PC0J_CAP;
std::fprintf(stdout, "psxrecomp runtime: [pc0 journal] %llu total, last %llu:\n",
(unsigned long long)g_pc0j_seq, (unsigned long long)n);
for (uint64_t i = 0; i < n; i++) {
const Pc0JournalEntry *e =
&g_pc0j_ring[(g_pc0j_seq - n + i) % PSX_PC0J_CAP];
std::fprintf(stdout,
" seq=%llu site=%u frame=%u depth=%d a=0x%08X "
"ra=0x%08X epc=0x%08X d=0x%08X\n",
(unsigned long long)e->seq, e->site, e->frame,
e->depth, e->a, e->b, e->c, e->d);
}
}
shutdown_runtime();
if (g_gl_active) gl_renderer_shutdown();
+28
View File
@@ -241,6 +241,24 @@ enum {
};
uint32_t g_pc0_reason = PSX_PC0_GUEST_RETURN;
/* pc=0 escape journal — see cpu_state.h for the site enum and rationale. */
Pc0JournalEntry g_pc0j_ring[PSX_PC0J_CAP];
uint64_t g_pc0j_seq = 0;
void psx_pc0_journal_note(uint32_t site, CPUState *cpu, uint32_t a, uint32_t d)
{
extern int g_psx_dispatch_depth;
extern uint64_t s_frame_count; /* present frame counter (debug_server.c) */
Pc0JournalEntry *e = &g_pc0j_ring[g_pc0j_seq % PSX_PC0J_CAP];
e->seq = g_pc0j_seq++;
e->site = site;
e->frame = (uint32_t)s_frame_count;
e->depth = g_psx_dispatch_depth;
e->a = a;
e->b = cpu->gpr[31];
e->c = cpu->cop0[14];
e->d = d;
}
static uint32_t psx_current_tcb_ptr(CPUState* cpu)
{
uint32_t tcbh = cpu->read_word(0x00000108u);
@@ -640,6 +658,7 @@ static int psx_change_thread_fiber(CPUState* cpu, uint32_t target_tcb)
if (current_tcb == target_tcb) {
debug_server_log_thread_event(5, cpu, current_tcb, target_tcb, cpu->gpr[31]);
g_pc0_reason = PSX_PC0_CHANGE_SELF;
psx_pc0_journal_note(PSX_PC0J_TRAP_CHANGE_SELF, cpu, target_tcb, 0);
cpu->pc = 0;
return 1;
}
@@ -710,6 +729,7 @@ static int psx_change_thread_fiber(CPUState* cpu, uint32_t target_tcb)
debug_server_log_thread_event(9, cpu, target_tcb, current_tcb, 0);
g_pc0_reason = PSX_PC0_FIBER_SWITCH;
psx_pc0_journal_note(PSX_PC0J_TRAP_FIBER_SWITCH, cpu, 0, 0);
cpu->pc = 0;
return 1;
}
@@ -1028,6 +1048,7 @@ int psx_syscall(CPUState* cpu, uint32_t code) {
cpu->cop0[12] = sr & ~1u; /* clear IEc (bit 0) */
cpu->gpr[2] = sr & 1u; /* return old IEc */
g_pc0_reason = PSX_PC0_CRIT_SECTION;
psx_pc0_journal_note(PSX_PC0J_TRAP_CRIT_ENTER, cpu, sr, 0);
cpu->pc = 0;
/* Directly handled "void" syscall. Return 0: under CPS the caller
* (`if (psx_syscall(...)) return;`) falls through to the inline
@@ -1040,6 +1061,7 @@ int psx_syscall(CPUState* cpu, uint32_t code) {
cpu->cop0[12] = sr | 0x0401u; /* set IEc (bit 0) + IM[2] (bit 10) */
cpu->gpr[2] = 0;
g_pc0_reason = PSX_PC0_CRIT_SECTION;
psx_pc0_journal_note(PSX_PC0J_TRAP_CRIT_EXIT, cpu, sr, 0);
cpu->pc = 0;
return 0;
@@ -1191,6 +1213,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
* or uninitialized callback slot is dispatched. */
if (addr == 0) {
g_pc0_reason = PSX_PC0_DISPATCH_MISS; /* guest jumped/returned to a null (0) target */
psx_pc0_journal_note(PSX_PC0J_TRAP_NULL_TARGET, cpu, addr, 0);
cpu->pc = 0;
return;
}
@@ -1202,6 +1225,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
* the exception handler, longjmp back to psx_check_interrupts to
* properly unwind the handler call tree. */
if (addr == 0x80000048u && psx_get_in_exception()) {
psx_pc0_journal_note(PSX_PC0J_TRAP_SENTINEL, cpu, addr, 1);
cpu->pc = 0;
psx_exception_longjmp(); /* does not return */
}
@@ -1221,6 +1245,8 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
g_sentinel_reach_async = g_async_rfe_resume_pc;
if (g_async_rfe_resume_pc != 0u) {
g_async_rfe_fire_count++;
psx_pc0_journal_note(PSX_PC0J_TRAP_ASYNC_RFE, cpu,
g_async_rfe_resume_pc, 0);
cpu->pc = g_async_rfe_resume_pc;
return;
}
@@ -1236,6 +1262,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
* the psx_dispatch tail-call loop picking up $ra), it's a no-op — the
* continuation was already handled by the merged function. */
if (phys == 0x00000E10u) {
psx_pc0_journal_note(PSX_PC0J_TRAP_E10_NOOP, cpu, addr, 0);
cpu->pc = 0;
return;
}
@@ -1507,6 +1534,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
* ring-recorded above. Stale registers WILL corrupt the caller —
* diagnostic use only. */
g_pc0_reason = PSX_PC0_DISPATCH_MISS;
psx_pc0_journal_note(PSX_PC0J_TRAP_STALE_MISS, cpu, addr, 0);
cpu->pc = 0;
}
}
+17
View File
@@ -5262,6 +5262,19 @@ def main():
if _env_cap != args.captures:
print(f'[cache] PSX_OVERLAY_CAPTURES overrides --captures: {_env_cap}')
args.captures = _env_cap
# Flavor is pinned by the spawning runtime the same way as the cache
# locations: --flavor defaults to 0 (play), so an instrumented (flavor 2)
# runtime used to spawn compiles whose shards its loader then rejected —
# every overlay ran interpreted on debug builds, silently.
_env_flavor = os.environ.get('PSX_OVERLAY_FLAVOR')
if _env_flavor:
try:
_fl = int(_env_flavor, 0)
except ValueError:
_fl = None
if _fl is not None and _fl != args.flavor:
print(f'[cache] PSX_OVERLAY_FLAVOR overrides --flavor: {_fl}')
args.flavor = _fl
if not args.captures:
ap.error('no captures file: set PSX_OVERLAY_CAPTURES (runtime injects it) '
'or pass --captures for manual/offline use')
@@ -6585,6 +6598,10 @@ def main():
# interiors. Compile each as an isolated dispatch-root shard, then give
# every block in those fragments the same universal resume treatment.
unresolved = sorted(static_requested_entries - existing_entries)
# PSX_STATIC_NO_ISOLATED=1: A/B guard — skip the isolated-fragment pass
# entirely (its universal-resume treatment is the newest machinery).
if os.environ.get('PSX_STATIC_NO_ISOLATED'):
unresolved = []
fragment_built = 0
new_fragment_parts = []
for entry in unresolved:
+6 -3
View File
@@ -162,9 +162,12 @@ def _compact_region(region, drop_invalid_evidence=False):
if len(image) != size:
raise ValueError("capture size mismatch at 0x%08X: size=%d bytes=%d" %
(load, size, len(image)))
phys_lo = load & 0x1FFFFF
# 8 MB dev-RAM aware: titles running the 8MB enhancement stream code into
# the extended banks (0x200000..0x7FFFFF); the old 2 MB mask corrupted
# their capture addresses and the bound rejected legitimate regions.
phys_lo = load & 0x7FFFFF
phys_hi = phys_lo + size
if phys_hi > 2 * 1024 * 1024:
if phys_hi > 8 * 1024 * 1024:
raise ValueError("capture exceeds PSX RAM at 0x%08X" % load)
parsed = {}
@@ -178,7 +181,7 @@ def _compact_region(region, drop_invalid_evidence=False):
entries = []
for value in values:
address = _address(value, field)
phys = address & 0x1FFFFF
phys = address & 0x7FFFFF
if phys < phys_lo or phys >= phys_hi or (phys & 3):
if not drop_invalid_evidence:
raise ValueError(