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:
Matthew Stanley
2026-06-21 22:41:26 -07:00
22 changed files with 1467 additions and 329 deletions
+2 -2
View File
@@ -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 |
+1
View File
@@ -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
```
+8
View File
@@ -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;
};
+12
View File
@@ -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);
+4
View File
@@ -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)
+349 -26
View File
@@ -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
+80 -36
View File
@@ -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
View File
@@ -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);
+33
View File
@@ -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
}
+39
View File
@@ -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
}
+2
View File
@@ -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 {
+10 -4
View File
@@ -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
View File
@@ -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;
+189
View File
@@ -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
View File
@@ -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;
+27 -7
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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.
+86
View File
@@ -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.