mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
Merge Ape Escape forward-port into master
Validated CD-model rewrite (boot-EXE SeekL wedge fixed via deferred response presentation), recompiler pre-prologue function discovery, dma/iso/memory forward-port, and MMIO-READ (rtrace) instrumentation. Functional on Tomba (in-game), Mega Man X6 (in-game), and Ape Escape (3D title/gameplay), 0 dispatch misses each. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -52,8 +52,8 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring |
|
||||
| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) |
|
||||
| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint |
|
||||
| `set_input` | ✓ | ✓ | `buttons`, optional `frames` | Override pad1 buttons (PS1 inverted bitmask, 0 = pressed). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames |
|
||||
| `clear_input` | ✓ | ✓ | — | Remove input override |
|
||||
| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames |
|
||||
| `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides |
|
||||
| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation |
|
||||
| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state |
|
||||
| `pause` | ✓ | ✓ | — | Pause emulation |
|
||||
|
||||
@@ -103,6 +103,7 @@ and by `runtime/src/main.cpp` as the source of compiled-in defaults.
|
||||
[runtime]
|
||||
debug_port = 4370 # TCP port for the debug server
|
||||
window_title = "..." # SDL window title
|
||||
controller = "digital" # "digital" or "dualshock"
|
||||
memcard_dir = "." # memcard files location, relative to project root
|
||||
```
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ struct FunctionAnalysisResult {
|
||||
int prologue_count;
|
||||
int call_discovered_count = 0; // Functions found via JAL call-target following
|
||||
int strong_prologue_count = 0; // Functions found from prologues with saved $ra
|
||||
int bios_thunk_count = 0; // Packed A0/B0/C0 BIOS dispatch thunks
|
||||
int state_continuation_count = 0; // Split entries after calls to SaveState-style helpers
|
||||
int pointer_table_entry_count = 0; // Function entries found from executable pointer tables
|
||||
};
|
||||
|
||||
class FunctionAnalyzer {
|
||||
@@ -81,6 +83,12 @@ private:
|
||||
// Find function start by scanning backward from jr $ra
|
||||
uint32_t find_function_start(uint32_t return_addr);
|
||||
|
||||
// Detect PSY-Q style BIOS dispatch thunks:
|
||||
// addiu/ori rN, $zero, {0xA0,0xB0,0xC0}
|
||||
// jr rN
|
||||
// addiu/ori $t1, $zero, function_index
|
||||
bool is_bios_dispatch_thunk(uint32_t addr, uint32_t& jr_addr_out) const;
|
||||
|
||||
// Detect if a region is likely a data section masquerading as code
|
||||
bool is_likely_data_section(uint32_t start_addr, uint32_t end_addr) const;
|
||||
};
|
||||
|
||||
@@ -96,6 +96,18 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
rt.window_title = toml::find<std::string>(runtime, "window_title");
|
||||
rt.has_window_title = true;
|
||||
}
|
||||
if (runtime.contains("controller")) {
|
||||
rt.controller = toml::find<std::string>(runtime, "controller");
|
||||
for (char& c : rt.controller) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
if (rt.controller != "digital" && rt.controller != "dualshock") {
|
||||
throw std::runtime_error(
|
||||
fmt::format("[runtime] controller must be 'digital' or 'dualshock', got '{}'",
|
||||
rt.controller));
|
||||
}
|
||||
rt.has_controller = true;
|
||||
}
|
||||
if (runtime.contains("memcard_dir")) {
|
||||
const auto rel = toml::find<std::string>(runtime, "memcard_dir");
|
||||
rt.memcard_dir = fs::absolute(root / rel);
|
||||
|
||||
@@ -51,6 +51,10 @@ struct RuntimeConfig {
|
||||
bool has_window_title = false;
|
||||
std::string window_title;
|
||||
|
||||
// controller: "digital" (default) | "dualshock"
|
||||
bool has_controller = false;
|
||||
std::string controller;
|
||||
|
||||
bool has_memcard_dir = false;
|
||||
std::filesystem::path memcard_dir; // absolute path (resolved against project root)
|
||||
|
||||
|
||||
@@ -114,6 +114,51 @@ bool FunctionAnalyzer::is_branch_or_jump(uint32_t instr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_load_imm_zero_u16(uint32_t instr, uint32_t& rt_out, uint32_t& imm_out) {
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
if ((opcode != 0x09 && opcode != 0x0D) || rs != 0) {
|
||||
return false;
|
||||
}
|
||||
rt_out = (instr >> 16) & 0x1F;
|
||||
imm_out = instr & 0xFFFFu;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FunctionAnalyzer::is_bios_dispatch_thunk(uint32_t addr, uint32_t& jr_addr_out) const {
|
||||
auto w0 = exe_.read_word(addr);
|
||||
auto w1 = exe_.read_word(addr + 4);
|
||||
auto w2 = exe_.read_word(addr + 8);
|
||||
if (!w0.has_value() || !w1.has_value() || !w2.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t target_reg = 0;
|
||||
uint32_t vector = 0;
|
||||
if (!is_load_imm_zero_u16(*w0, target_reg, vector)) {
|
||||
return false;
|
||||
}
|
||||
if (vector != 0xA0u && vector != 0xB0u && vector != 0xC0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t op1 = (*w1 >> 26) & 0x3F;
|
||||
uint32_t rs1 = (*w1 >> 21) & 0x1F;
|
||||
uint32_t fn1 = *w1 & 0x3F;
|
||||
if (op1 != 0 || fn1 != 0x08u || rs1 != target_reg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t index_reg = 0;
|
||||
uint32_t index = 0;
|
||||
if (!is_load_imm_zero_u16(*w2, index_reg, index) || index_reg != 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
jr_addr_out = addr + 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_sw_reg_base(uint32_t instr, uint32_t base_reg, uint32_t value_reg) {
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
@@ -132,11 +177,162 @@ 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_nop(uint32_t instr) {
|
||||
return instr == 0;
|
||||
}
|
||||
|
||||
static uint32_t skip_leading_padding_nops(const PS1Executable& exe,
|
||||
uint32_t start_addr,
|
||||
uint32_t limit_addr) {
|
||||
constexpr uint32_t max_padding_bytes = 32;
|
||||
uint32_t addr = start_addr;
|
||||
uint32_t skipped = 0;
|
||||
|
||||
while (addr + 4u <= limit_addr && skipped < max_padding_bytes) {
|
||||
auto word_opt = exe.read_word(addr);
|
||||
if (!word_opt.has_value() || !is_nop(*word_opt)) {
|
||||
break;
|
||||
}
|
||||
addr += 4u;
|
||||
skipped += 4u;
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
static bool is_load_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;
|
||||
switch (opcode) {
|
||||
case 0x20: /* lb */
|
||||
case 0x21: /* lh */
|
||||
case 0x22: /* lwl */
|
||||
case 0x23: /* lw */
|
||||
case 0x24: /* lbu */
|
||||
case 0x25: /* lhu */
|
||||
case 0x26: /* lwr */
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (rs != base_reg) return false;
|
||||
rt_out = rt;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_load_imm_zero(uint32_t instr, uint32_t& rt_out) {
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
if ((opcode != 0x09 && opcode != 0x0D) || rs != 0) return false;
|
||||
rt_out = (instr >> 16) & 0x1F;
|
||||
return rt_out != 0;
|
||||
}
|
||||
|
||||
static bool preprologue_window_is_valid(const PS1Executable& exe,
|
||||
uint32_t start_addr,
|
||||
uint32_t prologue_addr,
|
||||
uint32_t exe_start) {
|
||||
if (start_addr >= prologue_addr) return false;
|
||||
|
||||
/* The candidate may start immediately after a previous return delay slot,
|
||||
* but it cannot itself be a branch delay slot. */
|
||||
if (start_addr >= exe_start + 4) {
|
||||
auto prev_opt = exe.read_word(start_addr - 4);
|
||||
if (prev_opt.has_value() && FunctionAnalyzer::is_branch_or_jump(*prev_opt)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::set<uint32_t> global_base_regs;
|
||||
bool saw_global_load = false;
|
||||
|
||||
for (uint32_t addr = start_addr; addr < prologue_addr; addr += 4) {
|
||||
auto word_opt = exe.read_word(addr);
|
||||
if (!word_opt.has_value()) return false;
|
||||
uint32_t instr = *word_opt;
|
||||
|
||||
if (FunctionAnalyzer::is_branch_or_jump(instr)) return false;
|
||||
|
||||
uint32_t rt = 0;
|
||||
if (is_lui(instr, rt) && rt != 0 && rt != 29 && rt != 31) {
|
||||
global_base_regs.insert(rt);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_load_imm_zero(instr, rt) && rt != 29 && rt != 31) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool valid_load = false;
|
||||
for (uint32_t base_reg : global_base_regs) {
|
||||
uint32_t load_rt = 0;
|
||||
if (is_load_from_reg_base(instr, base_reg, load_rt) &&
|
||||
load_rt != 0 && load_rt != 29 && load_rt != 31) {
|
||||
valid_load = true;
|
||||
saw_global_load = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid_load) continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return saw_global_load;
|
||||
}
|
||||
|
||||
static bool find_preprologue_setup_start(const PS1Executable& exe,
|
||||
uint32_t prologue_addr,
|
||||
uint32_t exe_start,
|
||||
uint32_t& setup_start_out) {
|
||||
constexpr uint32_t max_setup_insns = 6;
|
||||
bool found = false;
|
||||
uint32_t best_start = prologue_addr;
|
||||
|
||||
for (uint32_t count = 1; count <= max_setup_insns; count++) {
|
||||
uint32_t bytes = count * 4u;
|
||||
if (prologue_addr < exe_start + bytes) break;
|
||||
uint32_t candidate = prologue_addr - bytes;
|
||||
if (preprologue_window_is_valid(exe, candidate, prologue_addr, exe_start)) {
|
||||
best_start = candidate;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) return false;
|
||||
setup_start_out = best_start;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool pointer_target_has_near_prologue(const PS1Executable& exe,
|
||||
uint32_t target,
|
||||
uint32_t exe_start,
|
||||
uint32_t exe_end,
|
||||
uint32_t& prologue_addr_out) {
|
||||
constexpr uint32_t max_prelude_bytes = 32;
|
||||
for (uint32_t addr = target;
|
||||
addr < exe_end && addr <= target + max_prelude_bytes;
|
||||
addr += 4) {
|
||||
auto word_opt = exe.read_word(addr);
|
||||
if (!word_opt.has_value()) break;
|
||||
|
||||
int32_t stack_size = 0;
|
||||
if (!FunctionAnalyzer::is_prologue(*word_opt, stack_size)) continue;
|
||||
|
||||
if (addr == target) {
|
||||
prologue_addr_out = addr;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t setup_start = 0;
|
||||
if (find_preprologue_setup_start(exe, addr, exe_start, setup_start) &&
|
||||
setup_start == target) {
|
||||
prologue_addr_out = addr;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t FunctionAnalyzer::find_function_start(uint32_t return_addr) {
|
||||
@@ -176,15 +372,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;
|
||||
@@ -193,8 +386,11 @@ uint32_t FunctionAnalyzer::find_function_start(uint32_t return_addr) {
|
||||
// Check if we hit another function's return
|
||||
if (is_jr_ra(instr)) {
|
||||
// We've gone too far backward and hit another function
|
||||
// Return the address after this jr $ra (+ 8 for delay slot and alignment)
|
||||
return search_addr + 8;
|
||||
// Return the address after this jr $ra (+ 8 for delay slot), then
|
||||
// skip alignment padding. Explicit JAL/forced entries still keep
|
||||
// their exact target addresses; this only normalizes starts inferred
|
||||
// from the previous function's trailing gap.
|
||||
return skip_leading_padding_nops(exe_, search_addr + 8, return_addr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,6 +800,7 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() {
|
||||
result.prologue_count = 0;
|
||||
result.call_discovered_count = 0;
|
||||
result.strong_prologue_count = 0;
|
||||
result.bios_thunk_count = 0;
|
||||
result.state_continuation_count = 0;
|
||||
|
||||
fmt::print("\n=== Function Boundary Detection ===\n\n");
|
||||
@@ -680,6 +877,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);
|
||||
}
|
||||
}
|
||||
@@ -712,24 +919,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;
|
||||
}
|
||||
}
|
||||
@@ -747,7 +948,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++;
|
||||
}
|
||||
}
|
||||
@@ -755,6 +959,71 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() {
|
||||
fmt::print("Identified {} strong prologue entries\n\n",
|
||||
result.strong_prologue_count);
|
||||
|
||||
// Pass 2.56: promote packed PSY-Q BIOS dispatch thunks.
|
||||
//
|
||||
// PSY-Q libraries often pack tiny A0/B0/C0 syscall thunks back-to-back:
|
||||
// addiu rN, $zero, 0xA0/0xB0/0xC0
|
||||
// jr rN
|
||||
// addiu $t1, $zero, index
|
||||
//
|
||||
// They do not contain jr $ra and may sit directly before data tables, so
|
||||
// return/prologue scanning either misses them or lets the following data
|
||||
// make the region look like a data section. Treat each thunk as a real
|
||||
// function and cap it after the JR delay slot.
|
||||
fmt::print("Finding packed BIOS dispatch thunk entries...\n");
|
||||
int bios_thunks = 0;
|
||||
int bios_thunks_skipped = 0;
|
||||
for (uint32_t addr = exe_start; addr + 12u <= exe_end; addr += 4) {
|
||||
uint32_t jr_addr = 0;
|
||||
if (!is_bios_dispatch_thunk(addr, jr_addr)) continue;
|
||||
|
||||
// A li/jr BIOS-dispatch sequence is only a STANDALONE thunk when it
|
||||
// sits in a gap that earlier discovery (prologue scan, jr-$ra scan,
|
||||
// call-target following) missed. A real function may legitimately
|
||||
// *begin* with this sequence (a tail call into the BIOS) yet continue
|
||||
// with reachable code — e.g. a libcard routine that calls the BIOS and
|
||||
// then polls card status. Treating such a function as a thunk and
|
||||
// capping function_last_return at its first JR silently truncates the
|
||||
// rest of its body. That is exactly what gutted Tomba's card-poll
|
||||
// routine func_8005CDB8 (104B -> 12B) and stalled LOAD GAME.
|
||||
//
|
||||
// Mirror segagenesisrecomp's boundary-split discipline: a pattern
|
||||
// heuristic must never shrink or split an already-discovered function.
|
||||
// Only promote+cap thunks that are NOT already a known entry and do
|
||||
// NOT fall inside an existing function's discovered extent.
|
||||
if (function_starts.count(addr)) {
|
||||
// Already discovered as a real function start — leave its extent
|
||||
// alone; do not re-cap it to the first JR.
|
||||
bios_thunks_skipped++;
|
||||
continue;
|
||||
}
|
||||
auto owner_it = function_starts.upper_bound(addr); // first start > addr
|
||||
if (owner_it != function_starts.begin()) {
|
||||
--owner_it; // greatest start <= addr
|
||||
uint32_t owner = *owner_it;
|
||||
auto ret_it = function_last_return.find(owner);
|
||||
if (owner < addr && ret_it != function_last_return.end() &&
|
||||
addr <= ret_it->second) {
|
||||
// Inside an existing function's body — promoting here would
|
||||
// split that function. Leave it whole.
|
||||
bios_thunks_skipped++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (function_starts.insert(addr).second) {
|
||||
bios_thunks++;
|
||||
}
|
||||
auto it = function_last_return.find(addr);
|
||||
if (it == function_last_return.end() || jr_addr > it->second) {
|
||||
function_last_return[addr] = jr_addr;
|
||||
}
|
||||
}
|
||||
result.bios_thunk_count = bios_thunks;
|
||||
fmt::print("Identified {} packed BIOS dispatch thunk entries "
|
||||
"({} candidates skipped: already discovered / inside a function)\n\n",
|
||||
result.bios_thunk_count, bios_thunks_skipped);
|
||||
|
||||
// Pass 2.6: discover continuations saved by setjmp/SaveState-style helpers.
|
||||
//
|
||||
// Some games save the caller's $ra into an application context object and
|
||||
@@ -789,6 +1058,60 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() {
|
||||
fmt::print("Identified {} SaveState continuation entries\n\n",
|
||||
result.state_continuation_count);
|
||||
|
||||
// Pass 2.65: promote executable pointer-table entries.
|
||||
//
|
||||
// Many PS1 libraries store callback vectors as raw function addresses in
|
||||
// the loaded EXE image. Some callback entries perform a few global loads
|
||||
// before allocating their stack frame, so prologue-only discovery emits
|
||||
// the later ADDIU $sp address while the game calls the earlier table
|
||||
// address. Treat aligned image words that point at a near-prologue entry
|
||||
// as first-class function starts, and collapse the later prologue-only
|
||||
// start when it belongs to that prelude.
|
||||
fmt::print("Finding executable pointer-table function entries...\n");
|
||||
std::map<uint32_t, uint32_t> pointer_refs;
|
||||
for (uint32_t addr = exe_start; addr < exe_end; addr += 4) {
|
||||
auto word_opt = exe_.read_word(addr);
|
||||
if (!word_opt.has_value()) break;
|
||||
uint32_t target = *word_opt;
|
||||
if ((target & 3u) != 0) continue;
|
||||
if (target < exe_start || target >= exe_end) continue;
|
||||
|
||||
uint32_t prologue_addr = 0;
|
||||
if (!pointer_target_has_near_prologue(exe_, target, exe_start, exe_end,
|
||||
prologue_addr)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pointer_refs[target]++;
|
||||
|
||||
if (prologue_addr != target) {
|
||||
auto prologue_it = function_starts.find(prologue_addr);
|
||||
if (prologue_it != function_starts.end()) {
|
||||
auto ret_it = function_last_return.find(prologue_addr);
|
||||
if (ret_it != function_last_return.end()) {
|
||||
auto dst_it = function_last_return.find(target);
|
||||
if (dst_it == function_last_return.end() ||
|
||||
ret_it->second > dst_it->second) {
|
||||
function_last_return[target] = ret_it->second;
|
||||
}
|
||||
function_last_return.erase(ret_it);
|
||||
}
|
||||
function_starts.erase(prologue_it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int pointer_entries = 0;
|
||||
for (const auto& [target, refs] : pointer_refs) {
|
||||
(void)refs;
|
||||
if (function_starts.insert(target).second) {
|
||||
pointer_entries++;
|
||||
}
|
||||
}
|
||||
result.pointer_table_entry_count = pointer_entries;
|
||||
fmt::print("Identified {} pointer-table function entries\n\n",
|
||||
result.pointer_table_entry_count);
|
||||
|
||||
// Pass 2.7: Add forced entry points
|
||||
// These are function starts that do not have a standard prologue (e.g. the
|
||||
// PS1 EXE entry point, which is launched directly by the BIOS without a
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
@@ -438,6 +439,60 @@ std::vector<PSXRecompV4::Seed> load_seeds(const fs::path& seed_path) {
|
||||
return seeds;
|
||||
}
|
||||
|
||||
size_t add_bios_vector_target_seeds(
|
||||
std::vector<PSXRecompV4::Seed>& seeds,
|
||||
const std::vector<uint8_t>& rom,
|
||||
uint32_t base_addr,
|
||||
uint32_t rom_end,
|
||||
const std::vector<PSXRecompV4::BiosVectorTable>& bios_vectors)
|
||||
{
|
||||
std::set<uint32_t> known;
|
||||
for (const auto& seed : seeds) {
|
||||
known.insert(seed.address);
|
||||
}
|
||||
|
||||
const uint32_t base_phys = base_addr & 0x1FFFFFFFu;
|
||||
const uint32_t rom_size = static_cast<uint32_t>(rom.size());
|
||||
size_t added = 0;
|
||||
|
||||
for (const auto& bvt : bios_vectors) {
|
||||
const uint32_t table_phys = bvt.table_rom_addr & 0x1FFFFFFFu;
|
||||
if (table_phys < base_phys ||
|
||||
table_phys + bvt.table_count * 4u > base_phys + rom_size) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp-bios: WARNING: vector table 0x%08X out of ROM range; "
|
||||
"not augmenting seeds from it\n",
|
||||
bvt.table_rom_addr);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t file_off = table_phys - base_phys;
|
||||
for (uint32_t i = 0; i < bvt.table_count; ++i) {
|
||||
const uint32_t off = file_off + i * 4u;
|
||||
const uint32_t entry =
|
||||
static_cast<uint32_t>(rom[off]) |
|
||||
(static_cast<uint32_t>(rom[off + 1]) << 8) |
|
||||
(static_cast<uint32_t>(rom[off + 2]) << 16) |
|
||||
(static_cast<uint32_t>(rom[off + 3]) << 24);
|
||||
if (entry == 0u) continue;
|
||||
if (entry < base_addr || entry > rom_end) continue;
|
||||
if (!known.insert(entry).second) continue;
|
||||
|
||||
PSXRecompV4::Seed seed;
|
||||
seed.address = entry;
|
||||
seed.label = fmt::format("bios_vector_{:02X}_{:02X}",
|
||||
bvt.ram_addr & 0xFFu, i);
|
||||
seed.rationale = fmt::format(
|
||||
"ROM-resident target of configured BIOS vector 0x{:02X} table index 0x{:02X}",
|
||||
bvt.ram_addr & 0xFFu, i);
|
||||
seeds.push_back(std::move(seed));
|
||||
++added;
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
// ----- Phase 1c: artifact emission -----------------------------------------
|
||||
|
||||
std::string make_function_manifest_json(const PSXRecompV4::DiscoveryResult& dr,
|
||||
@@ -718,51 +773,40 @@ int run_emit_full(const fs::path& bios_path, const fs::path& out_dir,
|
||||
|
||||
// 2. Load seeds.
|
||||
auto seeds = load_seeds(seed_path);
|
||||
const size_t vector_seed_count =
|
||||
add_bios_vector_target_seeds(seeds, rom, kBiosBase,
|
||||
kBiosBase + static_cast<uint32_t>(kBiosSize) - 1,
|
||||
bios_vectors);
|
||||
std::fprintf(stdout, "psxrecomp-bios: loaded %zu seeds from %s\n",
|
||||
seeds.size(), seed_path.string().c_str());
|
||||
if (vector_seed_count != 0) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp-bios: added %zu ROM-resident BIOS vector target seeds\n",
|
||||
vector_seed_count);
|
||||
}
|
||||
|
||||
// 2b. Seed every BIOS vector-table entry and alias target as a discovery
|
||||
// root. Vector-table targets (A0/B0/C0 kernel functions) are reached only
|
||||
// through the RAM-installed dispatch stubs — never by a direct in-ROM jal —
|
||||
// so JAL-transitive discovery cannot find ones nothing else calls
|
||||
// (e.g. A0:0x16 strncat at 0xBFC03200). Without a root here the dispatch
|
||||
// generator emits "not in dispatch table, skipped" and the kernel call
|
||||
// silently becomes a no-op at runtime.
|
||||
// Also seed bios_alias targets — same reachability hole as vector-table
|
||||
// entries: a fixed-target trampoline's callee may never be jal'd in ROM.
|
||||
{
|
||||
const uint32_t base_phys = kBiosBase & 0x1FFFFFFFu;
|
||||
std::set<uint32_t> have;
|
||||
for (const auto& sd : seeds) have.insert(sd.address & 0x1FFFFFFFu);
|
||||
size_t added = 0;
|
||||
auto add_rom_seed = [&](uint32_t entry, std::string label) {
|
||||
const uint32_t phys = entry & 0x1FFFFFFFu;
|
||||
if (phys < base_phys || phys >= base_phys + (uint32_t)kBiosSize) return;
|
||||
if ((phys & 3u) != 0u) return;
|
||||
if (!have.insert(phys).second) return;
|
||||
size_t alias_added = 0;
|
||||
for (const auto& ba : bios_aliases) {
|
||||
const uint32_t phys = ba.target_key & 0x1FFFFFFFu;
|
||||
if (phys < base_phys || phys >= base_phys + (uint32_t)kBiosSize) continue;
|
||||
if ((phys & 3u) != 0u) continue;
|
||||
if (!have.insert(phys).second) continue;
|
||||
seeds.push_back({kBiosBase | (phys - base_phys),
|
||||
std::move(label), "bios vector/alias target"});
|
||||
added++;
|
||||
};
|
||||
for (const auto& bvt : bios_vectors) {
|
||||
const uint32_t table_phys = bvt.table_rom_addr & 0x1FFFFFFFu;
|
||||
if (table_phys < base_phys ||
|
||||
table_phys + bvt.table_count * 4u > base_phys + (uint32_t)kBiosSize)
|
||||
continue;
|
||||
const uint32_t off = table_phys - base_phys;
|
||||
for (uint32_t i = 0; i < bvt.table_count; i++) {
|
||||
const uint32_t entry =
|
||||
(uint32_t)rom[off + i*4] | ((uint32_t)rom[off + i*4 + 1] << 8) |
|
||||
((uint32_t)rom[off + i*4 + 2] << 16) | ((uint32_t)rom[off + i*4 + 3] << 24);
|
||||
if (entry == 0u) continue;
|
||||
add_rom_seed(entry, fmt::format("bios_vec_{:02X}[0x{:02X}]",
|
||||
bvt.ram_addr, i));
|
||||
}
|
||||
fmt::format("bios_alias_{:08X}", ba.ram_addr),
|
||||
"ROM-resident target of configured BIOS alias"});
|
||||
alias_added++;
|
||||
}
|
||||
if (alias_added != 0) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp-bios: added %zu BIOS alias target seeds\n",
|
||||
alias_added);
|
||||
}
|
||||
for (const auto& ba : bios_aliases)
|
||||
add_rom_seed(ba.target_key,
|
||||
fmt::format("bios_alias_{:08X}", ba.ram_addr));
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp-bios: seeded %zu additional bios vector/alias targets\n",
|
||||
added);
|
||||
}
|
||||
|
||||
// 3. Run discovery.
|
||||
|
||||
+37
-1
@@ -64,6 +64,7 @@ void cdrom_tick(void);
|
||||
/* DMA channel 3 interface */
|
||||
uint32_t cdrom_dma_read(void);
|
||||
int cdrom_dma_ready(void);
|
||||
uint32_t cdrom_dma_sector_word_count(void);
|
||||
|
||||
typedef struct CDROMDebugState {
|
||||
uint64_t seq;
|
||||
@@ -77,6 +78,9 @@ typedef struct CDROMDebugState {
|
||||
uint8_t seek_sec;
|
||||
uint8_t seek_sect;
|
||||
uint8_t pending_cmd;
|
||||
uint8_t queued_cmd;
|
||||
uint8_t queued_pending;
|
||||
uint8_t queued_param_count;
|
||||
int has_disc;
|
||||
int param_count;
|
||||
int response_read;
|
||||
@@ -146,9 +150,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;
|
||||
@@ -171,6 +205,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);
|
||||
|
||||
@@ -83,6 +83,10 @@ typedef struct {
|
||||
* port=0 uses the default (4370). */
|
||||
void debug_server_init(int port);
|
||||
|
||||
/* Current listener status for heartbeat diagnostics. error is the platform
|
||||
* socket error captured by init, or 0 when the listener is active. */
|
||||
void debug_server_get_status(int *listening, int *port, int *error);
|
||||
|
||||
/* Poll for incoming connections and commands. Non-blocking.
|
||||
* Call once per vblank. */
|
||||
void debug_server_poll(void);
|
||||
@@ -125,6 +129,7 @@ void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width);
|
||||
|
||||
/* Dispatch trace — record every function dispatched. */
|
||||
void debug_server_trace_dispatch(uint32_t func_addr);
|
||||
void debug_server_trace_dispatch_return(uint32_t func_addr, CPUState *cpu);
|
||||
|
||||
/* Direct-call entry hook — emitted by the recompiler at the top of every
|
||||
* generated function so we can see direct-jal targets that never go through
|
||||
@@ -132,6 +137,7 @@ void debug_server_trace_dispatch(uint32_t func_addr);
|
||||
* 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
|
||||
@@ -157,6 +163,12 @@ void debug_server_log_thread_event(uint32_t kind, CPUState *cpu,
|
||||
uint32_t current_tcb,
|
||||
uint32_t target_tcb,
|
||||
uint32_t target_pc);
|
||||
void debug_server_log_thread_event_ex(uint32_t kind, CPUState *cpu,
|
||||
uint32_t current_tcb,
|
||||
uint32_t target_tcb,
|
||||
uint32_t target_pc,
|
||||
uintptr_t current_fiber,
|
||||
uintptr_t target_fiber);
|
||||
|
||||
/* Dirty-RAM dispatch break. Used by the dynamic-code interpreter to pause
|
||||
* immediately when dispatch enters a configured address range, before a hot
|
||||
@@ -172,6 +184,14 @@ void debug_server_check_watchpoints(void);
|
||||
/* Returns >= 0 if the debug server wants to override pad input,
|
||||
* -1 if no override is active. Value is PS1 16-bit button mask. */
|
||||
int debug_server_get_input_override(void);
|
||||
int debug_server_get_input_state(uint16_t *buttons,
|
||||
int *axes_override,
|
||||
uint8_t *lx, uint8_t *ly,
|
||||
uint8_t *rx, uint8_t *ry);
|
||||
|
||||
/* TCP-controlled turbo mode. When enabled the frontend skips presentation and
|
||||
* wall-clock pacing at vblank, matching the keyboard TAB turbo path. */
|
||||
int debug_server_turbo_enabled(void);
|
||||
|
||||
/* TCP-controlled turbo mode. When enabled the frontend skips presentation and
|
||||
* wall-clock pacing at vblank, matching the keyboard TAB turbo path. */
|
||||
@@ -204,12 +224,25 @@ void debug_server_send_fmt(const char *fmt, ...);
|
||||
* stable. */
|
||||
void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_wtrace_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_mmio_json(FILE *f, uint32_t max_count);
|
||||
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);
|
||||
void debug_server_freeze_dump_cdrom_trace_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_cdrom_sector_history_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_dma_state_json(FILE *f);
|
||||
void debug_server_freeze_dump_dma_trace_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_exception_trace_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_thread_ctx_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_fn_entry_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_dirty_block_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_dirty_insn_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_evcb_json(FILE *f, uint32_t max_count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask);
|
||||
void dma_advance(uint32_t cycles);
|
||||
uint32_t dma_get_dicr(void);
|
||||
uint32_t dma_get_dpcr(void);
|
||||
int dma_cdrom_transfer_active(void);
|
||||
|
||||
typedef struct DMAChannelDebugState {
|
||||
uint32_t madr;
|
||||
@@ -52,6 +53,9 @@ typedef struct DMATraceEntry {
|
||||
uint32_t kind;
|
||||
uint32_t channel;
|
||||
uint32_t total_words;
|
||||
uint32_t addr;
|
||||
uint32_t val;
|
||||
uint32_t mask;
|
||||
uint32_t madr;
|
||||
uint32_t bcr;
|
||||
uint32_t chcr;
|
||||
@@ -65,10 +69,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
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ typedef enum {
|
||||
GATE_COOLDOWN = 2, /* post-exception cooldown block */
|
||||
GATE_SR_IE = 3, /* COP0 SR IEc clear (interrupts globally off) */
|
||||
GATE_SR_IM2 = 4, /* COP0 SR IM2 clear (HW int line masked) */
|
||||
GATE_CALL_DEPTH = 5, /* ape-fw: inside nested generated call, not a safe point */
|
||||
GATE_BAD_RESUME = 6, /* ape-fw: no valid guest resume PC at this check */
|
||||
} EventGateReason;
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -92,10 +92,16 @@ public:
|
||||
/**
|
||||
* Get the resolved path to the BIN file (set after Open())
|
||||
*/
|
||||
std::string GetBinPath() const;
|
||||
|
||||
/**
|
||||
* Get root directory information
|
||||
std::string GetBinPath() const;
|
||||
|
||||
/**
|
||||
* Get number of addressable sectors in the mounted image.
|
||||
* Raw BIN images count 2352-byte sectors; cooked ISO images count 2048-byte sectors.
|
||||
*/
|
||||
uint32_t GetSectorCount();
|
||||
|
||||
/**
|
||||
* Get root directory information
|
||||
* @return RootDirectoryInfo containing LBA and size
|
||||
*/
|
||||
RootDirectoryInfo GetRootDirectory() const;
|
||||
|
||||
+342
-219
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
#include "cdrom.h"
|
||||
#include "dma.h"
|
||||
#include "spu.h"
|
||||
#include "event_ring.h"
|
||||
#include <string.h>
|
||||
@@ -26,6 +27,7 @@
|
||||
extern void* iso_open(const char* path);
|
||||
extern int iso_read_sector(void* handle, uint32_t lba, uint8_t* buffer, int size);
|
||||
extern int iso_read_raw_sector(void* handle, uint32_t lba, uint8_t* buffer, int size);
|
||||
extern uint32_t iso_sector_count(void* handle);
|
||||
extern void iso_close(void* handle);
|
||||
|
||||
/* I_STAT owned by memory.c — set bit 2 for CDROM IRQ */
|
||||
@@ -41,6 +43,52 @@ static uint8_t request_reg;
|
||||
static uint8_t irq_enable;
|
||||
static uint8_t irq_flag;
|
||||
|
||||
/* CPS-native CD interrupt single-outstanding latch.
|
||||
*
|
||||
* The CD controller serializes responses: it presents one INT to the CPU
|
||||
* interrupt controller per visible IRQ generation, and the next is presented
|
||||
* only after the guest acknowledges the controller (write to 0x1F801803.1).
|
||||
*
|
||||
* Under the recompiled CPS interrupt model, psx_check_interrupts re-checks
|
||||
* (i_stat & i_mask) at every dispatch boundary. Without this latch,
|
||||
* refresh_cdrom_irq_line() re-asserts i_stat bit 2 from an already-set
|
||||
* irq_flag on every cdrom_advance, so an unacked CD INT becomes an unbounded
|
||||
* exception re-entry storm. The latch presents each generation to INTC exactly
|
||||
* once; the next generation can raise i_stat only after a controller ack.
|
||||
*
|
||||
* cdrom_intc_request_latched == 1 -> the current CD INT has been presented.
|
||||
* cdrom_irq_generation -> increments per visible INT (set_irq); debug.
|
||||
* cdrom_intc_latched_generation -> the generation last presented to INTC; debug.
|
||||
*/
|
||||
static int cdrom_intc_request_latched;
|
||||
static uint32_t cdrom_irq_generation;
|
||||
static uint32_t cdrom_intc_latched_generation;
|
||||
|
||||
/* CD response presentation latency.
|
||||
*
|
||||
* Real CD hardware does not produce a command's first response instantly: the
|
||||
* controller takes thousands of cycles to process the command before raising
|
||||
* the response IRQ. The previous model presented the first response (and the
|
||||
* delayed second response) SYNCHRONOUSLY inside the guest's command/ack MMIO
|
||||
* write. That re-creates a lost-interrupt hazard: when the BIOS issues the next
|
||||
* CD command from *inside* its CD interrupt handler (e.g. SeekL, issued right
|
||||
* before the handler's trailing I_STAT ack), the synchronous response raises
|
||||
* i_stat bit2, the handler's own INTC ack then clears it while irq_flag is
|
||||
* still set, and the single-outstanding latch suppresses re-presentation
|
||||
* forever -> the command never completes (boot-EXE-load SeekL wedge).
|
||||
*
|
||||
* Fix: arm a presentation delay on every new response (set_irq) and gate
|
||||
* present_cdrom_irq() on it. The present then lands at a later cdrom_advance,
|
||||
* at a clean instruction boundary after the issuing ISR has returned and
|
||||
* re-enabled interrupts — exactly as on real hardware. The latch is unchanged
|
||||
* (it still models edge-latched delivery: one present per generation).
|
||||
*
|
||||
* Sized to comfortably outlast an ISR teardown (tens-to-hundreds of cycles)
|
||||
* while staying well under the second-response delays (10k-30k), so command
|
||||
* throughput is unaffected. */
|
||||
#define CDROM_IRQ_PRESENT_DELAY 5000
|
||||
static int cdrom_irq_present_delay;
|
||||
|
||||
/* Parameter FIFO */
|
||||
#define PARAM_FIFO_SIZE 16
|
||||
static uint8_t param_fifo[PARAM_FIFO_SIZE];
|
||||
@@ -79,6 +127,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;
|
||||
@@ -148,6 +198,22 @@ void cdrom_notify_game_started(void) {
|
||||
|
||||
int cdrom_get_setloc_lba(void) { return s_setloc_lba; }
|
||||
|
||||
/* Frontend XA-stream probe (FMV auto-skip / turbo-load gating in main.cpp). */
|
||||
int cdrom_xa_stream_active(void) { return xa_stream_active; }
|
||||
|
||||
/* Response-overwrite diagnostics consumed by debug_server.c. master's CD
|
||||
* response arbiter tracks how often an undelivered response was overwritten;
|
||||
* this CD model (forwarded from the Ape bring-up branch) delivers responses
|
||||
* directly and does not overwrite, so these stay zero. Defined here so the
|
||||
* debug command links against either CD model. */
|
||||
uint32_t g_cd_overwrite_count = 0;
|
||||
uint8_t g_cd_overwrite_first_prev = 0;
|
||||
uint8_t g_cd_overwrite_first_new = 0;
|
||||
uint32_t g_cd_overwrite_first_frame = 0;
|
||||
uint8_t g_cd_overwrite_last_prev = 0;
|
||||
uint8_t g_cd_overwrite_last_new = 0;
|
||||
uint32_t g_cd_overwrite_last_frame = 0;
|
||||
|
||||
/* Minimum cycles between CD-ROM IRQs in fast modes. Must be enough for the
|
||||
* interrupt handler to save state, check the IRQ flag, process data, and
|
||||
* return. Too low → interrupt fires before the previous one is processed →
|
||||
@@ -183,10 +249,6 @@ void cdrom_set_instant_rate(int per_frame) {
|
||||
}
|
||||
int cdrom_get_instant_rate(void) { return g_instant_max_per_frame; }
|
||||
|
||||
/* Frontend FMV detector reads this to distinguish FMV/streaming XA from other
|
||||
* sector traffic (FMV = streaming XA + active MDEC). */
|
||||
int cdrom_xa_stream_active(void) { return xa_stream_active; }
|
||||
|
||||
static int instant_period(void) {
|
||||
int p = VBLANK_CYCLES_NTSC / g_instant_max_per_frame;
|
||||
return p < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : p;
|
||||
@@ -195,30 +257,13 @@ static int instant_period(void) {
|
||||
static int apply_speed(int delay) {
|
||||
/* XA streaming (FMV / CDDA background music): preserve authentic timing.
|
||||
* FMVs interleave XA audio + MDEC video — speeding up sector delivery
|
||||
* would desync the player (it depends on the authentic per-sector cadence;
|
||||
* flooding it instantly hangs the movie on a white screen). FMV auto-skip
|
||||
* therefore does NOT touch CD timing — it runs the whole machine faster via
|
||||
* uncapped frame pacing (frontend), which speeds XA+MDEC+display together,
|
||||
* preserving their relative sync. */
|
||||
* would cause both to play faster than the display refresh rate. */
|
||||
if (xa_stream_active) return delay;
|
||||
if (g_disc_speed_divisor == 0) return instant_period(); /* bounded 'instant' */
|
||||
int d = delay / g_disc_speed_divisor;
|
||||
return d < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : d;
|
||||
}
|
||||
|
||||
/* Command-completion latency (Pause/Init/SeekL/GetID/ReadTOC second response).
|
||||
* These are CONTROLLER/FIRMWARE operations whose timing is essentially FIXED on
|
||||
* real hardware — a faster-spinning drive does NOT complete them proportionally
|
||||
* faster. Scaling them by the disc-speed divisor (as apply_speed does for DATA
|
||||
* sectors) desyncs the BIOS CD driver: only 1x booted, while 4x and instant both
|
||||
* wedged the Pause (0x09) completion handshake in a black-screen spin. So
|
||||
* command latencies use authentic timing regardless of disc_speed; only sector
|
||||
* DATA delivery (sector_delay_cycles) scales with speed — and since essentially
|
||||
* all load time is sector reads, loads still get as fast as the speed allows. */
|
||||
static int cmd_latency(int cycles) {
|
||||
return cycles < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : cycles;
|
||||
}
|
||||
|
||||
/* ---- CD load-burst ring (always-on; CLAUDE.md ring-buffer rule) ----------
|
||||
* A "burst" is a run of delivered sectors with no gap longer than
|
||||
* CD_BURST_GAP_FRAMES — i.e. one load. Records make "load duration" a
|
||||
@@ -286,40 +331,15 @@ typedef struct {
|
||||
} PendingCmd;
|
||||
static PendingCmd pending;
|
||||
|
||||
/* ---- Response arbiter (CLAUDE.md: hardware-accurate, not a rate hack) -------
|
||||
* The PS1 CD controller SERIALIZES responses: only one occupies the IRQ slot
|
||||
* (irq_flag + the response FIFO) at a time; a later response is queued and
|
||||
* exposed only after the guest ACKs the current one. A command written while a
|
||||
* response is still visible is LATCHED (the controller is busy) and executed
|
||||
* when the guest ACKs. The old model overwrote irq_flag directly, which DROPPED
|
||||
* responses whenever they arrived faster than the guest acked — proven to cause
|
||||
* 24 dropped responses (INT2 over unacked INT3, INT3 over INT3) at MMX6 boot
|
||||
* under disc_speed=instant, wedging the loader. With the arbiter, the guest's
|
||||
* own ACK cadence paces delivery, so "instant" needs no per-game sectors/frame
|
||||
* cap. set_irq() now enqueues-or-exposes; the guest ACK actively pumps the
|
||||
* arbiter (the wakeup path a naive defer-guard lacked). */
|
||||
typedef struct {
|
||||
uint8_t int_type;
|
||||
uint8_t bytes[RESPONSE_FIFO_SIZE];
|
||||
uint8_t count;
|
||||
} CdQResp;
|
||||
#define CD_RESP_Q 8
|
||||
static CdQResp cd_respq[CD_RESP_Q];
|
||||
static int cd_respq_head;
|
||||
static int cd_respq_len;
|
||||
static int cd_gap_remaining; /* guest cycles until the slot may expose the next response */
|
||||
#define CD_MIN_IRQ_GAP 800 /* controller delivery latency (~DuckStation MINIMUM_INTERRUPT_DELAY) */
|
||||
uint32_t g_cd_resp_drops = 0; /* queue overflow (should stay 0 with correct pacing) */
|
||||
uint8_t cmd;
|
||||
uint8_t params[PARAM_FIFO_SIZE];
|
||||
int param_count;
|
||||
int pending;
|
||||
} QueuedCmd;
|
||||
static QueuedCmd queued_cmd;
|
||||
|
||||
/* Single pending-command latch: a command written while the slot is busy is
|
||||
* held and executed at the guest ACK (real HW: transmission begins at ACK). */
|
||||
static int cd_cmd_latched;
|
||||
static uint8_t cd_cmd_latched_cmd;
|
||||
static uint8_t cd_cmd_latched_params[PARAM_FIFO_SIZE];
|
||||
static int cd_cmd_latched_pcount;
|
||||
|
||||
static void exec_command(uint8_t cmd); /* forward decl (arbiter pumps it on ACK) */
|
||||
static void cd_arbiter_pump(void);
|
||||
static void exec_command(uint8_t cmd);
|
||||
|
||||
/* ISO reader */
|
||||
static void* iso_handle = NULL;
|
||||
@@ -358,6 +378,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 &&
|
||||
@@ -442,120 +499,58 @@ static void response_push(uint8_t val) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Overwrite (dropped-response) accounting — survives the trace ring's eviction.
|
||||
* Counts how many times a new response clobbered an unacked one, and captures
|
||||
* the first occurrence's context. Queryable via cdrom_state. */
|
||||
uint32_t g_cd_overwrite_count = 0;
|
||||
uint8_t g_cd_overwrite_first_prev = 0;
|
||||
uint8_t g_cd_overwrite_first_new = 0;
|
||||
uint32_t g_cd_overwrite_first_frame = 0;
|
||||
uint8_t g_cd_overwrite_last_prev = 0;
|
||||
uint8_t g_cd_overwrite_last_new = 0;
|
||||
uint32_t g_cd_overwrite_last_frame = 0;
|
||||
|
||||
static void fire_cdrom_irq(void);
|
||||
|
||||
/* Load a response into the visible hardware slot (irq_flag + response FIFO) and
|
||||
* raise the line. The slot must be free (irq_flag==0) on entry. */
|
||||
static void cd_expose(const CdQResp *r) {
|
||||
irq_flag = r->int_type;
|
||||
response_read = 0;
|
||||
response_count = r->count;
|
||||
if (r->count) memcpy(response_fifo, r->bytes, r->count);
|
||||
cd_gap_remaining = CD_MIN_IRQ_GAP;
|
||||
trace_cdrom('I', 0, (uint32_t)r->int_type, 0);
|
||||
/* DEQUEUE: CD response/data event fired (aux = CD irq type). */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_CD_IRQ, (uint32_t)r->int_type);
|
||||
fire_cdrom_irq();
|
||||
}
|
||||
|
||||
/* set_irq() — the single response-producing entry point. Callers build the
|
||||
* response bytes in response_fifo (via response_clear/response_push) then call
|
||||
* set_irq(int_type). We SNAPSHOT those bytes and either expose immediately (slot
|
||||
* free, nothing queued ahead, inter-IRQ gap elapsed) or QUEUE behind the visible
|
||||
* response. The guest ACK (and the advance pump) drains the queue in order, so
|
||||
* no response is ever clobbered — this replaces the old direct irq_flag=type
|
||||
* overwrite that DROPPED responses under fast (instant) disc timing. */
|
||||
static void set_irq(int type) {
|
||||
/* Authentic-1x bypass. The response arbiter (queue + inter-IRQ gap + command
|
||||
* latch) exists to stop responses being DROPPED when we deliver them FASTER
|
||||
* than hardware (disc_speed 2x/4x/instant), where a new response can land
|
||||
* before the guest acks the previous one. At authentic 1x that race cannot
|
||||
* happen — responses arrive at hardware cadence — and any queue/gap we add
|
||||
* only PERTURBS timing the game was validated against. That perturbation
|
||||
* desynced CD-streamed audio (boss/stage-select music looped/restarted
|
||||
* early). So at 1x, behave exactly as the pre-arbiter code did: set the
|
||||
* visible IRQ immediately; the caller's fire_cdrom_irq() raises the line. */
|
||||
if (g_disc_speed_divisor == 1) {
|
||||
irq_flag = (uint8_t)type;
|
||||
cd_gap_remaining = 0;
|
||||
trace_cdrom('I', 0, (uint32_t)type, 0);
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_CD_IRQ, (uint32_t)type);
|
||||
return;
|
||||
}
|
||||
|
||||
CdQResp r;
|
||||
r.int_type = (uint8_t)type;
|
||||
r.count = (uint8_t)(response_count > RESPONSE_FIFO_SIZE ? RESPONSE_FIFO_SIZE : response_count);
|
||||
if (r.count) memcpy(r.bytes, response_fifo, r.count);
|
||||
|
||||
if (irq_flag == 0 && cd_respq_len == 0 && cd_gap_remaining <= 0) {
|
||||
cd_expose(&r);
|
||||
} else if (cd_respq_len < CD_RESP_Q) {
|
||||
cd_respq[(cd_respq_head + cd_respq_len) % CD_RESP_Q] = r;
|
||||
cd_respq_len++;
|
||||
trace_cdrom('Q', 0, (uint32_t)type, 0); /* queued behind a visible/earlier response */
|
||||
} else {
|
||||
/* Queue overflow — should never happen with correct ACK pacing. Fall
|
||||
* back to clobber and count it so a regression is visible, not silent. */
|
||||
g_cd_resp_drops++;
|
||||
if (g_cd_overwrite_count == 0) {
|
||||
g_cd_overwrite_first_prev = irq_flag;
|
||||
g_cd_overwrite_first_new = (uint8_t)type;
|
||||
g_cd_overwrite_first_frame = (uint32_t)s_frame_count;
|
||||
}
|
||||
g_cd_overwrite_count++;
|
||||
g_cd_overwrite_last_prev = irq_flag;
|
||||
g_cd_overwrite_last_new = (uint8_t)type;
|
||||
g_cd_overwrite_last_frame = (uint32_t)s_frame_count;
|
||||
irq_flag = 0; /* force expose to proceed */
|
||||
cd_gap_remaining = 0;
|
||||
cd_expose(&r);
|
||||
}
|
||||
irq_flag = (uint8_t)type;
|
||||
/* New visible CD INT generation: it has not yet been presented to INTC,
|
||||
* so re-arm the latch (the delayed present below raises it once). */
|
||||
cdrom_irq_generation++;
|
||||
cdrom_intc_request_latched = 0;
|
||||
/* Arm the presentation latency: this response must NOT be presented to INTC
|
||||
* synchronously inside the guest store that triggered it (see the
|
||||
* CDROM_IRQ_PRESENT_DELAY note). It is presented at a later cdrom_advance. */
|
||||
cdrom_irq_present_delay = CDROM_IRQ_PRESENT_DELAY;
|
||||
trace_cdrom('I', 0, (uint32_t)type, 0);
|
||||
/* DEQUEUE: CD response/data event fired (aux = CD irq type). */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_CD_IRQ, (uint32_t)type);
|
||||
}
|
||||
|
||||
/* Advance the controller when the visible slot frees: drain the older queued
|
||||
* responses first (they precede any latched command on the wire), then start a
|
||||
* command latched while the controller was busy. Driven by the guest ACK edge
|
||||
* and by cdrom_advance once the inter-IRQ gap elapses. */
|
||||
static void cd_arbiter_pump(void) {
|
||||
if (irq_flag != 0) return; /* slot still holds an unacked response */
|
||||
if (cd_gap_remaining > 0) return; /* controller inter-IRQ delivery latency */
|
||||
if (cd_respq_len > 0) {
|
||||
CdQResp r = cd_respq[cd_respq_head];
|
||||
cd_respq_head = (cd_respq_head + 1) % CD_RESP_Q;
|
||||
cd_respq_len--;
|
||||
cd_expose(&r);
|
||||
return;
|
||||
}
|
||||
if (cd_cmd_latched) {
|
||||
cd_cmd_latched = 0;
|
||||
memcpy(param_fifo, cd_cmd_latched_params, cd_cmd_latched_pcount);
|
||||
param_count = (uint8_t)cd_cmd_latched_pcount;
|
||||
trace_cdrom('X', 0, (uint32_t)cd_cmd_latched_cmd, 0); /* latched cmd now executing */
|
||||
exec_command(cd_cmd_latched_cmd); /* its ACK flows through set_irq -> exposes */
|
||||
}
|
||||
}
|
||||
|
||||
/* Fire CDROM IRQ into the interrupt controller */
|
||||
static void fire_cdrom_irq(void) {
|
||||
if (irq_flag && (irq_enable & (1 << (irq_flag - 1)))) {
|
||||
/* Present the current CD INT to the CPU interrupt controller exactly once per
|
||||
* generation. Re-presentation of the same unacked INT is suppressed by the
|
||||
* latch (this is what stops the CPS exception re-entry storm). The next
|
||||
* generation re-arms via set_irq; a controller ack re-arms via the ack path. */
|
||||
/* Present the current CD INT to INTC at most once per generation. Silent
|
||||
* except when it actually raises i_stat (trace 'F', low-frequency: once per
|
||||
* generation). Suppression of a duplicate presentation is intentionally NOT
|
||||
* traced — refresh runs every cdrom_advance, so tracing it would flood the CD
|
||||
* trace ring and evict useful history. Suppression is implicit (no 'F' fires
|
||||
* between an ack and the next set_irq). */
|
||||
static void present_cdrom_irq(void) {
|
||||
/* Presentation latency: hold off raising i_stat until the response's
|
||||
* processing delay has elapsed (decremented in cdrom_advance). This is what
|
||||
* keeps a freshly-issued response from being presented synchronously inside
|
||||
* the guest's command/ack write — and thus from being lost to that ISR's
|
||||
* own trailing INTC ack. */
|
||||
if (cdrom_irq_present_delay > 0) return;
|
||||
if (irq_flag && (irq_enable & (1 << (irq_flag - 1))) && !cdrom_intc_request_latched) {
|
||||
i_stat |= (1u << 2); /* IRQ_CDROM */
|
||||
cdrom_intc_request_latched = 1;
|
||||
cdrom_intc_latched_generation = cdrom_irq_generation;
|
||||
event_ring_record(EV_ISTAT_RAISE, 2 /* IRQ_CDROM bit */);
|
||||
trace_cdrom('F', 0, irq_flag, 0);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
/* Fire CDROM IRQ into the interrupt controller (explicit, per command/response).
|
||||
* Trace the masked case here only (low-frequency); refresh stays silent. */
|
||||
static void fire_cdrom_irq(void) {
|
||||
if (irq_flag && !(irq_enable & (1 << (irq_flag - 1)))) {
|
||||
trace_cdrom('f', 0, irq_flag, 0);
|
||||
}
|
||||
present_cdrom_irq();
|
||||
}
|
||||
|
||||
static void refresh_cdrom_irq_line(void) {
|
||||
present_cdrom_irq();
|
||||
}
|
||||
|
||||
static int bcd_to_bin(uint8_t bcd) {
|
||||
@@ -885,6 +880,12 @@ static void stop_read_stream(void) {
|
||||
read_delay = 0;
|
||||
}
|
||||
|
||||
static int data_fifo_ready(void) {
|
||||
return (request_reg & CDROM_REQUEST_BFRD) &&
|
||||
sector_available &&
|
||||
sector_read_pos < sector_size;
|
||||
}
|
||||
|
||||
static int deliver_read_sector(void) {
|
||||
int delivered = read_sector_at(read_min, read_sec, read_sect);
|
||||
advance_msf(&read_min, &read_sec, &read_sect);
|
||||
@@ -896,10 +897,59 @@ static int deliver_read_sector(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int deliver_read_sector_without_irq(void) {
|
||||
int delivered = read_sector_at(read_min, read_sec, read_sect);
|
||||
advance_msf(&read_min, &read_sec, &read_sect);
|
||||
return delivered;
|
||||
}
|
||||
|
||||
static void try_execute_queued_command(void) {
|
||||
if (!queued_cmd.pending || irq_flag != 0) return;
|
||||
|
||||
uint8_t cmd = queued_cmd.cmd;
|
||||
int count = queued_cmd.param_count;
|
||||
if (count < 0) count = 0;
|
||||
if (count > PARAM_FIFO_SIZE) count = PARAM_FIFO_SIZE;
|
||||
|
||||
memcpy(param_fifo, queued_cmd.params, (size_t)count);
|
||||
param_count = count;
|
||||
|
||||
queued_cmd.pending = 0;
|
||||
queued_cmd.cmd = 0;
|
||||
queued_cmd.param_count = 0;
|
||||
memset(queued_cmd.params, 0, sizeof(queued_cmd.params));
|
||||
|
||||
exec_command(cmd);
|
||||
}
|
||||
|
||||
static void queue_or_exec_command(uint8_t cmd) {
|
||||
if (irq_flag == 0) {
|
||||
exec_command(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
queued_cmd.cmd = cmd;
|
||||
queued_cmd.param_count = param_count;
|
||||
if (queued_cmd.param_count < 0) queued_cmd.param_count = 0;
|
||||
if (queued_cmd.param_count > PARAM_FIFO_SIZE) queued_cmd.param_count = PARAM_FIFO_SIZE;
|
||||
memcpy(queued_cmd.params, param_fifo, (size_t)queued_cmd.param_count);
|
||||
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);
|
||||
/* ENQUEUE: a CD command was issued (aux = command byte). */
|
||||
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_CD_CMD, (uint32_t)cmd);
|
||||
/* Snapshot the param FIFO before handlers consume it, so the
|
||||
* command-history record at the end sees the original params. */
|
||||
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) {
|
||||
@@ -944,7 +994,7 @@ static void exec_command(uint8_t cmd) {
|
||||
set_irq(CDIRQ_ACK);
|
||||
pending.cmd = 0x09;
|
||||
pending.pending = 1;
|
||||
pending.delay = cmd_latency(10000);
|
||||
pending.delay = apply_speed(10000);
|
||||
pending.phase = 1;
|
||||
break;
|
||||
|
||||
@@ -957,7 +1007,7 @@ static void exec_command(uint8_t cmd) {
|
||||
set_irq(CDIRQ_ACK);
|
||||
pending.cmd = 0x0A;
|
||||
pending.pending = 1;
|
||||
pending.delay = cmd_latency(50000);
|
||||
pending.delay = apply_speed(50000);
|
||||
pending.phase = 1;
|
||||
break;
|
||||
|
||||
@@ -1049,6 +1099,33 @@ static void exec_command(uint8_t cmd) {
|
||||
set_irq(CDIRQ_ACK);
|
||||
break;
|
||||
|
||||
case 0x14: { /* GetTD */
|
||||
if (!has_disc()) {
|
||||
response_push(stat_reg | CDSTAT_ERROR);
|
||||
response_push(0x80);
|
||||
set_irq(CDIRQ_ERROR);
|
||||
break;
|
||||
}
|
||||
|
||||
int track = (param_count >= 1) ? bcd_to_bin(param_fifo[0]) : 0;
|
||||
int lba = 0;
|
||||
if (track == 0 || track == 0xAA) {
|
||||
uint32_t sectors = iso_sector_count(iso_handle);
|
||||
lba = sectors ? (int)sectors : 0;
|
||||
} else {
|
||||
lba = 0; /* Single data track starts at absolute MSF 00:02:00. */
|
||||
}
|
||||
|
||||
int m, s, f;
|
||||
(void)f;
|
||||
lba_to_msf(lba, 150, &m, &s, &f);
|
||||
response_push(stat_reg);
|
||||
response_push(bin_to_bcd(m));
|
||||
response_push(bin_to_bcd(s));
|
||||
set_irq(CDIRQ_ACK);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x15: /* SeekL */
|
||||
if (!has_disc()) {
|
||||
response_push(stat_reg | CDSTAT_ERROR | CDSTAT_SEEKERR);
|
||||
@@ -1062,7 +1139,7 @@ static void exec_command(uint8_t cmd) {
|
||||
set_irq(CDIRQ_ACK);
|
||||
pending.cmd = 0x15;
|
||||
pending.pending = 1;
|
||||
pending.delay = cmd_latency(20000);
|
||||
pending.delay = apply_speed(20000);
|
||||
pending.phase = 1;
|
||||
break;
|
||||
|
||||
@@ -1074,7 +1151,7 @@ static void exec_command(uint8_t cmd) {
|
||||
set_irq(CDIRQ_ACK);
|
||||
pending.cmd = 0x1A;
|
||||
pending.pending = 1;
|
||||
pending.delay = cmd_latency(30000);
|
||||
pending.delay = apply_speed(30000);
|
||||
pending.phase = 1;
|
||||
break;
|
||||
|
||||
@@ -1094,7 +1171,7 @@ static void exec_command(uint8_t cmd) {
|
||||
set_irq(CDIRQ_ACK);
|
||||
pending.cmd = 0x1E;
|
||||
pending.pending = 1;
|
||||
pending.delay = cmd_latency(100000);
|
||||
pending.delay = apply_speed(100000);
|
||||
pending.phase = 1;
|
||||
break;
|
||||
|
||||
@@ -1122,12 +1199,18 @@ 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) {
|
||||
if (!pending.pending) return;
|
||||
|
||||
if (cycles == 0) return;
|
||||
/* Serialized responses: a delayed completion (e.g. SeekL / Init COMPLETE)
|
||||
* cannot be presented while a prior response IRQ is still unacked. Freeze
|
||||
* pending.delay while held so it does not accrue fake time debt and
|
||||
* underflow once the guest finally acks (mirrors process_read_stream). */
|
||||
if (irq_flag != 0) return;
|
||||
pending.delay -= (int)cycles;
|
||||
if (pending.delay > 0) return;
|
||||
|
||||
@@ -1202,20 +1285,49 @@ static void process_pending(uint32_t cycles) {
|
||||
static void process_read_stream(uint32_t cycles) {
|
||||
if (!reading) return;
|
||||
|
||||
/*
|
||||
* The controller exposes one sector buffer. Do not let the stream timer
|
||||
* accumulate a data backlog while software is still handling the previous
|
||||
* data-ready IRQ. Once software acknowledges that IRQ, any unread tail is
|
||||
* discardable: later sectors replace the controller's single data buffer.
|
||||
*
|
||||
* Software can also start a multi-sector CD DMA from inside the
|
||||
* data-ready callback before acknowledging the IRQ. In that case the IRQ
|
||||
* line remains asserted, but the disc stream must still refill an empty
|
||||
* sector buffer so the active DMA can continue.
|
||||
*/
|
||||
/* Serialized CD responses: hold the read stream while a controller INT is
|
||||
* unacked (presenting a new sector INT would clobber the single irq_flag /
|
||||
* response FIFO the guest is still reading), or while the single sector
|
||||
* buffer holds unread data. The DMA-drain path is the exception: during an
|
||||
* active CD DMA the data-ready INT stays asserted while the guest drains
|
||||
* the sector via DMA, and the stream must still refill the buffer.
|
||||
*
|
||||
* CRITICAL: freeze read_delay while held. Decrementing it during the hold
|
||||
* (the previous shape) accumulates fake time debt and underflows hard once
|
||||
* the guest finally acks, so the next sector is never scheduled correctly. */
|
||||
if (irq_flag != 0 && !dma_cdrom_transfer_active()) return;
|
||||
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) {
|
||||
if (sector_available) {
|
||||
trace_cdrom('O', 0, (uint32_t)sector_read_pos, 0);
|
||||
if (irq_flag == 0) {
|
||||
if (sector_available) {
|
||||
trace_cdrom('O', 0, (uint32_t)sector_read_pos, 0);
|
||||
}
|
||||
deliver_read_sector();
|
||||
} else {
|
||||
deliver_read_sector_without_irq();
|
||||
}
|
||||
deliver_read_sector();
|
||||
read_delay += sector_delay_cycles();
|
||||
/* Clamp pathological underflow to one sector period: never replay a
|
||||
* catch-up burst of missed sectors, never leave a huge negative debt. */
|
||||
if (read_delay <= 0) {
|
||||
read_delay = sector_delay_cycles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1229,6 +1341,10 @@ void cdrom_init(const char* cue_path) {
|
||||
request_reg = 0;
|
||||
irq_enable = 0x1F;
|
||||
irq_flag = 0;
|
||||
cdrom_intc_request_latched = 0;
|
||||
cdrom_irq_generation = 0;
|
||||
cdrom_intc_latched_generation = 0;
|
||||
cdrom_irq_present_delay = 0;
|
||||
param_count = 0;
|
||||
response_read = 0;
|
||||
response_count = 0;
|
||||
@@ -1253,13 +1369,8 @@ void cdrom_init(const char* cue_path) {
|
||||
xa_reset_decode();
|
||||
spu_cd_audio_reset();
|
||||
pending.pending = 0;
|
||||
memset(&queued_cmd, 0, sizeof(queued_cmd));
|
||||
seek_min = seek_sec = seek_sect = 0;
|
||||
/* Response arbiter / command latch state */
|
||||
cd_respq_head = 0;
|
||||
cd_respq_len = 0;
|
||||
cd_gap_remaining = 0;
|
||||
cd_cmd_latched = 0;
|
||||
cd_cmd_latched_pcount = 0;
|
||||
cdrom_debug_clear_sector_history();
|
||||
|
||||
if (cue_path) {
|
||||
@@ -1268,6 +1379,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);
|
||||
}
|
||||
|
||||
@@ -1280,7 +1392,7 @@ uint32_t cdrom_read(uint32_t addr) {
|
||||
if (param_count == 0) s |= (1 << 3);
|
||||
if (param_count < PARAM_FIFO_SIZE) s |= (1 << 4);
|
||||
if (response_read < response_count) s |= (1 << 5);
|
||||
if (request_reg & CDROM_REQUEST_BFRD) s |= (1 << 6);
|
||||
if (data_fifo_ready()) s |= (1 << 6);
|
||||
ret = s;
|
||||
break;
|
||||
}
|
||||
@@ -1292,11 +1404,10 @@ uint32_t cdrom_read(uint32_t addr) {
|
||||
break;
|
||||
|
||||
case 0x1F801802:
|
||||
if ((request_reg & CDROM_REQUEST_BFRD) && sector_available && sector_read_pos < sector_size) {
|
||||
if (data_fifo_ready()) {
|
||||
ret = sector_buffer[sector_read_pos++];
|
||||
if (sector_read_pos >= sector_size) {
|
||||
sector_available = 0;
|
||||
request_reg &= (uint8_t)~CDROM_REQUEST_BFRD;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1328,23 +1439,7 @@ void cdrom_write(uint32_t addr, uint32_t value) {
|
||||
|
||||
case 0x1F801801:
|
||||
if (index_reg == 0) {
|
||||
/* The controller is busy while a response is still visible/unacked,
|
||||
* something is queued ahead, a command is already latched, or the
|
||||
* inter-IRQ gap hasn't elapsed. Real HW lets software write the next
|
||||
* command but holds its processing until the slot frees — LATCH it
|
||||
* (the guest ACK pumps the arbiter, which executes it). Only execute
|
||||
* inline when the controller is genuinely idle. */
|
||||
if (g_disc_speed_divisor != 1 &&
|
||||
(irq_flag != 0 || cd_respq_len > 0 || cd_cmd_latched || cd_gap_remaining > 0)) {
|
||||
cd_cmd_latched = 1;
|
||||
cd_cmd_latched_cmd = val;
|
||||
cd_cmd_latched_pcount = param_count;
|
||||
memcpy(cd_cmd_latched_params, param_fifo, param_count);
|
||||
param_count = 0; /* params consumed by the latch */
|
||||
trace_cdrom('L', 0, (uint32_t)val, 0); /* command latched (controller busy) */
|
||||
} else {
|
||||
exec_command(val);
|
||||
}
|
||||
queue_or_exec_command(val);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1365,21 +1460,21 @@ void cdrom_write(uint32_t addr, uint32_t value) {
|
||||
sector_read_pos = 0;
|
||||
}
|
||||
} else if (index_reg == 1) {
|
||||
/* TRACE 'K': guest ACK of CD IRQ bits (clears irq_flag). Pairs with
|
||||
* 'I'/'W' to reconstruct the response<->ack lifecycle and prove
|
||||
* whether a response was overwritten before the guest acked it. */
|
||||
trace_cdrom('K', 0, (uint32_t)(val & 0x1F), 0);
|
||||
/* Controller IRQ acknowledge. irq_flag is a single numeric response
|
||||
* code (1=INT1 data, 2=INT2, 3=INT3 ack, ...); a full ack clears it
|
||||
* to 0. When the current visible INT is fully acked, re-arm the
|
||||
* latch so the NEXT generation can be presented to INTC. Use the
|
||||
* active->inactive edge rather than a bare !=0->0 so partial-clear
|
||||
* writes don't mis-rearm. */
|
||||
int had_active_irq = (irq_flag & 0x1F) != 0;
|
||||
irq_flag &= ~(val & 0x1F);
|
||||
if (had_active_irq && (irq_flag & 0x1F) == 0) {
|
||||
cdrom_intc_request_latched = 0;
|
||||
}
|
||||
if (val & 0x40) {
|
||||
param_count = 0;
|
||||
}
|
||||
/* The ACK edge is the wakeup that drives the arbiter forward: once
|
||||
* the visible response is retired, expose the next queued response
|
||||
* or start a latched command. (A naive "defer + return" guard
|
||||
* deadlocks precisely because it lacks this active pump.) */
|
||||
if (g_disc_speed_divisor != 1 && irq_flag == 0) {
|
||||
cd_arbiter_pump();
|
||||
}
|
||||
try_execute_queued_command();
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1389,17 +1484,19 @@ void cdrom_write(uint32_t addr, uint32_t value) {
|
||||
}
|
||||
|
||||
void cdrom_advance(uint32_t cycles) {
|
||||
/* Bleed down the controller inter-IRQ gap, then run the producers and pump
|
||||
* the arbiter so a queued response is exposed as soon as the slot is free
|
||||
* and the gap elapses (covers the case where the guest already acked and no
|
||||
* further ACK edge will arrive to drive delivery). */
|
||||
if (cd_gap_remaining > 0) {
|
||||
cd_gap_remaining -= (int)cycles;
|
||||
if (cd_gap_remaining < 0) cd_gap_remaining = 0;
|
||||
/* Age the response presentation latency. Once it elapses, the following
|
||||
* refresh_cdrom_irq_line() presents the held response to INTC — at this
|
||||
* cdrom_advance boundary, not synchronously inside the guest store that
|
||||
* armed it. */
|
||||
if (cdrom_irq_present_delay > 0) {
|
||||
cdrom_irq_present_delay -= (int)cycles;
|
||||
if (cdrom_irq_present_delay < 0) cdrom_irq_present_delay = 0;
|
||||
}
|
||||
refresh_cdrom_irq_line();
|
||||
try_execute_queued_command();
|
||||
process_pending(cycles);
|
||||
process_read_stream(cycles);
|
||||
cd_arbiter_pump();
|
||||
refresh_cdrom_irq_line();
|
||||
}
|
||||
|
||||
void cdrom_tick(void) {
|
||||
@@ -1408,12 +1505,12 @@ void cdrom_tick(void) {
|
||||
|
||||
uint32_t cdrom_dma_read(void) {
|
||||
uint32_t val = 0;
|
||||
if ((request_reg & CDROM_REQUEST_BFRD) && sector_available && sector_read_pos + 4 <= sector_size) {
|
||||
if ((request_reg & CDROM_REQUEST_BFRD) && sector_available &&
|
||||
sector_read_pos + 4 <= sector_size) {
|
||||
memcpy(&val, sector_buffer + sector_read_pos, 4);
|
||||
sector_read_pos += 4;
|
||||
if (sector_read_pos >= sector_size) {
|
||||
sector_available = 0;
|
||||
request_reg &= (uint8_t)~CDROM_REQUEST_BFRD;
|
||||
}
|
||||
}
|
||||
trace_cdrom('D', 0, val, 4);
|
||||
@@ -1426,6 +1523,19 @@ int cdrom_dma_ready(void) {
|
||||
(sector_read_pos + 4 <= sector_size);
|
||||
}
|
||||
|
||||
uint32_t cdrom_dma_sector_word_count(void) {
|
||||
int size = sector_size;
|
||||
|
||||
/* CD DMA BCR low half zero means transfer one sector-sized payload.
|
||||
* If DMA is armed just before the next sector becomes available, fall
|
||||
* back to the active mode's payload size. */
|
||||
if (size <= 0) {
|
||||
size = (mode_reg & 0x20u) ? WHOLE_SECTOR_SIZE : SECTOR_SIZE;
|
||||
}
|
||||
|
||||
return (uint32_t)((size + 3) / 4);
|
||||
}
|
||||
|
||||
void cdrom_debug_snapshot(CDROMDebugState* out) {
|
||||
if (!out) return;
|
||||
memset(out, 0, sizeof(*out));
|
||||
@@ -1440,6 +1550,9 @@ void cdrom_debug_snapshot(CDROMDebugState* out) {
|
||||
out->seek_sec = seek_sec;
|
||||
out->seek_sect = seek_sect;
|
||||
out->pending_cmd = pending.cmd;
|
||||
out->queued_cmd = queued_cmd.cmd;
|
||||
out->queued_pending = (uint8_t)queued_cmd.pending;
|
||||
out->queued_param_count = (uint8_t)queued_cmd.param_count;
|
||||
out->has_disc = has_disc();
|
||||
out->param_count = param_count;
|
||||
out->response_read = response_read;
|
||||
@@ -1477,6 +1590,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;
|
||||
|
||||
@@ -3440,6 +3440,22 @@ static MmioTraceEntry *s_gp1_trace = NULL;
|
||||
static uint64_t s_gp1_trace_seq = 0;
|
||||
static uint32_t s_gp1_trace_head = 0;
|
||||
|
||||
/* ---- MMIO-READ trace (rtrace) ----
|
||||
* The read counterpart to the MMIO write ring. CPU loads are far more
|
||||
* voluminous than stores, so this ring is RANGE-FILTERED (default-armed to the
|
||||
* CD regs + I_STAT/I_MASK — the device regs the IRQ handler reads to decide
|
||||
* whether to ack). This is THE decisive signal for the verifier-vs-lowering
|
||||
* classification: what value did 0x2458 actually read from 0x1F801803 (CD
|
||||
* IRQ-flag) / I_STAT at exception time. Reuses MmioTraceEntry so each read also
|
||||
* snapshots func/pc/ra + i_stat/i_mask at read time. Always-on for the armed
|
||||
* ranges from init (Rule: ring buffers capture continuously, probes query). */
|
||||
static MmioTraceEntry *s_mmio_rtrace = NULL;
|
||||
static uint64_t s_mmio_rtrace_seq = 0;
|
||||
static uint32_t s_mmio_rtrace_head = 0;
|
||||
#define MMIO_RTRACE_MAX_RANGES 8
|
||||
static struct { uint32_t lo, hi; } s_mmio_rtrace_ranges[MMIO_RTRACE_MAX_RANGES];
|
||||
static int s_mmio_rtrace_range_count = 0;
|
||||
|
||||
/* ---- Platform helpers ---- */
|
||||
static void set_nonblocking(sock_t s)
|
||||
{
|
||||
@@ -6684,6 +6700,47 @@ void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width)
|
||||
}
|
||||
}
|
||||
|
||||
/* MMIO read trace — called from memory.c mmio_read32/16/8 wrappers AFTER the
|
||||
* value is computed. Range-filtered (default CD + I_STAT) so the ring retains a
|
||||
* long window of just the device-register reads of interest, even across a
|
||||
* livelock that polls them. `val` is the value the CPU actually loaded. */
|
||||
void debug_server_trace_mmio_read(uint32_t addr, uint32_t val, uint8_t width)
|
||||
{
|
||||
#ifdef PSX_NO_DEBUG_TOOLS
|
||||
(void)addr; (void)val; (void)width;
|
||||
return;
|
||||
#endif
|
||||
if (!s_mmio_rtrace || s_mmio_rtrace_range_count == 0) return;
|
||||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
int hit = 0;
|
||||
for (int i = 0; i < s_mmio_rtrace_range_count; i++) {
|
||||
if (phys + width > s_mmio_rtrace_ranges[i].lo &&
|
||||
phys < s_mmio_rtrace_ranges[i].hi) { hit = 1; break; }
|
||||
}
|
||||
if (!hit) return;
|
||||
|
||||
MmioTraceEntry *e = &s_mmio_rtrace[s_mmio_rtrace_head];
|
||||
e->seq = s_mmio_rtrace_seq++;
|
||||
e->addr = addr;
|
||||
e->val = val;
|
||||
e->func_addr = g_debug_current_func_addr;
|
||||
e->pc = g_debug_last_store_pc;
|
||||
e->cpu_pc = debug_cpu_ptr ? debug_cpu_ptr->pc : 0;
|
||||
e->ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||||
e->sp = debug_cpu_ptr ? debug_cpu_ptr->gpr[29] : 0;
|
||||
e->a0 = debug_cpu_ptr ? debug_cpu_ptr->gpr[4] : 0;
|
||||
e->a1 = debug_cpu_ptr ? debug_cpu_ptr->gpr[5] : 0;
|
||||
e->a2 = debug_cpu_ptr ? debug_cpu_ptr->gpr[6] : 0;
|
||||
e->a3 = debug_cpu_ptr ? debug_cpu_ptr->gpr[7] : 0;
|
||||
e->sr = debug_cpu_ptr ? debug_cpu_ptr->cop0[12] : 0;
|
||||
e->epc = debug_cpu_ptr ? debug_cpu_ptr->cop0[14] : 0;
|
||||
e->istat = i_stat;
|
||||
e->imask = i_mask;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->width = width;
|
||||
s_mmio_rtrace_head = (s_mmio_rtrace_head + 1) % MMIO_TRACE_CAP;
|
||||
}
|
||||
|
||||
static void handle_wtrace_range(int id, const char *json)
|
||||
{
|
||||
/* Backward compat: sets slot 0, clears all other slots. */
|
||||
@@ -7696,6 +7753,120 @@ static void handle_mmio_clear(int id, const char *json)
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
/* ---- rtrace (MMIO-READ trace) command handlers ----
|
||||
* Field names of the dumped entries are a SUPERSET of the oracle's rtrace_dump
|
||||
* (addr/val/pc/ra/frame/w common; func/cpu_pc/sr/epc/i_stat/i_mask extra) so a
|
||||
* cross-port tool reads both ports with one parser (Rule 16). */
|
||||
static void handle_rtrace_dump(int id, const char *json)
|
||||
{
|
||||
if (!s_mmio_rtrace) { send_err(id, "rtrace not initialized"); return; }
|
||||
|
||||
char addr_str[32];
|
||||
uint32_t filter_addr = 0;
|
||||
int has_filter = json_get_str(json, "addr", addr_str, sizeof(addr_str)) != NULL;
|
||||
if (has_filter) filter_addr = hex_to_u32(addr_str);
|
||||
|
||||
uint64_t total = s_mmio_rtrace_seq;
|
||||
uint32_t avail = (total < MMIO_TRACE_CAP) ? (uint32_t)total : MMIO_TRACE_CAP;
|
||||
uint32_t start = (total < MMIO_TRACE_CAP) ? 0 : s_mmio_rtrace_head;
|
||||
|
||||
int max_out = json_get_int(json, "count", 65536);
|
||||
if (max_out < 1) max_out = 1;
|
||||
if (max_out > (int)MMIO_TRACE_CAP) max_out = (int)MMIO_TRACE_CAP;
|
||||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||||
|
||||
const uint32_t MAX_OUT = (uint32_t)max_out;
|
||||
size_t BUF_SZ = 256u + (size_t)MAX_OUT * 512u;
|
||||
if (BUF_SZ > (size_t)128 * 1024 * 1024) BUF_SZ = (size_t)128 * 1024 * 1024;
|
||||
char *buf = (char *)malloc(BUF_SZ);
|
||||
if (!buf) { send_err(id, "oom"); return; }
|
||||
size_t pos = 0;
|
||||
uint32_t emitted = 0;
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
|
||||
id, (unsigned long long)total, avail);
|
||||
for (uint32_t i = 0; i < avail && emitted < MAX_OUT && pos < BUF_SZ - 256; i++) {
|
||||
uint32_t idx;
|
||||
if (newest_first) {
|
||||
uint32_t newest = (s_mmio_rtrace_head + MMIO_TRACE_CAP - 1u) % MMIO_TRACE_CAP;
|
||||
idx = (newest + MMIO_TRACE_CAP - (i % MMIO_TRACE_CAP)) % MMIO_TRACE_CAP;
|
||||
} else {
|
||||
idx = (start + i) % MMIO_TRACE_CAP;
|
||||
}
|
||||
MmioTraceEntry *e = &s_mmio_rtrace[idx];
|
||||
if (has_filter && e->addr != filter_addr) continue;
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"val\":\"0x%08X\","
|
||||
"\"func\":\"0x%08X\",\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\","
|
||||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||||
"\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\","
|
||||
"\"frame\":%u,\"w\":%u}",
|
||||
(emitted == 0) ? "" : ",",
|
||||
(unsigned long long)e->seq,
|
||||
e->addr, e->val, e->func_addr, e->pc, e->cpu_pc,
|
||||
e->ra, e->sp, e->a0, e->a1, e->a2, e->a3,
|
||||
e->sr, e->epc, e->istat, e->imask,
|
||||
e->frame, (unsigned)e->width);
|
||||
emitted++;
|
||||
}
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%u}", emitted);
|
||||
debug_server_send_line(buf);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void handle_rtrace_clear(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
s_mmio_rtrace_seq = 0;
|
||||
s_mmio_rtrace_head = 0;
|
||||
if (s_mmio_rtrace) memset(s_mmio_rtrace, 0, (size_t)MMIO_TRACE_CAP * sizeof(MmioTraceEntry));
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static void handle_rtrace_arm(int id, const char *json)
|
||||
{
|
||||
if (s_mmio_rtrace_range_count >= MMIO_RTRACE_MAX_RANGES) {
|
||||
send_err(id, "max ranges reached"); return;
|
||||
}
|
||||
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; }
|
||||
int slot = s_mmio_rtrace_range_count++;
|
||||
s_mmio_rtrace_ranges[slot].lo = hex_to_u32(lo_str) & 0x1FFFFFFFu;
|
||||
s_mmio_rtrace_ranges[slot].hi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"slot\":%d,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
|
||||
id, slot, s_mmio_rtrace_ranges[slot].lo, s_mmio_rtrace_ranges[slot].hi);
|
||||
}
|
||||
|
||||
static void handle_rtrace_ranges(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"count\":%d,\"ranges\":[",
|
||||
id, s_mmio_rtrace_range_count);
|
||||
for (int i = 0; i < s_mmio_rtrace_range_count; i++) {
|
||||
if (i > 0) send_fmt(",");
|
||||
send_fmt("{\"slot\":%d,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
|
||||
i, s_mmio_rtrace_ranges[i].lo, s_mmio_rtrace_ranges[i].hi);
|
||||
}
|
||||
send_fmt("]}");
|
||||
}
|
||||
|
||||
static void handle_rtrace_stats(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
uint64_t total = s_mmio_rtrace_seq;
|
||||
uint64_t oldest = (total <= (uint64_t)MMIO_TRACE_CAP) ? 0 : total - (uint64_t)MMIO_TRACE_CAP;
|
||||
uint64_t newest = (total > 0) ? total - 1 : 0;
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"capacity\":%d,"
|
||||
"\"oldest_seq\":%llu,\"newest_seq\":%llu,\"ranges\":%d}",
|
||||
id, (unsigned long long)total, (int)MMIO_TRACE_CAP,
|
||||
(unsigned long long)oldest, (unsigned long long)newest,
|
||||
s_mmio_rtrace_range_count);
|
||||
}
|
||||
|
||||
/* gp1_dump — dump the dedicated GP1 display-control ring. Optional
|
||||
* `frame_lo`/`frame_hi` filter (applied server-side over the FULL ring,
|
||||
* like wtrace_dump's address filter), optional `count` (default 4096),
|
||||
@@ -9323,6 +9494,11 @@ static const CmdEntry s_commands[] = {
|
||||
{ "freeze_check", handle_freeze_check },
|
||||
{ "mmio_dump", handle_mmio_dump },
|
||||
{ "mmio_clear", handle_mmio_clear },
|
||||
{ "rtrace_dump", handle_rtrace_dump },
|
||||
{ "rtrace_clear", handle_rtrace_clear },
|
||||
{ "rtrace_arm", handle_rtrace_arm },
|
||||
{ "rtrace_ranges", handle_rtrace_ranges },
|
||||
{ "rtrace_stats", handle_rtrace_stats },
|
||||
{ "gp1_dump", handle_gp1_dump },
|
||||
{ "mdec_state", handle_mdec_state },
|
||||
{ "mdec_trace", handle_mdec_trace },
|
||||
@@ -9767,6 +9943,19 @@ void debug_server_init(int port)
|
||||
s_gp1_trace_seq = 0;
|
||||
s_gp1_trace_head = 0;
|
||||
|
||||
/* MMIO-READ trace ring (range-filtered). Default-armed from init to the CD
|
||||
* regs + I_STAT/I_MASK so the IRQ handler's reads of the CD IRQ-flag and
|
||||
* interrupt status are captured continuously (Rule: always-on ring; probes
|
||||
* query the window). Reuses MMIO_TRACE_CAP entries (~21 MB). */
|
||||
if (!s_mmio_rtrace) {
|
||||
s_mmio_rtrace = (MmioTraceEntry *)calloc(MMIO_TRACE_CAP, sizeof(MmioTraceEntry));
|
||||
}
|
||||
s_mmio_rtrace_seq = 0;
|
||||
s_mmio_rtrace_head = 0;
|
||||
s_mmio_rtrace_ranges[0].lo = 0x1F801800u; s_mmio_rtrace_ranges[0].hi = 0x1F801804u; /* CD regs (idx/IRQ-flag) */
|
||||
s_mmio_rtrace_ranges[1].lo = 0x1F801070u; s_mmio_rtrace_ranges[1].hi = 0x1F801078u; /* I_STAT / I_MASK */
|
||||
s_mmio_rtrace_range_count = 2;
|
||||
|
||||
memset(s_watchpoints, 0, sizeof(s_watchpoints));
|
||||
memset(s_snapshot_addrs, 0, sizeof(s_snapshot_addrs));
|
||||
memset(s_snapshot_active, 0, sizeof(s_snapshot_active));
|
||||
|
||||
+168
-23
@@ -107,11 +107,17 @@ static DMADelayedComplete delayed_complete[7];
|
||||
static uint32_t dpcr; /* 0x1F8010F0: DMA control (enable bits) */
|
||||
static uint32_t dicr; /* 0x1F8010F4: DMA interrupt control */
|
||||
|
||||
#define DICR_WRITE_MASK 0x00FF803Fu
|
||||
#define DICR_WRITE_MASK 0x00FF807Fu
|
||||
#define DICR_RESET_MASK 0x7F000000u
|
||||
|
||||
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);
|
||||
|
||||
static void trace_dma(uint32_t kind, int ch, uint32_t total_words,
|
||||
uint32_t dicr_before, uint32_t i_stat_before) {
|
||||
@@ -121,12 +127,39 @@ static void trace_dma(uint32_t kind, int ch, uint32_t total_words,
|
||||
e->kind = kind;
|
||||
e->channel = (uint32_t)ch;
|
||||
e->total_words = total_words;
|
||||
e->addr = 0;
|
||||
e->val = 0;
|
||||
e->mask = 0;
|
||||
e->madr = (ch >= 0 && ch < 7) ? channels[ch].madr : 0;
|
||||
e->bcr = (ch >= 0 && ch < 7) ? channels[ch].bcr : 0;
|
||||
e->chcr = (ch >= 0 && ch < 7) ? channels[ch].chcr : 0;
|
||||
e->dpcr = dpcr;
|
||||
e->dicr_before = dicr_before;
|
||||
e->dicr_after = dicr;
|
||||
e->dicr_before = dicr_read_value(dicr_before);
|
||||
e->dicr_after = dicr_read_value(dicr);
|
||||
e->i_stat_before = i_stat_before;
|
||||
e->i_stat_after = i_stat;
|
||||
e->func = g_debug_current_func_addr;
|
||||
e->pc = g_debug_last_store_pc;
|
||||
}
|
||||
|
||||
static void trace_dma_reg_write(uint32_t addr, uint32_t val, uint32_t mask,
|
||||
uint32_t dicr_before,
|
||||
uint32_t i_stat_before) {
|
||||
DMATraceEntry *e = &dma_trace[dma_trace_seq % DMA_TRACE_CAP];
|
||||
e->seq = dma_trace_seq++;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->kind = 'W';
|
||||
e->channel = 0xFFFFFFFFu;
|
||||
e->total_words = 0;
|
||||
e->addr = addr;
|
||||
e->val = val;
|
||||
e->mask = mask;
|
||||
e->madr = 0;
|
||||
e->bcr = 0;
|
||||
e->chcr = 0;
|
||||
e->dpcr = dpcr;
|
||||
e->dicr_before = dicr_read_value(dicr_before);
|
||||
e->dicr_after = dicr_read_value(dicr);
|
||||
e->i_stat_before = i_stat_before;
|
||||
e->i_stat_after = i_stat;
|
||||
e->func = g_debug_current_func_addr;
|
||||
@@ -141,15 +174,95 @@ static void update_master_irq(void) {
|
||||
*
|
||||
* We store bits 0-30 in dicr and compute bit 31 on read.
|
||||
*
|
||||
* I_STAT bit 3 is set by direct assertion in complete_transfer(),
|
||||
* not here. The BIOS shell DMA-IRQ handler at 0x80058628 only
|
||||
* clears DICR bit 27 (ch3 flag) — any other channel flag (e.g. ch2
|
||||
* after a GPU DMA) stays high. If this routine re-asserted
|
||||
* i_stat |= (1<<3) on every DICR read or write, the BIOS handler's
|
||||
* "lw $v0, 0($v1)" of DICR right after writing 0xFFFFFFF7 to I_STAT
|
||||
* would put bit 3 back, and the kernel would loop forever in the
|
||||
* exception handler. Discless BIOS boot (memcard / CD player
|
||||
* screen) is the case that exhibits this. */
|
||||
* I_STAT bit 3 is set on the aggregate DICR bit31 0->1 transition.
|
||||
* Per-channel DICR flags are latched by complete_transfer() only
|
||||
* when both master IRQ and that channel IRQ are enabled. */
|
||||
}
|
||||
|
||||
static uint32_t dicr_master_flag(uint32_t v) {
|
||||
return ((v & (1u << 15)) ||
|
||||
((v & (1u << 23)) && (v & DICR_RESET_MASK))) ? (1u << 31) : 0;
|
||||
}
|
||||
|
||||
static uint32_t dicr_read_value(uint32_t v) {
|
||||
return (v & ~(1u << 31)) | dicr_master_flag(v);
|
||||
}
|
||||
|
||||
static void raise_dma_irq_on_master_edge(uint32_t before) {
|
||||
/* IRQ3 is latched only when DICR's aggregate master flag transitions
|
||||
* from 0 to 1. Pending channel flags keep bit 31 high, but do not
|
||||
* continuously re-latch I_STAT after software acknowledges IRQ3. */
|
||||
if (!dicr_master_flag(before) && dicr_master_flag(dicr)) {
|
||||
i_stat |= (1u << 3);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -158,7 +271,12 @@ static int channel_enabled(int ch) {
|
||||
return (dpcr >> (ch * 4 + 3)) & 1;
|
||||
}
|
||||
|
||||
static int channel_irq_enabled(int ch) {
|
||||
static int channel_irq_flag_armed(int ch) {
|
||||
/* DICR channel completion flags (24+n) are latched only when both
|
||||
* the per-channel interrupt bit and the master DMA interrupt bit are
|
||||
* enabled at completion time. Old flags still contribute to bit31
|
||||
* until acknowledged, but masked/master-disabled completions do not
|
||||
* create new stale flags. */
|
||||
return ((dicr >> (16 + ch)) & 1u) && ((dicr >> 23) & 1u);
|
||||
}
|
||||
|
||||
@@ -168,6 +286,10 @@ static uint32_t transfer_word_count(int ch) {
|
||||
uint32_t block_size = channels[ch].bcr & 0xFFFFu;
|
||||
uint32_t block_count = (channels[ch].bcr >> 16) & 0xFFFFu;
|
||||
|
||||
if (ch == 3 && block_size == 0) {
|
||||
return cdrom_dma_sector_word_count();
|
||||
}
|
||||
|
||||
if (sync_mode == 1) {
|
||||
if (block_size == 0) block_size = 0x10000u;
|
||||
if (block_count == 0) block_count = 1u;
|
||||
@@ -206,12 +328,9 @@ static void complete_transfer(int ch) {
|
||||
uint32_t dicr_before = dicr;
|
||||
uint32_t i_stat_before = i_stat;
|
||||
channels[ch].chcr &= ~((1u << 24) | (1u << 28));
|
||||
if (channel_irq_enabled(ch)) {
|
||||
if (channel_irq_flag_armed(ch)) {
|
||||
dicr |= (1u << (24 + ch));
|
||||
update_master_irq();
|
||||
/* Edge-trigger I_STAT bit 3 once per completion. See comment in
|
||||
* update_master_irq() for why this is not done there. */
|
||||
i_stat |= (1u << 3);
|
||||
raise_dma_irq_on_master_edge(dicr_before);
|
||||
}
|
||||
trace_dma('C', ch, 0, dicr_before, i_stat_before);
|
||||
event_ring_record_aux(EV_DMA_DONE, (uint8_t)ch, channels[ch].chcr);
|
||||
@@ -229,6 +348,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;
|
||||
@@ -310,14 +430,17 @@ static void start_async_cdrom_transfer(void) {
|
||||
a->remaining_words = a->total_words;
|
||||
a->cycles_accum = 0;
|
||||
a->start_addr = channels[3].madr & 0x1FFFFCu;
|
||||
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);
|
||||
DMAAsyncChannel *a = &cdrom_async;
|
||||
uint32_t step = (channels[3].chcr >> 1) & 1u;
|
||||
uint32_t load_start = a->start_addr;
|
||||
@@ -662,15 +785,21 @@ static void try_execute(int ch) {
|
||||
|
||||
/* ---- Public interface ---- */
|
||||
|
||||
uint32_t dma_get_dicr(void) { update_master_irq(); return dicr; }
|
||||
uint32_t dma_get_dicr(void) { return dicr_read_value(dicr); }
|
||||
uint32_t dma_get_dpcr(void) { return dpcr; }
|
||||
int dma_cdrom_transfer_active(void) {
|
||||
return cdrom_async.active &&
|
||||
((channels[3].chcr >> 24) & 1u) &&
|
||||
channel_enabled(3) &&
|
||||
((channels[3].chcr & 1u) == 0);
|
||||
}
|
||||
|
||||
void dma_advance(uint32_t cycles) {
|
||||
if (cycles == 0) return;
|
||||
advance_mdec_channel(0, cycles);
|
||||
advance_mdec_channel(1, cycles);
|
||||
DMAAsyncChannel *a = &cdrom_async;
|
||||
if (a->active && ((channels[3].chcr >> 24) & 1u) && channel_enabled(3)) {
|
||||
if (dma_cdrom_transfer_active()) {
|
||||
uint32_t chcr = channels[3].chcr;
|
||||
uint32_t direction = chcr & 1u;
|
||||
uint32_t step = (chcr >> 1) & 1u;
|
||||
@@ -692,6 +821,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--;
|
||||
@@ -719,6 +849,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) {
|
||||
@@ -726,8 +857,7 @@ uint32_t dma_read(uint32_t addr) {
|
||||
if (addr == 0x1F8010F0u) return dpcr;
|
||||
/* DICR */
|
||||
if (addr == 0x1F8010F4u) {
|
||||
update_master_irq();
|
||||
return dicr;
|
||||
return dma_get_dicr();
|
||||
}
|
||||
|
||||
/* Per-channel registers: 0x1F801080 + ch*0x10 + offset */
|
||||
@@ -770,11 +900,14 @@ void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) {
|
||||
/* Bit 23: master IRQ enable */
|
||||
/* Bits 24-30: IRQ flags, write 1 to clear */
|
||||
/* Bit 31: master flag, read-only (computed) */
|
||||
uint32_t dicr_before = dicr;
|
||||
uint32_t i_stat_before = i_stat;
|
||||
uint32_t write_mask = DICR_WRITE_MASK & mask;
|
||||
uint32_t reset_mask = DICR_RESET_MASK & mask;
|
||||
dicr = (dicr & ~write_mask) | (val & write_mask);
|
||||
dicr &= ~(val & reset_mask);
|
||||
update_master_irq();
|
||||
raise_dma_irq_on_master_edge(dicr_before);
|
||||
trace_dma_reg_write(addr, val, mask, dicr_before, i_stat_before);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -832,6 +965,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;
|
||||
|
||||
@@ -197,13 +197,33 @@ std::string ISOReader::GetVolumeID() const {
|
||||
return volume_id_;
|
||||
}
|
||||
|
||||
std::string ISOReader::GetBinPath() const {
|
||||
return bin_path_;
|
||||
}
|
||||
|
||||
RootDirectoryInfo ISOReader::GetRootDirectory() const {
|
||||
return root_dir_;
|
||||
}
|
||||
std::string ISOReader::GetBinPath() const {
|
||||
return bin_path_;
|
||||
}
|
||||
|
||||
uint32_t ISOReader::GetSectorCount() {
|
||||
if (!is_open_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
file_.clear();
|
||||
file_.seekg(0, std::ios::end);
|
||||
std::streampos file_size = file_.tellg();
|
||||
file_.clear();
|
||||
if (file_size <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint64_t size = static_cast<uint64_t>(file_size);
|
||||
if ((size % RAW_SECTOR_SIZE) == 0) {
|
||||
return static_cast<uint32_t>(size / RAW_SECTOR_SIZE);
|
||||
}
|
||||
return static_cast<uint32_t>(size / SECTOR_SIZE);
|
||||
}
|
||||
|
||||
RootDirectoryInfo ISOReader::GetRootDirectory() const {
|
||||
return root_dir_;
|
||||
}
|
||||
|
||||
uint32_t ISOReader::Read733(const uint8_t* data) const {
|
||||
// Read little-endian half of both-endian 32-bit value
|
||||
|
||||
@@ -31,6 +31,12 @@ int iso_read_raw_sector(void* handle, uint32_t lba, uint8_t* buffer, int size) {
|
||||
return reader->ReadRawSector(lba, buffer) ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t iso_sector_count(void* handle) {
|
||||
if (!handle) return 0;
|
||||
auto* reader = static_cast<PS1::ISOReader*>(handle);
|
||||
return reader->GetSectorCount();
|
||||
}
|
||||
|
||||
void iso_close(void* handle) {
|
||||
if (!handle) return;
|
||||
auto* reader = static_cast<PS1::ISOReader*>(handle);
|
||||
|
||||
+71
-10
@@ -206,7 +206,7 @@ typedef struct {
|
||||
uint32_t new_mask;
|
||||
uint32_t caller; /* g_debug_current_func_addr */
|
||||
uint32_t store_pc; /* g_debug_last_store_pc — exact PC of the SW/SH */
|
||||
uint8_t width; /* 16 or 32 */
|
||||
uint8_t width; /* 8, 16, or 32 */
|
||||
uint8_t bit7_set; /* 1 if this write SET bit 7 */
|
||||
uint8_t bit7_clear; /* 1 if this write CLEARED bit 7 */
|
||||
uint8_t in_exc;
|
||||
@@ -236,6 +236,17 @@ static void imask_trace_record(uint32_t old_val, uint32_t new_val, uint8_t width
|
||||
imask_trace_count++;
|
||||
}
|
||||
|
||||
static void interrupt_write_stat_masked(uint32_t val, uint32_t mask) {
|
||||
uint32_t ack_mask = mask & 0x7FFu;
|
||||
i_stat = (i_stat & ~ack_mask) | (i_stat & val & ack_mask);
|
||||
}
|
||||
|
||||
static void interrupt_write_mask_masked(uint32_t val, uint32_t mask, uint8_t width) {
|
||||
uint32_t old = i_mask;
|
||||
i_mask = ((i_mask & ~mask) | (val & mask)) & 0x7FFu;
|
||||
imask_trace_record(old, i_mask, width);
|
||||
}
|
||||
|
||||
/* Getters for debug server */
|
||||
int memory_get_imask_bit7_set_count(void) { return imask_bit7_set_count; }
|
||||
int memory_get_imask_bit7_clear_count(void) { return imask_bit7_clear_count; }
|
||||
@@ -249,6 +260,9 @@ const ImaskTraceEntry *memory_get_imask_trace(int *idx_out, int *count_out) {
|
||||
extern void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||||
uint32_t new_val, uint8_t width);
|
||||
extern void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width);
|
||||
extern void debug_server_trace_mmio_read(uint32_t addr, uint32_t val, uint8_t width);
|
||||
extern void debug_server_trace_entryint_write(uint32_t phys, uint32_t old_val,
|
||||
uint32_t new_val, uint8_t width);
|
||||
extern CPUState *debug_cpu_ptr;
|
||||
extern uint32_t g_debug_last_store_pc;
|
||||
|
||||
@@ -325,7 +339,7 @@ static void unmapped_fatal(uint32_t vaddr, uint32_t phys, const char* op) {
|
||||
|
||||
/* --- MMIO read/write helpers --- */
|
||||
|
||||
static uint32_t mmio_read32(uint32_t addr) {
|
||||
static uint32_t mmio_read32_impl(uint32_t addr) {
|
||||
SHADOW_NOTE_MMIO();
|
||||
/* Memory control: 0x1F801000..0x1F801020 */
|
||||
if (addr >= 0x1F801000u && addr <= 0x1F80103Cu) {
|
||||
@@ -371,6 +385,15 @@ static uint32_t mmio_read32(uint32_t addr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Thin wrapper: record the loaded value into the MMIO-READ trace ring AFTER the
|
||||
* single (side-effecting) read. Callers use mmio_read32; the body is _impl, so
|
||||
* the device read executes exactly once. */
|
||||
static uint32_t mmio_read32(uint32_t addr) {
|
||||
uint32_t v = mmio_read32_impl(addr);
|
||||
debug_server_trace_mmio_read(addr, v, 4);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void mmio_write32(uint32_t addr, uint32_t val) {
|
||||
SHADOW_NOTE_MMIO();
|
||||
debug_server_trace_mmio_write(addr, val, 4);
|
||||
@@ -390,8 +413,8 @@ static void mmio_write32(uint32_t addr, uint32_t val) {
|
||||
return;
|
||||
}
|
||||
/* Interrupts: 0x1F801070, 0x1F801074 */
|
||||
if (addr == 0x1F801070u) { i_stat &= val; return; } /* AND-acknowledge */
|
||||
if (addr == 0x1F801074u) { uint32_t old = i_mask; i_mask = val & 0x7FFu; imask_trace_record(old, i_mask, 32); return; }
|
||||
if (addr == 0x1F801070u) { interrupt_write_stat_masked(val, 0xFFFFFFFFu); return; }
|
||||
if (addr == 0x1F801074u) { interrupt_write_mask_masked(val, 0xFFFFFFFFu, 32); return; }
|
||||
/* DMA: 0x1F801080..0x1F8010FF */
|
||||
if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) {
|
||||
dma_write(addr, val);
|
||||
@@ -432,15 +455,22 @@ static void mmio_write32(uint32_t addr, uint32_t val) {
|
||||
mmio_fatal(addr, addr, "WRITE32");
|
||||
}
|
||||
|
||||
static uint16_t mmio_read16(uint32_t addr) {
|
||||
static uint16_t mmio_read16_impl(uint32_t addr) {
|
||||
SHADOW_NOTE_MMIO();
|
||||
/* SIO: 0x1F801040..0x1F80105F */
|
||||
if (addr >= 0x1F801040u && addr <= 0x1F80105Fu) {
|
||||
return (uint16_t)sio_read(addr);
|
||||
}
|
||||
/* Interrupts */
|
||||
if (addr == 0x1F801070u) { sio_tick(0); return (uint16_t)i_stat; }
|
||||
if (addr == 0x1F801074u) return (uint16_t)i_mask;
|
||||
if (addr >= 0x1F801070u && addr <= 0x1F801072u) {
|
||||
sio_tick(0);
|
||||
uint32_t shift = (addr & 2u) ? 16u : 0u;
|
||||
return (uint16_t)(i_stat >> shift);
|
||||
}
|
||||
if (addr >= 0x1F801074u && addr <= 0x1F801076u) {
|
||||
uint32_t shift = (addr & 2u) ? 16u : 0u;
|
||||
return (uint16_t)(i_mask >> shift);
|
||||
}
|
||||
/* Timers: 0x1F801100..0x1F80112F */
|
||||
if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) {
|
||||
return (uint16_t)timers_read(addr);
|
||||
@@ -463,6 +493,12 @@ static uint16_t mmio_read16(uint32_t addr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint16_t mmio_read16(uint32_t addr) {
|
||||
uint16_t v = mmio_read16_impl(addr);
|
||||
debug_server_trace_mmio_read(addr, (uint32_t)v, 2);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void mmio_write16(uint32_t addr, uint16_t val) {
|
||||
SHADOW_NOTE_MMIO();
|
||||
debug_server_trace_mmio_write(addr, (uint32_t)val, 2);
|
||||
@@ -472,8 +508,16 @@ static void mmio_write16(uint32_t addr, uint16_t val) {
|
||||
return;
|
||||
}
|
||||
/* Interrupts */
|
||||
if (addr == 0x1F801070u) { i_stat &= val; return; }
|
||||
if (addr == 0x1F801074u) { uint32_t old = i_mask; i_mask = val & 0x7FFu; imask_trace_record(old, i_mask, 16); return; }
|
||||
if (addr >= 0x1F801070u && addr <= 0x1F801072u) {
|
||||
uint32_t shift = (addr & 2u) ? 16u : 0u;
|
||||
interrupt_write_stat_masked((uint32_t)val << shift, 0xFFFFu << shift);
|
||||
return;
|
||||
}
|
||||
if (addr >= 0x1F801074u && addr <= 0x1F801076u) {
|
||||
uint32_t shift = (addr & 2u) ? 16u : 0u;
|
||||
interrupt_write_mask_masked((uint32_t)val << shift, 0xFFFFu << shift, 16);
|
||||
return;
|
||||
}
|
||||
/* Timers: 0x1F801100..0x1F80112F */
|
||||
if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) {
|
||||
timers_write(addr, val);
|
||||
@@ -506,7 +550,7 @@ static void mmio_write16(uint32_t addr, uint16_t val) {
|
||||
mmio_fatal(addr, addr, "WRITE16");
|
||||
}
|
||||
|
||||
static uint8_t mmio_read8(uint32_t addr) {
|
||||
static uint8_t mmio_read8_impl(uint32_t addr) {
|
||||
SHADOW_NOTE_MMIO();
|
||||
/* Interrupts: 0x1F801070..0x1F801077 (I_STAT, I_MASK) */
|
||||
if (addr >= 0x1F801070u && addr <= 0x1F801077u) {
|
||||
@@ -543,9 +587,26 @@ static uint8_t mmio_read8(uint32_t addr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t mmio_read8(uint32_t addr) {
|
||||
uint8_t v = mmio_read8_impl(addr);
|
||||
debug_server_trace_mmio_read(addr, (uint32_t)v, 1);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void mmio_write8(uint32_t addr, uint8_t val) {
|
||||
SHADOW_NOTE_MMIO();
|
||||
debug_server_trace_mmio_write(addr, (uint32_t)val, 1);
|
||||
/* Interrupts: partial stores affect only the addressed byte lane. */
|
||||
if (addr >= 0x1F801070u && addr <= 0x1F801073u) {
|
||||
uint32_t shift = 8u * (addr & 3u);
|
||||
interrupt_write_stat_masked((uint32_t)val << shift, 0xFFu << shift);
|
||||
return;
|
||||
}
|
||||
if (addr >= 0x1F801074u && addr <= 0x1F801077u) {
|
||||
uint32_t shift = 8u * (addr & 3u);
|
||||
interrupt_write_mask_masked((uint32_t)val << shift, 0xFFu << shift, 8);
|
||||
return;
|
||||
}
|
||||
/* SIO: 0x1F801040..0x1F80105F */
|
||||
if (addr >= 0x1F801040u && addr <= 0x1F80105Fu) {
|
||||
sio_write(addr & ~3u, (uint32_t)val);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -42,6 +42,30 @@ uint32_t g_debug_current_func_addr = 0;
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; }
|
||||
uint32_t memory_get_sr(void) { return 0; }
|
||||
void debug_server_poll(void) {}
|
||||
void debug_server_log_sio_write(uint32_t addr, uint32_t value, uint8_t width) {
|
||||
(void)addr; (void)value; (void)width;
|
||||
}
|
||||
void starvation_ring_record(uint8_t kind, uint8_t tx, uint8_t rx,
|
||||
uint16_t ctrl, uint16_t stat, uint8_t active_device,
|
||||
uint8_t selected_slot, uint16_t mc_state,
|
||||
uint8_t mc_cmd, uint16_t mc_sector,
|
||||
uint8_t mc_data_idx, uint32_t func_addr) {
|
||||
(void)kind; (void)tx; (void)rx; (void)ctrl; (void)stat;
|
||||
(void)active_device; (void)selected_slot; (void)mc_state;
|
||||
(void)mc_cmd; (void)mc_sector; (void)mc_data_idx; (void)func_addr;
|
||||
}
|
||||
void card_read_summary_record(uint8_t slot, uint8_t cmd, uint16_t sector,
|
||||
uint8_t end, uint8_t checksum,
|
||||
uint8_t data0, uint8_t data1,
|
||||
uint32_t func_addr) {
|
||||
(void)slot; (void)cmd; (void)sector; (void)end; (void)checksum;
|
||||
(void)data0; (void)data1; (void)func_addr;
|
||||
}
|
||||
void card_data_writes_arm(uint8_t value, uint16_t mc_state,
|
||||
uint8_t mc_data_idx, uint8_t slot) {
|
||||
(void)value; (void)mc_state; (void)mc_data_idx; (void)slot;
|
||||
}
|
||||
|
||||
/* ---- Test harness ---- */
|
||||
|
||||
@@ -81,12 +105,19 @@ static uint8_t card_xchg(uint8_t tx, int slot) {
|
||||
| (slot ? CTRL_SLOT : 0);
|
||||
sio_write(SIO_CTRL, ctrl);
|
||||
sio_write(SIO_TX_DATA, tx);
|
||||
sio_tick(2000);
|
||||
uint8_t rx = (uint8_t)sio_read(SIO_RX_DATA);
|
||||
/* Acknowledge IRQ between bytes (BIOS pattern) */
|
||||
sio_write(SIO_CTRL, ctrl | CTRL_ACK);
|
||||
return rx;
|
||||
}
|
||||
|
||||
static void card_deselect(int slot) {
|
||||
uint16_t ctrl = CTRL_TX_EN | CTRL_ACK_IRQ_EN | (slot ? CTRL_SLOT : 0);
|
||||
sio_write(SIO_CTRL, ctrl);
|
||||
sio_tick(2000);
|
||||
}
|
||||
|
||||
/* Drive a complete card READ for the given sector and capture every RX byte.
|
||||
* Returns count of RX bytes captured (137 for a successful read). */
|
||||
static int run_card_read(int slot, uint16_t sector, uint8_t out_rx[140]) {
|
||||
@@ -113,6 +144,30 @@ static int run_card_read(int slot, uint16_t sector, uint8_t out_rx[140]) {
|
||||
return n;
|
||||
}
|
||||
|
||||
static int run_card_write(int slot, uint16_t sector, const uint8_t data[128],
|
||||
uint8_t out_rx[140]) {
|
||||
int n = 0;
|
||||
uint8_t msb = (uint8_t)(sector >> 8);
|
||||
uint8_t lsb = (uint8_t)(sector & 0xFF);
|
||||
uint8_t chk = msb ^ lsb;
|
||||
for (int i = 0; i < 128; i++) chk ^= data[i];
|
||||
|
||||
out_rx[n++] = card_xchg(0x81, slot); /* idx 0: prefix */
|
||||
out_rx[n++] = card_xchg(0x57, slot); /* idx 1: cmd -> FLAG */
|
||||
out_rx[n++] = card_xchg(0x00, slot); /* idx 2: -> 5A */
|
||||
out_rx[n++] = card_xchg(0x00, slot); /* idx 3: -> 5D */
|
||||
out_rx[n++] = card_xchg(msb, slot); /* idx 4: address MSB */
|
||||
out_rx[n++] = card_xchg(lsb, slot); /* idx 5: address LSB */
|
||||
for (int i = 0; i < 128; i++) {
|
||||
out_rx[n++] = card_xchg(data[i], slot);
|
||||
}
|
||||
out_rx[n++] = card_xchg(chk, slot);
|
||||
out_rx[n++] = card_xchg(0x00, slot); /* ack 1 */
|
||||
out_rx[n++] = card_xchg(0x00, slot); /* ack 2 */
|
||||
out_rx[n++] = card_xchg(0x00, slot); /* end byte */
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Pin idx 8/9 explicitly (MSB/LSB echo) for non-zero sectors. */
|
||||
static void test_msb_lsb_echo(void) {
|
||||
sio_init();
|
||||
@@ -214,6 +269,27 @@ static void test_card_read_sector_0(void) {
|
||||
EXPECT_EQ("read.idx[138]_checksum", expected_chk, rx[138]);
|
||||
/* idx 139: end byte */
|
||||
EXPECT_EQ("read.idx[139]_end", 0x47, rx[139]);
|
||||
|
||||
EXPECT_EQ("read2.prefix", 0xFF, card_xchg(0x81, 0));
|
||||
EXPECT_EQ("read2.flag_still_set", 0x08, card_xchg(0x52, 0));
|
||||
}
|
||||
|
||||
static void test_flag_clears_on_write(void) {
|
||||
sio_init();
|
||||
memcard_init("test_dir");
|
||||
|
||||
uint8_t data[128];
|
||||
memset(data, 0x00, sizeof(data));
|
||||
data[0] = 'M';
|
||||
data[1] = 'C';
|
||||
uint8_t rx[140];
|
||||
int n = run_card_write(0, 0x003F, data, rx);
|
||||
EXPECT_EQ("write.count", 138, n);
|
||||
EXPECT_EQ("write.idx[1]_initial_FLAG", 0x08, rx[1]);
|
||||
EXPECT_EQ("write.end", 0x47, rx[137]);
|
||||
|
||||
EXPECT_EQ("post_write.prefix", 0xFF, card_xchg(0x81, 0));
|
||||
EXPECT_EQ("post_write.flag_clear", 0x00, card_xchg(0x52, 0));
|
||||
}
|
||||
|
||||
/* Test 3: pad-poll mid-card-read — does our SIO model preserve card state? */
|
||||
@@ -232,6 +308,7 @@ static void test_pad_poll_during_card_read(void) {
|
||||
EXPECT_EQ("pad_intr.0x52", 0x08, card_xchg(0x52, 0));
|
||||
EXPECT_EQ("pad_intr.id1", 0x5A, card_xchg(0x00, 0));
|
||||
EXPECT_EQ("pad_intr.id2", 0x5D, card_xchg(0x00, 0));
|
||||
card_deselect(0);
|
||||
|
||||
/* Inject pad poll between card bytes */
|
||||
(void)card_xchg(0x01, 0); /* pad select — by spec, breaks card protocol */
|
||||
@@ -241,6 +318,7 @@ static void test_pad_poll_during_card_read(void) {
|
||||
(void)card_xchg(0x00, 0);
|
||||
|
||||
/* Resume card protocol by issuing 0x81 again — must restart cleanly */
|
||||
card_deselect(0);
|
||||
EXPECT_EQ("pad_intr.restart_0x81", 0xFF, card_xchg(0x81, 0));
|
||||
EXPECT_EQ("pad_intr.restart_0x52", 0x08, card_xchg(0x52, 0));
|
||||
/* Note: FLAG should still be 0x08 since no successful WRITE has occurred */
|
||||
@@ -254,11 +332,13 @@ static void test_per_slot_state(void) {
|
||||
/* Start protocol on slot 0 */
|
||||
EXPECT_EQ("perslot.s0_0x81", 0xFF, card_xchg(0x81, 0));
|
||||
EXPECT_EQ("perslot.s0_0x52", 0x08, card_xchg(0x52, 0));
|
||||
card_deselect(0);
|
||||
/* Switch to slot 1 mid-protocol — start fresh on slot 1 */
|
||||
EXPECT_EQ("perslot.s1_0x81", 0xFF, card_xchg(0x81, 1));
|
||||
/* Slot 1 might have no card (test_dir/card2.mcd doesn't exist) — accept either */
|
||||
|
||||
/* Switch back to slot 0 — should restart fresh (real card forgets state) */
|
||||
card_deselect(1);
|
||||
EXPECT_EQ("perslot.s0_resume_0x81", 0xFF, card_xchg(0x81, 0));
|
||||
EXPECT_EQ("perslot.s0_resume_0x52", 0x08, card_xchg(0x52, 0));
|
||||
}
|
||||
@@ -289,6 +369,12 @@ int main(void) {
|
||||
(g_checks - prev_checks) - (g_failures - prev_fails),
|
||||
g_checks - prev_checks);
|
||||
|
||||
prev_checks = g_checks; prev_fails = g_failures;
|
||||
test_flag_clears_on_write();
|
||||
fprintf(stderr, "test_flag_clears_on_write: %d/%d ok\n",
|
||||
(g_checks - prev_checks) - (g_failures - prev_fails),
|
||||
g_checks - prev_checks);
|
||||
|
||||
prev_checks = g_checks; prev_fails = g_failures;
|
||||
test_pad_poll_during_card_read();
|
||||
fprintf(stderr, "test_pad_poll_during_card_read: %d/%d ok\n",
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user