wip(tomba2): ABI v8 check_interrupts_at + emitter charge-cycle work (pre-rebase snapshot)

In-progress investigation work snapshotted before rebasing onto the
tcc-refactored psxrecomp master. Includes overlay ABI v8
(check_interrupts_at resume-PC callback for async RFE recovery) and
code_generator/full_function_emitter delay-slot cycle accounting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-25 14:23:57 -07:00
parent 00aa9ec1a7
commit 8ab24c5e62
13 changed files with 2842 additions and 2493 deletions
+11
View File
@@ -208,6 +208,17 @@ private:
// continuation into its function's entry-switch.
std::map<uint32_t, uint32_t> cps_continuation_owner_;
// Cycle accounting for legal entries that land inside an already-emitted
// block instead of at its leader. Block leaders charge the whole block; an
// entry-switch or jump-table target that bypasses the leader must charge the
// remaining instructions from the interior label to the end of that block.
uint32_t partial_block_cycle_count(uint32_t addr, const ControlFlowGraph& cfg) const;
std::string emit_mid_block_cycle_charge(uint32_t addr, const ControlFlowGraph& cfg,
const std::string& indent) const;
std::string emit_interrupt_check(uint32_t resume_pc, const std::string& indent) const;
std::string emit_interrupt_check_expr(const std::string& resume_pc_expr,
const std::string& indent) const;
public:
bool cps_enabled() const { return cps_enabled_; }
// continuation_addr -> owning function entry (CPS dispatch routing).
File diff suppressed because it is too large Load Diff
+5
View File
@@ -148,6 +148,11 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (runtime.contains("overlay_backend")) {
rt.overlay_backend = toml::find<std::string>(runtime, "overlay_backend");
}
if (runtime.contains("overlay_native_block")) {
for (const auto& a : toml::find<std::vector<std::string>>(runtime, "overlay_native_block")) {
rt.overlay_native_block.push_back(parse_hex(a, "runtime.overlay_native_block"));
}
}
// Optional [video] block — visual enhancement options. Kept on the same
// RuntimeConfig so main.cpp consumes them alongside the other knobs.
+6
View File
@@ -116,6 +116,12 @@ struct RuntimeConfig {
// gcc shards still load). The env var PSX_OVERLAY_BACKEND overrides at runtime.
std::string overlay_backend;
// overlay_native_block: per-game overlay function entries that must stay on
// the dirty-RAM interpreter even when a matching native DLL exists. Intended
// for small timing-sensitive setup/task routines while the rest of the
// overlay runs native.
std::vector<uint32_t> overlay_native_block;
// ---- [video] block — visual enhancement options ----
// supersampling: internal-resolution SSAA factor (per axis). 1 = native
// (default, behaves exactly as before). 2..4 render geometry/shading into
+66 -20
View File
@@ -105,6 +105,13 @@ bool FullFunctionEmitter::emit_function(
const char* e = std::getenv("PSX_CPS");
return e == nullptr || e[0] != '0';
}();
auto emit_irq_check = [](uint32_t resume_pc, const std::string& indent = " ") {
return indent + fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", resume_pc);
};
auto emit_irq_check_expr = [](const std::string& resume_pc_expr,
const std::string& indent = " ") {
return indent + fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr);
};
// Build sorted instruction list and a set for O(1) membership test.
// sfr.instructions is already sorted by address.
@@ -369,7 +376,6 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" psx_advance_cycles({}u);\n", bcyc);
out += "#endif\n";
}
out += " psx_check_interrupts(cpu);\n";
if (should_probe_pc(addr)) {
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n", addr);
}
@@ -427,12 +433,16 @@ bool FullFunctionEmitter::emit_function(
uint8_t rs = (pb.raw >> 21) & 0x1F;
if (rs == 31) {
if (ra_loaded_from_non_sp)
out += " cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return; /* longjmp-return */\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return; /* longjmp-return */\n";
else if (cps)
out += " cpu->pc = cpu->gpr[31]; return; /* CPS: publish $ra */\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" cpu->pc = cpu->gpr[31]; return; /* CPS: publish $ra */\n";
else
out += " return;\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" return;\n";
} else {
out += emit_irq_check_expr(fmt::format("cpu->gpr[{}]", static_cast<int>(rs)));
out += fmt::format(" cpu->pc = cpu->gpr[{}]; return;\n",
static_cast<int>(rs));
}
@@ -466,12 +476,16 @@ bool FullFunctionEmitter::emit_function(
uint8_t rs = (raw >> 21) & 0x1F;
if (rs == 31) {
if (ra_loaded_from_non_sp)
out += " cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return; /* longjmp-return */\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return; /* longjmp-return */\n";
else if (cps)
out += " cpu->pc = cpu->gpr[31]; return; /* CPS: publish $ra */\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" cpu->pc = cpu->gpr[31]; return; /* CPS: publish $ra */\n";
else
out += " return;\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" return;\n";
} else {
out += emit_irq_check_expr(fmt::format("cpu->gpr[{}]", static_cast<int>(rs)));
out += fmt::format(" cpu->pc = cpu->gpr[{}]; return;\n",
static_cast<int>(rs));
}
@@ -494,14 +508,17 @@ bool FullFunctionEmitter::emit_function(
// Resolve branch. Target was decoded as absolute virtual addr.
uint32_t target = tr.terminator_target;
register_cross_function_target(target);
out += fmt::format(" if (psx_taken_{:08X}) {{ cpu->pc = 0x{:08X}u; return; }}\n",
addr, target);
out += fmt::format(" if (psx_taken_{:08X}) {{\n", addr);
out += emit_irq_check(target, " ");
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", target);
out += " }\n";
// Not-taken: dispatch to delay-slot address (next function's entry).
// The next function's prologue will re-execute the delay slot — that's
// a duplicate side effect we accept for correctness on the not-taken
// path. (For unconditional branches like beq $zero,$zero this path
// is structurally dead.)
register_cross_function_target(ds_addr);
out += emit_irq_check(ds_addr);
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", ds_addr);
} else if (kind == "j") {
// Inline orphaned delay slot, then unconditional jump.
@@ -521,6 +538,7 @@ bool FullFunctionEmitter::emit_function(
}
uint32_t target = relocate_j_target(addr, tr.terminator_target);
register_cross_function_target(target);
out += emit_irq_check(target);
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", target);
} else if (kind == "jal") {
// Inline orphaned delay slot, set $ra, dispatch to target.
@@ -549,10 +567,12 @@ bool FullFunctionEmitter::emit_function(
register_cross_function_target(addr + 8);
}
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u;\n", return_addr);
out += emit_irq_check(target);
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", target);
} else {
out += " { uint32_t _csp = cpu->gpr[29];\n";
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u;\n", return_addr);
out += emit_irq_check(target);
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u);\n", target);
out += fmt::format(" if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n",
return_addr);
@@ -590,6 +610,7 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" cpu->gpr[{}] = 0x{:08X}u;\n",
static_cast<int>(rd), return_addr);
}
out += emit_irq_check_expr("_t");
out += " cpu->pc = _t; return; }\n";
} else {
out += " { uint32_t _csp = cpu->gpr[29];\n";
@@ -597,6 +618,7 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" cpu->gpr[{}] = 0x{:08X}u;\n",
static_cast<int>(rd), return_addr);
}
out += emit_irq_check_expr(fmt::format("cpu->gpr[{}]", static_cast<int>(rs)));
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
static_cast<int>(rs));
if (rd == 31) {
@@ -666,9 +688,10 @@ bool FullFunctionEmitter::emit_function(
" * After the stub's jalr returns, ra=RAM 0x{:08X} routes\n"
" * 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"
" cpu->pc = 0x{:08X}u; return;\n"
" }}\n",
addr, ram_pc, ram_pc + 0x10u, ram_pc, ram_pc);
addr, ram_pc, ram_pc + 0x10u, ram_pc, ram_pc, ram_pc);
}
// Non-terminator: emit normally.
@@ -685,27 +708,35 @@ bool FullFunctionEmitter::emit_function(
if (is_branch_kind(kind.c_str())) {
// Conditional branch resolution.
uint32_t target = pb.target;
uint32_t fallthrough = pb.terminator_addr + 8;
bool target_in_function = addr_to_raw.count(target) != 0;
if (target_in_function) {
out += fmt::format(" if (psx_taken_{:08X}) goto label_{:08X};\n",
pb.terminator_addr, target);
out += fmt::format(" if (psx_taken_{:08X}) {{\n", pb.terminator_addr);
out += emit_irq_check(target, " ");
out += fmt::format(" goto label_{:08X};\n", target);
out += " }\n";
} else {
// Tail call: set cpu->pc and return; dispatch loop re-dispatches.
// Register the cross-function target as a continuation so
// the dispatch table can route to it (otherwise mid-function
// targets miss the table — root cause of card-read failure).
register_cross_function_target(target);
out += fmt::format(" if (psx_taken_{:08X}) {{ cpu->pc = 0x{:08X}u; return; }}\n",
pb.terminator_addr, target);
out += fmt::format(" if (psx_taken_{:08X}) {{\n", pb.terminator_addr);
out += emit_irq_check(target, " ");
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", target);
out += " }\n";
}
out += emit_irq_check(fallthrough);
} else if (kind == "j") {
uint32_t target = pb.target;
bool target_in_function = addr_to_raw.count(target) != 0;
if (target_in_function) {
out += emit_irq_check(target);
out += fmt::format(" goto label_{:08X};\n", target);
} else {
// Tail call: set cpu->pc and return; dispatch loop re-dispatches.
register_cross_function_target(target);
out += emit_irq_check(target);
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", target);
}
} else if (kind == "jal") {
@@ -734,6 +765,7 @@ bool FullFunctionEmitter::emit_function(
pb.terminator_addr);
}
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u;\n", return_addr);
out += emit_irq_check(target);
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", target);
} else {
out += " { uint32_t _csp = cpu->gpr[29];\n";
@@ -743,6 +775,7 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n",
pb.terminator_addr);
}
out += emit_irq_check(target);
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u);\n", target);
if (target == 0x00006380u) {
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n",
@@ -778,6 +811,7 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" cpu->gpr[{}] = 0x{:08X}u;\n",
static_cast<int>(rd), return_addr);
}
out += emit_irq_check_expr("_t");
out += " cpu->pc = _t; return; }\n";
} else {
out += " { uint32_t _csp = cpu->gpr[29];\n";
@@ -785,6 +819,7 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" cpu->gpr[{}] = 0x{:08X}u;\n",
static_cast<int>(rd), return_addr);
}
out += emit_irq_check_expr(fmt::format("cpu->gpr[{}]", static_cast<int>(rs)));
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
static_cast<int>(rs));
if (rd == 31) {
@@ -803,11 +838,14 @@ bool FullFunctionEmitter::emit_function(
uint8_t rs = (pb.raw >> 21) & 0x1F;
if (rs == 31) {
if (ra_loaded_from_non_sp)
out += " cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return; /* longjmp-return */\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return; /* longjmp-return */\n";
else if (cps)
out += " cpu->pc = cpu->gpr[31]; return; /* CPS: publish $ra for trampoline dispatch */\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" cpu->pc = cpu->gpr[31]; return; /* CPS: publish $ra for trampoline dispatch */\n";
else
out += " return;\n";
out += emit_irq_check_expr("cpu->gpr[31]") +
" return;\n";
} else {
// Attempt jump table resolution: scan backward from the
// jr instruction to find SLTIU/SLL/LUI/ADDU/LW pattern.
@@ -884,10 +922,14 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" switch (cpu->gpr[{}]) {{\n",
static_cast<int>(jr_rs));
for (auto& [rt, rom_t] : targets) {
out += fmt::format(" case 0x{:08X}u: goto label_{:08X};\n",
rt, rom_t);
out += fmt::format(" case 0x{:08X}u:\n", rt);
out += emit_irq_check(rom_t, " ");
out += fmt::format(" goto label_{:08X};\n", rom_t);
}
out += fmt::format(" default: cpu->pc = cpu->gpr[{}]; return;\n",
out += " default:\n";
out += emit_irq_check_expr(fmt::format("cpu->gpr[{}]", static_cast<int>(jr_rs)),
" ");
out += fmt::format(" cpu->pc = cpu->gpr[{}]; return;\n",
static_cast<int>(jr_rs));
out += " }\n";
emitted_switch = true;
@@ -896,6 +938,7 @@ bool FullFunctionEmitter::emit_function(
}
if (!emitted_switch) {
// Tail call: set cpu->pc and return; dispatch loop re-dispatches.
out += emit_irq_check_expr(fmt::format("cpu->gpr[{}]", static_cast<int>(rs)));
out += fmt::format(" cpu->pc = cpu->gpr[{}]; return;\n",
static_cast<int>(rs));
}
@@ -933,6 +976,7 @@ bool FullFunctionEmitter::emit_function(
if (!has_control_flow) {
// Fallthrough tail call: set cpu->pc and return; dispatch loop re-dispatches.
uint32_t next_addr = last_addr + 4;
out += emit_irq_check(next_addr);
out += fmt::format(" cpu->pc = 0x{:08X}u; return; /* fallthrough */\n", next_addr);
}
}
@@ -1041,6 +1085,7 @@ void FullFunctionEmitter::emit_dispatch(
// Extern declarations for runtime-provided functions.
out += "extern void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys);\n";
out += "extern void psx_check_interrupts(CPUState* cpu);\n";
out += "extern void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc);\n";
out += "extern void psx_restore_state_escape(void);\n";
out += "extern void gte_execute(CPUState* cpu, uint32_t cmd);\n";
out += "extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);\n";
@@ -1416,6 +1461,7 @@ EmitStats FullFunctionEmitter::emit(
full_c += "extern void psx_dispatch(CPUState* cpu, uint32_t addr);\n";
full_c += "extern void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys);\n";
full_c += "extern void psx_check_interrupts(CPUState* cpu);\n";
full_c += "extern void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc);\n";
full_c += "extern void psx_restore_state_escape(void);\n";
full_c += "extern void gte_execute(CPUState* cpu, uint32_t cmd);\n";
full_c += "extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);\n";
+2 -1
View File
@@ -28,7 +28,8 @@ void interrupts_init(void);
* Fires vblank on schedule, checks (i_stat & i_mask), and if
* pending + COP0 allows, dispatches the exception handler. */
void psx_check_interrupts(struct CPUState* cpu);
/* Compat shim for ape-flavored generated code (forwards to psx_check_interrupts). */
/* Interrupt check with the compiled guest PC to resume if a game-installed
* handler later RFEs to the sentinel outside the synchronous host window. */
void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc);
/* Accumulate emitted PSX cycles toward the next VBlank trigger.
+6 -1
View File
@@ -34,7 +34,9 @@
/* v7: advance_cycles callback added; overlays now built with
* PSX_ENABLE_BLOCK_CYCLES and charge the shared host cycle/timer timeline.
* Bumping rejects stale DLLs that charged no cycles (Tomba2 Timer1 fork). */
#define PSX_OVERLAY_ABI_VERSION 7
/* v8: check_interrupts_at callback added so generated native overlay block
* checks can provide the real guest resume PC for async RFE recovery. */
#define PSX_OVERLAY_ABI_VERSION 8
/* Codegen flavor of the recompiled output the overlays + runtime were built
* against. Overlays are keyed in the cache by guest-bytes CRC, which is
@@ -99,6 +101,9 @@ typedef struct {
void (*dispatch_call)(CPUState *cpu, uint32_t addr, uint32_t ra);
/* Interrupt check: called after every function return in overlay */
void (*check_interrupts)(CPUState *cpu);
/* Interrupt check with the guest PC that should resume if a game-installed
* handler later RFEs to the sentinel outside the synchronous host window. */
void (*check_interrupts_at)(CPUState *cpu, uint32_t resume_pc);
/* Guest-cycle accounting (ABI v7): block-cycle charge. Overlay code built
* with PSX_ENABLE_BLOCK_CYCLES must charge the SAME shared host cycle/timer
* timeline as the dirty-RAM interpreter and the BIOS, or timer-sensitive
+4
View File
@@ -24,6 +24,10 @@ extern "C" {
* game_id: product code (e.g. "SCUS-94236") */
void overlay_loader_init(const char *cache_dir, const char *game_id);
/* Per-function native-disable for small timing-sensitive overlay routines.
* Addresses may be KUSEG/KSEG0/KSEG1; the loader keys by physical address. */
int overlay_loader_native_block_add(uint32_t addr);
/* Apply the sljit live-execution policy once the Tier-2 backend is resolved.
* Call AFTER code_provider_init(). Default live ON iff the resolved backend is
* sljit (validated-live production); PSX_OVERLAY_SLJIT_LIVE env overrides. */
+7 -2
View File
@@ -9044,10 +9044,15 @@ static void handle_overlay_native_ring(int id, const char *json)
{
(void)json;
extern int overlay_loader_dump_native_ring(char *out, int cap);
static char rbuf[16384];
static char rbuf[2 * 1024 * 1024];
int len = overlay_loader_dump_native_ring(rbuf, (int)sizeof(rbuf));
if (len < 0) len = 0;
send_fmt("{\"id\":%d,\"ok\":true,\"ring\":%s}\n", id, rbuf);
int cap = len + 128;
char *out = (char *)malloc((size_t)cap);
if (!out) { send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id); return; }
snprintf(out, (size_t)cap, "{\"id\":%d,\"ok\":true,\"ring\":%s}", id, rbuf);
debug_server_send_line(out);
free(out);
}
/* overlay_diff_on/off: same-state native↔interp differential. With it on, each
+12 -1
View File
@@ -143,6 +143,12 @@ void* g_exception_owner_fiber = NULL;
int g_pending_exception_longjmp = 0;
extern int g_psx_dispatch_depth;
/* Set by psx_check_interrupts_at while a compiled block-leader interrupt check
* is in progress. If the delivered handler later RFEs to the sentinel outside
* the synchronous host window, dirty_ram_dispatch can resume at this guest PC
* instead of treating the sentinel as pc=0 termination. */
static uint32_t s_compiled_interrupt_resume_pc = 0;
int psx_get_in_exception(void) { return in_exception; }
void psx_get_freeze_diag(uint64_t *out_total_checks,
@@ -382,6 +388,9 @@ void psx_check_interrupts(CPUState* cpu) {
extern uint64_t g_async_rfe_set_count;
{
uint32_t safe_pc = g_dirty_safe_resume_pc;
if (safe_pc == 0u) {
safe_pc = s_compiled_interrupt_resume_pc;
}
if (safe_pc != 0u && (safe_pc & 0x3u) == 0u &&
(safe_pc & 0x1FFFFFFFu) < 0x00200000u) {
g_async_rfe_resume_pc = safe_pc;
@@ -553,6 +562,8 @@ void psx_check_interrupts(CPUState* cpu) {
* here gives the mmx6 baseline interrupt behavior — sufficient to build+run the
* current generated code on the good baseline for instrumented comparison. */
void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc) {
(void)resume_pc;
uint32_t prev = s_compiled_interrupt_resume_pc;
s_compiled_interrupt_resume_pc = resume_pc;
psx_check_interrupts(cpu);
s_compiled_interrupt_resume_pc = prev;
}
+9
View File
@@ -1996,6 +1996,15 @@ int main(int argc, char** argv) {
overlay_capture_set_out_dir(exe_dir.string().c_str());
overlay_capture_set_enabled(1);
overlay_loader_init(cache_dir.c_str(), game_id.c_str());
for (uint32_t addr : gc.runtime.overlay_native_block) {
overlay_loader_native_block_add(addr);
}
if (!gc.runtime.overlay_native_block.empty()) {
std::fprintf(stdout,
"psxrecomp: overlay native blocklist seeded with %zu entr%s\n",
gc.runtime.overlay_native_block.size(),
gc.runtime.overlay_native_block.size() == 1 ? "y" : "ies");
}
/* Step 2.8 variant-capture automation. Autocapture is ON
* whenever the cache is on: it writes the player-shareable
* overlay_captures.json (the contribution file the README
+143 -14
View File
@@ -7,6 +7,7 @@
#include "dirty_ram_interp.h"
#include "interrupts.h"
#include "debug_server.h"
#include "psx_cycles.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -125,13 +126,14 @@ static int s_active_depth = 0;
* interest — never "arm a trace then hope". s_native_inprogress holds the entry
* currently executing (nonzero at dump => a native fn was entered and never
* returned: a freeze INSIDE native code, the strongest single suspect). */
#define NRING_CAP 1024
typedef struct { uint32_t addr; uint32_t crc; uint64_t seq; int returned; } NRingEnt;
#define NRING_CAP 16384
typedef struct { uint32_t addr; uint32_t crc; uint32_t frame; uint64_t seq; int returned; } NRingEnt;
static NRingEnt s_nring[NRING_CAP];
static uint32_t s_nring_pos = 0;
static uint64_t s_nring_seq = 0;
static uint32_t s_native_inprogress = 0; /* addr of fn currently in native, 0 = none */
static uint64_t s_native_calls_total = 0;
extern uint64_t s_frame_count;
/* Runtime A/B: when 0, candidates are still hashed/validated and recorded, but
* NEVER executed native — execution falls to the interpreter. Lets us prove
@@ -912,6 +914,7 @@ int overlay_loader_has_cached_crc(uint32_t region_start, uint32_t crc) {
extern void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra);
extern void psx_check_interrupts(CPUState *cpu);
extern void psx_check_interrupts_at(CPUState *cpu, uint32_t resume_pc);
extern void gte_execute(CPUState *cpu, uint32_t cmd);
extern int psx_syscall(CPUState *cpu, uint32_t code);
extern void psx_unknown_dispatch(CPUState *cpu, uint32_t addr, uint32_t phys);
@@ -937,6 +940,14 @@ static int s_suppress_irq = 0;
static uint32_t s_irq_ratelimit = 0; /* 0 = full suppress; N = every Nth call */
static uint32_t s_irq_callcount = 0;
static uint64_t s_irq_suppressed = 0;
static uint32_t s_irq_budget_cycles = 0; /* 0 = off; otherwise guest cycles/check */
static uint64_t s_irq_last_check_cycle = UINT64_MAX;
static int s_irq_suppress_cdrom_only = 0;
static int s_irq_post_dispatch_pump = 0;
static int s_irq_defer_cdrom = 0;
extern uint32_t i_stat;
extern uint32_t i_mask;
void overlay_loader_set_irq_suppress(int mode, uint32_t ratelimit) {
s_suppress_irq = mode ? 1 : 0;
@@ -952,18 +963,78 @@ void overlay_loader_get_irq_suppress(int *mode, uint32_t *rl, uint64_t *supp) {
if (supp) *supp = s_irq_suppressed;
}
static void overlay_ci_wrapper(CPUState *cpu) {
static int overlay_irq_suppressed_now(void) {
if (s_suppress_irq) {
if (s_irq_ratelimit == 0) { s_irq_suppressed++; return; }
if ((++s_irq_callcount % s_irq_ratelimit) != 0) { s_irq_suppressed++; return; }
if (s_irq_ratelimit == 0) { s_irq_suppressed++; return 1; }
if ((++s_irq_callcount % s_irq_ratelimit) != 0) { s_irq_suppressed++; return 1; }
}
if (s_irq_suppress_cdrom_only) {
uint32_t pending = i_stat & i_mask;
if (pending == (1u << IRQ_CDROM)) {
s_irq_suppressed++;
return 1;
}
}
if (s_irq_budget_cycles != 0) {
uint64_t now = psx_get_cycle_count();
if (s_irq_last_check_cycle != UINT64_MAX &&
now - s_irq_last_check_cycle < (uint64_t)s_irq_budget_cycles) {
s_irq_suppressed++;
return 1;
}
s_irq_last_check_cycle = now;
}
return 0;
}
static void overlay_ci_wrapper(CPUState *cpu) {
if (overlay_irq_suppressed_now()) return;
if (s_irq_defer_cdrom && (i_stat & (1u << IRQ_CDROM))) {
uint32_t saved_cd = i_stat & (1u << IRQ_CDROM);
i_stat &= ~(1u << IRQ_CDROM);
psx_check_interrupts(cpu);
i_stat |= saved_cd;
return;
}
psx_check_interrupts(cpu);
}
static void overlay_ci_at_wrapper(CPUState *cpu, uint32_t resume_pc) {
if (overlay_irq_suppressed_now()) return;
if (s_irq_defer_cdrom && (i_stat & (1u << IRQ_CDROM))) {
uint32_t saved_cd = i_stat & (1u << IRQ_CDROM);
i_stat &= ~(1u << IRQ_CDROM);
psx_check_interrupts_at(cpu, resume_pc);
i_stat |= saved_cd;
return;
}
psx_check_interrupts_at(cpu, resume_pc);
}
static int overlay_irq_budget_blocks_now(void) {
if (s_irq_budget_cycles == 0) return 0;
uint64_t now = psx_get_cycle_count();
if (s_irq_last_check_cycle != UINT64_MAX &&
now - s_irq_last_check_cycle < (uint64_t)s_irq_budget_cycles) {
s_irq_suppressed++;
return 1;
}
s_irq_last_check_cycle = now;
return 0;
}
static void overlay_post_dispatch_irq_pump(CPUState *cpu) {
if (!s_irq_post_dispatch_pump) return;
if (overlay_irq_budget_blocks_now()) return;
if (cpu->pc != 0u) psx_check_interrupts_at(cpu, cpu->pc);
else psx_check_interrupts(cpu);
}
static void init_callbacks(void) {
extern void psx_restore_state_escape(void);
s_callbacks.dispatch_call = psx_dispatch_call;
s_callbacks.check_interrupts = overlay_ci_wrapper;
s_callbacks.check_interrupts_at = overlay_ci_at_wrapper;
{ extern void psx_advance_cycles(uint32_t cycles);
s_callbacks.advance_cycles = psx_advance_cycles; }
s_callbacks.gte_execute = gte_execute;
@@ -1146,6 +1217,57 @@ void overlay_loader_init(const char *cache_dir, const char *game_id) {
loader_log("PSX_OVERLAY_NATIVE_OFF set: native overlay exec OFF from boot");
}
}
{
const char *sup = getenv("PSX_OVERLAY_IRQ_SUPPRESS");
const char *rl = getenv("PSX_OVERLAY_IRQ_RATELIMIT");
if (sup && *sup && *sup != '0') {
overlay_loader_set_irq_suppress(1, 0);
loader_log("PSX_OVERLAY_IRQ_SUPPRESS set: native overlay IRQ checks suppressed from boot");
} else if (rl && *rl) {
uint32_t n = (uint32_t)strtoul(rl, NULL, 0);
if (n == 0) n = 1;
overlay_loader_set_irq_suppress(1, n);
loader_log("PSX_OVERLAY_IRQ_RATELIMIT set: native overlay IRQ checks every %u call(s)", n);
}
}
{
const char *budget = getenv("PSX_OVERLAY_IRQ_BUDGET");
if (budget && *budget) {
uint32_t n = (uint32_t)strtoul(budget, NULL, 0);
s_irq_budget_cycles = n;
s_irq_last_check_cycle = UINT64_MAX;
loader_log("PSX_OVERLAY_IRQ_BUDGET set: native overlay IRQ checks every %u guest cycle(s)", n);
}
}
{
const char *no_cd = getenv("PSX_OVERLAY_IRQ_NO_CDROM");
if (no_cd && *no_cd && *no_cd != '0') {
s_irq_suppress_cdrom_only = 1;
loader_log("PSX_OVERLAY_IRQ_NO_CDROM set: native overlay CDROM-only IRQ checks suppressed");
}
}
{
const char *defer_cd = getenv("PSX_OVERLAY_IRQ_DEFER_CDROM");
if (defer_cd && *defer_cd && *defer_cd != '0') {
s_irq_defer_cdrom = 1;
loader_log("PSX_OVERLAY_IRQ_DEFER_CDROM set: native overlay CDROM IRQ delivery deferred");
}
}
{
const char *post = getenv("PSX_OVERLAY_IRQ_POST_PUMP");
if (post && *post && *post != '0') {
s_irq_post_dispatch_pump = 1;
s_irq_last_check_cycle = UINT64_MAX;
loader_log("PSX_OVERLAY_IRQ_POST_PUMP set: native overlay dispatch-return IRQ pump enabled");
}
}
{
const char *diff = getenv("PSX_OVERLAY_DIFF");
if (diff && *diff && *diff != '0') {
s_diff_mode = 1;
loader_log("PSX_OVERLAY_DIFF set: native/interp shadow diff ON from boot");
}
}
/* Live-mode policy is applied later via overlay_loader_apply_live_policy(),
* AFTER code_provider_init resolves the backend (the default depends on it). */
s_active = 1;
@@ -1413,12 +1535,13 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
if (matched) {
if (c->state != ENTRY_VALID) { c->state = ENTRY_VALID; s_valid_count++; }
if (c->device_touch) { if (_probe) s_cps_probe_outcome = 3; s_disp_interp++; return 0; }
if (!s_native_exec || overlay_native_blocked(c->addr))
if (!s_native_exec || overlay_native_blocked(c->addr) || overlay_native_blocked(addr))
{ if (_probe) s_cps_probe_outcome = 4; s_would_run_native++; s_disp_interp++; return 0; }
if (_probe) s_cps_probe_outcome = 2;
uint32_t slot = s_nring_pos++ & (NRING_CAP - 1u);
s_nring[slot].addr = addr;
s_nring[slot].crc = c->crc_code;
s_nring[slot].frame = (uint32_t)s_frame_count;
s_nring[slot].seq = ++s_nring_seq;
s_nring[slot].returned = 0;
uint32_t prev_inprogress = s_native_inprogress;
@@ -1429,6 +1552,7 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
s_disp_native++;
cpu->pc = addr; /* route the func's entry-switch to the block */
c->fn(cpu);
overlay_post_dispatch_irq_pump(cpu);
if (s_active_depth > 0) s_active_depth--;
s_nring[slot].returned = 1;
s_native_inprogress = prev_inprogress;
@@ -1516,6 +1640,7 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
uint32_t slot = s_nring_pos++ & (NRING_CAP - 1u);
s_nring[slot].addr = c->addr;
s_nring[slot].crc = c->crc_code;
s_nring[slot].frame = (uint32_t)s_frame_count;
s_nring[slot].seq = ++s_nring_seq;
s_nring[slot].returned = 0;
uint32_t prev_inprogress = s_native_inprogress;
@@ -1531,6 +1656,7 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
uint32_t mtag = (uint32_t)s_nring[slot].seq; /* stable across nesting */
dirty_ram_log_marker(c->addr, mtag, 0);
c->fn(cpu);
overlay_post_dispatch_irq_pump(cpu);
dirty_ram_log_marker(c->addr, mtag, 1);
if (s_active_depth > 0) s_active_depth--;
@@ -1740,6 +1866,7 @@ static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) {
extern uint8_t *memory_get_ram_ptr(void);
extern uint8_t *memory_get_scratchpad_ptr(void);
extern int dirty_ram_dispatch(CPUState *cpu, uint32_t addr, uint32_t stop_addr);
extern void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_addr);
extern int g_shadow_mmio_watch; /* memory.c — device-access detector */
extern uint64_t g_shadow_mmio_hits;
uint8_t *ram = memory_get_ram_ptr();
@@ -1801,16 +1928,18 @@ static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) {
uint32_t stop_ra = cpu->gpr[31]; /* entry $ra = the function's return point */
c->fn(cpu);
/* A shard that ends in a computed `jr rX` (jump table) exits with cpu->pc =
* target rather than running to the return; chain through tail-transfers to
* the same stop the interp used, so we compare FULL native vs FULL interp. */
/* CPS shards exit with cpu->pc set to the next tail target. Chain through
* the normal dispatcher to the original caller return, with s_native_exec=0
* above so nested overlay calls still run through the interpreter on both
* passes. dirty_ram_dispatch alone cannot follow clean/static BIOS targets
* and creates false shadow divergences for tail-transfer-heavy functions. */
{
int guard = 0;
while (cpu->pc != 0 && !g_psx_call_bail && guard++ < 8192) {
uint32_t tv = cpu->pc;
if ((tv & 0x1FFFFFFFu) == (stop_ra & 0x1FFFFFFFu)) break; /* returned */
cpu->pc = 0;
if (!dirty_ram_dispatch(cpu, tv, stop_ra)) { cpu->pc = tv; break; }
psx_dispatch_call(cpu, tv, stop_ra);
}
}
CPUState cpuN = *cpu;
@@ -2115,14 +2244,14 @@ int overlay_loader_dump_native_ring(char *out, int cap) {
"\"in_progress\":\"0x%08X\",\"recent\":[",
s_native_exec, (unsigned long long)s_native_calls_total,
(unsigned long long)s_would_run_native, s_native_inprogress);
/* Walk backward from the most recent up to 64 entries. */
/* Walk backward from the most recent entries kept in the diagnostic ring. */
int shown = 0;
for (uint32_t k = 0; k < NRING_CAP && shown < 64 && n < cap - 120; k++) {
for (uint32_t k = 0; k < NRING_CAP && n < cap - 140; k++) {
uint32_t idx = (s_nring_pos - 1u - k) & (NRING_CAP - 1u);
if (s_nring[idx].seq == 0) break;
n += snprintf(out + n, cap - n,
"%s{\"addr\":\"0x%08X\",\"crc\":\"0x%08X\",\"seq\":%llu,\"returned\":%d}",
shown ? "," : "", s_nring[idx].addr, s_nring[idx].crc,
"%s{\"addr\":\"0x%08X\",\"crc\":\"0x%08X\",\"frame\":%u,\"seq\":%llu,\"returned\":%d}",
shown ? "," : "", s_nring[idx].addr, s_nring[idx].crc, s_nring[idx].frame,
(unsigned long long)s_nring[idx].seq, s_nring[idx].returned);
shown++;
}
+4
View File
@@ -829,6 +829,10 @@ void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra) {
void psx_check_interrupts(CPUState *cpu) {
g_cbs.check_interrupts(cpu);
}
void psx_check_interrupts_at(CPUState *cpu, uint32_t resume_pc) {
if (g_cbs.check_interrupts_at) g_cbs.check_interrupts_at(cpu, resume_pc);
else g_cbs.check_interrupts(cpu);
}
void psx_advance_cycles(uint32_t cycles) {
if (g_cbs.advance_cycles) g_cbs.advance_cycles(cycles);
}