diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index 184dc3e7..6efeef2e 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -5,6 +5,7 @@ #include #include #include +#include #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& continuation_owners() const { + return continuation_owners_; + } + private: const PS1Executable& exe_; CodeGenConfig config_; std::set known_functions_; // Addresses of functions in this compilation unit std::set extra_labels_; // Mid-block addresses that need inline labels (jump table targets) + std::map continuation_owners_; + std::map> continuation_entries_by_function_; const AnnotationTable* annotations_ = nullptr; + void set_continuation_entries(const std::map& owners); + // Instruction translation std::string translate_instruction(uint32_t addr, uint32_t instr); diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 96edab42..47384821 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -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& 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(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 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()) { diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index b8573ffe..96591840 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -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(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(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(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"; diff --git a/recompiler/src/function_analysis.cpp b/recompiler/src/function_analysis.cpp index 0d14ed22..4653716d 100644 --- a/recompiler/src/function_analysis.cpp +++ b/recompiler/src/function_analysis.cpp @@ -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++; } } diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index dde3ae91..ffd82f06 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #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 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"); } diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index a3c7323c..e7122a1e 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -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); diff --git a/runtime/include/debug_server.h b/runtime/include/debug_server.h index 0ad38d76..5659b455 100644 --- a/runtime/include/debug_server.h +++ b/runtime/include/debug_server.h @@ -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); diff --git a/runtime/include/dirty_ram_interp.h b/runtime/include/dirty_ram_interp.h index 95664fa6..6e397f17 100644 --- a/runtime/include/dirty_ram_interp.h +++ b/runtime/include/dirty_ram_interp.h @@ -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 diff --git a/runtime/include/dma.h b/runtime/include/dma.h index 54243549..49522170 100644 --- a/runtime/include/dma.h +++ b/runtime/include/dma.h @@ -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 } diff --git a/runtime/include/interrupts.h b/runtime/include/interrupts.h index 6e5fb4a4..27e7f105 100644 --- a/runtime/include/interrupts.h +++ b/runtime/include/interrupts.h @@ -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. * diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 44e226f9..dfa72637 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -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; diff --git a/runtime/src/crash_trace.c b/runtime/src/crash_trace.c index 87133aaa..11448202 100644 --- a/runtime/src/crash_trace.c +++ b/runtime/src/crash_trace.c @@ -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 ──────────────────────────────────────────────────── */ diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 2265b27e..41634f70 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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 }, diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 647f0190..9fc788ba 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -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); +} diff --git a/runtime/src/dma.c b/runtime/src/dma.c index be54de0c..9160b48a 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -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; diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index a05abe35..60ccff47 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -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]; diff --git a/runtime/src/sio.c b/runtime/src/sio.c index a3257fdf..623c043e 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -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(); diff --git a/runtime/src/traps.c b/runtime/src/traps.c index f6072d35..319d6825 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -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