Ape Escape bring-up: attract demo now loads

Framework changes that get Ape Escape (USA) booting through to its
attract/demo sequence. Captures the working state at this milestone.

Runtime:
- cdrom: read/seek + data-path fixes for disc streaming
- dma / interrupts: channel + IRQ handling needed by the boot chain
- dirty_ram_interp: expanded coverage for self-modifying / RAM handlers
- sio, traps, crash_trace: supporting fixes
- debug_server: probe/inspection commands used during bring-up

Recompiler:
- code_generator / full_function_emitter / function_analysis / main_psx:
  codegen fixes surfaced by the Ape Escape boot path

Committed as-is (no further edits) since the build is in a known-good,
demo-loading state. Branch: codex/ape-escape-support.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-04 19:32:36 -07:00
parent ec7e482691
commit d62d51ba2f
18 changed files with 1571 additions and 156 deletions
+9
View File
@@ -5,6 +5,7 @@
#include <vector>
#include <sstream>
#include <set>
#include <map>
#include "ps1_exe_parser.h"
#include "function_analysis.h"
#include "control_flow.h"
@@ -85,13 +86,21 @@ public:
// Set annotation table (optional — no-op if not called)
void set_annotations(const AnnotationTable* at) { annotations_ = at; }
const std::map<uint32_t, uint32_t>& continuation_owners() const {
return continuation_owners_;
}
private:
const PS1Executable& exe_;
CodeGenConfig config_;
std::set<uint32_t> known_functions_; // Addresses of functions in this compilation unit
std::set<uint32_t> extra_labels_; // Mid-block addresses that need inline labels (jump table targets)
std::map<uint32_t, uint32_t> continuation_owners_;
std::map<uint32_t, std::set<uint32_t>> continuation_entries_by_function_;
const AnnotationTable* annotations_ = nullptr;
void set_continuation_entries(const std::map<uint32_t, uint32_t>& owners);
// Instruction translation
std::string translate_instruction(uint32_t addr, uint32_t instr);
+125 -31
View File
@@ -42,10 +42,18 @@ static uint32_t ram_to_rom(uint32_t addr, const PS1Executable& exe) {
return addr;
}
CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& config)
: exe_(exe), config_(config) {}
std::string CodeGenerator::reg_name(int reg_num) {
CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& config)
: exe_(exe), config_(config) {}
void CodeGenerator::set_continuation_entries(const std::map<uint32_t, uint32_t>& owners) {
continuation_owners_ = owners;
continuation_entries_by_function_.clear();
for (const auto& [continuation_addr, owner_addr] : continuation_owners_) {
continuation_entries_by_function_[owner_addr].insert(continuation_addr);
}
}
std::string CodeGenerator::reg_name(int reg_num) {
if (reg_num >= 0 && reg_num < 32) {
return fmt::format("cpu->gpr[{}]", reg_num);
}
@@ -865,7 +873,8 @@ std::string CodeGenerator::translate_basic_block(
static_cast<uint32_t>(block.instruction_count));
ss << "#endif\n";
}
ss << config_.indent << "psx_check_interrupts(cpu);\n";
ss << config_.indent
<< fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", block.start_addr);
// Translate each instruction in the block
uint32_t addr = block.start_addr;
@@ -1202,12 +1211,13 @@ std::string CodeGenerator::translate_basic_block(
// Function call (jal)
ss << config_.indent << "cpu->gpr[31] = " << fmt::format("0x{:08X}; /* return address */\n", addr + 8);
uint32_t target = block.exit_instr.target;
if (known_functions_.count(target) > 0) {
ss << config_.indent << fmt::format("func_{:08X}(cpu); /* jal */\n", target);
} else {
ss << config_.indent << fmt::format("call_by_address(cpu, 0x{:08X}u); /* external jal */\n", target);
}
if (!block.successors.empty()) {
if (known_functions_.count(target) > 0) {
ss << config_.indent << fmt::format("func_{:08X}(cpu); /* jal */\n", target);
} else {
ss << config_.indent << fmt::format("call_by_address(cpu, 0x{:08X}u); /* external jal */\n", target);
}
ss << config_.indent << "if (cpu->pc != 0) return;\n";
if (!block.successors.empty()) {
ss << config_.indent
<< fmt::format("goto block_{:08X}; /* continue after call */\n", block.successors[0]);
} else {
@@ -1232,10 +1242,11 @@ std::string CodeGenerator::translate_basic_block(
reg_name(rd), addr + 8);
// Dispatch indirect call — target is a runtime register value
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jalr {} */\n",
reg_name(rs), reg_name(rs));
// Continue to successor block (instruction at return address)
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jalr {} */\n",
reg_name(rs), reg_name(rs));
ss << config_.indent << "if (cpu->pc != 0) return;\n";
// Continue to successor block (instruction at return address)
if (!block.successors.empty()) {
ss << config_.indent
<< fmt::format("goto block_{:08X}; /* continue after jalr */\n",
@@ -1289,19 +1300,34 @@ GeneratedFunction CodeGenerator::generate_function(
std::stringstream body_ss;
body_ss << "{\n";
body_ss << config_.indent
<< fmt::format("debug_server_log_call_entry(0x{:08X}u);\n",
<< fmt::format("debug_server_log_call_entry_cpu(0x{:08X}u, cpu);\n",
func.start_addr);
// Add function comment
if (config_.emit_comments) {
body_ss << config_.indent
<< fmt::format("/* Address: 0x{:08X}, Size: {} bytes, Blocks: {} */\n",
func.start_addr, func.size, cfg.blocks.size());
}
// Pre-scan: find jump table targets that land mid-block (not a block boundary).
// These need inline labels so goto statements in the switch can reach them.
extra_labels_.clear();
if (config_.emit_comments) {
body_ss << config_.indent
<< fmt::format("/* Address: 0x{:08X}, Size: {} bytes, Blocks: {} */\n",
func.start_addr, func.size, cfg.blocks.size());
}
auto cont_it = continuation_entries_by_function_.find(func.start_addr);
if (cont_it != continuation_entries_by_function_.end() && !cont_it->second.empty()) {
body_ss << config_.indent << "switch (cpu->pc) {\n";
for (uint32_t continuation_addr : cont_it->second) {
if (!cfg.blocks.count(continuation_addr)) {
continue;
}
body_ss << config_.indent
<< fmt::format(" case 0x{:08X}u: cpu->pc = 0; goto block_{:08X};\n",
continuation_addr, continuation_addr);
}
body_ss << config_.indent << " default: break;\n";
body_ss << config_.indent << "}\n";
}
// Pre-scan: find jump table targets that land mid-block (not a block boundary).
// These need inline labels so goto statements in the switch can reach them.
extra_labels_.clear();
for (const auto& [baddr, blk] : cfg.blocks) {
if (blk.exit_instr.type != ControlFlowType::JumpRegister) continue;
uint32_t jr_r = get_rs(blk.exit_instr.instruction);
@@ -1473,7 +1499,7 @@ std::string CodeGenerator::generate_file(
// Include the generic PSX runtime header.
// This provides CPUState, GTE/trap declarations, and call_by_address().
ss << "#include \"psx_runtime.h\"\n\n";
ss << "extern void debug_server_log_call_entry(uint32_t func_addr);\n\n";
ss << "extern void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu);\n\n";
// Emit reference implementations for unaligned memory helpers.
// These implement the MIPS lwl/lwr/swl/swr semantics.
@@ -1691,11 +1717,79 @@ std::string CodeGenerator::generate_file(
total_new, functions_mut.size());
}
}
set_known_functions(known_addrs);
// Generate all functions first (to collect names)
auto gen_funcs = generate_all_functions(functions_mut, cfgs_mut);
set_known_functions(known_addrs);
std::map<uint32_t, uint32_t> continuation_owners;
size_t call_continuation_count = 0;
size_t internal_continuation_count = 0;
for (const Function& func : functions_mut) {
if (func.is_data_section) {
continue;
}
auto cfg_it = cfgs_mut.find(func.start_addr);
if (cfg_it == cfgs_mut.end()) {
continue;
}
const ControlFlowGraph& cfg = cfg_it->second;
/*
* Any block leader can become a non-local re-entry point. Hardware
* interrupts resume at block leaders after RFE, and unrecognised
* computed jumps (for example jump tables whose pattern was not
* statically decoded) fall back through psx_dispatch. Registering the
* owning function plus cpu->pc lets dispatch land on the exact local
* label instead of treating valid in-function control flow as unknown.
*/
for (const auto& [block_addr, block] : cfg.blocks) {
if (block_addr == cfg.function_start) {
continue;
}
if (block_addr < cfg.function_start || block_addr >= cfg.function_end) {
continue;
}
if (known_addrs.count(block_addr)) {
continue;
}
if (continuation_owners.emplace(block_addr, func.start_addr).second) {
internal_continuation_count++;
}
}
for (const auto& [block_addr, block] : cfg.blocks) {
if (block.exit_instr.type != ControlFlowType::JumpLink &&
block.exit_instr.type != ControlFlowType::JumpLinkReg) {
continue;
}
uint32_t continuation_addr = block.exit_instr.address + 8;
if (continuation_addr == cfg.function_start) {
continue;
}
if (continuation_addr < cfg.function_start || continuation_addr >= cfg.function_end) {
continue;
}
if (!cfg.blocks.count(continuation_addr)) {
continue;
}
if (known_addrs.count(continuation_addr)) {
continue;
}
if (continuation_owners.emplace(continuation_addr, func.start_addr).second) {
call_continuation_count++;
}
}
}
set_continuation_entries(continuation_owners);
fmt::print("Dispatchable internal continuations: {} ({} block leaders, {} call-only)\n",
continuation_owners_.size(), internal_continuation_count, call_continuation_count);
// Generate all functions first (to collect names)
auto gen_funcs = generate_all_functions(functions_mut, cfgs_mut);
// Forward declarations for all functions
if (!gen_funcs.empty()) {
+24 -12
View File
@@ -354,7 +354,7 @@ bool FullFunctionEmitter::emit_function(
out += fmt::format(" psx_advance_cycles({}u);\n", bcyc);
out += "#endif\n";
}
out += " psx_check_interrupts(cpu);\n";
out += fmt::format(" psx_check_interrupts_at(cpu, 0x{:08X}u);\n", addr);
if (should_probe_pc(addr)) {
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n", addr);
}
@@ -416,7 +416,7 @@ bool FullFunctionEmitter::emit_function(
else
out += " return;\n";
} else {
out += fmt::format(" cpu->pc = cpu->gpr[{}]; return;\n",
out += fmt::format(" cpu->pc = cpu->gpr[{}]; psx_rfe_return(cpu); return;\n",
static_cast<int>(rs));
}
} else {
@@ -523,6 +523,7 @@ bool FullFunctionEmitter::emit_function(
uint32_t return_addr = relocate_ra(addr + 8);
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u;\n", return_addr);
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u);\n", target);
out += " if (cpu->pc != 0) return;\n";
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", return_addr);
} else if (kind == "jalr") {
uint32_t ds_addr = addr + 4;
@@ -548,6 +549,7 @@ bool FullFunctionEmitter::emit_function(
}
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
static_cast<int>(rs));
out += " if (cpu->pc != 0) return;\n";
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", return_addr);
}
}
@@ -659,6 +661,7 @@ bool FullFunctionEmitter::emit_function(
pb.terminator_addr);
}
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u);\n", target);
out += " if (cpu->pc != 0) return;\n";
if (target == 0x00006380u) {
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n",
pb.terminator_addr + 1);
@@ -689,6 +692,7 @@ bool FullFunctionEmitter::emit_function(
}
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
static_cast<int>(rs));
out += " if (cpu->pc != 0) return;\n";
// Safety net: if continuation falls outside this function, tail-call to it.
if (!addr_to_raw.count(pb.terminator_addr + 8)) {
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u); /* jalr cont: outside func */\n", return_addr);
@@ -868,7 +872,7 @@ bool FullFunctionEmitter::emit_function(
// Direct-call entry hook: captures into fn_entry ring (gated by
// fn_filter at runtime). Lets us see direct-jal call paths that
// never go through psx_dispatch.
out += fmt::format(" debug_server_log_call_entry(0x{:08X}u);\n", norm);
out += fmt::format(" debug_server_log_call_entry_cpu(0x{:08X}u, cpu);\n", norm);
// Branch predicate locals must be initialized before the continuation
// switch. Continuation entry can jump directly to a merge block that reads
// a predicate set only on a different inbound path.
@@ -924,13 +928,16 @@ 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 psx_rfe_return(CPUState* cpu);\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";
out += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n";
out += "extern uint32_t g_debug_current_func_addr;\n";
out += "extern void debug_server_trace_dispatch(uint32_t func_addr);\n";
out += "extern void debug_server_trace_dispatch_return(uint32_t func_addr, CPUState* cpu);\n\n";
out += "extern uint64_t g_psx_nonlocal_epoch;\n\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
out += "extern int psx_game_address_in_text(uint32_t addr);\n";
out += "#endif\n\n";
@@ -1073,12 +1080,13 @@ void FullFunctionEmitter::emit_dispatch(
out += " return phys;\n";
out += "}\n\n";
out += "extern int dirty_ram_dispatch(CPUState* cpu, uint32_t addr);\n";
out += "extern int dirty_ram_dispatch_until(CPUState* cpu, uint32_t addr, uint32_t stop_addr);\n";
out += "extern int dirty_ram_is_dirty(uint32_t phys);\n";
out += "extern void fntrace_record(CPUState* cpu, uint32_t target);\n";
out += "extern uint64_t g_dispatch_static_hits;\n";
out += "\n";
out += "int g_psx_dispatch_depth = 0;\n\n";
out += "int g_psx_dispatch_depth = 0;\n";
out += "int g_psx_call_depth = 0;\n\n";
out += "static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {\n";
out += " /* Tail-call trampoline: functions signal tail calls by setting\n";
out += " * cpu->pc to the target and returning. We loop here to re-dispatch\n";
@@ -1086,6 +1094,7 @@ void FullFunctionEmitter::emit_dispatch(
out += " * the outermost dispatch returns, so a nested callee cannot interrupt\n";
out += " * before its generated caller runs the post-call continuation. */\n";
out += " int outermost = (g_psx_dispatch_depth++ == 0);\n";
out += " uint64_t nonlocal_epoch = g_psx_nonlocal_epoch;\n";
out += " for (;;) {\n";
out += " /* Always-on call ring: every iteration counts as a separate\n";
out += " * call (initial entry + each tail-call re-dispatch). a0..a3\n";
@@ -1101,7 +1110,7 @@ void FullFunctionEmitter::emit_dispatch(
out += " * path before normalizing it to shell ROM. */\n";
out += " uint32_t game_phys = addr & 0x1FFFFFFFu;\n";
out += " if (psx_game_address_in_text(addr) && dirty_ram_is_dirty(game_phys)) {\n";
out += " found = dirty_ram_dispatch(cpu, addr);\n";
out += " found = dirty_ram_dispatch_until(cpu, addr, stop_addr);\n";
out += " }\n";
out += "#endif\n";
out += " uint32_t phys = normalize(addr);\n";
@@ -1129,7 +1138,7 @@ void FullFunctionEmitter::emit_dispatch(
out += " * boot, interpret the basic block on cpu state. Falls back to\n";
out += " * psx_unknown_dispatch for genuinely unmapped PCs. */\n";
out += " if (!found) {\n";
out += " if (dirty_ram_dispatch(cpu, addr)) {\n";
out += " if (dirty_ram_dispatch_until(cpu, addr, stop_addr)) {\n";
out += " found = 1;\n";
out += " } else {\n";
out += " psx_unknown_dispatch(cpu, addr, phys);\n";
@@ -1137,15 +1146,16 @@ void FullFunctionEmitter::emit_dispatch(
out += " }\n";
out += " if (cpu->pc == 0) {\n";
out += " --g_psx_dispatch_depth;\n";
out += " if (outermost) {\n";
out += " if (outermost && g_psx_call_depth == 0) {\n";
out += " psx_check_interrupts(cpu);\n";
out += " }\n";
out += " return;\n";
out += " }\n";
out += " if (stop_addr != 0 && cpu->pc == stop_addr) {\n";
out += " cpu->pc = 0;\n";
out += " if (stop_addr != 0 && ((cpu->pc ^ stop_addr) & 0x1FFFFFFFu) == 0) {\n";
out += " int consume_stop = (g_psx_nonlocal_epoch == nonlocal_epoch);\n";
out += " if (consume_stop) cpu->pc = 0;\n";
out += " --g_psx_dispatch_depth;\n";
out += " if (outermost) {\n";
out += " if (consume_stop && outermost && g_psx_call_depth == 0) {\n";
out += " psx_check_interrupts(cpu);\n";
out += " }\n";
out += " return;\n";
@@ -1220,11 +1230,13 @@ 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 psx_rfe_return(CPUState* cpu);\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";
full_c += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n";
full_c += "extern void debug_server_log_call_entry(uint32_t func_addr);\n";
full_c += "extern void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu);\n";
full_c += "extern void debug_server_log_probe(uint32_t pc, CPUState *cpu);\n";
full_c += "extern uint32_t g_debug_last_store_pc;\n";
full_c += "/* Phase 1.0e-d: per-block guest cycle accounting.\n";
+62 -24
View File
@@ -133,11 +133,45 @@ static bool is_lui(uint32_t instr, uint32_t& rt_out) {
return true;
}
static bool is_lw_same_reg_base(uint32_t instr, uint32_t reg) {
static bool is_lw_from_reg_base(uint32_t instr, uint32_t base_reg, uint32_t& rt_out) {
uint32_t opcode = (instr >> 26) & 0x3F;
uint32_t rs = (instr >> 21) & 0x1F;
uint32_t rt = (instr >> 16) & 0x1F;
return opcode == 0x23 && rs == reg && rt == reg;
if (opcode != 0x23 || rs != base_reg) return false;
rt_out = rt;
return true;
}
static bool find_preprologue_setup_start(const PS1Executable& exe,
uint32_t prologue_addr,
uint32_t exe_start,
uint32_t& setup_start_out) {
if (prologue_addr < exe_start + 8) return false;
uint32_t setup_start = prologue_addr;
constexpr uint32_t max_setup_pairs = 4;
uint32_t pair_count = 0;
while (setup_start >= exe_start + 8 && pair_count < max_setup_pairs) {
auto prev2_opt = exe.read_word(setup_start - 8);
auto prev1_opt = exe.read_word(setup_start - 4);
if (!prev2_opt.has_value() || !prev1_opt.has_value()) break;
uint32_t lui_rt = 0;
uint32_t load_rt = 0;
if (!is_lui(*prev2_opt, lui_rt) ||
!is_lw_from_reg_base(*prev1_opt, lui_rt, load_rt) ||
load_rt == 0) {
break;
}
setup_start -= 8;
pair_count++;
}
if (pair_count == 0) return false;
setup_start_out = setup_start;
return true;
}
uint32_t FunctionAnalyzer::find_function_start(uint32_t return_addr) {
@@ -177,15 +211,12 @@ uint32_t FunctionAnalyzer::find_function_start(uint32_t return_addr) {
continue; // delay slot, not a real prologue — keep scanning
}
}
if (search_addr >= exe_.header.load_address + 8) {
auto prev2_opt = exe_.read_word(search_addr - 8);
auto prev1_opt = exe_.read_word(search_addr - 4);
if (prev2_opt.has_value() && prev1_opt.has_value()) {
uint32_t lui_rt = 0;
if (is_lui(*prev2_opt, lui_rt) &&
is_lw_same_reg_base(*prev1_opt, lui_rt)) {
return search_addr - 8;
}
{
uint32_t setup_start = 0;
if (find_preprologue_setup_start(exe_, search_addr,
exe_.header.load_address,
setup_start)) {
return setup_start;
}
}
return search_addr;
@@ -346,6 +377,16 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() {
// Only add if target is within EXE range and word-aligned
if (target >= exe_start && target < exe_end && (target & 3) == 0) {
auto target_word = exe_.read_word(target);
int32_t target_stack_size = 0;
if (target_word.has_value() &&
is_prologue(*target_word, target_stack_size)) {
uint32_t setup_start = 0;
if (find_preprologue_setup_start(exe_, target, exe_start,
setup_start)) {
target = setup_start;
}
}
function_starts.insert(target);
}
}
@@ -378,24 +419,18 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() {
}
}
if (addr >= exe_start + 8) {
auto prev2_opt = exe_.read_word(addr - 8);
auto prev1_opt = exe_.read_word(addr - 4);
if (prev2_opt.has_value() && prev1_opt.has_value()) {
uint32_t lui_rt = 0;
if (is_lui(*prev2_opt, lui_rt) &&
is_lw_same_reg_base(*prev1_opt, lui_rt)) {
continue;
}
}
uint32_t entry_addr = addr;
uint32_t setup_start = 0;
if (find_preprologue_setup_start(exe_, addr, exe_start, setup_start)) {
entry_addr = setup_start;
}
auto existing_it = function_starts.upper_bound(addr);
auto existing_it = function_starts.upper_bound(entry_addr);
if (existing_it != function_starts.begin()) {
--existing_it;
auto return_it = function_last_return.find(*existing_it);
if (return_it != function_last_return.end() &&
return_it->second + 8u > addr) {
return_it->second + 8u > entry_addr) {
continue;
}
}
@@ -413,7 +448,10 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() {
}
}
if (saves_ra && function_starts.insert(addr).second) {
if (saves_ra && function_starts.insert(entry_addr).second) {
if (entry_addr != addr) {
function_starts.erase(addr);
}
strong_prologues++;
}
}
+23 -1
View File
@@ -5,6 +5,7 @@
#include <string>
#include <algorithm>
#include <set>
#include <map>
#include "ps1_exe_parser.h"
#include "gte.h"
@@ -388,6 +389,7 @@ int main(int argc, char** argv) {
fmt::print("Generating complete C file: {}\n", output_filename.string());
std::string full_c_code = codegen.generate_file(analysis_result.functions, all_cfgs);
const auto& continuation_owners = codegen.continuation_owners();
std::set<uint32_t> dispatch_addrs;
{
const std::string marker = "void func_";
@@ -448,11 +450,29 @@ int main(int argc, char** argv) {
ds << "/* Maps PS1 address to compiled game code. Returns 1 if dispatched, 0 if unknown. */\n";
ds << "int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr) {\n";
ds << " uint32_t phys = addr & 0x1FFFFFFFu;\n";
ds << fmt::format(" if (phys >= 0x{:08X}u && phys < 0x{:08X}u) {{\n",
game_text_start, game_text_end);
ds << fmt::format(" addr = 0x{:08X}u + (phys - 0x{:08X}u);\n",
exe->load_address(), game_text_start);
ds << " }\n";
ds << " switch (addr) {\n";
for (uint32_t addr : dispatch_addrs) {
ds << fmt::format(" case 0x{:08X}u: func_{:08X}(cpu); return 1;\n",
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0; func_{:08X}(cpu); return 1;\n",
addr, addr);
}
size_t continuation_dispatch_count = 0;
for (const auto& [continuation_addr, owner_addr] : continuation_owners) {
if (dispatch_addrs.count(continuation_addr)) {
continue;
}
if (!dispatch_addrs.count(owner_addr)) {
continue;
}
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0x{:08X}u; func_{:08X}(cpu); return 1;\n",
continuation_addr, continuation_addr, owner_addr);
continuation_dispatch_count++;
}
ds << " default: return 0;\n";
ds << " }\n";
ds << "}\n";
@@ -463,6 +483,8 @@ int main(int argc, char** argv) {
dispatch_file.close();
fmt::print("✓ Dispatch table written ({} entries)\n\n",
dispatch_addrs.size());
fmt::print("Dispatch continuation entries: {}\n\n",
continuation_dispatch_count);
} else {
fmt::print(stderr, "⚠ Failed to write dispatch file\n\n");
}
+33 -1
View File
@@ -116,9 +116,39 @@ typedef struct CDROMTraceEntry {
} CDROMTraceEntry;
#define CDROM_TRACE_CAP (1 << 16)
#define CDROM_SECTOR_HISTORY_CAP (1 << 10)
#define CDROM_COMMAND_HISTORY_CAP (1 << 13)
#define CDROM_SECTOR_HISTORY_CAP (1 << 13)
#define CDROM_SECTOR_HISTORY_BYTES 128
typedef struct CDROMCommandHistoryEntry {
uint64_t seq;
uint32_t frame;
uint32_t func;
uint32_t pc;
uint32_t i_stat;
uint8_t kind;
uint8_t cmd;
uint8_t param_count;
uint8_t params[16];
uint8_t stat;
uint8_t request;
uint8_t irq_enable;
uint8_t irq_flag;
uint8_t mode;
uint8_t seek_min;
uint8_t seek_sec;
uint8_t seek_sect;
uint8_t read_min;
uint8_t read_sec;
uint8_t read_sect;
uint8_t read_cmd;
uint8_t reading;
uint8_t pending_cmd;
uint8_t pending_pending;
uint8_t queued_cmd;
uint8_t queued_pending;
} CDROMCommandHistoryEntry;
typedef struct CDROMSectorHistoryEntry {
uint64_t seq;
int lba;
@@ -141,6 +171,8 @@ typedef struct CDROMSectorHistoryEntry {
void cdrom_debug_snapshot(CDROMDebugState* out);
uint64_t cdrom_debug_get_trace(const CDROMTraceEntry** out_entries);
void cdrom_debug_clear_trace(void);
uint64_t cdrom_debug_get_command_history(const CDROMCommandHistoryEntry** out_entries);
void cdrom_debug_clear_command_history(void);
uint32_t cdrom_debug_copy_last_sector(uint32_t offset, uint32_t len,
uint8_t* out,
CDROMSectorDebugState* state);
+2
View File
@@ -126,6 +126,7 @@ void debug_server_trace_dispatch_return(uint32_t func_addr, CPUState *cpu);
* touching the shadow stack — the native C call/return discipline already
* handles unwinding for direct calls. */
void debug_server_log_call_entry(uint32_t func_addr);
void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu);
/* Last store instruction PC — set by the recompiler before every memory
* store (sb/sh/sw/swl/swr/swc2). Read by SIO/MMIO write handlers to
@@ -211,6 +212,7 @@ void debug_server_freeze_dump_frame_history_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_sio_pc_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_thread_trace_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_restore_trace_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_irq_check_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_first_zero_restore_json(FILE *f);
void debug_server_freeze_dump_entryint_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_cdrom_state_json(FILE *f);
+75
View File
@@ -28,6 +28,7 @@ extern "C" {
* either 0 (block ended on jr $ra style return) or the target of a tail
* jump that the dispatch trampoline should re-enter. */
int dirty_ram_dispatch(CPUState* cpu, uint32_t addr);
int dirty_ram_dispatch_until(CPUState* cpu, uint32_t addr, uint32_t stop_addr);
/* Test whether a given physical kernel-RAM address is in a page that was
* written-to since boot. Defined in memory.c. */
@@ -115,6 +116,68 @@ typedef struct {
extern DirtyRamFlowLogEntry g_dirty_ram_flow_log[DIRTY_RAM_FLOW_LOG_CAP];
extern uint64_t g_dirty_ram_flow_log_seq;
#define DIRTY_RAM_EXIT_LOG_CAP (1u << 18)
typedef struct {
uint64_t seq;
uint32_t entry;
uint32_t pc;
uint32_t target;
uint32_t stop_addr;
uint32_t reason;
uint32_t insns;
uint32_t s0;
uint32_t s1;
uint32_t s2;
uint32_t s3;
uint32_t sp;
uint32_t ra;
uint32_t v0;
uint32_t v1;
uint32_t a0;
uint32_t a1;
uint32_t a2;
uint32_t a3;
uint32_t current_tcb;
uint32_t frame;
uint8_t handled;
uint8_t in_exception;
uint8_t pad[2];
} DirtyRamExitLogEntry;
extern DirtyRamExitLogEntry g_dirty_ram_exit_log[DIRTY_RAM_EXIT_LOG_CAP];
extern uint64_t g_dirty_ram_exit_log_seq;
#define DIRTY_RAM_CALL_LOG_CAP (1u << 16)
typedef struct {
uint64_t seq;
uint32_t pc;
uint32_t target;
uint32_t return_pc;
uint32_t before_s0;
uint32_t after_s0;
uint32_t before_s1;
uint32_t after_s1;
uint32_t before_s2;
uint32_t after_s2;
uint32_t before_s3;
uint32_t after_s3;
uint32_t before_sp;
uint32_t after_sp;
uint32_t before_ra;
uint32_t after_ra;
uint32_t a0;
uint32_t a1;
uint32_t a2;
uint32_t a3;
uint32_t current_tcb;
uint32_t frame;
uint8_t defer_interrupts;
uint8_t in_exception;
uint8_t returned_pc_nonzero;
uint8_t pad;
} DirtyRamCallLogEntry;
extern DirtyRamCallLogEntry g_dirty_ram_call_log[DIRTY_RAM_CALL_LOG_CAP];
extern uint64_t g_dirty_ram_call_log_seq;
#define DIRTY_RAM_INSN_LOG_CAP (1u << 16)
typedef struct {
uint64_t seq;
@@ -124,6 +187,12 @@ typedef struct {
uint32_t target;
uint32_t before_s0;
uint32_t after_s0;
uint32_t before_s1;
uint32_t after_s1;
uint32_t before_s2;
uint32_t after_s2;
uint32_t before_s3;
uint32_t after_s3;
uint32_t sp;
uint32_t ra;
uint32_t v0;
@@ -146,6 +215,12 @@ typedef struct {
extern DirtyRamInsnLogEntry g_dirty_ram_insn_log[DIRTY_RAM_INSN_LOG_CAP];
extern uint64_t g_dirty_ram_insn_log_seq;
#define DIRTY_RAM_TRACE_MAX_RANGES 16
int dirty_ram_trace_add_range(uint32_t lo, uint32_t hi);
void dirty_ram_trace_clear_ranges(void);
int dirty_ram_trace_range_count(void);
int dirty_ram_trace_get_range(int slot, uint32_t *lo, uint32_t *hi);
#ifdef __cplusplus
}
#endif
+35
View File
@@ -68,10 +68,45 @@ typedef struct DMATraceEntry {
} DMATraceEntry;
#define DMA_TRACE_CAP (1 << 14)
#define DMA_CDROM_HISTORY_CAP (1 << 13)
#define DMA_CDROM_HISTORY_WORDS 16
typedef struct DMACDROMHistoryEntry {
uint64_t seq;
uint32_t frame_start;
uint32_t frame_end;
uint32_t start_addr;
uint32_t final_addr;
uint32_t requested_words;
uint32_t moved_words;
uint32_t bcr;
uint32_t chcr;
uint32_t dpcr;
uint32_t dicr_start;
uint32_t dicr_end;
uint32_t i_stat_start;
uint32_t i_stat_end;
uint32_t func;
uint32_t pc;
int lba;
int sector_size;
int sector_read_pos_start;
int sector_read_pos_end;
uint8_t mode;
uint8_t sector_available_start;
uint8_t sector_available_end;
uint8_t completed;
uint8_t first_count;
uint8_t last_count;
uint32_t first_words[DMA_CDROM_HISTORY_WORDS];
uint32_t last_words[DMA_CDROM_HISTORY_WORDS];
} DMACDROMHistoryEntry;
uint64_t dma_debug_get_trace(const DMATraceEntry** out_entries);
void dma_debug_clear_trace(void);
void dma_debug_get_state(DMADebugState* out);
uint64_t dma_debug_get_cdrom_history(const DMACDROMHistoryEntry** out_entries);
void dma_debug_clear_cdrom_history(void);
#ifdef __cplusplus
}
+25
View File
@@ -28,6 +28,7 @@ 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);
void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc);
/* Accumulate emitted PSX cycles toward the next VBlank trigger.
* Called from psx_advance_cycles() so the VBlank rate is gated on
@@ -49,10 +50,34 @@ void psx_get_freeze_diag(uint64_t *out_total_checks,
uint64_t *out_exc_entries,
uint64_t *out_exc_reentry_blocks);
#define PSX_IRQ_CHECK_TRACE_CAP (1u << 18)
typedef struct PsxIrqCheckTraceEntry {
uint64_t seq;
uint32_t frame;
uint32_t resume_pc;
uint32_t i_stat;
uint32_t i_mask;
uint32_t sr;
uint32_t cause;
uint32_t call_depth;
uint32_t reason;
} PsxIrqCheckTraceEntry;
extern PsxIrqCheckTraceEntry g_psx_irq_check_trace[PSX_IRQ_CHECK_TRACE_CAP];
extern uint64_t g_psx_irq_check_trace_seq;
extern uint64_t g_psx_nonlocal_epoch;
/* Bumped whenever BIOS exception/RestoreState/RFE flow abandons the current
* generated C call stack. Dispatch uses this to avoid consuming a synthetic
* JAL/JALR return address as a normal inline return after guest context has
* been restored elsewhere. */
void psx_note_nonlocal_control_flow(void);
/* longjmp back to psx_check_interrupts, unwinding the exception handler.
* Called by ReturnFromException (B0:0x17 or SYSCALL(3)) when inside
* the exception handler to model real hardware's RFE+JR $k0 unwind. */
void psx_exception_longjmp(void);
void psx_rfe_return(struct CPUState* cpu);
/* longjmp back to psx_check_interrupts for RestoreState redirection.
*
+66 -2
View File
@@ -73,6 +73,8 @@ static uint8_t last_sector_xa_submode;
static uint8_t last_sector_xa_coding;
static CDROMSectorHistoryEntry sector_history[CDROM_SECTOR_HISTORY_CAP];
static uint64_t sector_history_seq;
static CDROMCommandHistoryEntry command_history[CDROM_COMMAND_HISTORY_CAP];
static uint64_t command_history_seq;
/* Seek target */
static uint8_t seek_min, seek_sec, seek_sect;
@@ -218,6 +220,43 @@ static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width
e->read_delay = read_delay;
}
static void record_command_history(uint8_t kind, uint8_t cmd,
const uint8_t* params, int count) {
CDROMCommandHistoryEntry *e =
&command_history[command_history_seq % CDROM_COMMAND_HISTORY_CAP];
memset(e, 0, sizeof(*e));
e->seq = command_history_seq++;
e->frame = (uint32_t)s_frame_count;
e->func = g_debug_current_func_addr;
e->pc = g_debug_last_store_pc;
e->i_stat = i_stat;
e->kind = kind;
e->cmd = cmd;
if (count < 0) count = 0;
if (count > PARAM_FIFO_SIZE) count = PARAM_FIFO_SIZE;
e->param_count = (uint8_t)count;
if (params && count > 0) {
memcpy(e->params, params, (size_t)count);
}
e->stat = stat_reg;
e->request = request_reg;
e->irq_enable = irq_enable;
e->irq_flag = irq_flag;
e->mode = mode_reg;
e->seek_min = seek_min;
e->seek_sec = seek_sec;
e->seek_sect = seek_sect;
e->read_min = (uint8_t)read_min;
e->read_sec = (uint8_t)read_sec;
e->read_sect = (uint8_t)read_sect;
e->read_cmd = read_cmd;
e->reading = (uint8_t)(reading ? 1 : 0);
e->pending_cmd = pending.cmd;
e->pending_pending = (uint8_t)(pending.pending ? 1 : 0);
e->queued_cmd = queued_cmd.cmd;
e->queued_pending = (uint8_t)(queued_cmd.pending ? 1 : 0);
}
static int xa_is_audio_realtime(const CDROMSectorDelivery *d) {
return d &&
d->raw_mode == CDROM_SECTOR_MODE2 &&
@@ -689,10 +728,16 @@ static void queue_or_exec_command(uint8_t cmd) {
queued_cmd.pending = 1;
param_count = 0;
trace_cdrom('Q', 0, cmd, 0);
record_command_history('Q', cmd, queued_cmd.params, queued_cmd.param_count);
}
static void exec_command(uint8_t cmd) {
trace_cdrom('C', 0, cmd, 0);
uint8_t cmd_params[PARAM_FIFO_SIZE];
int cmd_param_count = param_count;
if (cmd_param_count < 0) cmd_param_count = 0;
if (cmd_param_count > PARAM_FIFO_SIZE) cmd_param_count = PARAM_FIFO_SIZE;
memcpy(cmd_params, param_fifo, (size_t)cmd_param_count);
response_clear();
switch (cmd) {
@@ -941,6 +986,7 @@ static void exec_command(uint8_t cmd) {
/* Fire the CDROM IRQ for the immediate response.
* Delayed responses (pending) fire from process_pending(). */
fire_cdrom_irq();
record_command_history('C', cmd, cmd_params, cmd_param_count);
}
static void process_pending(uint32_t cycles) {
@@ -1022,12 +1068,19 @@ static void process_pending(uint32_t cycles) {
static void process_read_stream(uint32_t cycles) {
if (!reading) return;
/*
* The controller exposes one sector buffer and one pending interrupt at a
* time. Do not let the stream timer accumulate a backlog while software is
* still draining or acknowledging the previous sector, or games that issue
* a Pause/next command from their data-ready callback can observe an extra
* immediate sector.
*/
if (sector_available || irq_flag != 0) return;
if (cycles > 0) {
read_delay -= (int)cycles;
}
if (sector_available || irq_flag != 0) return;
if (read_delay <= 0) {
deliver_read_sector();
read_delay += sector_delay_cycles();
@@ -1078,6 +1131,7 @@ void cdrom_init(const char* cue_path) {
stat_reg = has_disc() ? CDSTAT_MOTOR : CDSTAT_SHELL;
cdrom_debug_clear_trace();
cdrom_debug_clear_command_history();
trace_cdrom('N', 0, has_disc() ? 1u : 0u, 0);
}
@@ -1271,6 +1325,16 @@ void cdrom_debug_clear_trace(void) {
cdrom_trace_seq = 0;
}
uint64_t cdrom_debug_get_command_history(const CDROMCommandHistoryEntry** out_entries) {
if (out_entries) *out_entries = command_history;
return command_history_seq;
}
void cdrom_debug_clear_command_history(void) {
memset(command_history, 0, sizeof(command_history));
command_history_seq = 0;
}
uint64_t cdrom_debug_get_sector_history(const CDROMSectorHistoryEntry** out_entries) {
if (out_entries) *out_entries = sector_history;
return sector_history_seq;
+51 -4
View File
@@ -32,6 +32,7 @@
#include "cpu_state.h"
#include "crash_trace.h"
#include "debug_server.h"
/* Output path — overwritten per dump. */
static const char *kReportPath = "psx_last_run_report.json";
@@ -55,7 +56,9 @@ extern uint64_t crash_trace_dispatch_seq_get(void);
* UnknownDispatchEntry. Accessor wrappers exported by debug_server.c. */
typedef struct {
uint64_t seq;
uint32_t addr, phys, ra, a0, a1, frame, pad;
uint32_t addr, phys, ra, sp, v0, v1;
uint32_t a0, a1, a2, a3, t0, t1, t2;
uint32_t s0, s1, s2, s3, frame, current_func;
} UnknownDispatchEntry;
#define UNKNOWN_DISPATCH_CAP (1 << 16)
extern UnknownDispatchEntry crash_trace_unknown_get(uint64_t seq);
@@ -64,6 +67,26 @@ extern uint64_t crash_trace_unknown_seq_get(void);
/* Dirty-RAM block log (defined in dirty_ram_interp.c). */
#include "dirty_ram_interp.h"
static void write_sidecar_trace(const char *path, const char *field,
void (*dump_fn)(FILE *, uint32_t),
uint32_t max_count,
const char *reason)
{
FILE *f = fopen(path, "wb");
if (!f) return;
fprintf(f,
"{\n"
" \"reason\": \"%s\",\n"
" \"frame\": %llu,\n"
" \"%s\": ",
reason ? reason : "(unknown)",
(unsigned long long)s_frame_count,
field);
dump_fn(f, max_count);
fputs("\n}\n", f);
fclose(f);
}
/* JSON helpers. Hand-rolled to avoid allocations on the SEH path. */
static int append_str(char *buf, size_t cap, size_t *pos, const char *s) {
@@ -201,11 +224,19 @@ void psx_crash_trace_dump(const char *reason, void *seh_info) {
UnknownDispatchEntry e = crash_trace_unknown_get(start + i);
append_fmt(buf, sizeof(buf), &pos,
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"phys\":\"0x%08X\","
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
"\"frame\":%u}",
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\",\"t2\":\"0x%08X\","
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
"\"current_func\":\"0x%08X\",\"frame\":%u}",
i == 0 ? "" : ",",
(unsigned long long)e.seq, e.addr, e.phys,
e.ra, e.a0, e.a1, e.frame);
e.ra, e.sp, e.v0, e.v1, e.a0, e.a1, e.a2, e.a3,
e.t0, e.t1, e.t2, e.s0, e.s1, e.s2, e.s3,
e.current_func, e.frame);
}
append_str(buf, sizeof(buf), &pos, "]\n },\n");
}
@@ -243,6 +274,22 @@ void psx_crash_trace_dump(const char *reason, void *seh_info) {
fwrite(buf, 1, pos, f);
fclose(f);
}
write_sidecar_trace("psx_last_restore_trace.json", "restore_trace",
debug_server_freeze_dump_restore_trace_json, 2048,
reason);
write_sidecar_trace("psx_last_thread_trace.json", "thread_trace",
debug_server_freeze_dump_thread_trace_json, 1024,
reason);
write_sidecar_trace("psx_last_irq_check_log.json", "irq_check_log",
debug_server_freeze_dump_irq_check_json, 2048,
reason);
write_sidecar_trace("psx_last_fn_entry_trace.json", "fn_entry_trace",
debug_server_freeze_dump_fn_entry_json, 2048,
reason);
write_sidecar_trace("psx_last_evcb_trace.json", "evcb_trace",
debug_server_freeze_dump_evcb_json, 1024,
reason);
}
/* ── Crash handlers ──────────────────────────────────────────────────── */
+604 -22
View File
@@ -377,6 +377,13 @@ typedef struct {
uint32_t current_tcb;
uint32_t current_tcb_state;
uint32_t current_tcb_cause;
uint32_t saved_s0;
uint32_t saved_s1;
uint32_t saved_s2;
uint32_t saved_s3;
uint32_t saved_sp;
uint32_t saved_ra;
uint32_t saved_pc;
uint32_t sr;
uint32_t epc;
uint32_t istat;
@@ -567,6 +574,16 @@ void debug_server_log_restore_event(uint32_t kind, uint32_t target_pc, uint32_t
e->current_tcb = (cpu && e->tcbh_ptr) ? trace_read_word(cpu, e->tcbh_ptr) : 0;
e->current_tcb_state = (cpu && e->current_tcb) ? trace_read_word(cpu, e->current_tcb) : 0;
e->current_tcb_cause = (cpu && e->current_tcb) ? trace_read_word(cpu, e->current_tcb + 152u) : 0;
if (cpu && e->current_tcb) {
uint32_t save = e->current_tcb + 8u;
e->saved_s0 = trace_read_word(cpu, save + 64u);
e->saved_s1 = trace_read_word(cpu, save + 68u);
e->saved_s2 = trace_read_word(cpu, save + 72u);
e->saved_s3 = trace_read_word(cpu, save + 76u);
e->saved_sp = trace_read_word(cpu, save + 116u);
e->saved_ra = trace_read_word(cpu, save + 124u);
e->saved_pc = trace_read_word(cpu, save + 128u);
}
e->queue_base = cpu ? trace_read_word(cpu, 0x00000100u) : 0;
for (size_t q = 0; q < RESTORE_TRACE_QUEUE_COUNT; q++) {
uint32_t queue_addr = e->queue_base + (uint32_t)(q * 8u);
@@ -854,10 +871,22 @@ typedef struct {
uint32_t addr;
uint32_t phys;
uint32_t ra;
uint32_t sp;
uint32_t v0;
uint32_t v1;
uint32_t a0;
uint32_t a1;
uint32_t a2;
uint32_t a3;
uint32_t t0;
uint32_t t1;
uint32_t t2;
uint32_t s0;
uint32_t s1;
uint32_t s2;
uint32_t s3;
uint32_t frame;
uint32_t pad;
uint32_t current_func;
} UnknownDispatchEntry;
static UnknownDispatchEntry s_unknown_ring[UNKNOWN_DISPATCH_CAP];
static uint64_t s_unknown_seq = 0;
@@ -875,7 +904,13 @@ static UnknownUniqueEntry s_unknown_unique[UNKNOWN_UNIQUE_CAP];
static int s_unknown_unique_count = 0;
void psx_unknown_dispatch_record(uint32_t addr, uint32_t phys,
uint32_t ra, uint32_t a0, uint32_t a1)
uint32_t ra, uint32_t sp,
uint32_t v0, uint32_t v1,
uint32_t a0, uint32_t a1,
uint32_t a2, uint32_t a3,
uint32_t t0, uint32_t t1, uint32_t t2,
uint32_t s0, uint32_t s1,
uint32_t s2, uint32_t s3)
{
uint64_t seq = s_unknown_seq++;
UnknownDispatchEntry *e = &s_unknown_ring[seq & (UNKNOWN_DISPATCH_CAP - 1u)];
@@ -883,9 +918,22 @@ void psx_unknown_dispatch_record(uint32_t addr, uint32_t phys,
e->addr = addr;
e->phys = phys;
e->ra = ra;
e->sp = sp;
e->v0 = v0;
e->v1 = v1;
e->a0 = a0;
e->a1 = a1;
e->a2 = a2;
e->a3 = a3;
e->t0 = t0;
e->t1 = t1;
e->t2 = t2;
e->s0 = s0;
e->s1 = s1;
e->s2 = s2;
e->s3 = s3;
e->frame = (uint32_t)s_frame_count;
e->current_func = g_debug_current_func_addr;
/* Per-target count (linear probe). */
uint32_t idx = (phys >> 2) % UNKNOWN_UNIQUE_CAP;
for (int i = 0; i < UNKNOWN_UNIQUE_CAP; i++) {
@@ -1728,16 +1776,17 @@ static int is_chain_epilogue(uint32_t phys) {
* psx_dispatch (e.g. shell -> firstfile -> bu_read -> card_read). The shadow
* stack is owned by function_trace_record and is only meaningful for
* indirect dispatches; direct calls return through the native C stack. */
void debug_server_log_call_entry(uint32_t func_addr) {
static void debug_server_log_call_entry_with_cpu(uint32_t func_addr, CPUState *cpu) {
#ifdef PSX_NO_DEBUG_TOOLS
/* Hottest call site in the binary — called at the top of every
* recompiled function. Early-return makes it a 1-instruction
* function-call cost; the compiler will likely PLT it through. */
(void)func_addr;
(void)cpu;
return;
#endif
s_fn_direct_seen++;
if (!debug_cpu_ptr) {
if (!cpu) {
s_fn_direct_no_cpu++;
return;
}
@@ -1751,21 +1800,29 @@ void debug_server_log_call_entry(uint32_t func_addr) {
e->seq = s_fn_entry_seq;
e->paired_exit_seq = 0;
e->func_addr = func_addr;
e->ra = debug_cpu_ptr->gpr[31];
e->a0 = debug_cpu_ptr->gpr[4];
e->a1 = debug_cpu_ptr->gpr[5];
e->a2 = debug_cpu_ptr->gpr[6];
e->a3 = debug_cpu_ptr->gpr[7];
e->t1 = debug_cpu_ptr->gpr[9];
e->s0 = debug_cpu_ptr->gpr[16];
e->s1 = debug_cpu_ptr->gpr[17];
e->s2 = debug_cpu_ptr->gpr[18];
e->s3 = debug_cpu_ptr->gpr[19];
e->ra = cpu->gpr[31];
e->a0 = cpu->gpr[4];
e->a1 = cpu->gpr[5];
e->a2 = cpu->gpr[6];
e->a3 = cpu->gpr[7];
e->t1 = cpu->gpr[9];
e->s0 = cpu->gpr[16];
e->s1 = cpu->gpr[17];
e->s2 = cpu->gpr[18];
e->s3 = cpu->gpr[19];
e->depth = (uint32_t)s_fn_stack_top;
e->frame = (uint32_t)s_frame_count;
s_fn_entry_seq++;
}
void debug_server_log_call_entry(uint32_t func_addr) {
debug_server_log_call_entry_with_cpu(func_addr, debug_cpu_ptr);
}
void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu) {
debug_server_log_call_entry_with_cpu(func_addr, cpu);
}
static void exception_trace_record(uint32_t kind, uint32_t func_addr, CPUState *cpu)
{
if (!cpu || !psx_get_in_exception()) return;
@@ -2075,6 +2132,157 @@ static void handle_dirty_flow_log(int id, const char *json)
free(out);
}
static void handle_dirty_exit_log(int id, const char *json)
{
char buf[32];
uint32_t entry_lo = 0, entry_hi = 0xFFFFFFFFu;
uint32_t target_lo = 0, target_hi = 0xFFFFFFFFu;
uint32_t reason = 0;
int has_reason = 0;
if (json_get_str(json, "entry_lo", buf, sizeof(buf))) entry_lo = hex_to_u32(buf);
if (json_get_str(json, "entry_hi", buf, sizeof(buf))) entry_hi = hex_to_u32(buf);
if (json_get_str(json, "target_lo", buf, sizeof(buf))) target_lo = hex_to_u32(buf);
if (json_get_str(json, "target_hi", buf, sizeof(buf))) target_hi = hex_to_u32(buf);
if (json_get_str(json, "reason", buf, sizeof(buf))) {
reason = hex_to_u32(buf);
has_reason = 1;
}
int count = json_get_int(json, "count", 256);
if (count < 1) count = 1;
if (count > (int)DIRTY_RAM_EXIT_LOG_CAP) count = DIRTY_RAM_EXIT_LOG_CAP;
uint64_t total = g_dirty_ram_exit_log_seq;
uint64_t avail = (total < DIRTY_RAM_EXIT_LOG_CAP) ? total : DIRTY_RAM_EXIT_LOG_CAP;
const size_t BUF_SZ = 16 * 1024 * 1024;
char *out = (char *)malloc(BUF_SZ);
if (!out) {
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id);
return;
}
size_t pos = 0;
pos += snprintf(out + pos, BUF_SZ - pos,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
"\"entry_lo\":\"0x%08X\",\"entry_hi\":\"0x%08X\","
"\"target_lo\":\"0x%08X\",\"target_hi\":\"0x%08X\","
"\"has_reason\":%d,\"reason\":%u,\"entries\":[",
id, (unsigned long long)total, (unsigned long long)avail,
entry_lo, entry_hi, target_lo, target_hi,
has_reason ? 1 : 0, reason);
int emitted = 0;
for (uint64_t i = 0; i < avail && emitted < count; i++) {
uint64_t seq = total - 1 - i;
DirtyRamExitLogEntry *e =
&g_dirty_ram_exit_log[seq & (DIRTY_RAM_EXIT_LOG_CAP - 1u)];
if (e->entry < entry_lo || e->entry >= entry_hi) continue;
if (e->target < target_lo || e->target >= target_hi) continue;
if (has_reason && e->reason != reason) continue;
if (pos > BUF_SZ - 1024) break;
pos += snprintf(out + pos, BUF_SZ - pos,
"%s{\"seq\":%llu,\"entry\":\"0x%08X\",\"pc\":\"0x%08X\","
"\"target\":\"0x%08X\",\"stop_addr\":\"0x%08X\","
"\"reason\":%u,\"insns\":%u,"
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
"\"current_tcb\":\"0x%08X\",\"frame\":%u,"
"\"handled\":%u,\"in_exception\":%u}",
emitted == 0 ? "" : ",",
(unsigned long long)e->seq, e->entry, e->pc,
e->target, e->stop_addr, e->reason, e->insns,
e->s0, e->s1, e->s2, e->s3, e->sp, e->ra,
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
e->current_tcb, e->frame,
(unsigned)e->handled, (unsigned)e->in_exception);
emitted++;
}
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
debug_server_send_line(out);
free(out);
}
static void handle_dirty_call_log(int id, const char *json)
{
char buf[32];
uint32_t pc_lo = 0, pc_hi = 0xFFFFFFFFu;
uint32_t target_lo = 0, target_hi = 0xFFFFFFFFu;
if (json_get_str(json, "pc_lo", buf, sizeof(buf))) pc_lo = hex_to_u32(buf);
if (json_get_str(json, "pc_hi", buf, sizeof(buf))) pc_hi = hex_to_u32(buf);
if (json_get_str(json, "target_lo", buf, sizeof(buf))) target_lo = hex_to_u32(buf);
if (json_get_str(json, "target_hi", buf, sizeof(buf))) target_hi = hex_to_u32(buf);
int changed_only = json_get_int(json, "changed_only", 0);
int count = json_get_int(json, "count", 256);
if (count < 1) count = 1;
if (count > (int)DIRTY_RAM_CALL_LOG_CAP) count = DIRTY_RAM_CALL_LOG_CAP;
uint64_t total = g_dirty_ram_call_log_seq;
uint64_t avail = (total < DIRTY_RAM_CALL_LOG_CAP) ? total : DIRTY_RAM_CALL_LOG_CAP;
const size_t BUF_SZ = 16 * 1024 * 1024;
char *out = (char *)malloc(BUF_SZ);
if (!out) {
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id);
return;
}
size_t pos = 0;
pos += snprintf(out + pos, BUF_SZ - pos,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
"\"pc_lo\":\"0x%08X\",\"pc_hi\":\"0x%08X\","
"\"target_lo\":\"0x%08X\",\"target_hi\":\"0x%08X\","
"\"changed_only\":%d,\"entries\":[",
id, (unsigned long long)total, (unsigned long long)avail,
pc_lo, pc_hi, target_lo, target_hi, changed_only ? 1 : 0);
int emitted = 0;
for (uint64_t i = 0; i < avail && emitted < count; i++) {
uint64_t seq = total - 1 - i;
DirtyRamCallLogEntry *e =
&g_dirty_ram_call_log[seq & (DIRTY_RAM_CALL_LOG_CAP - 1u)];
if (e->pc < pc_lo || e->pc >= pc_hi) continue;
if (e->target < target_lo || e->target >= target_hi) continue;
if (changed_only &&
e->before_s0 == e->after_s0 &&
e->before_s1 == e->after_s1 &&
e->before_s2 == e->after_s2 &&
e->before_s3 == e->after_s3) {
continue;
}
if (pos > BUF_SZ - 1024) break;
pos += snprintf(out + pos, BUF_SZ - pos,
"%s{\"seq\":%llu,\"pc\":\"0x%08X\","
"\"target\":\"0x%08X\",\"return_pc\":\"0x%08X\","
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
"\"before_s1\":\"0x%08X\",\"after_s1\":\"0x%08X\","
"\"before_s2\":\"0x%08X\",\"after_s2\":\"0x%08X\","
"\"before_s3\":\"0x%08X\",\"after_s3\":\"0x%08X\","
"\"before_sp\":\"0x%08X\",\"after_sp\":\"0x%08X\","
"\"before_ra\":\"0x%08X\",\"after_ra\":\"0x%08X\","
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
"\"current_tcb\":\"0x%08X\",\"frame\":%u,"
"\"defer_interrupts\":%u,\"in_exception\":%u,"
"\"returned_pc_nonzero\":%u}",
emitted == 0 ? "" : ",",
(unsigned long long)e->seq, e->pc, e->target,
e->return_pc,
e->before_s0, e->after_s0,
e->before_s1, e->after_s1,
e->before_s2, e->after_s2,
e->before_s3, e->after_s3,
e->before_sp, e->after_sp,
e->before_ra, e->after_ra,
e->a0, e->a1, e->a2, e->a3,
e->current_tcb, e->frame,
(unsigned)e->defer_interrupts,
(unsigned)e->in_exception,
(unsigned)e->returned_pc_nonzero);
emitted++;
}
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
debug_server_send_line(out);
free(out);
}
static void handle_dirty_insn_log(int id, const char *json)
{
char buf[32];
@@ -2107,12 +2315,21 @@ static void handle_dirty_insn_log(int id, const char *json)
DirtyRamInsnLogEntry *e =
&g_dirty_ram_insn_log[seq & (DIRTY_RAM_INSN_LOG_CAP - 1u)];
if (e->pc < pc_lo || e->pc >= pc_hi) continue;
if (changed_only && e->before_s0 == e->after_s0) continue;
if (changed_only &&
e->before_s0 == e->after_s0 &&
e->before_s1 == e->after_s1 &&
e->before_s2 == e->after_s2 &&
e->before_s3 == e->after_s3) {
continue;
}
if (pos > BUF_SZ - 768) break;
pos += snprintf(out + pos, BUF_SZ - pos,
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"insn\":\"0x%08X\","
"\"next_pc\":\"0x%08X\",\"target\":\"0x%08X\","
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
"\"before_s1\":\"0x%08X\",\"after_s1\":\"0x%08X\","
"\"before_s2\":\"0x%08X\",\"after_s2\":\"0x%08X\","
"\"before_s3\":\"0x%08X\",\"after_s3\":\"0x%08X\","
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
@@ -2125,7 +2342,11 @@ static void handle_dirty_insn_log(int id, const char *json)
emitted == 0 ? "" : ",",
(unsigned long long)e->seq,
e->pc, e->insn, e->next_pc, e->target,
e->before_s0, e->after_s0, e->sp, e->ra,
e->before_s0, e->after_s0,
e->before_s1, e->after_s1,
e->before_s2, e->after_s2,
e->before_s3, e->after_s3,
e->sp, e->ra,
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
e->t0, e->t1, e->t2,
e->current_tcb, e->task_ptr, e->task_mode,
@@ -2137,6 +2358,53 @@ static void handle_dirty_insn_log(int id, const char *json)
free(out);
}
static void handle_dirty_insn_add_range(int id, const char *json)
{
char lo_str[32], hi_str[32];
if (!json_get_str(json, "lo", lo_str, sizeof(lo_str))) { send_err(id, "missing lo"); return; }
if (!json_get_str(json, "hi", hi_str, sizeof(hi_str))) { send_err(id, "missing hi"); return; }
uint32_t lo = hex_to_u32(lo_str);
uint32_t hi = hex_to_u32(hi_str);
if (!dirty_ram_trace_add_range(lo, hi)) {
send_err(id, "range add failed");
return;
}
send_fmt("{\"id\":%d,\"ok\":true,\"count\":%d}",
id, dirty_ram_trace_range_count());
}
static void handle_dirty_insn_clear_ranges(int id, const char *json)
{
(void)json;
dirty_ram_trace_clear_ranges();
send_ok(id);
}
static void handle_dirty_insn_ranges(int id, const char *json)
{
(void)json;
const size_t BUF_SZ = 2048;
char *out = (char *)malloc(BUF_SZ);
if (!out) { send_err(id, "oom"); return; }
int count = dirty_ram_trace_range_count();
size_t pos = 0;
pos += snprintf(out + pos, BUF_SZ - pos,
"{\"id\":%d,\"ok\":true,\"count\":%d,\"ranges\":[",
id, count);
for (int i = 0; i < count; i++) {
uint32_t lo = 0, hi = 0;
dirty_ram_trace_get_range(i, &lo, &hi);
pos += snprintf(out + pos, BUF_SZ - pos,
"%s{\"slot\":%d,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
i == 0 ? "" : ",", i, lo, hi);
}
pos += snprintf(out + pos, BUF_SZ - pos, "]}");
debug_server_send_line(out);
free(out);
}
/* ---- fntrace: always-on psx_dispatch call ring ----
* Mirrors beetle_libretro.cpp's fntrace; covers every static-recomp +
* dirty-RAM dispatch on this side. Use to find indirect callers, walk
@@ -2287,14 +2555,23 @@ static void handle_unknown_dispatch_log(int id, const char *json)
for (int i = 0; i < tail; i++) {
UnknownDispatchEntry *e =
&s_unknown_ring[(start + i) & (UNKNOWN_DISPATCH_CAP - 1u)];
if (pos > BUF_SZ - 256) break;
if (pos > BUF_SZ - 768) break;
pos += snprintf(out + pos, BUF_SZ - pos,
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"phys\":\"0x%08X\","
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
"\"frame\":%u}",
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\",\"t2\":\"0x%08X\","
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
"\"current_func\":\"0x%08X\",\"frame\":%u}",
i == 0 ? "" : ",",
(unsigned long long)e->seq,
e->addr, e->phys, e->ra, e->a0, e->a1, e->frame);
e->addr, e->phys, e->ra, e->sp, e->v0, e->v1,
e->a0, e->a1, e->a2, e->a3, e->t0, e->t1, e->t2,
e->s0, e->s1, e->s2, e->s3, e->current_func,
e->frame);
}
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
debug_server_send_line(out);
@@ -2548,13 +2825,20 @@ static void emit_restore_entry(const RestoreTraceEntry *e, int first)
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\",\"a0\":\"0x%08X\","
"\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"sr\":\"0x%08X\","
"\"saved_s0\":\"0x%08X\",\"saved_s1\":\"0x%08X\","
"\"saved_s2\":\"0x%08X\",\"saved_s3\":\"0x%08X\","
"\"saved_sp\":\"0x%08X\",\"saved_ra\":\"0x%08X\","
"\"saved_pc\":\"0x%08X\","
"\"epc\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
"\"frame\":%u,\"in_exc\":%u}",
first ? "" : ",",
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
e->jmp_val, e->target_pc, e->cpu_pc, e->func, e->last_store_pc,
e->byte_seq, e->ra, e->sp, e->v0, e->v1, e->a0, e->a1, e->a2,
e->a3, e->s0, e->s1, e->sr, e->epc, e->istat, e->imask,
e->a3, e->s0, e->s1, e->sr,
e->saved_s0, e->saved_s1, e->saved_s2, e->saved_s3,
e->saved_sp, e->saved_ra, e->saved_pc,
e->epc, e->istat, e->imask,
e->frame, (unsigned)e->in_exception);
}
@@ -2570,13 +2854,20 @@ static size_t append_restore_entry(char *buf, size_t pos, size_t cap,
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\",\"a0\":\"0x%08X\","
"\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"sr\":\"0x%08X\","
"\"saved_s0\":\"0x%08X\",\"saved_s1\":\"0x%08X\","
"\"saved_s2\":\"0x%08X\",\"saved_s3\":\"0x%08X\","
"\"saved_sp\":\"0x%08X\",\"saved_ra\":\"0x%08X\","
"\"saved_pc\":\"0x%08X\","
"\"epc\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
"\"frame\":%u,\"in_exc\":%u}",
first ? "" : ",",
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
e->jmp_val, e->target_pc, e->cpu_pc, e->func, e->last_store_pc,
e->byte_seq, e->ra, e->sp, e->v0, e->v1, e->a0, e->a1, e->a2,
e->a3, e->s0, e->s1, e->sr, e->epc, e->istat, e->imask,
e->a3, e->s0, e->s1, e->sr,
e->saved_s0, e->saved_s1, e->saved_s2, e->saved_s3,
e->saved_sp, e->saved_ra, e->saved_pc,
e->epc, e->istat, e->imask,
e->frame, (unsigned)e->in_exception);
return pos;
}
@@ -3590,6 +3881,108 @@ static void handle_irq_state(int id, const char *json)
dma_get_dpcr(), dma_get_dicr());
}
static const char *irq_check_reason_name(uint32_t reason)
{
switch (reason) {
case 1: return "in_exception";
case 2: return "call_depth";
case 3: return "cooldown";
case 4: return "ie_disabled";
case 5: return "im2_disabled";
case 6: return "invalid_resume_pc";
case 7: return "delivered";
default: return "unknown";
}
}
static void handle_irq_check_log(int id, const char *json)
{
char buf32[32];
int count = json_get_int(json, "count", 256);
if (count < 1) count = 1;
if (count > 4096) count = 4096;
uint32_t pc_lo = 0, pc_hi = 0xFFFFFFFFu;
if (json_get_str(json, "pc_lo", buf32, sizeof(buf32))) pc_lo = hex_to_u32(buf32);
if (json_get_str(json, "pc_hi", buf32, sizeof(buf32))) pc_hi = hex_to_u32(buf32);
int reason_filter = -1;
if (json_get_str(json, "reason", buf32, sizeof(buf32))) {
reason_filter = (int)strtol(buf32, NULL, 0);
}
uint64_t total = g_psx_irq_check_trace_seq;
uint64_t oldest = (total > PSX_IRQ_CHECK_TRACE_CAP)
? total - PSX_IRQ_CHECK_TRACE_CAP : 0;
size_t cap = 256u + (size_t)count * 320u;
char *out = (char *)malloc(cap);
if (!out) { send_err(id, "oom"); return; }
size_t pos = 0;
pos += snprintf(out + pos, cap - pos,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,"
"\"entries\":[",
id, (unsigned long long)total, (unsigned long long)oldest);
int emitted = 0;
int first = 1;
for (uint64_t n = 0; n < total - oldest && emitted < count && pos < cap - 320u; n++) {
uint64_t seq = total - 1u - n;
if (seq < oldest) break;
const PsxIrqCheckTraceEntry *e =
&g_psx_irq_check_trace[seq & (PSX_IRQ_CHECK_TRACE_CAP - 1u)];
if (e->seq != seq) continue;
if (e->resume_pc < pc_lo || e->resume_pc >= pc_hi) continue;
if (reason_filter >= 0 && e->reason != (uint32_t)reason_filter) continue;
pos += snprintf(out + pos, cap - pos,
"%s{\"seq\":%llu,\"frame\":%u,"
"\"resume_pc\":\"0x%08X\",\"i_stat\":\"0x%08X\","
"\"i_mask\":\"0x%08X\",\"sr\":\"0x%08X\","
"\"cause\":\"0x%08X\",\"call_depth\":%u,"
"\"reason\":%u,\"name\":\"%s\"}",
first ? "" : ",",
(unsigned long long)e->seq, e->frame,
e->resume_pc, e->i_stat, e->i_mask, e->sr,
e->cause, e->call_depth, e->reason,
irq_check_reason_name(e->reason));
first = 0;
emitted++;
}
pos += snprintf(out + pos, cap - pos, "],\"emitted\":%d}", emitted);
debug_server_send_line(out);
free(out);
}
void debug_server_freeze_dump_irq_check_json(FILE *f, uint32_t max_count)
{
if (!f) return;
uint64_t total = g_psx_irq_check_trace_seq;
uint64_t avail = (total < PSX_IRQ_CHECK_TRACE_CAP)
? total : PSX_IRQ_CHECK_TRACE_CAP;
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
if (max_count == 0) { fputs("[]", f); return; }
uint64_t start = total - (uint64_t)max_count;
fputc('[', f);
for (uint32_t i = 0; i < max_count; i++) {
uint64_t seq = start + (uint64_t)i;
const PsxIrqCheckTraceEntry *e =
&g_psx_irq_check_trace[seq & (PSX_IRQ_CHECK_TRACE_CAP - 1u)];
fprintf(f,
"%s{\"seq\":%llu,\"frame\":%u,"
"\"resume_pc\":\"0x%08X\",\"i_stat\":\"0x%08X\","
"\"i_mask\":\"0x%08X\",\"sr\":\"0x%08X\","
"\"cause\":\"0x%08X\",\"call_depth\":%u,"
"\"reason\":%u,\"name\":\"%s\"}",
i ? "," : "",
(unsigned long long)e->seq, e->frame,
e->resume_pc, e->i_stat, e->i_mask, e->sr,
e->cause, e->call_depth, e->reason,
irq_check_reason_name(e->reason));
}
fputc(']', f);
}
static void handle_timers_state(int id, const char *json)
{
(void)json;
@@ -3800,6 +4193,79 @@ static void handle_cdrom_trace_clear(int id, const char *json)
send_ok(id);
}
static void handle_cdrom_command_history_clear(int id, const char *json)
{
(void)json;
cdrom_debug_clear_command_history();
send_ok(id);
}
static const char *cdrom_command_kind_name(uint8_t kind)
{
switch (kind) {
case 'C': return "exec";
case 'Q': return "queue";
default: return "unknown";
}
}
static void handle_cdrom_command_history(int id, const char *json)
{
int count = json_get_int(json, "count", 128);
if (count < 1) count = 1;
if (count > CDROM_COMMAND_HISTORY_CAP) count = CDROM_COMMAND_HISTORY_CAP;
const CDROMCommandHistoryEntry *entries = NULL;
uint64_t total = cdrom_debug_get_command_history(&entries);
uint64_t oldest = (total > CDROM_COMMAND_HISTORY_CAP)
? total - CDROM_COMMAND_HISTORY_CAP : 0;
uint64_t start = (total > (uint64_t)count) ? total - (uint64_t)count : 0;
if (start < oldest) start = oldest;
size_t bufsz = 256u + (size_t)count * 520u;
char *buf = (char *)malloc(bufsz);
if (!buf) { send_err(id, "oom"); return; }
size_t pos = 0;
int emitted = 0;
pos += snprintf(buf + pos, bufsz - pos,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,\"entries\":[",
id, (unsigned long long)total, (unsigned long long)oldest);
for (uint64_t seq = start; seq < total && pos < bufsz - 520; seq++) {
const CDROMCommandHistoryEntry *e =
&entries[seq % CDROM_COMMAND_HISTORY_CAP];
if (e->seq != seq) continue;
pos += snprintf(buf + pos, bufsz - pos,
"%s{\"seq\":%llu,\"kind\":\"%s\",\"cmd\":\"0x%02X\","
"\"params_len\":%u,\"params\":\"",
emitted ? "," : "",
(unsigned long long)e->seq,
cdrom_command_kind_name(e->kind),
e->cmd, e->param_count);
append_hex_bytes(buf, bufsz, &pos, e->params, e->param_count);
pos += snprintf(buf + pos, bufsz - pos,
"\",\"frame\":%u,\"func\":\"0x%08X\",\"pc\":\"0x%08X\","
"\"i_stat\":\"0x%08X\",\"stat\":\"0x%02X\","
"\"request\":\"0x%02X\",\"irq_enable\":\"0x%02X\","
"\"irq_flag\":\"0x%02X\",\"mode\":\"0x%02X\","
"\"seek\":[%u,%u,%u],\"read\":[%u,%u,%u],"
"\"read_cmd\":\"0x%02X\",\"reading\":%u,"
"\"pending_cmd\":\"0x%02X\",\"pending\":%u,"
"\"queued_cmd\":\"0x%02X\",\"queued\":%u}",
e->frame, e->func, e->pc, e->i_stat,
e->stat, e->request, e->irq_enable, e->irq_flag,
e->mode, e->seek_min, e->seek_sec, e->seek_sect,
e->read_min, e->read_sec, e->read_sect,
e->read_cmd, e->reading, e->pending_cmd,
e->pending_pending, e->queued_cmd, e->queued_pending);
emitted++;
}
pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted);
debug_server_send_line(buf);
free(buf);
}
static void handle_cdrom_trace_dump(int id, const char *json)
{
int count = json_get_int(json, "count", 256);
@@ -3865,6 +4331,17 @@ static const char *dma_trace_kind_name(uint32_t kind)
}
}
static void append_hex_word_array(char *buf, size_t bufsz, size_t *pos,
const uint32_t *words, uint32_t count)
{
*pos += snprintf(buf + *pos, bufsz - *pos, "[");
for (uint32_t i = 0; i < count && *pos < bufsz - 16; i++) {
*pos += snprintf(buf + *pos, bufsz - *pos,
"%s\"0x%08X\"", i ? "," : "", words[i]);
}
*pos += snprintf(buf + *pos, bufsz - *pos, "]");
}
static void handle_dma_state(int id, const char *json)
{
(void)json;
@@ -3900,6 +4377,95 @@ static void handle_dma_trace_clear(int id, const char *json)
send_ok(id);
}
static void handle_dma_cdrom_history_clear(int id, const char *json)
{
(void)json;
dma_debug_clear_cdrom_history();
send_ok(id);
}
static void handle_dma_cdrom_history(int id, const char *json)
{
int count = json_get_int(json, "count", 256);
if (count < 1) count = 1;
if (count > DMA_CDROM_HISTORY_CAP) count = DMA_CDROM_HISTORY_CAP;
int filter_lba = -1;
char lba_str[32];
if (json_get_str(json, "lba", lba_str, sizeof(lba_str))) {
filter_lba = (int)hex_to_u32(lba_str);
}
const DMACDROMHistoryEntry *entries = NULL;
uint64_t total = dma_debug_get_cdrom_history(&entries);
uint64_t oldest = (total > DMA_CDROM_HISTORY_CAP)
? total - DMA_CDROM_HISTORY_CAP : 0;
uint64_t start = (total > (uint64_t)count) ? total - (uint64_t)count : 0;
if (start < oldest) start = oldest;
size_t bufsz = 256u + (size_t)count * 1150u;
char *buf = (char *)malloc(bufsz);
if (!buf) { send_err(id, "oom"); return; }
size_t pos = 0;
int emitted = 0;
pos += snprintf(buf + pos, bufsz - pos,
"{\"id\":%d,\"ok\":true,\"total\":%llu,"
"\"oldest\":%llu,\"entries\":[",
id, (unsigned long long)total,
(unsigned long long)oldest);
for (uint64_t seq = start; seq < total && pos < bufsz - 1150; seq++) {
const DMACDROMHistoryEntry *e =
&entries[seq % DMA_CDROM_HISTORY_CAP];
if (e->seq != seq) continue;
if (filter_lba >= 0 && e->lba != filter_lba) continue;
pos += snprintf(buf + pos, bufsz - pos,
"%s{\"seq\":%llu,\"frame_start\":%u,"
"\"frame_end\":%u,\"start_addr\":\"0x%06X\","
"\"final_addr\":\"0x%06X\",\"requested_words\":%u,"
"\"moved_words\":%u,\"bcr\":\"0x%08X\","
"\"chcr\":\"0x%08X\",\"dpcr\":\"0x%08X\","
"\"dicr_start\":\"0x%08X\",\"dicr_end\":\"0x%08X\","
"\"i_stat_start\":\"0x%08X\","
"\"i_stat_end\":\"0x%08X\",\"func\":\"0x%08X\","
"\"pc\":\"0x%08X\",\"lba\":%d,"
"\"sector_size\":%d,\"mode\":\"0x%02X\","
"\"sector_read_pos_start\":%d,"
"\"sector_read_pos_end\":%d,"
"\"sector_available_start\":%u,"
"\"sector_available_end\":%u,\"completed\":%u,"
"\"first_words\":",
emitted ? "," : "",
(unsigned long long)e->seq,
e->frame_start, e->frame_end,
e->start_addr, e->final_addr,
e->requested_words, e->moved_words,
e->bcr, e->chcr, e->dpcr,
e->dicr_start, e->dicr_end,
e->i_stat_start, e->i_stat_end,
e->func, e->pc, e->lba,
e->sector_size, e->mode,
e->sector_read_pos_start,
e->sector_read_pos_end,
e->sector_available_start,
e->sector_available_end,
e->completed);
append_hex_word_array(buf, bufsz, &pos,
e->first_words, e->first_count);
pos += snprintf(buf + pos, bufsz - pos, ",\"last_words\":");
append_hex_word_array(buf, bufsz, &pos,
e->last_words, e->last_count);
pos += snprintf(buf + pos, bufsz - pos, "}");
emitted++;
}
pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted);
debug_server_send_line(buf);
free(buf);
}
static void handle_dma_trace_dump(int id, const char *json)
{
int count = json_get_int(json, "count", 256);
@@ -8210,6 +8776,9 @@ void debug_server_freeze_dump_dirty_insn_json(FILE *f, uint32_t max_count)
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"insn\":\"0x%08X\","
"\"next_pc\":\"0x%08X\",\"target\":\"0x%08X\","
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
"\"before_s1\":\"0x%08X\",\"after_s1\":\"0x%08X\","
"\"before_s2\":\"0x%08X\",\"after_s2\":\"0x%08X\","
"\"before_s3\":\"0x%08X\",\"after_s3\":\"0x%08X\","
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
@@ -8222,6 +8791,9 @@ void debug_server_freeze_dump_dirty_insn_json(FILE *f, uint32_t max_count)
(unsigned long long)e->seq, e->pc, e->insn,
e->next_pc, e->target,
e->before_s0, e->after_s0,
e->before_s1, e->after_s1,
e->before_s2, e->after_s2,
e->before_s3, e->after_s3,
e->sp, e->ra,
e->v0, e->v1,
e->a0, e->a1,
@@ -8295,16 +8867,21 @@ static const CmdEntry s_commands[] = {
{ "mem_words", handle_mem_words },
{ "vram_peek", handle_vram_peek },
{ "irq_state", handle_irq_state },
{ "irq_check_log", handle_irq_check_log },
{ "timers_state", handle_timers_state },
{ "cdrom_state", handle_cdrom_state },
{ "cdrom_sector_dump", handle_cdrom_sector_dump },
{ "cdrom_sector_history", handle_cdrom_sector_history },
{ "cdrom_sector_history_clear", handle_cdrom_sector_history_clear },
{ "cdrom_command_history", handle_cdrom_command_history },
{ "cdrom_command_history_clear", handle_cdrom_command_history_clear },
{ "cdrom_trace_dump", handle_cdrom_trace_dump },
{ "cdrom_trace_clear", handle_cdrom_trace_clear },
{ "dma_state", handle_dma_state },
{ "dma_trace_dump", handle_dma_trace_dump },
{ "dma_trace_clear", handle_dma_trace_clear },
{ "dma_cdrom_history", handle_dma_cdrom_history },
{ "dma_cdrom_history_clear", handle_dma_cdrom_history_clear },
{ "sio_state", handle_sio_state },
{ "mc_status", handle_mc_status },
{ "spu_status", handle_spu_status },
@@ -8339,7 +8916,12 @@ static const CmdEntry s_commands[] = {
{ "dirty_ram_unsupported", handle_dirty_ram_unsupported },
{ "dirty_block_log", handle_dirty_block_log },
{ "dirty_flow_log", handle_dirty_flow_log },
{ "dirty_exit_log", handle_dirty_exit_log },
{ "dirty_call_log", handle_dirty_call_log },
{ "dirty_insn_log", handle_dirty_insn_log },
{ "dirty_insn_add_range", handle_dirty_insn_add_range },
{ "dirty_insn_clear_ranges", handle_dirty_insn_clear_ranges },
{ "dirty_insn_ranges", handle_dirty_insn_ranges },
{ "fntrace_arm", handle_fntrace_arm },
{ "fntrace_arm_clear", handle_fntrace_arm_clear },
{ "fntrace_armed", handle_fntrace_armed },
+210 -38
View File
@@ -58,9 +58,16 @@ DirtyRamBlockLogEntry g_dirty_ram_block_log[DIRTY_RAM_BLOCK_LOG_CAP] = {0};
uint64_t g_dirty_ram_block_log_seq = 0;
DirtyRamFlowLogEntry g_dirty_ram_flow_log[DIRTY_RAM_FLOW_LOG_CAP] = {0};
uint64_t g_dirty_ram_flow_log_seq = 0;
DirtyRamExitLogEntry g_dirty_ram_exit_log[DIRTY_RAM_EXIT_LOG_CAP] = {0};
uint64_t g_dirty_ram_exit_log_seq = 0;
DirtyRamCallLogEntry g_dirty_ram_call_log[DIRTY_RAM_CALL_LOG_CAP] = {0};
uint64_t g_dirty_ram_call_log_seq = 0;
DirtyRamInsnLogEntry g_dirty_ram_insn_log[DIRTY_RAM_INSN_LOG_CAP] = {0};
uint64_t g_dirty_ram_insn_log_seq = 0;
static struct { uint32_t lo, hi; } g_dirty_ram_trace_ranges[DIRTY_RAM_TRACE_MAX_RANGES];
static int g_dirty_ram_trace_range_count = 0;
/* Current frame counter, defined in debug_server.c. */
extern uint64_t s_frame_count;
@@ -88,6 +95,9 @@ extern int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr);
extern int psx_game_address_in_text(uint32_t addr);
#endif
extern void psx_dispatch_call(CPUState* cpu, uint32_t addr, uint32_t return_addr);
extern int g_psx_call_depth;
extern uint32_t i_stat;
extern uint32_t i_mask;
/* Forward decls from memory.c — used to read instruction bytes. */
extern uint8_t *memory_get_ram_ptr(void);
@@ -113,14 +123,55 @@ static inline uint32_t fetch_word(uint32_t phys) {
| ((uint32_t)ram[phys + 3] << 24);
}
int dirty_ram_trace_add_range(uint32_t lo, uint32_t hi) {
if (g_dirty_ram_trace_range_count >= DIRTY_RAM_TRACE_MAX_RANGES) return 0;
lo &= 0x1FFFFFFFu;
hi &= 0x1FFFFFFFu;
if (lo == hi) return 0;
int slot = g_dirty_ram_trace_range_count++;
g_dirty_ram_trace_ranges[slot].lo = lo;
g_dirty_ram_trace_ranges[slot].hi = hi;
return 1;
}
void dirty_ram_trace_clear_ranges(void) {
g_dirty_ram_trace_range_count = 0;
}
int dirty_ram_trace_range_count(void) {
return g_dirty_ram_trace_range_count;
}
int dirty_ram_trace_get_range(int slot, uint32_t *lo, uint32_t *hi) {
if (slot < 0 || slot >= g_dirty_ram_trace_range_count) return 0;
if (lo) *lo = g_dirty_ram_trace_ranges[slot].lo;
if (hi) *hi = g_dirty_ram_trace_ranges[slot].hi;
return 1;
}
static int dirty_ram_trace_should_log(uint32_t phys) {
if (g_dirty_ram_trace_range_count > 0) {
for (int i = 0; i < g_dirty_ram_trace_range_count; i++) {
if (phys >= g_dirty_ram_trace_ranges[i].lo &&
phys < g_dirty_ram_trace_ranges[i].hi) {
return 1;
}
}
return 0;
}
return ((phys >= 0x000E0000u && phys < 0x000F0000u) ||
(phys >= 0x000000A0u && phys < 0x000000C0u) ||
(phys >= 0x000005C0u && phys < 0x00000620u) ||
(phys >= 0x00001E00u && phys < 0x00002000u));
}
static void dirty_ram_log_instruction(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t before_s0, uint32_t next_pc,
const uint32_t before_s[4],
uint32_t next_pc,
uint32_t target, int transferred) {
uint32_t phys = pc & 0x1FFFFFFFu;
if (!((phys >= 0x000E0000u && phys < 0x000F0000u) ||
(phys >= 0x000000A0u && phys < 0x000000C0u) ||
(phys >= 0x000005C0u && phys < 0x00000620u) ||
(phys >= 0x00001E00u && phys < 0x00002000u))) {
if (!dirty_ram_trace_should_log(phys)) {
return;
}
@@ -136,8 +187,14 @@ static void dirty_ram_log_instruction(CPUState *cpu, uint32_t pc, uint32_t insn,
e->insn = insn;
e->next_pc = next_pc;
e->target = target;
e->before_s0 = before_s0;
e->before_s0 = before_s[0];
e->after_s0 = cpu->gpr[16];
e->before_s1 = before_s[1];
e->after_s1 = cpu->gpr[17];
e->before_s2 = before_s[2];
e->after_s2 = cpu->gpr[18];
e->before_s3 = before_s[3];
e->after_s3 = cpu->gpr[19];
e->sp = cpu->gpr[29];
e->ra = cpu->gpr[31];
e->v0 = cpu->gpr[2];
@@ -233,29 +290,125 @@ static int abort_unsupported(uint32_t pc, uint32_t insn, const char *reason) {
static int is_local_dirty_target(uint32_t target) {
uint32_t phys = target & 0x1FFFFFFFu;
if (!dirty_ram_is_dirty(phys)) return 0;
if (phys >= 0x00098000u) return 1;
#ifdef PSX_HAS_GAME_DISPATCH
return fntrace_is_game_started() && psx_game_address_in_text(target);
#else
return 0;
if (fntrace_is_game_started() && psx_game_address_in_text(target)) return 0;
#endif
return phys >= 0x00098000u;
}
static int allow_dirty_local_flow_for_addr(uint32_t addr) {
uint32_t phys = addr & 0x1FFFFFFFu;
if (phys >= 0x00098000u) return 1;
#ifdef PSX_HAS_GAME_DISPATCH
return fntrace_is_game_started() && psx_game_address_in_text(addr);
#else
return 0;
if (fntrace_is_game_started() && psx_game_address_in_text(addr)) return 0;
#endif
return phys >= 0x00098000u;
}
static uint32_t dirty_ram_current_tcb(CPUState *cpu) {
uint32_t tcb_ptr_addr = cpu->read_word(0x00000108u);
return tcb_ptr_addr ? cpu->read_word(tcb_ptr_addr) : 0;
}
enum {
DIRTY_EXIT_ZERO_ADDR = 1,
DIRTY_EXIT_EXCEPTION_VECTOR = 2,
DIRTY_EXIT_UNSUPPORTED_ENTRY = 3,
DIRTY_EXIT_UNSUPPORTED_MID = 4,
DIRTY_EXIT_STOP_ADDR = 5,
DIRTY_EXIT_NONLOCAL_TARGET = 6,
DIRTY_EXIT_CLEAN_NEXT = 7,
DIRTY_EXIT_GUARD = 8,
};
static void dirty_ram_log_exit(CPUState *cpu, uint32_t entry, uint32_t pc,
uint32_t target, uint32_t stop_addr,
uint32_t reason, uint32_t insns,
int handled) {
uint32_t entry_phys = entry & 0x1FFFFFFFu;
if (entry_phys < 0x00098000u && stop_addr == 0) {
return;
}
uint64_t s = g_dirty_ram_exit_log_seq++;
DirtyRamExitLogEntry *e =
&g_dirty_ram_exit_log[s & (DIRTY_RAM_EXIT_LOG_CAP - 1u)];
e->seq = s;
e->entry = entry;
e->pc = pc;
e->target = target;
e->stop_addr = stop_addr;
e->reason = reason;
e->insns = insns;
e->s0 = cpu ? cpu->gpr[16] : 0;
e->s1 = cpu ? cpu->gpr[17] : 0;
e->s2 = cpu ? cpu->gpr[18] : 0;
e->s3 = cpu ? cpu->gpr[19] : 0;
e->sp = cpu ? cpu->gpr[29] : 0;
e->ra = cpu ? cpu->gpr[31] : 0;
e->v0 = cpu ? cpu->gpr[2] : 0;
e->v1 = cpu ? cpu->gpr[3] : 0;
e->a0 = cpu ? cpu->gpr[4] : 0;
e->a1 = cpu ? cpu->gpr[5] : 0;
e->a2 = cpu ? cpu->gpr[6] : 0;
e->a3 = cpu ? cpu->gpr[7] : 0;
e->current_tcb = cpu ? dirty_ram_current_tcb(cpu) : 0;
e->frame = (uint32_t)s_frame_count;
e->handled = (uint8_t)(handled ? 1u : 0u);
e->in_exception = (uint8_t)(psx_get_in_exception() ? 1u : 0u);
}
static int dispatch_nonlocal_call(CPUState *cpu, uint32_t target,
uint32_t return_pc,
uint32_t *next_pc_out) {
cpu->pc = 0;
int defer_interrupts = 0;
#ifdef PSX_HAS_GAME_DISPATCH
defer_interrupts = fntrace_is_game_started() &&
psx_game_address_in_text(target) &&
((i_stat & i_mask) != 0);
#endif
/* Use the normal dispatcher call path for compiled/static callees too,
* so stop-address handling matches generated JAL/JALR calls. The dirty
* interpreter's caller continuation is not represented on the native C
* stack. When an overlay callback calls into statically recompiled game
* text while an IRQ is already pending, defer interrupt delivery until the
* post-JAL interpreter PC is selected; otherwise the callback can reenter
* before its immediate stores run. Calls made before an IRQ is pending
* remain interruptible because they can legitimately wait for CD/SIO IRQs.
* Hardware sources are still ticked by psx_check_interrupts. */
uint64_t call_seq = g_dirty_ram_call_log_seq++;
DirtyRamCallLogEntry *call_log =
&g_dirty_ram_call_log[call_seq & (DIRTY_RAM_CALL_LOG_CAP - 1u)];
call_log->seq = call_seq;
call_log->pc = return_pc - 8u;
call_log->target = target;
call_log->return_pc = return_pc;
call_log->before_s0 = cpu->gpr[16];
call_log->before_s1 = cpu->gpr[17];
call_log->before_s2 = cpu->gpr[18];
call_log->before_s3 = cpu->gpr[19];
call_log->before_sp = cpu->gpr[29];
call_log->before_ra = cpu->gpr[31];
call_log->a0 = cpu->gpr[4];
call_log->a1 = cpu->gpr[5];
call_log->a2 = cpu->gpr[6];
call_log->a3 = cpu->gpr[7];
call_log->current_tcb = dirty_ram_current_tcb(cpu);
call_log->frame = (uint32_t)s_frame_count;
call_log->defer_interrupts = (uint8_t)(defer_interrupts ? 1u : 0u);
call_log->in_exception = (uint8_t)(psx_get_in_exception() ? 1u : 0u);
call_log->returned_pc_nonzero = 0;
if (defer_interrupts) g_psx_call_depth++;
psx_dispatch_call(cpu, target, return_pc);
if (defer_interrupts) g_psx_call_depth--;
call_log->after_s0 = cpu->gpr[16];
call_log->after_s1 = cpu->gpr[17];
call_log->after_s2 = cpu->gpr[18];
call_log->after_s3 = cpu->gpr[19];
call_log->after_sp = cpu->gpr[29];
call_log->after_ra = cpu->gpr[31];
call_log->returned_pc_nonzero = (uint8_t)(cpu->pc != 0 ? 1u : 0u);
if (cpu->pc != 0) return 1;
*next_pc_out = return_pc;
return 0;
@@ -350,14 +503,6 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
cpu->gpr[rd ? rd : 31] = return_pc;
cpu->gpr[0] = 0;
exec_delay_slot(cpu, pc + 4);
#ifdef PSX_HAS_GAME_DISPATCH
cpu->pc = 0;
if (psx_dispatch_game_compiled(cpu, target)) {
if (cpu->pc != 0) return 1;
*next_pc_out = return_pc;
return 0;
}
#endif
if (!is_local_dirty_target(target)) {
return dispatch_nonlocal_call(cpu, target, return_pc, next_pc_out);
}
@@ -473,14 +618,6 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
uint32_t return_pc = pc + 8;
cpu->gpr[31] = return_pc;
exec_delay_slot(cpu, pc + 4);
#ifdef PSX_HAS_GAME_DISPATCH
cpu->pc = 0;
if (psx_dispatch_game_compiled(cpu, target)) {
if (cpu->pc != 0) return 1;
*next_pc_out = return_pc;
return 0;
}
#endif
if (!is_local_dirty_target(target)) {
return dispatch_nonlocal_call(cpu, target, return_pc, next_pc_out);
}
@@ -690,9 +827,11 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
* mask. Returns 1 if interpretation handled the basic block; 0 if the
* caller should fall back (e.g. unsupported opcode at the entry, page
* not dirty). */
int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
int dirty_ram_dispatch_until(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
if (addr == 0) {
cpu->pc = 0;
dirty_ram_log_exit(cpu, addr, addr, 0, stop_addr,
DIRTY_EXIT_ZERO_ADDR, 0, 1);
return 1;
}
@@ -700,6 +839,8 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
if (addr == 0x80000048u) {
cpu->pc = 0;
dirty_ram_log_exit(cpu, addr, addr, 0, stop_addr,
DIRTY_EXIT_EXCEPTION_VECTOR, 0, 1);
if (psx_get_in_exception()) {
psx_exception_longjmp(); /* does not return */
}
@@ -708,6 +849,7 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
#ifdef PSX_HAS_GAME_DISPATCH
if (psx_dispatch_game_compiled(cpu, addr)) return 1;
if (fntrace_is_game_started() && psx_game_address_in_text(addr)) return 0;
#endif
if (!dirty_ram_is_dirty(phys)) return 0;
@@ -744,6 +886,7 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
if (debug_server_dirty_break_maybe_pause(addr, cpu)) {
debug_server_wait_if_paused();
}
psx_check_interrupts_at(cpu, addr);
/* Run dirty code locally until it returns to compiled/non-dirty code.
* This is required for overlays and for any static text page that has
@@ -756,12 +899,14 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
for (int i = 0; i < MAX_INSNS_PER_DISPATCH; i++) {
uint32_t next_pc = 0;
uint32_t insn = fetch_word(pc & 0x1FFFFFFFu);
uint32_t before_s0 = cpu->gpr[16];
uint32_t before_s[4] = {
cpu->gpr[16], cpu->gpr[17], cpu->gpr[18], cpu->gpr[19]
};
int transferred = exec_one(cpu, pc, &next_pc);
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_advance_cycles(1u);
#endif
dirty_ram_log_instruction(cpu, pc, insn, before_s0, next_pc,
dirty_ram_log_instruction(cpu, pc, insn, before_s, next_pc,
transferred ? cpu->pc : next_pc,
transferred);
if (g_unsupported_seen) {
@@ -771,6 +916,9 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
* code (stale data, return-target into save area, etc.).
* Hand off to psx_unknown_dispatch which has its own
* pattern-matching trampoline resolver. */
dirty_ram_log_exit(cpu, addr, pc, cpu->pc, stop_addr,
DIRTY_EXIT_UNSUPPORTED_ENTRY,
(uint32_t)insns_executed, 0);
return 0;
}
/* Made some progress, then hit unknown. Treat as a no-op
@@ -792,19 +940,24 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
g_dirty_ram_last_unsupported_insn = g_unsupported_insn;
g_dirty_ram_last_unsupported_reason = g_unsupported_reason;
cpu->pc = 0;
dirty_ram_log_exit(cpu, addr, pc, 0, stop_addr,
DIRTY_EXIT_UNSUPPORTED_MID,
(uint32_t)insns_executed, 1);
return 1;
}
g_dirty_ram_insns_run++;
insns_executed++;
if (transferred) {
uint32_t target = cpu->pc;
#ifdef PSX_HAS_GAME_DISPATCH
if (target != 0 && psx_dispatch_game_compiled(cpu, target)) {
if (stop_addr != 0 &&
((target ^ stop_addr) & 0x1FFFFFFFu) == 0) {
g_dirty_ram_blocks_run++;
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
dirty_ram_log_exit(cpu, addr, pc, target, stop_addr,
DIRTY_EXIT_STOP_ADDR,
(uint32_t)insns_executed, 1);
return 1;
}
#endif
if (allow_local_dirty_flow && target != 0 &&
is_local_dirty_target(target)) {
uint64_t s = g_dirty_ram_flow_log_seq++;
@@ -824,12 +977,15 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
if ((insns_executed & 0xFFF) == 0) {
debug_server_poll();
debug_server_wait_if_paused();
psx_check_interrupts(cpu);
psx_check_interrupts_at(cpu, pc);
}
continue;
}
g_dirty_ram_blocks_run++;
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
dirty_ram_log_exit(cpu, addr, pc, target, stop_addr,
DIRTY_EXIT_NONLOCAL_TARGET,
(uint32_t)insns_executed, 1);
return 1;
}
pc = next_pc;
@@ -840,6 +996,9 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
cpu->pc = pc;
g_dirty_ram_blocks_run++;
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
dirty_ram_log_exit(cpu, addr, pc, pc, stop_addr,
DIRTY_EXIT_CLEAN_NEXT,
(uint32_t)insns_executed, 1);
return 1;
}
}
@@ -850,6 +1009,19 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
g_dirty_ram_blocks_run++;
cpu->pc = pc;
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
psx_check_interrupts(cpu);
dirty_ram_log_exit(cpu, addr, pc, pc, stop_addr,
DIRTY_EXIT_GUARD,
(uint32_t)insns_executed, 1);
psx_check_interrupts_at(cpu, pc);
/* Generated static code resumes inline after RFE with cpu->pc == 0.
* Dirty code's resume point is this explicit interpreter PC, so keep the
* dispatcher from treating an interrupt return as a completed call. */
if (cpu->pc == 0) {
cpu->pc = pc;
}
return 1;
}
int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
return dirty_ram_dispatch_until(cpu, addr, 0);
}
+90
View File
@@ -77,6 +77,10 @@ static uint32_t dicr; /* 0x1F8010F4: DMA interrupt control */
static DMATraceEntry dma_trace[DMA_TRACE_CAP];
static uint64_t dma_trace_seq;
static DMACDROMHistoryEntry cdrom_dma_history[DMA_CDROM_HISTORY_CAP];
static uint64_t cdrom_dma_history_seq;
static DMACDROMHistoryEntry cdrom_dma_active_entry;
static uint8_t cdrom_dma_history_active;
static uint32_t dicr_read_value(uint32_t v);
@@ -158,6 +162,74 @@ static void raise_dma_irq_on_master_edge(uint32_t before) {
}
}
static void start_cdrom_dma_capture(uint32_t requested_words) {
CDROMDebugState s;
cdrom_debug_snapshot(&s);
memset(&cdrom_dma_active_entry, 0, sizeof(cdrom_dma_active_entry));
cdrom_dma_active_entry.seq = cdrom_dma_history_seq++;
cdrom_dma_active_entry.frame_start = (uint32_t)s_frame_count;
cdrom_dma_active_entry.start_addr = channels[3].madr & 0x1FFFFCu;
cdrom_dma_active_entry.final_addr = cdrom_dma_active_entry.start_addr;
cdrom_dma_active_entry.requested_words = requested_words;
cdrom_dma_active_entry.bcr = channels[3].bcr;
cdrom_dma_active_entry.chcr = channels[3].chcr;
cdrom_dma_active_entry.dpcr = dpcr;
cdrom_dma_active_entry.dicr_start = dicr_read_value(dicr);
cdrom_dma_active_entry.i_stat_start = i_stat;
cdrom_dma_active_entry.func = g_debug_current_func_addr;
cdrom_dma_active_entry.pc = g_debug_last_store_pc;
cdrom_dma_active_entry.lba = s.last_sector_lba;
cdrom_dma_active_entry.sector_size =
(s.sector_size > 0) ? s.sector_size : s.last_sector_size;
cdrom_dma_active_entry.sector_read_pos_start = s.sector_read_pos;
cdrom_dma_active_entry.mode =
s.last_sector_mode ? s.last_sector_mode : s.mode_reg;
cdrom_dma_active_entry.sector_available_start =
(uint8_t)(s.sector_available ? 1 : 0);
cdrom_dma_history_active = 1;
}
static void record_cdrom_dma_word(uint32_t word) {
if (!cdrom_dma_history_active) return;
DMACDROMHistoryEntry *e = &cdrom_dma_active_entry;
if (e->first_count < DMA_CDROM_HISTORY_WORDS) {
e->first_words[e->first_count++] = word;
}
if (e->last_count < DMA_CDROM_HISTORY_WORDS) {
e->last_words[e->last_count++] = word;
} else {
memmove(e->last_words, e->last_words + 1,
sizeof(e->last_words[0]) * (DMA_CDROM_HISTORY_WORDS - 1));
e->last_words[DMA_CDROM_HISTORY_WORDS - 1] = word;
}
e->moved_words++;
}
static void finish_cdrom_dma_capture(uint32_t final_addr, uint8_t completed) {
if (!cdrom_dma_history_active) return;
CDROMDebugState s;
cdrom_debug_snapshot(&s);
cdrom_dma_active_entry.frame_end = (uint32_t)s_frame_count;
cdrom_dma_active_entry.final_addr = final_addr & 0x1FFFFCu;
cdrom_dma_active_entry.dicr_end = dicr_read_value(dicr);
cdrom_dma_active_entry.i_stat_end = i_stat;
cdrom_dma_active_entry.sector_read_pos_end = s.sector_read_pos;
cdrom_dma_active_entry.sector_available_end =
(uint8_t)(s.sector_available ? 1 : 0);
cdrom_dma_active_entry.completed = completed ? 1 : 0;
cdrom_dma_history[
cdrom_dma_active_entry.seq % DMA_CDROM_HISTORY_CAP] =
cdrom_dma_active_entry;
cdrom_dma_history_active = 0;
}
static int channel_enabled(int ch) {
/* DPCR: each channel has 4 bits, bit 3 of each group is enable.
* Ch0 = bits 0-3, Ch1 = bits 4-7, etc. Enable = bit (ch*4 + 3). */
@@ -215,6 +287,7 @@ static void cancel_async_transfer(int ch) {
mdec_async[ch].cycles_accum = 0;
}
if (ch == 3) {
finish_cdrom_dma_capture(channels[3].madr & 0x1FFFFCu, 0);
cdrom_async.active = 0;
cdrom_async.debug_started = 0;
cdrom_async.total_words = 0;
@@ -294,14 +367,17 @@ static void start_async_cdrom_transfer(void) {
a->total_words = transfer_word_count(3);
a->remaining_words = a->total_words;
a->cycles_accum = 0;
start_cdrom_dma_capture(a->total_words);
if (a->total_words == 0) {
finish_cdrom_dma_capture(channels[3].madr & 0x1FFFFCu, 1);
cancel_async_transfer(3);
complete_transfer(3);
}
}
static void finish_async_cdrom_transfer(uint32_t final_addr) {
finish_cdrom_dma_capture(final_addr, 1);
channels[3].madr = final_addr;
cancel_async_transfer(3);
complete_transfer(3);
@@ -644,6 +720,7 @@ void dma_advance(uint32_t cycles) {
while (a->remaining_words > 0 && words_budget > 0 && cdrom_dma_ready()) {
uint32_t word = cdrom_dma_read();
psx_write_word(addr, word);
record_cdrom_dma_word(word);
dirty_ram_mark_executable_range(addr, 4);
addr = (addr + addr_step) & 0x1FFFFCu;
a->remaining_words--;
@@ -671,6 +748,7 @@ void dma_init(void) {
dpcr = 0x07654321u; /* default: priorities set, no channels enabled */
dicr = 0;
dma_debug_clear_trace();
dma_debug_clear_cdrom_history();
}
uint32_t dma_read(uint32_t addr) {
@@ -781,6 +859,18 @@ void dma_debug_clear_trace(void) {
dma_trace_seq = 0;
}
uint64_t dma_debug_get_cdrom_history(const DMACDROMHistoryEntry** out_entries) {
if (out_entries) *out_entries = cdrom_dma_history;
return cdrom_dma_history_seq;
}
void dma_debug_clear_cdrom_history(void) {
memset(cdrom_dma_history, 0, sizeof(cdrom_dma_history));
memset(&cdrom_dma_active_entry, 0, sizeof(cdrom_dma_active_entry));
cdrom_dma_history_seq = 0;
cdrom_dma_history_active = 0;
}
void dma_debug_get_state(DMADebugState* out) {
if (!out) return;
out->dpcr = dpcr;
+87 -10
View File
@@ -79,6 +79,38 @@ static uint64_t exception_reentry_blocks;
* very next psx_check_interrupts re-enters immediately. */
static int post_exception_cooldown;
extern int g_psx_call_depth;
PsxIrqCheckTraceEntry g_psx_irq_check_trace[PSX_IRQ_CHECK_TRACE_CAP] = {0};
uint64_t g_psx_irq_check_trace_seq = 0;
extern uint64_t s_frame_count;
enum {
IRQ_CHECK_IN_EXCEPTION = 1,
IRQ_CHECK_CALL_DEPTH = 2,
IRQ_CHECK_COOLDOWN = 3,
IRQ_CHECK_IE_DISABLED = 4,
IRQ_CHECK_IM2_DISABLED = 5,
IRQ_CHECK_INVALID_RESUME_PC = 6,
IRQ_CHECK_DELIVERED = 7
};
static void irq_check_trace_log(CPUState* cpu, uint32_t resume_pc, uint32_t reason)
{
uint64_t seq = g_psx_irq_check_trace_seq++;
PsxIrqCheckTraceEntry *e =
&g_psx_irq_check_trace[seq & (PSX_IRQ_CHECK_TRACE_CAP - 1u)];
e->seq = seq;
e->frame = (uint32_t)s_frame_count;
e->resume_pc = resume_pc;
e->i_stat = i_stat;
e->i_mask = i_mask;
e->sr = cpu ? cpu->cop0[COP0_SR] : 0;
e->cause = cpu ? cpu->cop0[COP0_CAUSE] : 0;
e->call_depth = (uint32_t)g_psx_call_depth;
e->reason = reason;
}
/* setjmp target for ReturnFromException during handler dispatch.
*
* IMPORTANT: longjmp(exception_jmpbuf, ...) MUST execute on the same
@@ -100,6 +132,12 @@ jmp_buf exception_jmpbuf; /* non-static so traps.c can deferred-longjmp */
void* g_exception_owner_fiber = NULL;
int g_pending_exception_longjmp = 0;
extern int g_psx_dispatch_depth;
extern int g_psx_call_depth;
uint64_t g_psx_nonlocal_epoch = 0;
void psx_note_nonlocal_control_flow(void) {
g_psx_nonlocal_epoch++;
}
int psx_get_in_exception(void) { return in_exception; }
@@ -121,10 +159,13 @@ void interrupts_init(void) {
dispatch_count = 0;
in_exception = 0;
g_psx_dispatch_depth = 0;
g_psx_call_depth = 0;
total_checks = 0;
post_exception_cooldown = 0;
exception_entries_total = 0;
exception_reentry_blocks = 0;
g_psx_irq_check_trace_seq = 0;
g_psx_nonlocal_epoch = 0;
}
/*
@@ -160,11 +201,21 @@ static void deferred_exception_longjmp(int code) {
}
void psx_exception_longjmp(void) {
psx_note_nonlocal_control_flow();
debug_server_log_restore_event(2, debug_cpu_ptr ? debug_cpu_ptr->pc : 0, 1);
deferred_exception_longjmp(1);
}
void psx_rfe_return(CPUState* cpu) {
(void)cpu;
if (in_exception) {
psx_exception_longjmp();
}
psx_note_nonlocal_control_flow();
}
void psx_restore_state_escape(void) {
psx_note_nonlocal_control_flow();
if (in_exception) {
debug_server_log_restore_event(1, debug_cpu_ptr ? debug_cpu_ptr->pc : 0, 2);
deferred_exception_longjmp(2);
@@ -173,6 +224,10 @@ void psx_restore_state_escape(void) {
}
void psx_check_interrupts(CPUState* cpu) {
psx_check_interrupts_at(cpu, cpu ? cpu->pc : 0);
}
void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc) {
total_checks++;
if ((total_checks & 0x3FFFu) == 0) {
debug_server_poll();
@@ -261,19 +316,36 @@ void psx_check_interrupts(CPUState* cpu) {
if ((i_stat & i_mask) == 0) return;
if (in_exception) {
exception_reentry_blocks++;
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_IN_EXCEPTION);
return;
}
if (g_psx_call_depth > 0) {
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_CALL_DEPTH);
return;
}
/* Post-exception cooldown: let at least one block execute after RFE. */
if (post_exception_cooldown > 0) {
post_exception_cooldown--;
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_COOLDOWN);
return;
}
/* Check COP0 SR: IEc (bit 0) must be set, and IM2 (bit 10) must be set. */
uint32_t sr = cpu->cop0[COP0_SR];
if (!(sr & 0x01)) return; /* Interrupts globally disabled */
if (!(sr & (1 << 10))) return; /* Hardware interrupt bit not enabled */
if (!(sr & 0x01)) {
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_IE_DISABLED);
return; /* Interrupts globally disabled */
}
if (!(sr & (1 << 10))) {
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_IM2_DISABLED);
return; /* Hardware interrupt bit not enabled */
}
if (resume_pc == 0 || (resume_pc & 3u) != 0) {
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_INVALID_RESUME_PC);
return;
}
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_DELIVERED);
in_exception = 1;
exception_entries_total++;
@@ -286,14 +358,12 @@ void psx_check_interrupts(CPUState* cpu) {
/* Push SR exception stack: shift bits [5:0] left by 2. */
cpu->cop0[COP0_SR] = (sr & ~0x3F) | ((sr & 0x0F) << 2);
/* EPC: set to a sentinel value. The recompiled exception handler reads
* memory at [EPC] to check for COP2 branch delay. We use a dedicated
* address in the kernel scratch area. Address 0x80000048 is chosen
* because it's between the exception vectors (0x80-0xBF) and the
* kernel data pointer area (0x100+), and not used by the BIOS. */
uint32_t sentinel = 0x80000048u;
cpu->write_word(sentinel, 0x00000000u); /* NOP */
cpu->cop0[COP0_EPC] = sentinel;
/* EPC must be the guest PC that will execute next after the interrupt.
* The BIOS saves this into the current TCB and ReturnFromException
* later restores it. A synthetic EPC is not safe: games can run event
* callbacks through BIOS RestoreState while the interrupted TCB is live,
* and those callbacks observe the same save area. */
cpu->cop0[COP0_EPC] = resume_pc;
/* Save the interrupted code's full register state.
*
@@ -348,6 +418,7 @@ void psx_check_interrupts(CPUState* cpu) {
int prev_pending = g_pending_exception_longjmp;
g_exception_owner_fiber = psx_fiber_current();
g_pending_exception_longjmp = 0;
int returned_from_exception = 0;
for (;;) {
int jmp_val = setjmp(exception_jmpbuf);
if (jmp_val == 2) {
@@ -355,12 +426,15 @@ void psx_check_interrupts(CPUState* cpu) {
* GPRs were already set by RestoreState — do NOT restore.
* Stay in exception context so ReturnFromException works. */
g_psx_dispatch_depth = 0;
g_psx_call_depth = 0;
debug_server_log_restore_event(3, cpu->pc, (uint32_t)jmp_val);
target_pc = cpu->pc;
continue;
}
if (jmp_val == 1) {
g_psx_dispatch_depth = 0;
g_psx_call_depth = 0;
returned_from_exception = 1;
debug_server_log_restore_event(4, cpu->pc, (uint32_t)jmp_val);
}
if (jmp_val == 0) {
@@ -389,6 +463,9 @@ void psx_check_interrupts(CPUState* cpu) {
for (int i = 0; i < 32; i++) cpu->gpr[i] = saved_gpr[i];
cpu->hi = saved_hi;
cpu->lo = saved_lo;
if (returned_from_exception) {
cpu->pc = 0;
}
if (!(cpu->cop0[COP0_SR] & 0x01)) {
uint32_t sr2 = cpu->cop0[COP0_SR];
+10 -6
View File
@@ -121,26 +121,30 @@ static void pad_prepare_command_response(uint8_t command) {
static const uint8_t info_46[6] = { 0x00, 0x00, 0x01, 0x02, 0x00, 0x0A };
static const uint8_t info_47[6] = { 0x00, 0x00, 0x02, 0x00, 0x01, 0x00 };
static const uint8_t info_4c[6] = { 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 };
const uint8_t id = 0xF3;
const uint8_t config_id = 0xF3;
switch (command) {
case 0x43: /* Enter/exit config mode */
/* The command itself is acknowledged using the controller's current
* mode ID; the first parameter takes effect on later bytes. */
pad_prepare_response(pad_poll_id(selected_slot), zeros, 6);
break;
case 0x44: /* Set analog/digital mode */
case 0x4D: /* Set vibration motor mapping */
case 0x4F: /* Set poll response byte count / DualShock extension */
pad_prepare_response(id, zeros, 6);
pad_prepare_response(config_id, zeros, 6);
break;
case 0x45: /* Get controller type */
pad_prepare_response(id, info_45, 6);
pad_prepare_response(config_id, info_45, 6);
break;
case 0x46: /* Get mode table */
pad_prepare_response(id, info_46, 6);
pad_prepare_response(config_id, info_46, 6);
break;
case 0x47: /* Get actuator alignment */
pad_prepare_response(id, info_47, 6);
pad_prepare_response(config_id, info_47, 6);
break;
case 0x4C: /* Get supported modes */
pad_prepare_response(id, info_4c, 6);
pad_prepare_response(config_id, info_4c, 6);
break;
default:
pad_clear_response();
+40 -5
View File
@@ -511,8 +511,8 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
/* Reject non-word-aligned targets — corrupt function pointer. Hard fail. */
if (addr & 3) {
char buf[512];
snprintf(buf, sizeof(buf),
char buf[2048];
int n = snprintf(buf, sizeof(buf),
"DISPATCH FATAL: misaligned target 0x%08X\n"
" aligned form: 0x%08X\n"
" physical: 0x%08X\n"
@@ -524,6 +524,11 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
" $a1: 0x%08X\n"
" $a2: 0x%08X\n"
" $a3: 0x%08X\n"
" $s0: 0x%08X\n"
" $s1: 0x%08X\n"
" $s2: 0x%08X\n"
" $s3: 0x%08X\n"
" $sp: 0x%08X\n"
" COP0_EPC: 0x%08X\n"
" COP0_SR: 0x%08X\n"
" COP0_Cause: 0x%08X\n",
@@ -531,7 +536,25 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
cpu->pc, cpu->gpr[31], cpu->gpr[25],
cpu->gpr[2], cpu->gpr[4], cpu->gpr[5],
cpu->gpr[6], cpu->gpr[7],
cpu->gpr[16], cpu->gpr[17], cpu->gpr[18], cpu->gpr[19],
cpu->gpr[29],
cpu->cop0[14], cpu->cop0[12], cpu->cop0[13]);
n += snprintf(buf + n, sizeof(buf) - (size_t)n,
" callback table:");
for (uint32_t off = 0; off < 32 && n < (int)sizeof(buf) - 32; off += 4) {
n += snprintf(buf + n, sizeof(buf) - (size_t)n,
" [%02X]=0x%08X",
off, cpu->read_word(0x800A9B90u + off));
}
n += snprintf(buf + n, sizeof(buf) - (size_t)n,
"\n s0 words:");
uint32_t s0 = cpu->gpr[16];
for (uint32_t off = 0; off < 32 && n < (int)sizeof(buf) - 32; off += 4) {
n += snprintf(buf + n, sizeof(buf) - (size_t)n,
" [%02X]=0x%08X",
off, cpu->read_word(s0 + off));
}
n += snprintf(buf + n, sizeof(buf) - (size_t)n, "\n");
trap_crash(buf);
fprintf(stderr, "%s", buf);
fflush(stderr);
@@ -685,10 +708,22 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
* `unknown_dispatch_log` debug command. Replaces the prior
* file-based log per CLAUDE.md §3. */
extern void psx_unknown_dispatch_record(uint32_t addr, uint32_t phys,
uint32_t ra, uint32_t a0,
uint32_t a1);
uint32_t ra, uint32_t sp,
uint32_t v0, uint32_t v1,
uint32_t a0, uint32_t a1,
uint32_t a2, uint32_t a3,
uint32_t t0, uint32_t t1,
uint32_t t2, uint32_t s0,
uint32_t s1, uint32_t s2,
uint32_t s3);
psx_unknown_dispatch_record(addr, phys, cpu->gpr[31],
cpu->gpr[4], cpu->gpr[5]);
cpu->gpr[29], cpu->gpr[2],
cpu->gpr[3], cpu->gpr[4],
cpu->gpr[5], cpu->gpr[6],
cpu->gpr[7], cpu->gpr[8],
cpu->gpr[9], cpu->gpr[10],
cpu->gpr[16], cpu->gpr[17],
cpu->gpr[18], cpu->gpr[19]);
/* Fail-fast mode: silent no-op masks bugs by leaving stale
* register state. When PSX_FAIL_FAST_UNKNOWN_DISPATCH=1, dump