diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 1d5ee098..7a1b173b 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -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) { diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index dc9bb0ce..7bcad55f 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -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 diff --git a/recompiler/src/pgxp_hook_emitter.cpp b/recompiler/src/pgxp_hook_emitter.cpp index 7f4c8921..247837a5 100644 --- a/recompiler/src/pgxp_hook_emitter.cpp +++ b/recompiler/src/pgxp_hook_emitter.cpp @@ -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); diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 5d1713bc..6595d2d0 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -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 diff --git a/runtime/include/overlay_dispatch_preamble.c.inc b/runtime/include/overlay_dispatch_preamble.c.inc index c2acb8e7..eee589b7 100644 --- a/runtime/include/overlay_dispatch_preamble.c.inc +++ b/runtime/include/overlay_dispatch_preamble.c.inc @@ -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); } diff --git a/runtime/src/autocompile.c b/runtime/src/autocompile.c index 2018428e..33641732 100644 --- a/runtime/src/autocompile.c +++ b/runtime/src/autocompile.c @@ -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 diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 4e124ad4..762caacd 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -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(); } diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 8c70ff63..07de5393 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -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 */ } } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 5b135024..f00c188a 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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(); diff --git a/runtime/src/traps.c b/runtime/src/traps.c index 2ba29448..0fd98bb1 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -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; } } diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index 8697f3bb..5da2d6f1 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -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: diff --git a/tools/coverage_vault.py b/tools/coverage_vault.py index d49c444e..079140cf 100644 --- a/tools/coverage_vault.py +++ b/tools/coverage_vault.py @@ -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(