mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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).
|
||||
|
||||
+2567
-2454
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user