mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Ape Escape bring-up: attract demo now loads
Framework changes that get Ape Escape (USA) booting through to its attract/demo sequence. Captures the working state at this milestone. Runtime: - cdrom: read/seek + data-path fixes for disc streaming - dma / interrupts: channel + IRQ handling needed by the boot chain - dirty_ram_interp: expanded coverage for self-modifying / RAM handlers - sio, traps, crash_trace: supporting fixes - debug_server: probe/inspection commands used during bring-up Recompiler: - code_generator / full_function_emitter / function_analysis / main_psx: codegen fixes surfaced by the Ape Escape boot path Committed as-is (no further edits) since the build is in a known-good, demo-loading state. Branch: codex/ape-escape-support. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include "ps1_exe_parser.h"
|
||||
#include "function_analysis.h"
|
||||
#include "control_flow.h"
|
||||
@@ -85,13 +86,21 @@ public:
|
||||
// Set annotation table (optional — no-op if not called)
|
||||
void set_annotations(const AnnotationTable* at) { annotations_ = at; }
|
||||
|
||||
const std::map<uint32_t, uint32_t>& continuation_owners() const {
|
||||
return continuation_owners_;
|
||||
}
|
||||
|
||||
private:
|
||||
const PS1Executable& exe_;
|
||||
CodeGenConfig config_;
|
||||
std::set<uint32_t> known_functions_; // Addresses of functions in this compilation unit
|
||||
std::set<uint32_t> extra_labels_; // Mid-block addresses that need inline labels (jump table targets)
|
||||
std::map<uint32_t, uint32_t> continuation_owners_;
|
||||
std::map<uint32_t, std::set<uint32_t>> continuation_entries_by_function_;
|
||||
const AnnotationTable* annotations_ = nullptr;
|
||||
|
||||
void set_continuation_entries(const std::map<uint32_t, uint32_t>& owners);
|
||||
|
||||
// Instruction translation
|
||||
std::string translate_instruction(uint32_t addr, uint32_t instr);
|
||||
|
||||
|
||||
@@ -42,10 +42,18 @@ static uint32_t ram_to_rom(uint32_t addr, const PS1Executable& exe) {
|
||||
return addr;
|
||||
}
|
||||
|
||||
CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& config)
|
||||
: exe_(exe), config_(config) {}
|
||||
|
||||
std::string CodeGenerator::reg_name(int reg_num) {
|
||||
CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& config)
|
||||
: exe_(exe), config_(config) {}
|
||||
|
||||
void CodeGenerator::set_continuation_entries(const std::map<uint32_t, uint32_t>& owners) {
|
||||
continuation_owners_ = owners;
|
||||
continuation_entries_by_function_.clear();
|
||||
for (const auto& [continuation_addr, owner_addr] : continuation_owners_) {
|
||||
continuation_entries_by_function_[owner_addr].insert(continuation_addr);
|
||||
}
|
||||
}
|
||||
|
||||
std::string CodeGenerator::reg_name(int reg_num) {
|
||||
if (reg_num >= 0 && reg_num < 32) {
|
||||
return fmt::format("cpu->gpr[{}]", reg_num);
|
||||
}
|
||||
@@ -865,7 +873,8 @@ std::string CodeGenerator::translate_basic_block(
|
||||
static_cast<uint32_t>(block.instruction_count));
|
||||
ss << "#endif\n";
|
||||
}
|
||||
ss << config_.indent << "psx_check_interrupts(cpu);\n";
|
||||
ss << config_.indent
|
||||
<< fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", block.start_addr);
|
||||
|
||||
// Translate each instruction in the block
|
||||
uint32_t addr = block.start_addr;
|
||||
@@ -1202,12 +1211,13 @@ std::string CodeGenerator::translate_basic_block(
|
||||
// Function call (jal)
|
||||
ss << config_.indent << "cpu->gpr[31] = " << fmt::format("0x{:08X}; /* return address */\n", addr + 8);
|
||||
uint32_t target = block.exit_instr.target;
|
||||
if (known_functions_.count(target) > 0) {
|
||||
ss << config_.indent << fmt::format("func_{:08X}(cpu); /* jal */\n", target);
|
||||
} else {
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, 0x{:08X}u); /* external jal */\n", target);
|
||||
}
|
||||
if (!block.successors.empty()) {
|
||||
if (known_functions_.count(target) > 0) {
|
||||
ss << config_.indent << fmt::format("func_{:08X}(cpu); /* jal */\n", target);
|
||||
} else {
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, 0x{:08X}u); /* external jal */\n", target);
|
||||
}
|
||||
ss << config_.indent << "if (cpu->pc != 0) return;\n";
|
||||
if (!block.successors.empty()) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("goto block_{:08X}; /* continue after call */\n", block.successors[0]);
|
||||
} else {
|
||||
@@ -1232,10 +1242,11 @@ std::string CodeGenerator::translate_basic_block(
|
||||
reg_name(rd), addr + 8);
|
||||
|
||||
// Dispatch indirect call — target is a runtime register value
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jalr {} */\n",
|
||||
reg_name(rs), reg_name(rs));
|
||||
|
||||
// Continue to successor block (instruction at return address)
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jalr {} */\n",
|
||||
reg_name(rs), reg_name(rs));
|
||||
ss << config_.indent << "if (cpu->pc != 0) return;\n";
|
||||
|
||||
// Continue to successor block (instruction at return address)
|
||||
if (!block.successors.empty()) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("goto block_{:08X}; /* continue after jalr */\n",
|
||||
@@ -1289,19 +1300,34 @@ GeneratedFunction CodeGenerator::generate_function(
|
||||
std::stringstream body_ss;
|
||||
body_ss << "{\n";
|
||||
body_ss << config_.indent
|
||||
<< fmt::format("debug_server_log_call_entry(0x{:08X}u);\n",
|
||||
<< fmt::format("debug_server_log_call_entry_cpu(0x{:08X}u, cpu);\n",
|
||||
func.start_addr);
|
||||
|
||||
// Add function comment
|
||||
if (config_.emit_comments) {
|
||||
body_ss << config_.indent
|
||||
<< fmt::format("/* Address: 0x{:08X}, Size: {} bytes, Blocks: {} */\n",
|
||||
func.start_addr, func.size, cfg.blocks.size());
|
||||
}
|
||||
|
||||
// Pre-scan: find jump table targets that land mid-block (not a block boundary).
|
||||
// These need inline labels so goto statements in the switch can reach them.
|
||||
extra_labels_.clear();
|
||||
if (config_.emit_comments) {
|
||||
body_ss << config_.indent
|
||||
<< fmt::format("/* Address: 0x{:08X}, Size: {} bytes, Blocks: {} */\n",
|
||||
func.start_addr, func.size, cfg.blocks.size());
|
||||
}
|
||||
|
||||
auto cont_it = continuation_entries_by_function_.find(func.start_addr);
|
||||
if (cont_it != continuation_entries_by_function_.end() && !cont_it->second.empty()) {
|
||||
body_ss << config_.indent << "switch (cpu->pc) {\n";
|
||||
for (uint32_t continuation_addr : cont_it->second) {
|
||||
if (!cfg.blocks.count(continuation_addr)) {
|
||||
continue;
|
||||
}
|
||||
body_ss << config_.indent
|
||||
<< fmt::format(" case 0x{:08X}u: cpu->pc = 0; goto block_{:08X};\n",
|
||||
continuation_addr, continuation_addr);
|
||||
}
|
||||
body_ss << config_.indent << " default: break;\n";
|
||||
body_ss << config_.indent << "}\n";
|
||||
}
|
||||
|
||||
// Pre-scan: find jump table targets that land mid-block (not a block boundary).
|
||||
// These need inline labels so goto statements in the switch can reach them.
|
||||
extra_labels_.clear();
|
||||
for (const auto& [baddr, blk] : cfg.blocks) {
|
||||
if (blk.exit_instr.type != ControlFlowType::JumpRegister) continue;
|
||||
uint32_t jr_r = get_rs(blk.exit_instr.instruction);
|
||||
@@ -1473,7 +1499,7 @@ std::string CodeGenerator::generate_file(
|
||||
// Include the generic PSX runtime header.
|
||||
// This provides CPUState, GTE/trap declarations, and call_by_address().
|
||||
ss << "#include \"psx_runtime.h\"\n\n";
|
||||
ss << "extern void debug_server_log_call_entry(uint32_t func_addr);\n\n";
|
||||
ss << "extern void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu);\n\n";
|
||||
|
||||
// Emit reference implementations for unaligned memory helpers.
|
||||
// These implement the MIPS lwl/lwr/swl/swr semantics.
|
||||
@@ -1691,11 +1717,79 @@ std::string CodeGenerator::generate_file(
|
||||
total_new, functions_mut.size());
|
||||
}
|
||||
}
|
||||
|
||||
set_known_functions(known_addrs);
|
||||
|
||||
// Generate all functions first (to collect names)
|
||||
auto gen_funcs = generate_all_functions(functions_mut, cfgs_mut);
|
||||
|
||||
set_known_functions(known_addrs);
|
||||
|
||||
std::map<uint32_t, uint32_t> continuation_owners;
|
||||
size_t call_continuation_count = 0;
|
||||
size_t internal_continuation_count = 0;
|
||||
for (const Function& func : functions_mut) {
|
||||
if (func.is_data_section) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto cfg_it = cfgs_mut.find(func.start_addr);
|
||||
if (cfg_it == cfgs_mut.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ControlFlowGraph& cfg = cfg_it->second;
|
||||
|
||||
/*
|
||||
* Any block leader can become a non-local re-entry point. Hardware
|
||||
* interrupts resume at block leaders after RFE, and unrecognised
|
||||
* computed jumps (for example jump tables whose pattern was not
|
||||
* statically decoded) fall back through psx_dispatch. Registering the
|
||||
* owning function plus cpu->pc lets dispatch land on the exact local
|
||||
* label instead of treating valid in-function control flow as unknown.
|
||||
*/
|
||||
for (const auto& [block_addr, block] : cfg.blocks) {
|
||||
if (block_addr == cfg.function_start) {
|
||||
continue;
|
||||
}
|
||||
if (block_addr < cfg.function_start || block_addr >= cfg.function_end) {
|
||||
continue;
|
||||
}
|
||||
if (known_addrs.count(block_addr)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (continuation_owners.emplace(block_addr, func.start_addr).second) {
|
||||
internal_continuation_count++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [block_addr, block] : cfg.blocks) {
|
||||
if (block.exit_instr.type != ControlFlowType::JumpLink &&
|
||||
block.exit_instr.type != ControlFlowType::JumpLinkReg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t continuation_addr = block.exit_instr.address + 8;
|
||||
if (continuation_addr == cfg.function_start) {
|
||||
continue;
|
||||
}
|
||||
if (continuation_addr < cfg.function_start || continuation_addr >= cfg.function_end) {
|
||||
continue;
|
||||
}
|
||||
if (!cfg.blocks.count(continuation_addr)) {
|
||||
continue;
|
||||
}
|
||||
if (known_addrs.count(continuation_addr)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (continuation_owners.emplace(continuation_addr, func.start_addr).second) {
|
||||
call_continuation_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
set_continuation_entries(continuation_owners);
|
||||
fmt::print("Dispatchable internal continuations: {} ({} block leaders, {} call-only)\n",
|
||||
continuation_owners_.size(), internal_continuation_count, call_continuation_count);
|
||||
|
||||
// Generate all functions first (to collect names)
|
||||
auto gen_funcs = generate_all_functions(functions_mut, cfgs_mut);
|
||||
|
||||
// Forward declarations for all functions
|
||||
if (!gen_funcs.empty()) {
|
||||
|
||||
@@ -354,7 +354,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
out += fmt::format(" psx_advance_cycles({}u);\n", bcyc);
|
||||
out += "#endif\n";
|
||||
}
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += fmt::format(" psx_check_interrupts_at(cpu, 0x{:08X}u);\n", addr);
|
||||
if (should_probe_pc(addr)) {
|
||||
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n", addr);
|
||||
}
|
||||
@@ -416,7 +416,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
else
|
||||
out += " return;\n";
|
||||
} else {
|
||||
out += fmt::format(" cpu->pc = cpu->gpr[{}]; return;\n",
|
||||
out += fmt::format(" cpu->pc = cpu->gpr[{}]; psx_rfe_return(cpu); return;\n",
|
||||
static_cast<int>(rs));
|
||||
}
|
||||
} else {
|
||||
@@ -523,6 +523,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
uint32_t return_addr = relocate_ra(addr + 8);
|
||||
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u;\n", return_addr);
|
||||
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u);\n", target);
|
||||
out += " if (cpu->pc != 0) return;\n";
|
||||
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", return_addr);
|
||||
} else if (kind == "jalr") {
|
||||
uint32_t ds_addr = addr + 4;
|
||||
@@ -548,6 +549,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
}
|
||||
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
|
||||
static_cast<int>(rs));
|
||||
out += " if (cpu->pc != 0) return;\n";
|
||||
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", return_addr);
|
||||
}
|
||||
}
|
||||
@@ -659,6 +661,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
pb.terminator_addr);
|
||||
}
|
||||
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u);\n", target);
|
||||
out += " if (cpu->pc != 0) return;\n";
|
||||
if (target == 0x00006380u) {
|
||||
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n",
|
||||
pb.terminator_addr + 1);
|
||||
@@ -689,6 +692,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
}
|
||||
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
|
||||
static_cast<int>(rs));
|
||||
out += " if (cpu->pc != 0) return;\n";
|
||||
// Safety net: if continuation falls outside this function, tail-call to it.
|
||||
if (!addr_to_raw.count(pb.terminator_addr + 8)) {
|
||||
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u); /* jalr cont: outside func */\n", return_addr);
|
||||
@@ -868,7 +872,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// Direct-call entry hook: captures into fn_entry ring (gated by
|
||||
// fn_filter at runtime). Lets us see direct-jal call paths that
|
||||
// never go through psx_dispatch.
|
||||
out += fmt::format(" debug_server_log_call_entry(0x{:08X}u);\n", norm);
|
||||
out += fmt::format(" debug_server_log_call_entry_cpu(0x{:08X}u, cpu);\n", norm);
|
||||
// Branch predicate locals must be initialized before the continuation
|
||||
// switch. Continuation entry can jump directly to a merge block that reads
|
||||
// a predicate set only on a different inbound path.
|
||||
@@ -924,13 +928,16 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
// Extern declarations for runtime-provided functions.
|
||||
out += "extern void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys);\n";
|
||||
out += "extern void psx_check_interrupts(CPUState* cpu);\n";
|
||||
out += "extern void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc);\n";
|
||||
out += "extern void psx_restore_state_escape(void);\n";
|
||||
out += "extern void psx_rfe_return(CPUState* cpu);\n";
|
||||
out += "extern void gte_execute(CPUState* cpu, uint32_t cmd);\n";
|
||||
out += "extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);\n";
|
||||
out += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n";
|
||||
out += "extern uint32_t g_debug_current_func_addr;\n";
|
||||
out += "extern void debug_server_trace_dispatch(uint32_t func_addr);\n";
|
||||
out += "extern void debug_server_trace_dispatch_return(uint32_t func_addr, CPUState* cpu);\n\n";
|
||||
out += "extern uint64_t g_psx_nonlocal_epoch;\n\n";
|
||||
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
|
||||
out += "extern int psx_game_address_in_text(uint32_t addr);\n";
|
||||
out += "#endif\n\n";
|
||||
@@ -1073,12 +1080,13 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " return phys;\n";
|
||||
out += "}\n\n";
|
||||
|
||||
out += "extern int dirty_ram_dispatch(CPUState* cpu, uint32_t addr);\n";
|
||||
out += "extern int dirty_ram_dispatch_until(CPUState* cpu, uint32_t addr, uint32_t stop_addr);\n";
|
||||
out += "extern int dirty_ram_is_dirty(uint32_t phys);\n";
|
||||
out += "extern void fntrace_record(CPUState* cpu, uint32_t target);\n";
|
||||
out += "extern uint64_t g_dispatch_static_hits;\n";
|
||||
out += "\n";
|
||||
out += "int g_psx_dispatch_depth = 0;\n\n";
|
||||
out += "int g_psx_dispatch_depth = 0;\n";
|
||||
out += "int g_psx_call_depth = 0;\n\n";
|
||||
out += "static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {\n";
|
||||
out += " /* Tail-call trampoline: functions signal tail calls by setting\n";
|
||||
out += " * cpu->pc to the target and returning. We loop here to re-dispatch\n";
|
||||
@@ -1086,6 +1094,7 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " * the outermost dispatch returns, so a nested callee cannot interrupt\n";
|
||||
out += " * before its generated caller runs the post-call continuation. */\n";
|
||||
out += " int outermost = (g_psx_dispatch_depth++ == 0);\n";
|
||||
out += " uint64_t nonlocal_epoch = g_psx_nonlocal_epoch;\n";
|
||||
out += " for (;;) {\n";
|
||||
out += " /* Always-on call ring: every iteration counts as a separate\n";
|
||||
out += " * call (initial entry + each tail-call re-dispatch). a0..a3\n";
|
||||
@@ -1101,7 +1110,7 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " * path before normalizing it to shell ROM. */\n";
|
||||
out += " uint32_t game_phys = addr & 0x1FFFFFFFu;\n";
|
||||
out += " if (psx_game_address_in_text(addr) && dirty_ram_is_dirty(game_phys)) {\n";
|
||||
out += " found = dirty_ram_dispatch(cpu, addr);\n";
|
||||
out += " found = dirty_ram_dispatch_until(cpu, addr, stop_addr);\n";
|
||||
out += " }\n";
|
||||
out += "#endif\n";
|
||||
out += " uint32_t phys = normalize(addr);\n";
|
||||
@@ -1129,7 +1138,7 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " * boot, interpret the basic block on cpu state. Falls back to\n";
|
||||
out += " * psx_unknown_dispatch for genuinely unmapped PCs. */\n";
|
||||
out += " if (!found) {\n";
|
||||
out += " if (dirty_ram_dispatch(cpu, addr)) {\n";
|
||||
out += " if (dirty_ram_dispatch_until(cpu, addr, stop_addr)) {\n";
|
||||
out += " found = 1;\n";
|
||||
out += " } else {\n";
|
||||
out += " psx_unknown_dispatch(cpu, addr, phys);\n";
|
||||
@@ -1137,15 +1146,16 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " }\n";
|
||||
out += " if (cpu->pc == 0) {\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " if (outermost && g_psx_call_depth == 0) {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += " }\n";
|
||||
out += " return;\n";
|
||||
out += " }\n";
|
||||
out += " if (stop_addr != 0 && cpu->pc == stop_addr) {\n";
|
||||
out += " cpu->pc = 0;\n";
|
||||
out += " if (stop_addr != 0 && ((cpu->pc ^ stop_addr) & 0x1FFFFFFFu) == 0) {\n";
|
||||
out += " int consume_stop = (g_psx_nonlocal_epoch == nonlocal_epoch);\n";
|
||||
out += " if (consume_stop) cpu->pc = 0;\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " if (consume_stop && outermost && g_psx_call_depth == 0) {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += " }\n";
|
||||
out += " return;\n";
|
||||
@@ -1220,11 +1230,13 @@ EmitStats FullFunctionEmitter::emit(
|
||||
full_c += "extern void psx_dispatch(CPUState* cpu, uint32_t addr);\n";
|
||||
full_c += "extern void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys);\n";
|
||||
full_c += "extern void psx_check_interrupts(CPUState* cpu);\n";
|
||||
full_c += "extern void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc);\n";
|
||||
full_c += "extern void psx_restore_state_escape(void);\n";
|
||||
full_c += "extern void psx_rfe_return(CPUState* cpu);\n";
|
||||
full_c += "extern void gte_execute(CPUState* cpu, uint32_t cmd);\n";
|
||||
full_c += "extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);\n";
|
||||
full_c += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n";
|
||||
full_c += "extern void debug_server_log_call_entry(uint32_t func_addr);\n";
|
||||
full_c += "extern void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu);\n";
|
||||
full_c += "extern void debug_server_log_probe(uint32_t pc, CPUState *cpu);\n";
|
||||
full_c += "extern uint32_t g_debug_last_store_pc;\n";
|
||||
full_c += "/* Phase 1.0e-d: per-block guest cycle accounting.\n";
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <map>
|
||||
|
||||
#include "ps1_exe_parser.h"
|
||||
#include "gte.h"
|
||||
@@ -388,6 +389,7 @@ int main(int argc, char** argv) {
|
||||
fmt::print("Generating complete C file: {}\n", output_filename.string());
|
||||
|
||||
std::string full_c_code = codegen.generate_file(analysis_result.functions, all_cfgs);
|
||||
const auto& continuation_owners = codegen.continuation_owners();
|
||||
std::set<uint32_t> dispatch_addrs;
|
||||
{
|
||||
const std::string marker = "void func_";
|
||||
@@ -448,11 +450,29 @@ int main(int argc, char** argv) {
|
||||
|
||||
ds << "/* Maps PS1 address to compiled game code. Returns 1 if dispatched, 0 if unknown. */\n";
|
||||
ds << "int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr) {\n";
|
||||
ds << " uint32_t phys = addr & 0x1FFFFFFFu;\n";
|
||||
ds << fmt::format(" if (phys >= 0x{:08X}u && phys < 0x{:08X}u) {{\n",
|
||||
game_text_start, game_text_end);
|
||||
ds << fmt::format(" addr = 0x{:08X}u + (phys - 0x{:08X}u);\n",
|
||||
exe->load_address(), game_text_start);
|
||||
ds << " }\n";
|
||||
ds << " switch (addr) {\n";
|
||||
for (uint32_t addr : dispatch_addrs) {
|
||||
ds << fmt::format(" case 0x{:08X}u: func_{:08X}(cpu); return 1;\n",
|
||||
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0; func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr);
|
||||
}
|
||||
size_t continuation_dispatch_count = 0;
|
||||
for (const auto& [continuation_addr, owner_addr] : continuation_owners) {
|
||||
if (dispatch_addrs.count(continuation_addr)) {
|
||||
continue;
|
||||
}
|
||||
if (!dispatch_addrs.count(owner_addr)) {
|
||||
continue;
|
||||
}
|
||||
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0x{:08X}u; func_{:08X}(cpu); return 1;\n",
|
||||
continuation_addr, continuation_addr, owner_addr);
|
||||
continuation_dispatch_count++;
|
||||
}
|
||||
ds << " default: return 0;\n";
|
||||
ds << " }\n";
|
||||
ds << "}\n";
|
||||
@@ -463,6 +483,8 @@ int main(int argc, char** argv) {
|
||||
dispatch_file.close();
|
||||
fmt::print("✓ Dispatch table written ({} entries)\n\n",
|
||||
dispatch_addrs.size());
|
||||
fmt::print("Dispatch continuation entries: {}\n\n",
|
||||
continuation_dispatch_count);
|
||||
} else {
|
||||
fmt::print(stderr, "⚠ Failed to write dispatch file\n\n");
|
||||
}
|
||||
|
||||
+33
-1
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ void interrupts_init(void);
|
||||
* Fires vblank on schedule, checks (i_stat & i_mask), and if
|
||||
* pending + COP0 allows, dispatches the exception handler. */
|
||||
void psx_check_interrupts(struct CPUState* cpu);
|
||||
void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc);
|
||||
|
||||
/* Accumulate emitted PSX cycles toward the next VBlank trigger.
|
||||
* Called from psx_advance_cycles() so the VBlank rate is gated on
|
||||
@@ -49,10 +50,34 @@ void psx_get_freeze_diag(uint64_t *out_total_checks,
|
||||
uint64_t *out_exc_entries,
|
||||
uint64_t *out_exc_reentry_blocks);
|
||||
|
||||
#define PSX_IRQ_CHECK_TRACE_CAP (1u << 18)
|
||||
typedef struct PsxIrqCheckTraceEntry {
|
||||
uint64_t seq;
|
||||
uint32_t frame;
|
||||
uint32_t resume_pc;
|
||||
uint32_t i_stat;
|
||||
uint32_t i_mask;
|
||||
uint32_t sr;
|
||||
uint32_t cause;
|
||||
uint32_t call_depth;
|
||||
uint32_t reason;
|
||||
} PsxIrqCheckTraceEntry;
|
||||
|
||||
extern PsxIrqCheckTraceEntry g_psx_irq_check_trace[PSX_IRQ_CHECK_TRACE_CAP];
|
||||
extern uint64_t g_psx_irq_check_trace_seq;
|
||||
extern uint64_t g_psx_nonlocal_epoch;
|
||||
|
||||
/* Bumped whenever BIOS exception/RestoreState/RFE flow abandons the current
|
||||
* generated C call stack. Dispatch uses this to avoid consuming a synthetic
|
||||
* JAL/JALR return address as a normal inline return after guest context has
|
||||
* been restored elsewhere. */
|
||||
void psx_note_nonlocal_control_flow(void);
|
||||
|
||||
/* longjmp back to psx_check_interrupts, unwinding the exception handler.
|
||||
* Called by ReturnFromException (B0:0x17 or SYSCALL(3)) when inside
|
||||
* the exception handler to model real hardware's RFE+JR $k0 unwind. */
|
||||
void psx_exception_longjmp(void);
|
||||
void psx_rfe_return(struct CPUState* cpu);
|
||||
|
||||
/* longjmp back to psx_check_interrupts for RestoreState redirection.
|
||||
*
|
||||
|
||||
+66
-2
@@ -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;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "crash_trace.h"
|
||||
#include "debug_server.h"
|
||||
|
||||
/* Output path — overwritten per dump. */
|
||||
static const char *kReportPath = "psx_last_run_report.json";
|
||||
@@ -55,7 +56,9 @@ extern uint64_t crash_trace_dispatch_seq_get(void);
|
||||
* UnknownDispatchEntry. Accessor wrappers exported by debug_server.c. */
|
||||
typedef struct {
|
||||
uint64_t seq;
|
||||
uint32_t addr, phys, ra, a0, a1, frame, pad;
|
||||
uint32_t addr, phys, ra, sp, v0, v1;
|
||||
uint32_t a0, a1, a2, a3, t0, t1, t2;
|
||||
uint32_t s0, s1, s2, s3, frame, current_func;
|
||||
} UnknownDispatchEntry;
|
||||
#define UNKNOWN_DISPATCH_CAP (1 << 16)
|
||||
extern UnknownDispatchEntry crash_trace_unknown_get(uint64_t seq);
|
||||
@@ -64,6 +67,26 @@ extern uint64_t crash_trace_unknown_seq_get(void);
|
||||
/* Dirty-RAM block log (defined in dirty_ram_interp.c). */
|
||||
#include "dirty_ram_interp.h"
|
||||
|
||||
static void write_sidecar_trace(const char *path, const char *field,
|
||||
void (*dump_fn)(FILE *, uint32_t),
|
||||
uint32_t max_count,
|
||||
const char *reason)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return;
|
||||
fprintf(f,
|
||||
"{\n"
|
||||
" \"reason\": \"%s\",\n"
|
||||
" \"frame\": %llu,\n"
|
||||
" \"%s\": ",
|
||||
reason ? reason : "(unknown)",
|
||||
(unsigned long long)s_frame_count,
|
||||
field);
|
||||
dump_fn(f, max_count);
|
||||
fputs("\n}\n", f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* JSON helpers. Hand-rolled to avoid allocations on the SEH path. */
|
||||
|
||||
static int append_str(char *buf, size_t cap, size_t *pos, const char *s) {
|
||||
@@ -201,11 +224,19 @@ void psx_crash_trace_dump(const char *reason, void *seh_info) {
|
||||
UnknownDispatchEntry e = crash_trace_unknown_get(start + i);
|
||||
append_fmt(buf, sizeof(buf), &pos,
|
||||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"phys\":\"0x%08X\","
|
||||
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"frame\":%u}",
|
||||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\",\"t2\":\"0x%08X\","
|
||||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||||
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||||
"\"current_func\":\"0x%08X\",\"frame\":%u}",
|
||||
i == 0 ? "" : ",",
|
||||
(unsigned long long)e.seq, e.addr, e.phys,
|
||||
e.ra, e.a0, e.a1, e.frame);
|
||||
e.ra, e.sp, e.v0, e.v1, e.a0, e.a1, e.a2, e.a3,
|
||||
e.t0, e.t1, e.t2, e.s0, e.s1, e.s2, e.s3,
|
||||
e.current_func, e.frame);
|
||||
}
|
||||
append_str(buf, sizeof(buf), &pos, "]\n },\n");
|
||||
}
|
||||
@@ -243,6 +274,22 @@ void psx_crash_trace_dump(const char *reason, void *seh_info) {
|
||||
fwrite(buf, 1, pos, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
write_sidecar_trace("psx_last_restore_trace.json", "restore_trace",
|
||||
debug_server_freeze_dump_restore_trace_json, 2048,
|
||||
reason);
|
||||
write_sidecar_trace("psx_last_thread_trace.json", "thread_trace",
|
||||
debug_server_freeze_dump_thread_trace_json, 1024,
|
||||
reason);
|
||||
write_sidecar_trace("psx_last_irq_check_log.json", "irq_check_log",
|
||||
debug_server_freeze_dump_irq_check_json, 2048,
|
||||
reason);
|
||||
write_sidecar_trace("psx_last_fn_entry_trace.json", "fn_entry_trace",
|
||||
debug_server_freeze_dump_fn_entry_json, 2048,
|
||||
reason);
|
||||
write_sidecar_trace("psx_last_evcb_trace.json", "evcb_trace",
|
||||
debug_server_freeze_dump_evcb_json, 1024,
|
||||
reason);
|
||||
}
|
||||
|
||||
/* ── Crash handlers ──────────────────────────────────────────────────── */
|
||||
|
||||
+604
-22
@@ -377,6 +377,13 @@ typedef struct {
|
||||
uint32_t current_tcb;
|
||||
uint32_t current_tcb_state;
|
||||
uint32_t current_tcb_cause;
|
||||
uint32_t saved_s0;
|
||||
uint32_t saved_s1;
|
||||
uint32_t saved_s2;
|
||||
uint32_t saved_s3;
|
||||
uint32_t saved_sp;
|
||||
uint32_t saved_ra;
|
||||
uint32_t saved_pc;
|
||||
uint32_t sr;
|
||||
uint32_t epc;
|
||||
uint32_t istat;
|
||||
@@ -567,6 +574,16 @@ void debug_server_log_restore_event(uint32_t kind, uint32_t target_pc, uint32_t
|
||||
e->current_tcb = (cpu && e->tcbh_ptr) ? trace_read_word(cpu, e->tcbh_ptr) : 0;
|
||||
e->current_tcb_state = (cpu && e->current_tcb) ? trace_read_word(cpu, e->current_tcb) : 0;
|
||||
e->current_tcb_cause = (cpu && e->current_tcb) ? trace_read_word(cpu, e->current_tcb + 152u) : 0;
|
||||
if (cpu && e->current_tcb) {
|
||||
uint32_t save = e->current_tcb + 8u;
|
||||
e->saved_s0 = trace_read_word(cpu, save + 64u);
|
||||
e->saved_s1 = trace_read_word(cpu, save + 68u);
|
||||
e->saved_s2 = trace_read_word(cpu, save + 72u);
|
||||
e->saved_s3 = trace_read_word(cpu, save + 76u);
|
||||
e->saved_sp = trace_read_word(cpu, save + 116u);
|
||||
e->saved_ra = trace_read_word(cpu, save + 124u);
|
||||
e->saved_pc = trace_read_word(cpu, save + 128u);
|
||||
}
|
||||
e->queue_base = cpu ? trace_read_word(cpu, 0x00000100u) : 0;
|
||||
for (size_t q = 0; q < RESTORE_TRACE_QUEUE_COUNT; q++) {
|
||||
uint32_t queue_addr = e->queue_base + (uint32_t)(q * 8u);
|
||||
@@ -854,10 +871,22 @@ typedef struct {
|
||||
uint32_t addr;
|
||||
uint32_t phys;
|
||||
uint32_t ra;
|
||||
uint32_t sp;
|
||||
uint32_t v0;
|
||||
uint32_t v1;
|
||||
uint32_t a0;
|
||||
uint32_t a1;
|
||||
uint32_t a2;
|
||||
uint32_t a3;
|
||||
uint32_t t0;
|
||||
uint32_t t1;
|
||||
uint32_t t2;
|
||||
uint32_t s0;
|
||||
uint32_t s1;
|
||||
uint32_t s2;
|
||||
uint32_t s3;
|
||||
uint32_t frame;
|
||||
uint32_t pad;
|
||||
uint32_t current_func;
|
||||
} UnknownDispatchEntry;
|
||||
static UnknownDispatchEntry s_unknown_ring[UNKNOWN_DISPATCH_CAP];
|
||||
static uint64_t s_unknown_seq = 0;
|
||||
@@ -875,7 +904,13 @@ static UnknownUniqueEntry s_unknown_unique[UNKNOWN_UNIQUE_CAP];
|
||||
static int s_unknown_unique_count = 0;
|
||||
|
||||
void psx_unknown_dispatch_record(uint32_t addr, uint32_t phys,
|
||||
uint32_t ra, uint32_t a0, uint32_t a1)
|
||||
uint32_t ra, uint32_t sp,
|
||||
uint32_t v0, uint32_t v1,
|
||||
uint32_t a0, uint32_t a1,
|
||||
uint32_t a2, uint32_t a3,
|
||||
uint32_t t0, uint32_t t1, uint32_t t2,
|
||||
uint32_t s0, uint32_t s1,
|
||||
uint32_t s2, uint32_t s3)
|
||||
{
|
||||
uint64_t seq = s_unknown_seq++;
|
||||
UnknownDispatchEntry *e = &s_unknown_ring[seq & (UNKNOWN_DISPATCH_CAP - 1u)];
|
||||
@@ -883,9 +918,22 @@ void psx_unknown_dispatch_record(uint32_t addr, uint32_t phys,
|
||||
e->addr = addr;
|
||||
e->phys = phys;
|
||||
e->ra = ra;
|
||||
e->sp = sp;
|
||||
e->v0 = v0;
|
||||
e->v1 = v1;
|
||||
e->a0 = a0;
|
||||
e->a1 = a1;
|
||||
e->a2 = a2;
|
||||
e->a3 = a3;
|
||||
e->t0 = t0;
|
||||
e->t1 = t1;
|
||||
e->t2 = t2;
|
||||
e->s0 = s0;
|
||||
e->s1 = s1;
|
||||
e->s2 = s2;
|
||||
e->s3 = s3;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->current_func = g_debug_current_func_addr;
|
||||
/* Per-target count (linear probe). */
|
||||
uint32_t idx = (phys >> 2) % UNKNOWN_UNIQUE_CAP;
|
||||
for (int i = 0; i < UNKNOWN_UNIQUE_CAP; i++) {
|
||||
@@ -1728,16 +1776,17 @@ static int is_chain_epilogue(uint32_t phys) {
|
||||
* psx_dispatch (e.g. shell -> firstfile -> bu_read -> card_read). The shadow
|
||||
* stack is owned by function_trace_record and is only meaningful for
|
||||
* indirect dispatches; direct calls return through the native C stack. */
|
||||
void debug_server_log_call_entry(uint32_t func_addr) {
|
||||
static void debug_server_log_call_entry_with_cpu(uint32_t func_addr, CPUState *cpu) {
|
||||
#ifdef PSX_NO_DEBUG_TOOLS
|
||||
/* Hottest call site in the binary — called at the top of every
|
||||
* recompiled function. Early-return makes it a 1-instruction
|
||||
* function-call cost; the compiler will likely PLT it through. */
|
||||
(void)func_addr;
|
||||
(void)cpu;
|
||||
return;
|
||||
#endif
|
||||
s_fn_direct_seen++;
|
||||
if (!debug_cpu_ptr) {
|
||||
if (!cpu) {
|
||||
s_fn_direct_no_cpu++;
|
||||
return;
|
||||
}
|
||||
@@ -1751,21 +1800,29 @@ void debug_server_log_call_entry(uint32_t func_addr) {
|
||||
e->seq = s_fn_entry_seq;
|
||||
e->paired_exit_seq = 0;
|
||||
e->func_addr = func_addr;
|
||||
e->ra = debug_cpu_ptr->gpr[31];
|
||||
e->a0 = debug_cpu_ptr->gpr[4];
|
||||
e->a1 = debug_cpu_ptr->gpr[5];
|
||||
e->a2 = debug_cpu_ptr->gpr[6];
|
||||
e->a3 = debug_cpu_ptr->gpr[7];
|
||||
e->t1 = debug_cpu_ptr->gpr[9];
|
||||
e->s0 = debug_cpu_ptr->gpr[16];
|
||||
e->s1 = debug_cpu_ptr->gpr[17];
|
||||
e->s2 = debug_cpu_ptr->gpr[18];
|
||||
e->s3 = debug_cpu_ptr->gpr[19];
|
||||
e->ra = cpu->gpr[31];
|
||||
e->a0 = cpu->gpr[4];
|
||||
e->a1 = cpu->gpr[5];
|
||||
e->a2 = cpu->gpr[6];
|
||||
e->a3 = cpu->gpr[7];
|
||||
e->t1 = cpu->gpr[9];
|
||||
e->s0 = cpu->gpr[16];
|
||||
e->s1 = cpu->gpr[17];
|
||||
e->s2 = cpu->gpr[18];
|
||||
e->s3 = cpu->gpr[19];
|
||||
e->depth = (uint32_t)s_fn_stack_top;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
s_fn_entry_seq++;
|
||||
}
|
||||
|
||||
void debug_server_log_call_entry(uint32_t func_addr) {
|
||||
debug_server_log_call_entry_with_cpu(func_addr, debug_cpu_ptr);
|
||||
}
|
||||
|
||||
void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu) {
|
||||
debug_server_log_call_entry_with_cpu(func_addr, cpu);
|
||||
}
|
||||
|
||||
static void exception_trace_record(uint32_t kind, uint32_t func_addr, CPUState *cpu)
|
||||
{
|
||||
if (!cpu || !psx_get_in_exception()) return;
|
||||
@@ -2075,6 +2132,157 @@ static void handle_dirty_flow_log(int id, const char *json)
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void handle_dirty_exit_log(int id, const char *json)
|
||||
{
|
||||
char buf[32];
|
||||
uint32_t entry_lo = 0, entry_hi = 0xFFFFFFFFu;
|
||||
uint32_t target_lo = 0, target_hi = 0xFFFFFFFFu;
|
||||
uint32_t reason = 0;
|
||||
int has_reason = 0;
|
||||
if (json_get_str(json, "entry_lo", buf, sizeof(buf))) entry_lo = hex_to_u32(buf);
|
||||
if (json_get_str(json, "entry_hi", buf, sizeof(buf))) entry_hi = hex_to_u32(buf);
|
||||
if (json_get_str(json, "target_lo", buf, sizeof(buf))) target_lo = hex_to_u32(buf);
|
||||
if (json_get_str(json, "target_hi", buf, sizeof(buf))) target_hi = hex_to_u32(buf);
|
||||
if (json_get_str(json, "reason", buf, sizeof(buf))) {
|
||||
reason = hex_to_u32(buf);
|
||||
has_reason = 1;
|
||||
}
|
||||
int count = json_get_int(json, "count", 256);
|
||||
if (count < 1) count = 1;
|
||||
if (count > (int)DIRTY_RAM_EXIT_LOG_CAP) count = DIRTY_RAM_EXIT_LOG_CAP;
|
||||
|
||||
uint64_t total = g_dirty_ram_exit_log_seq;
|
||||
uint64_t avail = (total < DIRTY_RAM_EXIT_LOG_CAP) ? total : DIRTY_RAM_EXIT_LOG_CAP;
|
||||
const size_t BUF_SZ = 16 * 1024 * 1024;
|
||||
char *out = (char *)malloc(BUF_SZ);
|
||||
if (!out) {
|
||||
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id);
|
||||
return;
|
||||
}
|
||||
size_t pos = 0;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||||
"\"entry_lo\":\"0x%08X\",\"entry_hi\":\"0x%08X\","
|
||||
"\"target_lo\":\"0x%08X\",\"target_hi\":\"0x%08X\","
|
||||
"\"has_reason\":%d,\"reason\":%u,\"entries\":[",
|
||||
id, (unsigned long long)total, (unsigned long long)avail,
|
||||
entry_lo, entry_hi, target_lo, target_hi,
|
||||
has_reason ? 1 : 0, reason);
|
||||
int emitted = 0;
|
||||
for (uint64_t i = 0; i < avail && emitted < count; i++) {
|
||||
uint64_t seq = total - 1 - i;
|
||||
DirtyRamExitLogEntry *e =
|
||||
&g_dirty_ram_exit_log[seq & (DIRTY_RAM_EXIT_LOG_CAP - 1u)];
|
||||
if (e->entry < entry_lo || e->entry >= entry_hi) continue;
|
||||
if (e->target < target_lo || e->target >= target_hi) continue;
|
||||
if (has_reason && e->reason != reason) continue;
|
||||
if (pos > BUF_SZ - 1024) break;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"entry\":\"0x%08X\",\"pc\":\"0x%08X\","
|
||||
"\"target\":\"0x%08X\",\"stop_addr\":\"0x%08X\","
|
||||
"\"reason\":%u,\"insns\":%u,"
|
||||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||||
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||||
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"current_tcb\":\"0x%08X\",\"frame\":%u,"
|
||||
"\"handled\":%u,\"in_exception\":%u}",
|
||||
emitted == 0 ? "" : ",",
|
||||
(unsigned long long)e->seq, e->entry, e->pc,
|
||||
e->target, e->stop_addr, e->reason, e->insns,
|
||||
e->s0, e->s1, e->s2, e->s3, e->sp, e->ra,
|
||||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
|
||||
e->current_tcb, e->frame,
|
||||
(unsigned)e->handled, (unsigned)e->in_exception);
|
||||
emitted++;
|
||||
}
|
||||
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
|
||||
debug_server_send_line(out);
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void handle_dirty_call_log(int id, const char *json)
|
||||
{
|
||||
char buf[32];
|
||||
uint32_t pc_lo = 0, pc_hi = 0xFFFFFFFFu;
|
||||
uint32_t target_lo = 0, target_hi = 0xFFFFFFFFu;
|
||||
if (json_get_str(json, "pc_lo", buf, sizeof(buf))) pc_lo = hex_to_u32(buf);
|
||||
if (json_get_str(json, "pc_hi", buf, sizeof(buf))) pc_hi = hex_to_u32(buf);
|
||||
if (json_get_str(json, "target_lo", buf, sizeof(buf))) target_lo = hex_to_u32(buf);
|
||||
if (json_get_str(json, "target_hi", buf, sizeof(buf))) target_hi = hex_to_u32(buf);
|
||||
int changed_only = json_get_int(json, "changed_only", 0);
|
||||
int count = json_get_int(json, "count", 256);
|
||||
if (count < 1) count = 1;
|
||||
if (count > (int)DIRTY_RAM_CALL_LOG_CAP) count = DIRTY_RAM_CALL_LOG_CAP;
|
||||
|
||||
uint64_t total = g_dirty_ram_call_log_seq;
|
||||
uint64_t avail = (total < DIRTY_RAM_CALL_LOG_CAP) ? total : DIRTY_RAM_CALL_LOG_CAP;
|
||||
const size_t BUF_SZ = 16 * 1024 * 1024;
|
||||
char *out = (char *)malloc(BUF_SZ);
|
||||
if (!out) {
|
||||
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id);
|
||||
return;
|
||||
}
|
||||
size_t pos = 0;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||||
"\"pc_lo\":\"0x%08X\",\"pc_hi\":\"0x%08X\","
|
||||
"\"target_lo\":\"0x%08X\",\"target_hi\":\"0x%08X\","
|
||||
"\"changed_only\":%d,\"entries\":[",
|
||||
id, (unsigned long long)total, (unsigned long long)avail,
|
||||
pc_lo, pc_hi, target_lo, target_hi, changed_only ? 1 : 0);
|
||||
int emitted = 0;
|
||||
for (uint64_t i = 0; i < avail && emitted < count; i++) {
|
||||
uint64_t seq = total - 1 - i;
|
||||
DirtyRamCallLogEntry *e =
|
||||
&g_dirty_ram_call_log[seq & (DIRTY_RAM_CALL_LOG_CAP - 1u)];
|
||||
if (e->pc < pc_lo || e->pc >= pc_hi) continue;
|
||||
if (e->target < target_lo || e->target >= target_hi) continue;
|
||||
if (changed_only &&
|
||||
e->before_s0 == e->after_s0 &&
|
||||
e->before_s1 == e->after_s1 &&
|
||||
e->before_s2 == e->after_s2 &&
|
||||
e->before_s3 == e->after_s3) {
|
||||
continue;
|
||||
}
|
||||
if (pos > BUF_SZ - 1024) break;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\","
|
||||
"\"target\":\"0x%08X\",\"return_pc\":\"0x%08X\","
|
||||
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
|
||||
"\"before_s1\":\"0x%08X\",\"after_s1\":\"0x%08X\","
|
||||
"\"before_s2\":\"0x%08X\",\"after_s2\":\"0x%08X\","
|
||||
"\"before_s3\":\"0x%08X\",\"after_s3\":\"0x%08X\","
|
||||
"\"before_sp\":\"0x%08X\",\"after_sp\":\"0x%08X\","
|
||||
"\"before_ra\":\"0x%08X\",\"after_ra\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"current_tcb\":\"0x%08X\",\"frame\":%u,"
|
||||
"\"defer_interrupts\":%u,\"in_exception\":%u,"
|
||||
"\"returned_pc_nonzero\":%u}",
|
||||
emitted == 0 ? "" : ",",
|
||||
(unsigned long long)e->seq, e->pc, e->target,
|
||||
e->return_pc,
|
||||
e->before_s0, e->after_s0,
|
||||
e->before_s1, e->after_s1,
|
||||
e->before_s2, e->after_s2,
|
||||
e->before_s3, e->after_s3,
|
||||
e->before_sp, e->after_sp,
|
||||
e->before_ra, e->after_ra,
|
||||
e->a0, e->a1, e->a2, e->a3,
|
||||
e->current_tcb, e->frame,
|
||||
(unsigned)e->defer_interrupts,
|
||||
(unsigned)e->in_exception,
|
||||
(unsigned)e->returned_pc_nonzero);
|
||||
emitted++;
|
||||
}
|
||||
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
|
||||
debug_server_send_line(out);
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void handle_dirty_insn_log(int id, const char *json)
|
||||
{
|
||||
char buf[32];
|
||||
@@ -2107,12 +2315,21 @@ static void handle_dirty_insn_log(int id, const char *json)
|
||||
DirtyRamInsnLogEntry *e =
|
||||
&g_dirty_ram_insn_log[seq & (DIRTY_RAM_INSN_LOG_CAP - 1u)];
|
||||
if (e->pc < pc_lo || e->pc >= pc_hi) continue;
|
||||
if (changed_only && e->before_s0 == e->after_s0) continue;
|
||||
if (changed_only &&
|
||||
e->before_s0 == e->after_s0 &&
|
||||
e->before_s1 == e->after_s1 &&
|
||||
e->before_s2 == e->after_s2 &&
|
||||
e->before_s3 == e->after_s3) {
|
||||
continue;
|
||||
}
|
||||
if (pos > BUF_SZ - 768) break;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"insn\":\"0x%08X\","
|
||||
"\"next_pc\":\"0x%08X\",\"target\":\"0x%08X\","
|
||||
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
|
||||
"\"before_s1\":\"0x%08X\",\"after_s1\":\"0x%08X\","
|
||||
"\"before_s2\":\"0x%08X\",\"after_s2\":\"0x%08X\","
|
||||
"\"before_s3\":\"0x%08X\",\"after_s3\":\"0x%08X\","
|
||||
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
@@ -2125,7 +2342,11 @@ static void handle_dirty_insn_log(int id, const char *json)
|
||||
emitted == 0 ? "" : ",",
|
||||
(unsigned long long)e->seq,
|
||||
e->pc, e->insn, e->next_pc, e->target,
|
||||
e->before_s0, e->after_s0, e->sp, e->ra,
|
||||
e->before_s0, e->after_s0,
|
||||
e->before_s1, e->after_s1,
|
||||
e->before_s2, e->after_s2,
|
||||
e->before_s3, e->after_s3,
|
||||
e->sp, e->ra,
|
||||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
|
||||
e->t0, e->t1, e->t2,
|
||||
e->current_tcb, e->task_ptr, e->task_mode,
|
||||
@@ -2137,6 +2358,53 @@ static void handle_dirty_insn_log(int id, const char *json)
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void handle_dirty_insn_add_range(int id, const char *json)
|
||||
{
|
||||
char lo_str[32], hi_str[32];
|
||||
if (!json_get_str(json, "lo", lo_str, sizeof(lo_str))) { send_err(id, "missing lo"); return; }
|
||||
if (!json_get_str(json, "hi", hi_str, sizeof(hi_str))) { send_err(id, "missing hi"); return; }
|
||||
|
||||
uint32_t lo = hex_to_u32(lo_str);
|
||||
uint32_t hi = hex_to_u32(hi_str);
|
||||
if (!dirty_ram_trace_add_range(lo, hi)) {
|
||||
send_err(id, "range add failed");
|
||||
return;
|
||||
}
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"count\":%d}",
|
||||
id, dirty_ram_trace_range_count());
|
||||
}
|
||||
|
||||
static void handle_dirty_insn_clear_ranges(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
dirty_ram_trace_clear_ranges();
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static void handle_dirty_insn_ranges(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
const size_t BUF_SZ = 2048;
|
||||
char *out = (char *)malloc(BUF_SZ);
|
||||
if (!out) { send_err(id, "oom"); return; }
|
||||
|
||||
int count = dirty_ram_trace_range_count();
|
||||
size_t pos = 0;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"count\":%d,\"ranges\":[",
|
||||
id, count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint32_t lo = 0, hi = 0;
|
||||
dirty_ram_trace_get_range(i, &lo, &hi);
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"%s{\"slot\":%d,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
|
||||
i == 0 ? "" : ",", i, lo, hi);
|
||||
}
|
||||
pos += snprintf(out + pos, BUF_SZ - pos, "]}");
|
||||
debug_server_send_line(out);
|
||||
free(out);
|
||||
}
|
||||
|
||||
/* ---- fntrace: always-on psx_dispatch call ring ----
|
||||
* Mirrors beetle_libretro.cpp's fntrace; covers every static-recomp +
|
||||
* dirty-RAM dispatch on this side. Use to find indirect callers, walk
|
||||
@@ -2287,14 +2555,23 @@ static void handle_unknown_dispatch_log(int id, const char *json)
|
||||
for (int i = 0; i < tail; i++) {
|
||||
UnknownDispatchEntry *e =
|
||||
&s_unknown_ring[(start + i) & (UNKNOWN_DISPATCH_CAP - 1u)];
|
||||
if (pos > BUF_SZ - 256) break;
|
||||
if (pos > BUF_SZ - 768) break;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"phys\":\"0x%08X\","
|
||||
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"frame\":%u}",
|
||||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\",\"t2\":\"0x%08X\","
|
||||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||||
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||||
"\"current_func\":\"0x%08X\",\"frame\":%u}",
|
||||
i == 0 ? "" : ",",
|
||||
(unsigned long long)e->seq,
|
||||
e->addr, e->phys, e->ra, e->a0, e->a1, e->frame);
|
||||
e->addr, e->phys, e->ra, e->sp, e->v0, e->v1,
|
||||
e->a0, e->a1, e->a2, e->a3, e->t0, e->t1, e->t2,
|
||||
e->s0, e->s1, e->s2, e->s3, e->current_func,
|
||||
e->frame);
|
||||
}
|
||||
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
|
||||
debug_server_send_line(out);
|
||||
@@ -2548,13 +2825,20 @@ static void emit_restore_entry(const RestoreTraceEntry *e, int first)
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\",\"a0\":\"0x%08X\","
|
||||
"\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"sr\":\"0x%08X\","
|
||||
"\"saved_s0\":\"0x%08X\",\"saved_s1\":\"0x%08X\","
|
||||
"\"saved_s2\":\"0x%08X\",\"saved_s3\":\"0x%08X\","
|
||||
"\"saved_sp\":\"0x%08X\",\"saved_ra\":\"0x%08X\","
|
||||
"\"saved_pc\":\"0x%08X\","
|
||||
"\"epc\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||||
"\"frame\":%u,\"in_exc\":%u}",
|
||||
first ? "" : ",",
|
||||
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
|
||||
e->jmp_val, e->target_pc, e->cpu_pc, e->func, e->last_store_pc,
|
||||
e->byte_seq, e->ra, e->sp, e->v0, e->v1, e->a0, e->a1, e->a2,
|
||||
e->a3, e->s0, e->s1, e->sr, e->epc, e->istat, e->imask,
|
||||
e->a3, e->s0, e->s1, e->sr,
|
||||
e->saved_s0, e->saved_s1, e->saved_s2, e->saved_s3,
|
||||
e->saved_sp, e->saved_ra, e->saved_pc,
|
||||
e->epc, e->istat, e->imask,
|
||||
e->frame, (unsigned)e->in_exception);
|
||||
}
|
||||
|
||||
@@ -2570,13 +2854,20 @@ static size_t append_restore_entry(char *buf, size_t pos, size_t cap,
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\",\"a0\":\"0x%08X\","
|
||||
"\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"sr\":\"0x%08X\","
|
||||
"\"saved_s0\":\"0x%08X\",\"saved_s1\":\"0x%08X\","
|
||||
"\"saved_s2\":\"0x%08X\",\"saved_s3\":\"0x%08X\","
|
||||
"\"saved_sp\":\"0x%08X\",\"saved_ra\":\"0x%08X\","
|
||||
"\"saved_pc\":\"0x%08X\","
|
||||
"\"epc\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||||
"\"frame\":%u,\"in_exc\":%u}",
|
||||
first ? "" : ",",
|
||||
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
|
||||
e->jmp_val, e->target_pc, e->cpu_pc, e->func, e->last_store_pc,
|
||||
e->byte_seq, e->ra, e->sp, e->v0, e->v1, e->a0, e->a1, e->a2,
|
||||
e->a3, e->s0, e->s1, e->sr, e->epc, e->istat, e->imask,
|
||||
e->a3, e->s0, e->s1, e->sr,
|
||||
e->saved_s0, e->saved_s1, e->saved_s2, e->saved_s3,
|
||||
e->saved_sp, e->saved_ra, e->saved_pc,
|
||||
e->epc, e->istat, e->imask,
|
||||
e->frame, (unsigned)e->in_exception);
|
||||
return pos;
|
||||
}
|
||||
@@ -3590,6 +3881,108 @@ static void handle_irq_state(int id, const char *json)
|
||||
dma_get_dpcr(), dma_get_dicr());
|
||||
}
|
||||
|
||||
static const char *irq_check_reason_name(uint32_t reason)
|
||||
{
|
||||
switch (reason) {
|
||||
case 1: return "in_exception";
|
||||
case 2: return "call_depth";
|
||||
case 3: return "cooldown";
|
||||
case 4: return "ie_disabled";
|
||||
case 5: return "im2_disabled";
|
||||
case 6: return "invalid_resume_pc";
|
||||
case 7: return "delivered";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_irq_check_log(int id, const char *json)
|
||||
{
|
||||
char buf32[32];
|
||||
int count = json_get_int(json, "count", 256);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 4096) count = 4096;
|
||||
|
||||
uint32_t pc_lo = 0, pc_hi = 0xFFFFFFFFu;
|
||||
if (json_get_str(json, "pc_lo", buf32, sizeof(buf32))) pc_lo = hex_to_u32(buf32);
|
||||
if (json_get_str(json, "pc_hi", buf32, sizeof(buf32))) pc_hi = hex_to_u32(buf32);
|
||||
|
||||
int reason_filter = -1;
|
||||
if (json_get_str(json, "reason", buf32, sizeof(buf32))) {
|
||||
reason_filter = (int)strtol(buf32, NULL, 0);
|
||||
}
|
||||
|
||||
uint64_t total = g_psx_irq_check_trace_seq;
|
||||
uint64_t oldest = (total > PSX_IRQ_CHECK_TRACE_CAP)
|
||||
? total - PSX_IRQ_CHECK_TRACE_CAP : 0;
|
||||
|
||||
size_t cap = 256u + (size_t)count * 320u;
|
||||
char *out = (char *)malloc(cap);
|
||||
if (!out) { send_err(id, "oom"); return; }
|
||||
size_t pos = 0;
|
||||
pos += snprintf(out + pos, cap - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,"
|
||||
"\"entries\":[",
|
||||
id, (unsigned long long)total, (unsigned long long)oldest);
|
||||
|
||||
int emitted = 0;
|
||||
int first = 1;
|
||||
for (uint64_t n = 0; n < total - oldest && emitted < count && pos < cap - 320u; n++) {
|
||||
uint64_t seq = total - 1u - n;
|
||||
if (seq < oldest) break;
|
||||
const PsxIrqCheckTraceEntry *e =
|
||||
&g_psx_irq_check_trace[seq & (PSX_IRQ_CHECK_TRACE_CAP - 1u)];
|
||||
if (e->seq != seq) continue;
|
||||
if (e->resume_pc < pc_lo || e->resume_pc >= pc_hi) continue;
|
||||
if (reason_filter >= 0 && e->reason != (uint32_t)reason_filter) continue;
|
||||
pos += snprintf(out + pos, cap - pos,
|
||||
"%s{\"seq\":%llu,\"frame\":%u,"
|
||||
"\"resume_pc\":\"0x%08X\",\"i_stat\":\"0x%08X\","
|
||||
"\"i_mask\":\"0x%08X\",\"sr\":\"0x%08X\","
|
||||
"\"cause\":\"0x%08X\",\"call_depth\":%u,"
|
||||
"\"reason\":%u,\"name\":\"%s\"}",
|
||||
first ? "" : ",",
|
||||
(unsigned long long)e->seq, e->frame,
|
||||
e->resume_pc, e->i_stat, e->i_mask, e->sr,
|
||||
e->cause, e->call_depth, e->reason,
|
||||
irq_check_reason_name(e->reason));
|
||||
first = 0;
|
||||
emitted++;
|
||||
}
|
||||
pos += snprintf(out + pos, cap - pos, "],\"emitted\":%d}", emitted);
|
||||
debug_server_send_line(out);
|
||||
free(out);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_irq_check_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
uint64_t total = g_psx_irq_check_trace_seq;
|
||||
uint64_t avail = (total < PSX_IRQ_CHECK_TRACE_CAP)
|
||||
? total : PSX_IRQ_CHECK_TRACE_CAP;
|
||||
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
|
||||
if (max_count == 0) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t start = total - (uint64_t)max_count;
|
||||
fputc('[', f);
|
||||
for (uint32_t i = 0; i < max_count; i++) {
|
||||
uint64_t seq = start + (uint64_t)i;
|
||||
const PsxIrqCheckTraceEntry *e =
|
||||
&g_psx_irq_check_trace[seq & (PSX_IRQ_CHECK_TRACE_CAP - 1u)];
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"frame\":%u,"
|
||||
"\"resume_pc\":\"0x%08X\",\"i_stat\":\"0x%08X\","
|
||||
"\"i_mask\":\"0x%08X\",\"sr\":\"0x%08X\","
|
||||
"\"cause\":\"0x%08X\",\"call_depth\":%u,"
|
||||
"\"reason\":%u,\"name\":\"%s\"}",
|
||||
i ? "," : "",
|
||||
(unsigned long long)e->seq, e->frame,
|
||||
e->resume_pc, e->i_stat, e->i_mask, e->sr,
|
||||
e->cause, e->call_depth, e->reason,
|
||||
irq_check_reason_name(e->reason));
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
static void handle_timers_state(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -3800,6 +4193,79 @@ static void handle_cdrom_trace_clear(int id, const char *json)
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static void handle_cdrom_command_history_clear(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
cdrom_debug_clear_command_history();
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static const char *cdrom_command_kind_name(uint8_t kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case 'C': return "exec";
|
||||
case 'Q': return "queue";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_cdrom_command_history(int id, const char *json)
|
||||
{
|
||||
int count = json_get_int(json, "count", 128);
|
||||
if (count < 1) count = 1;
|
||||
if (count > CDROM_COMMAND_HISTORY_CAP) count = CDROM_COMMAND_HISTORY_CAP;
|
||||
|
||||
const CDROMCommandHistoryEntry *entries = NULL;
|
||||
uint64_t total = cdrom_debug_get_command_history(&entries);
|
||||
uint64_t oldest = (total > CDROM_COMMAND_HISTORY_CAP)
|
||||
? total - CDROM_COMMAND_HISTORY_CAP : 0;
|
||||
uint64_t start = (total > (uint64_t)count) ? total - (uint64_t)count : 0;
|
||||
if (start < oldest) start = oldest;
|
||||
|
||||
size_t bufsz = 256u + (size_t)count * 520u;
|
||||
char *buf = (char *)malloc(bufsz);
|
||||
if (!buf) { send_err(id, "oom"); return; }
|
||||
|
||||
size_t pos = 0;
|
||||
int emitted = 0;
|
||||
pos += snprintf(buf + pos, bufsz - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,\"entries\":[",
|
||||
id, (unsigned long long)total, (unsigned long long)oldest);
|
||||
for (uint64_t seq = start; seq < total && pos < bufsz - 520; seq++) {
|
||||
const CDROMCommandHistoryEntry *e =
|
||||
&entries[seq % CDROM_COMMAND_HISTORY_CAP];
|
||||
if (e->seq != seq) continue;
|
||||
|
||||
pos += snprintf(buf + pos, bufsz - pos,
|
||||
"%s{\"seq\":%llu,\"kind\":\"%s\",\"cmd\":\"0x%02X\","
|
||||
"\"params_len\":%u,\"params\":\"",
|
||||
emitted ? "," : "",
|
||||
(unsigned long long)e->seq,
|
||||
cdrom_command_kind_name(e->kind),
|
||||
e->cmd, e->param_count);
|
||||
append_hex_bytes(buf, bufsz, &pos, e->params, e->param_count);
|
||||
pos += snprintf(buf + pos, bufsz - pos,
|
||||
"\",\"frame\":%u,\"func\":\"0x%08X\",\"pc\":\"0x%08X\","
|
||||
"\"i_stat\":\"0x%08X\",\"stat\":\"0x%02X\","
|
||||
"\"request\":\"0x%02X\",\"irq_enable\":\"0x%02X\","
|
||||
"\"irq_flag\":\"0x%02X\",\"mode\":\"0x%02X\","
|
||||
"\"seek\":[%u,%u,%u],\"read\":[%u,%u,%u],"
|
||||
"\"read_cmd\":\"0x%02X\",\"reading\":%u,"
|
||||
"\"pending_cmd\":\"0x%02X\",\"pending\":%u,"
|
||||
"\"queued_cmd\":\"0x%02X\",\"queued\":%u}",
|
||||
e->frame, e->func, e->pc, e->i_stat,
|
||||
e->stat, e->request, e->irq_enable, e->irq_flag,
|
||||
e->mode, e->seek_min, e->seek_sec, e->seek_sect,
|
||||
e->read_min, e->read_sec, e->read_sect,
|
||||
e->read_cmd, e->reading, e->pending_cmd,
|
||||
e->pending_pending, e->queued_cmd, e->queued_pending);
|
||||
emitted++;
|
||||
}
|
||||
pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted);
|
||||
debug_server_send_line(buf);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void handle_cdrom_trace_dump(int id, const char *json)
|
||||
{
|
||||
int count = json_get_int(json, "count", 256);
|
||||
@@ -3865,6 +4331,17 @@ static const char *dma_trace_kind_name(uint32_t kind)
|
||||
}
|
||||
}
|
||||
|
||||
static void append_hex_word_array(char *buf, size_t bufsz, size_t *pos,
|
||||
const uint32_t *words, uint32_t count)
|
||||
{
|
||||
*pos += snprintf(buf + *pos, bufsz - *pos, "[");
|
||||
for (uint32_t i = 0; i < count && *pos < bufsz - 16; i++) {
|
||||
*pos += snprintf(buf + *pos, bufsz - *pos,
|
||||
"%s\"0x%08X\"", i ? "," : "", words[i]);
|
||||
}
|
||||
*pos += snprintf(buf + *pos, bufsz - *pos, "]");
|
||||
}
|
||||
|
||||
static void handle_dma_state(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -3900,6 +4377,95 @@ static void handle_dma_trace_clear(int id, const char *json)
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static void handle_dma_cdrom_history_clear(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
dma_debug_clear_cdrom_history();
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static void handle_dma_cdrom_history(int id, const char *json)
|
||||
{
|
||||
int count = json_get_int(json, "count", 256);
|
||||
if (count < 1) count = 1;
|
||||
if (count > DMA_CDROM_HISTORY_CAP) count = DMA_CDROM_HISTORY_CAP;
|
||||
|
||||
int filter_lba = -1;
|
||||
char lba_str[32];
|
||||
if (json_get_str(json, "lba", lba_str, sizeof(lba_str))) {
|
||||
filter_lba = (int)hex_to_u32(lba_str);
|
||||
}
|
||||
|
||||
const DMACDROMHistoryEntry *entries = NULL;
|
||||
uint64_t total = dma_debug_get_cdrom_history(&entries);
|
||||
uint64_t oldest = (total > DMA_CDROM_HISTORY_CAP)
|
||||
? total - DMA_CDROM_HISTORY_CAP : 0;
|
||||
uint64_t start = (total > (uint64_t)count) ? total - (uint64_t)count : 0;
|
||||
if (start < oldest) start = oldest;
|
||||
|
||||
size_t bufsz = 256u + (size_t)count * 1150u;
|
||||
char *buf = (char *)malloc(bufsz);
|
||||
if (!buf) { send_err(id, "oom"); return; }
|
||||
|
||||
size_t pos = 0;
|
||||
int emitted = 0;
|
||||
pos += snprintf(buf + pos, bufsz - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,"
|
||||
"\"oldest\":%llu,\"entries\":[",
|
||||
id, (unsigned long long)total,
|
||||
(unsigned long long)oldest);
|
||||
|
||||
for (uint64_t seq = start; seq < total && pos < bufsz - 1150; seq++) {
|
||||
const DMACDROMHistoryEntry *e =
|
||||
&entries[seq % DMA_CDROM_HISTORY_CAP];
|
||||
if (e->seq != seq) continue;
|
||||
if (filter_lba >= 0 && e->lba != filter_lba) continue;
|
||||
|
||||
pos += snprintf(buf + pos, bufsz - pos,
|
||||
"%s{\"seq\":%llu,\"frame_start\":%u,"
|
||||
"\"frame_end\":%u,\"start_addr\":\"0x%06X\","
|
||||
"\"final_addr\":\"0x%06X\",\"requested_words\":%u,"
|
||||
"\"moved_words\":%u,\"bcr\":\"0x%08X\","
|
||||
"\"chcr\":\"0x%08X\",\"dpcr\":\"0x%08X\","
|
||||
"\"dicr_start\":\"0x%08X\",\"dicr_end\":\"0x%08X\","
|
||||
"\"i_stat_start\":\"0x%08X\","
|
||||
"\"i_stat_end\":\"0x%08X\",\"func\":\"0x%08X\","
|
||||
"\"pc\":\"0x%08X\",\"lba\":%d,"
|
||||
"\"sector_size\":%d,\"mode\":\"0x%02X\","
|
||||
"\"sector_read_pos_start\":%d,"
|
||||
"\"sector_read_pos_end\":%d,"
|
||||
"\"sector_available_start\":%u,"
|
||||
"\"sector_available_end\":%u,\"completed\":%u,"
|
||||
"\"first_words\":",
|
||||
emitted ? "," : "",
|
||||
(unsigned long long)e->seq,
|
||||
e->frame_start, e->frame_end,
|
||||
e->start_addr, e->final_addr,
|
||||
e->requested_words, e->moved_words,
|
||||
e->bcr, e->chcr, e->dpcr,
|
||||
e->dicr_start, e->dicr_end,
|
||||
e->i_stat_start, e->i_stat_end,
|
||||
e->func, e->pc, e->lba,
|
||||
e->sector_size, e->mode,
|
||||
e->sector_read_pos_start,
|
||||
e->sector_read_pos_end,
|
||||
e->sector_available_start,
|
||||
e->sector_available_end,
|
||||
e->completed);
|
||||
append_hex_word_array(buf, bufsz, &pos,
|
||||
e->first_words, e->first_count);
|
||||
pos += snprintf(buf + pos, bufsz - pos, ",\"last_words\":");
|
||||
append_hex_word_array(buf, bufsz, &pos,
|
||||
e->last_words, e->last_count);
|
||||
pos += snprintf(buf + pos, bufsz - pos, "}");
|
||||
emitted++;
|
||||
}
|
||||
|
||||
pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted);
|
||||
debug_server_send_line(buf);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void handle_dma_trace_dump(int id, const char *json)
|
||||
{
|
||||
int count = json_get_int(json, "count", 256);
|
||||
@@ -8210,6 +8776,9 @@ void debug_server_freeze_dump_dirty_insn_json(FILE *f, uint32_t max_count)
|
||||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"insn\":\"0x%08X\","
|
||||
"\"next_pc\":\"0x%08X\",\"target\":\"0x%08X\","
|
||||
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
|
||||
"\"before_s1\":\"0x%08X\",\"after_s1\":\"0x%08X\","
|
||||
"\"before_s2\":\"0x%08X\",\"after_s2\":\"0x%08X\","
|
||||
"\"before_s3\":\"0x%08X\",\"after_s3\":\"0x%08X\","
|
||||
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
@@ -8222,6 +8791,9 @@ void debug_server_freeze_dump_dirty_insn_json(FILE *f, uint32_t max_count)
|
||||
(unsigned long long)e->seq, e->pc, e->insn,
|
||||
e->next_pc, e->target,
|
||||
e->before_s0, e->after_s0,
|
||||
e->before_s1, e->after_s1,
|
||||
e->before_s2, e->after_s2,
|
||||
e->before_s3, e->after_s3,
|
||||
e->sp, e->ra,
|
||||
e->v0, e->v1,
|
||||
e->a0, e->a1,
|
||||
@@ -8295,16 +8867,21 @@ static const CmdEntry s_commands[] = {
|
||||
{ "mem_words", handle_mem_words },
|
||||
{ "vram_peek", handle_vram_peek },
|
||||
{ "irq_state", handle_irq_state },
|
||||
{ "irq_check_log", handle_irq_check_log },
|
||||
{ "timers_state", handle_timers_state },
|
||||
{ "cdrom_state", handle_cdrom_state },
|
||||
{ "cdrom_sector_dump", handle_cdrom_sector_dump },
|
||||
{ "cdrom_sector_history", handle_cdrom_sector_history },
|
||||
{ "cdrom_sector_history_clear", handle_cdrom_sector_history_clear },
|
||||
{ "cdrom_command_history", handle_cdrom_command_history },
|
||||
{ "cdrom_command_history_clear", handle_cdrom_command_history_clear },
|
||||
{ "cdrom_trace_dump", handle_cdrom_trace_dump },
|
||||
{ "cdrom_trace_clear", handle_cdrom_trace_clear },
|
||||
{ "dma_state", handle_dma_state },
|
||||
{ "dma_trace_dump", handle_dma_trace_dump },
|
||||
{ "dma_trace_clear", handle_dma_trace_clear },
|
||||
{ "dma_cdrom_history", handle_dma_cdrom_history },
|
||||
{ "dma_cdrom_history_clear", handle_dma_cdrom_history_clear },
|
||||
{ "sio_state", handle_sio_state },
|
||||
{ "mc_status", handle_mc_status },
|
||||
{ "spu_status", handle_spu_status },
|
||||
@@ -8339,7 +8916,12 @@ static const CmdEntry s_commands[] = {
|
||||
{ "dirty_ram_unsupported", handle_dirty_ram_unsupported },
|
||||
{ "dirty_block_log", handle_dirty_block_log },
|
||||
{ "dirty_flow_log", handle_dirty_flow_log },
|
||||
{ "dirty_exit_log", handle_dirty_exit_log },
|
||||
{ "dirty_call_log", handle_dirty_call_log },
|
||||
{ "dirty_insn_log", handle_dirty_insn_log },
|
||||
{ "dirty_insn_add_range", handle_dirty_insn_add_range },
|
||||
{ "dirty_insn_clear_ranges", handle_dirty_insn_clear_ranges },
|
||||
{ "dirty_insn_ranges", handle_dirty_insn_ranges },
|
||||
{ "fntrace_arm", handle_fntrace_arm },
|
||||
{ "fntrace_arm_clear", handle_fntrace_arm_clear },
|
||||
{ "fntrace_armed", handle_fntrace_armed },
|
||||
|
||||
+210
-38
@@ -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);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,10 @@ static uint32_t dicr; /* 0x1F8010F4: DMA interrupt control */
|
||||
|
||||
static DMATraceEntry dma_trace[DMA_TRACE_CAP];
|
||||
static uint64_t dma_trace_seq;
|
||||
static DMACDROMHistoryEntry cdrom_dma_history[DMA_CDROM_HISTORY_CAP];
|
||||
static uint64_t cdrom_dma_history_seq;
|
||||
static DMACDROMHistoryEntry cdrom_dma_active_entry;
|
||||
static uint8_t cdrom_dma_history_active;
|
||||
|
||||
static uint32_t dicr_read_value(uint32_t v);
|
||||
|
||||
@@ -158,6 +162,74 @@ static void raise_dma_irq_on_master_edge(uint32_t before) {
|
||||
}
|
||||
}
|
||||
|
||||
static void start_cdrom_dma_capture(uint32_t requested_words) {
|
||||
CDROMDebugState s;
|
||||
cdrom_debug_snapshot(&s);
|
||||
|
||||
memset(&cdrom_dma_active_entry, 0, sizeof(cdrom_dma_active_entry));
|
||||
cdrom_dma_active_entry.seq = cdrom_dma_history_seq++;
|
||||
cdrom_dma_active_entry.frame_start = (uint32_t)s_frame_count;
|
||||
cdrom_dma_active_entry.start_addr = channels[3].madr & 0x1FFFFCu;
|
||||
cdrom_dma_active_entry.final_addr = cdrom_dma_active_entry.start_addr;
|
||||
cdrom_dma_active_entry.requested_words = requested_words;
|
||||
cdrom_dma_active_entry.bcr = channels[3].bcr;
|
||||
cdrom_dma_active_entry.chcr = channels[3].chcr;
|
||||
cdrom_dma_active_entry.dpcr = dpcr;
|
||||
cdrom_dma_active_entry.dicr_start = dicr_read_value(dicr);
|
||||
cdrom_dma_active_entry.i_stat_start = i_stat;
|
||||
cdrom_dma_active_entry.func = g_debug_current_func_addr;
|
||||
cdrom_dma_active_entry.pc = g_debug_last_store_pc;
|
||||
cdrom_dma_active_entry.lba = s.last_sector_lba;
|
||||
cdrom_dma_active_entry.sector_size =
|
||||
(s.sector_size > 0) ? s.sector_size : s.last_sector_size;
|
||||
cdrom_dma_active_entry.sector_read_pos_start = s.sector_read_pos;
|
||||
cdrom_dma_active_entry.mode =
|
||||
s.last_sector_mode ? s.last_sector_mode : s.mode_reg;
|
||||
cdrom_dma_active_entry.sector_available_start =
|
||||
(uint8_t)(s.sector_available ? 1 : 0);
|
||||
cdrom_dma_history_active = 1;
|
||||
}
|
||||
|
||||
static void record_cdrom_dma_word(uint32_t word) {
|
||||
if (!cdrom_dma_history_active) return;
|
||||
|
||||
DMACDROMHistoryEntry *e = &cdrom_dma_active_entry;
|
||||
if (e->first_count < DMA_CDROM_HISTORY_WORDS) {
|
||||
e->first_words[e->first_count++] = word;
|
||||
}
|
||||
|
||||
if (e->last_count < DMA_CDROM_HISTORY_WORDS) {
|
||||
e->last_words[e->last_count++] = word;
|
||||
} else {
|
||||
memmove(e->last_words, e->last_words + 1,
|
||||
sizeof(e->last_words[0]) * (DMA_CDROM_HISTORY_WORDS - 1));
|
||||
e->last_words[DMA_CDROM_HISTORY_WORDS - 1] = word;
|
||||
}
|
||||
|
||||
e->moved_words++;
|
||||
}
|
||||
|
||||
static void finish_cdrom_dma_capture(uint32_t final_addr, uint8_t completed) {
|
||||
if (!cdrom_dma_history_active) return;
|
||||
|
||||
CDROMDebugState s;
|
||||
cdrom_debug_snapshot(&s);
|
||||
|
||||
cdrom_dma_active_entry.frame_end = (uint32_t)s_frame_count;
|
||||
cdrom_dma_active_entry.final_addr = final_addr & 0x1FFFFCu;
|
||||
cdrom_dma_active_entry.dicr_end = dicr_read_value(dicr);
|
||||
cdrom_dma_active_entry.i_stat_end = i_stat;
|
||||
cdrom_dma_active_entry.sector_read_pos_end = s.sector_read_pos;
|
||||
cdrom_dma_active_entry.sector_available_end =
|
||||
(uint8_t)(s.sector_available ? 1 : 0);
|
||||
cdrom_dma_active_entry.completed = completed ? 1 : 0;
|
||||
|
||||
cdrom_dma_history[
|
||||
cdrom_dma_active_entry.seq % DMA_CDROM_HISTORY_CAP] =
|
||||
cdrom_dma_active_entry;
|
||||
cdrom_dma_history_active = 0;
|
||||
}
|
||||
|
||||
static int channel_enabled(int ch) {
|
||||
/* DPCR: each channel has 4 bits, bit 3 of each group is enable.
|
||||
* Ch0 = bits 0-3, Ch1 = bits 4-7, etc. Enable = bit (ch*4 + 3). */
|
||||
@@ -215,6 +287,7 @@ static void cancel_async_transfer(int ch) {
|
||||
mdec_async[ch].cycles_accum = 0;
|
||||
}
|
||||
if (ch == 3) {
|
||||
finish_cdrom_dma_capture(channels[3].madr & 0x1FFFFCu, 0);
|
||||
cdrom_async.active = 0;
|
||||
cdrom_async.debug_started = 0;
|
||||
cdrom_async.total_words = 0;
|
||||
@@ -294,14 +367,17 @@ static void start_async_cdrom_transfer(void) {
|
||||
a->total_words = transfer_word_count(3);
|
||||
a->remaining_words = a->total_words;
|
||||
a->cycles_accum = 0;
|
||||
start_cdrom_dma_capture(a->total_words);
|
||||
|
||||
if (a->total_words == 0) {
|
||||
finish_cdrom_dma_capture(channels[3].madr & 0x1FFFFCu, 1);
|
||||
cancel_async_transfer(3);
|
||||
complete_transfer(3);
|
||||
}
|
||||
}
|
||||
|
||||
static void finish_async_cdrom_transfer(uint32_t final_addr) {
|
||||
finish_cdrom_dma_capture(final_addr, 1);
|
||||
channels[3].madr = final_addr;
|
||||
cancel_async_transfer(3);
|
||||
complete_transfer(3);
|
||||
@@ -644,6 +720,7 @@ void dma_advance(uint32_t cycles) {
|
||||
while (a->remaining_words > 0 && words_budget > 0 && cdrom_dma_ready()) {
|
||||
uint32_t word = cdrom_dma_read();
|
||||
psx_write_word(addr, word);
|
||||
record_cdrom_dma_word(word);
|
||||
dirty_ram_mark_executable_range(addr, 4);
|
||||
addr = (addr + addr_step) & 0x1FFFFCu;
|
||||
a->remaining_words--;
|
||||
@@ -671,6 +748,7 @@ void dma_init(void) {
|
||||
dpcr = 0x07654321u; /* default: priorities set, no channels enabled */
|
||||
dicr = 0;
|
||||
dma_debug_clear_trace();
|
||||
dma_debug_clear_cdrom_history();
|
||||
}
|
||||
|
||||
uint32_t dma_read(uint32_t addr) {
|
||||
@@ -781,6 +859,18 @@ void dma_debug_clear_trace(void) {
|
||||
dma_trace_seq = 0;
|
||||
}
|
||||
|
||||
uint64_t dma_debug_get_cdrom_history(const DMACDROMHistoryEntry** out_entries) {
|
||||
if (out_entries) *out_entries = cdrom_dma_history;
|
||||
return cdrom_dma_history_seq;
|
||||
}
|
||||
|
||||
void dma_debug_clear_cdrom_history(void) {
|
||||
memset(cdrom_dma_history, 0, sizeof(cdrom_dma_history));
|
||||
memset(&cdrom_dma_active_entry, 0, sizeof(cdrom_dma_active_entry));
|
||||
cdrom_dma_history_seq = 0;
|
||||
cdrom_dma_history_active = 0;
|
||||
}
|
||||
|
||||
void dma_debug_get_state(DMADebugState* out) {
|
||||
if (!out) return;
|
||||
out->dpcr = dpcr;
|
||||
|
||||
+87
-10
@@ -79,6 +79,38 @@ static uint64_t exception_reentry_blocks;
|
||||
* very next psx_check_interrupts re-enters immediately. */
|
||||
static int post_exception_cooldown;
|
||||
|
||||
extern int g_psx_call_depth;
|
||||
|
||||
PsxIrqCheckTraceEntry g_psx_irq_check_trace[PSX_IRQ_CHECK_TRACE_CAP] = {0};
|
||||
uint64_t g_psx_irq_check_trace_seq = 0;
|
||||
extern uint64_t s_frame_count;
|
||||
|
||||
enum {
|
||||
IRQ_CHECK_IN_EXCEPTION = 1,
|
||||
IRQ_CHECK_CALL_DEPTH = 2,
|
||||
IRQ_CHECK_COOLDOWN = 3,
|
||||
IRQ_CHECK_IE_DISABLED = 4,
|
||||
IRQ_CHECK_IM2_DISABLED = 5,
|
||||
IRQ_CHECK_INVALID_RESUME_PC = 6,
|
||||
IRQ_CHECK_DELIVERED = 7
|
||||
};
|
||||
|
||||
static void irq_check_trace_log(CPUState* cpu, uint32_t resume_pc, uint32_t reason)
|
||||
{
|
||||
uint64_t seq = g_psx_irq_check_trace_seq++;
|
||||
PsxIrqCheckTraceEntry *e =
|
||||
&g_psx_irq_check_trace[seq & (PSX_IRQ_CHECK_TRACE_CAP - 1u)];
|
||||
e->seq = seq;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->resume_pc = resume_pc;
|
||||
e->i_stat = i_stat;
|
||||
e->i_mask = i_mask;
|
||||
e->sr = cpu ? cpu->cop0[COP0_SR] : 0;
|
||||
e->cause = cpu ? cpu->cop0[COP0_CAUSE] : 0;
|
||||
e->call_depth = (uint32_t)g_psx_call_depth;
|
||||
e->reason = reason;
|
||||
}
|
||||
|
||||
/* setjmp target for ReturnFromException during handler dispatch.
|
||||
*
|
||||
* IMPORTANT: longjmp(exception_jmpbuf, ...) MUST execute on the same
|
||||
@@ -100,6 +132,12 @@ jmp_buf exception_jmpbuf; /* non-static so traps.c can deferred-longjmp */
|
||||
void* g_exception_owner_fiber = NULL;
|
||||
int g_pending_exception_longjmp = 0;
|
||||
extern int g_psx_dispatch_depth;
|
||||
extern int g_psx_call_depth;
|
||||
uint64_t g_psx_nonlocal_epoch = 0;
|
||||
|
||||
void psx_note_nonlocal_control_flow(void) {
|
||||
g_psx_nonlocal_epoch++;
|
||||
}
|
||||
|
||||
int psx_get_in_exception(void) { return in_exception; }
|
||||
|
||||
@@ -121,10 +159,13 @@ void interrupts_init(void) {
|
||||
dispatch_count = 0;
|
||||
in_exception = 0;
|
||||
g_psx_dispatch_depth = 0;
|
||||
g_psx_call_depth = 0;
|
||||
total_checks = 0;
|
||||
post_exception_cooldown = 0;
|
||||
exception_entries_total = 0;
|
||||
exception_reentry_blocks = 0;
|
||||
g_psx_irq_check_trace_seq = 0;
|
||||
g_psx_nonlocal_epoch = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -160,11 +201,21 @@ static void deferred_exception_longjmp(int code) {
|
||||
}
|
||||
|
||||
void psx_exception_longjmp(void) {
|
||||
psx_note_nonlocal_control_flow();
|
||||
debug_server_log_restore_event(2, debug_cpu_ptr ? debug_cpu_ptr->pc : 0, 1);
|
||||
deferred_exception_longjmp(1);
|
||||
}
|
||||
|
||||
void psx_rfe_return(CPUState* cpu) {
|
||||
(void)cpu;
|
||||
if (in_exception) {
|
||||
psx_exception_longjmp();
|
||||
}
|
||||
psx_note_nonlocal_control_flow();
|
||||
}
|
||||
|
||||
void psx_restore_state_escape(void) {
|
||||
psx_note_nonlocal_control_flow();
|
||||
if (in_exception) {
|
||||
debug_server_log_restore_event(1, debug_cpu_ptr ? debug_cpu_ptr->pc : 0, 2);
|
||||
deferred_exception_longjmp(2);
|
||||
@@ -173,6 +224,10 @@ void psx_restore_state_escape(void) {
|
||||
}
|
||||
|
||||
void psx_check_interrupts(CPUState* cpu) {
|
||||
psx_check_interrupts_at(cpu, cpu ? cpu->pc : 0);
|
||||
}
|
||||
|
||||
void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc) {
|
||||
total_checks++;
|
||||
if ((total_checks & 0x3FFFu) == 0) {
|
||||
debug_server_poll();
|
||||
@@ -261,19 +316,36 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
if ((i_stat & i_mask) == 0) return;
|
||||
if (in_exception) {
|
||||
exception_reentry_blocks++;
|
||||
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_IN_EXCEPTION);
|
||||
return;
|
||||
}
|
||||
if (g_psx_call_depth > 0) {
|
||||
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_CALL_DEPTH);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Post-exception cooldown: let at least one block execute after RFE. */
|
||||
if (post_exception_cooldown > 0) {
|
||||
post_exception_cooldown--;
|
||||
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_COOLDOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check COP0 SR: IEc (bit 0) must be set, and IM2 (bit 10) must be set. */
|
||||
uint32_t sr = cpu->cop0[COP0_SR];
|
||||
if (!(sr & 0x01)) return; /* Interrupts globally disabled */
|
||||
if (!(sr & (1 << 10))) return; /* Hardware interrupt bit not enabled */
|
||||
if (!(sr & 0x01)) {
|
||||
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_IE_DISABLED);
|
||||
return; /* Interrupts globally disabled */
|
||||
}
|
||||
if (!(sr & (1 << 10))) {
|
||||
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_IM2_DISABLED);
|
||||
return; /* Hardware interrupt bit not enabled */
|
||||
}
|
||||
if (resume_pc == 0 || (resume_pc & 3u) != 0) {
|
||||
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_INVALID_RESUME_PC);
|
||||
return;
|
||||
}
|
||||
irq_check_trace_log(cpu, resume_pc, IRQ_CHECK_DELIVERED);
|
||||
|
||||
in_exception = 1;
|
||||
exception_entries_total++;
|
||||
@@ -286,14 +358,12 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
/* Push SR exception stack: shift bits [5:0] left by 2. */
|
||||
cpu->cop0[COP0_SR] = (sr & ~0x3F) | ((sr & 0x0F) << 2);
|
||||
|
||||
/* EPC: set to a sentinel value. The recompiled exception handler reads
|
||||
* memory at [EPC] to check for COP2 branch delay. We use a dedicated
|
||||
* address in the kernel scratch area. Address 0x80000048 is chosen
|
||||
* because it's between the exception vectors (0x80-0xBF) and the
|
||||
* kernel data pointer area (0x100+), and not used by the BIOS. */
|
||||
uint32_t sentinel = 0x80000048u;
|
||||
cpu->write_word(sentinel, 0x00000000u); /* NOP */
|
||||
cpu->cop0[COP0_EPC] = sentinel;
|
||||
/* EPC must be the guest PC that will execute next after the interrupt.
|
||||
* The BIOS saves this into the current TCB and ReturnFromException
|
||||
* later restores it. A synthetic EPC is not safe: games can run event
|
||||
* callbacks through BIOS RestoreState while the interrupted TCB is live,
|
||||
* and those callbacks observe the same save area. */
|
||||
cpu->cop0[COP0_EPC] = resume_pc;
|
||||
|
||||
/* Save the interrupted code's full register state.
|
||||
*
|
||||
@@ -348,6 +418,7 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
int prev_pending = g_pending_exception_longjmp;
|
||||
g_exception_owner_fiber = psx_fiber_current();
|
||||
g_pending_exception_longjmp = 0;
|
||||
int returned_from_exception = 0;
|
||||
for (;;) {
|
||||
int jmp_val = setjmp(exception_jmpbuf);
|
||||
if (jmp_val == 2) {
|
||||
@@ -355,12 +426,15 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
* GPRs were already set by RestoreState — do NOT restore.
|
||||
* Stay in exception context so ReturnFromException works. */
|
||||
g_psx_dispatch_depth = 0;
|
||||
g_psx_call_depth = 0;
|
||||
debug_server_log_restore_event(3, cpu->pc, (uint32_t)jmp_val);
|
||||
target_pc = cpu->pc;
|
||||
continue;
|
||||
}
|
||||
if (jmp_val == 1) {
|
||||
g_psx_dispatch_depth = 0;
|
||||
g_psx_call_depth = 0;
|
||||
returned_from_exception = 1;
|
||||
debug_server_log_restore_event(4, cpu->pc, (uint32_t)jmp_val);
|
||||
}
|
||||
if (jmp_val == 0) {
|
||||
@@ -389,6 +463,9 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
for (int i = 0; i < 32; i++) cpu->gpr[i] = saved_gpr[i];
|
||||
cpu->hi = saved_hi;
|
||||
cpu->lo = saved_lo;
|
||||
if (returned_from_exception) {
|
||||
cpu->pc = 0;
|
||||
}
|
||||
|
||||
if (!(cpu->cop0[COP0_SR] & 0x01)) {
|
||||
uint32_t sr2 = cpu->cop0[COP0_SR];
|
||||
|
||||
+10
-6
@@ -121,26 +121,30 @@ static void pad_prepare_command_response(uint8_t command) {
|
||||
static const uint8_t info_46[6] = { 0x00, 0x00, 0x01, 0x02, 0x00, 0x0A };
|
||||
static const uint8_t info_47[6] = { 0x00, 0x00, 0x02, 0x00, 0x01, 0x00 };
|
||||
static const uint8_t info_4c[6] = { 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 };
|
||||
const uint8_t id = 0xF3;
|
||||
const uint8_t config_id = 0xF3;
|
||||
|
||||
switch (command) {
|
||||
case 0x43: /* Enter/exit config mode */
|
||||
/* The command itself is acknowledged using the controller's current
|
||||
* mode ID; the first parameter takes effect on later bytes. */
|
||||
pad_prepare_response(pad_poll_id(selected_slot), zeros, 6);
|
||||
break;
|
||||
case 0x44: /* Set analog/digital mode */
|
||||
case 0x4D: /* Set vibration motor mapping */
|
||||
case 0x4F: /* Set poll response byte count / DualShock extension */
|
||||
pad_prepare_response(id, zeros, 6);
|
||||
pad_prepare_response(config_id, zeros, 6);
|
||||
break;
|
||||
case 0x45: /* Get controller type */
|
||||
pad_prepare_response(id, info_45, 6);
|
||||
pad_prepare_response(config_id, info_45, 6);
|
||||
break;
|
||||
case 0x46: /* Get mode table */
|
||||
pad_prepare_response(id, info_46, 6);
|
||||
pad_prepare_response(config_id, info_46, 6);
|
||||
break;
|
||||
case 0x47: /* Get actuator alignment */
|
||||
pad_prepare_response(id, info_47, 6);
|
||||
pad_prepare_response(config_id, info_47, 6);
|
||||
break;
|
||||
case 0x4C: /* Get supported modes */
|
||||
pad_prepare_response(id, info_4c, 6);
|
||||
pad_prepare_response(config_id, info_4c, 6);
|
||||
break;
|
||||
default:
|
||||
pad_clear_response();
|
||||
|
||||
+40
-5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user