Merge bug/memory-card-ape-escape-tomba-2: memcard write-handshake + RAM-0 store visibility (Tomba 2 saves; cross-title gate Ape/Tomba/MMX6 passed)

This commit is contained in:
Matthew Stanley
2026-07-09 23:25:51 -07:00
3 changed files with 77 additions and 3 deletions
+2
View File
@@ -113,7 +113,9 @@ void fntrace_record(CPUState* cpu, uint32_t target) {
* later overlay overwrites, so the dispatch runs clean text compiled and
* interprets only true overlays (Tomba 2 boot-text loader overlay). */
extern void dirty_ram_clear_image_baseline(void);
extern void memory_clear_low_boot_scratch(void);
dirty_ram_clear_image_baseline();
memory_clear_low_boot_scratch();
cdrom_notify_game_started();
boot_state_trigger_capture(cpu);
}
+57
View File
@@ -12,6 +12,7 @@
#include "cdrom.h"
#include "crash_trace.h"
#include "dma.h"
#include "fntrace.h"
#include "gpu.h"
#include "mdec.h"
#include "sio.h"
@@ -49,6 +50,10 @@ static inline uint32_t psx_phys_addr(uint32_t addr) {
uint8_t *memory_get_ram_ptr(void) { return ram; }
uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; }
void memory_clear_low_boot_scratch(void) {
memset(ram, 0, 0x10u);
}
/* ---- Dirty-page tracking for install-at-runtime code (CLAUDE.md Rule 18) ----
*
* The PS1 BIOS writes 4-instruction dispatch stubs into kernel RAM at runtime
@@ -1096,7 +1101,59 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
uint32_t phys = psx_phys_addr(addr);
/* The generated BIOS mirrors its exception trampoline to 0x80000000 during
* boot. On hardware this mirror copy is not visible in RAM; only the real
* exception vector at 0x80000080 is. Tomba 2 later passes buffer=0 to the
* BIOS card write routine, so stale mirror bytes at 0 corrupt the sector 63
* management write and leave the load menu stuck checking the card. */
if (fntrace_is_game_started() &&
phys < 0x10u && g_debug_last_store_pc == 0xBFC10A00u) return;
/* BIOS helpers use RAM address zero as a tiny delay-loop scratch between
* device-register polls. Treat these two dummy stores as non-visible; real
* hardware / Beetle preserve Tomba 2's sector-63 card-management payload
* when it passes buffer=0 to _card_write. */
if (fntrace_is_game_started() && phys == 0u) {
switch (g_debug_last_store_pc) {
case 0xBFC04E90u:
case 0xBFC04EF0u:
case 0xBFC05164u:
case 0xBFC0D634u:
case 0xBFC3EEB4u:
case 0xBFC405E4u:
case 0xBFC40788u:
case 0xBFC41C50u:
case 0x80012434u:
case 0x800125ACu:
return;
default:
break;
}
}
if (phys < RAM_SIZE) {
/* Tomba 2 load-game card check: the BIOS card-manager cleanup helper
* at kernel RAM 0x1C5C scans MARK events and re-arms any READY event
* matching F0000011/{4,8000,100,200,2000}. In the recomp timing path it
* can run after the card completion DeliverEvent but before the game's
* TestEvent poll, consuming the public card event and leaving the UI
* stuck at "Checking MEMORY CARD...". Let the real TestEvent consume
* public MARK card events instead; keep this narrowly keyed to the
* helper's status store and the EvCB layout. */
if (fntrace_is_game_started() &&
g_debug_last_store_pc == 0xBFC117E4u &&
val == 0x2000u &&
phys >= 4u && (phys + 8u) < RAM_SIZE &&
read_ram_word(phys) == 0x4000u &&
read_ram_word(phys - 4u) == 0xF0000011u &&
read_ram_word(phys + 8u) == 0x2000u) {
uint32_t spec = read_ram_word(phys + 4u);
if (spec == 0x00000004u || spec == 0x00008000u ||
spec == 0x00000100u || spec == 0x00000200u ||
spec == 0x00002000u) {
return;
}
}
if (phys == D44_PHYS) d44_note(phys, read_ram_word(phys), val);
debug_server_trace_write_check(phys, read_ram_word(phys), val, 4);
parity_trace_note_write(phys, 4, effective_store_pc());
+18 -3
View File
@@ -127,6 +127,7 @@ typedef enum {
MC_READ_CHK,
MC_READ_END,
/* Write states */
MC_WRITE_LSB_ECHO, /* rx = sector_lsb while accepting first data byte */
MC_WRITE_DATA,
MC_WRITE_CHK,
MC_WRITE_ACK1,
@@ -1066,11 +1067,17 @@ static void mc_process_byte(uint8_t tx_byte) {
for (int i = 0; i < 128; i++)
mc_checksum ^= mc_data[i];
mc_state = MC_READ_ACK1;
sio_rx_data = 0x00;
} else {
mc_data_idx = 0;
mc_state = MC_WRITE_DATA;
mc_state = MC_WRITE_LSB_ECHO;
/* Hardware/Beetle echo the high address byte on the address-LSB
* transfer (zero for all 1Mbit card sectors), then echo the low
* address byte while receiving the first payload byte. Some BIOS
* card-write paths validate this handshake before they schedule the
* follow-up directory reads. */
sio_rx_data = mc_sector_msb;
}
sio_rx_data = 0x00;
sio_stat |= SIO_STAT_ACK;
break;
@@ -1135,6 +1142,13 @@ static void mc_process_byte(uint8_t tx_byte) {
}
/* ---- WRITE states ---- */
case MC_WRITE_LSB_ECHO:
mc_data[mc_data_idx++] = tx_byte;
mc_state = MC_WRITE_DATA;
sio_rx_data = mc_sector_lsb;
sio_stat |= SIO_STAT_ACK;
break;
case MC_WRITE_DATA:
mc_data[mc_data_idx++] = tx_byte;
sio_rx_data = 0x00;
@@ -1313,7 +1327,8 @@ static void sio_process_byte(uint8_t tx_byte) {
/* ---- Card transaction tracking: record + maybe-close ---- */
if (txn_was_card_byte && sio_txn_open) {
int got_ack = (sio_mc_ack_count > trace_ack_before) ? 1 : 0;
uint16_t sector_now = (mc_state >= MC_READ_ACK1 || mc_state == MC_WRITE_DATA
uint16_t sector_now = (mc_state >= MC_READ_ACK1 || mc_state == MC_WRITE_LSB_ECHO
|| mc_state == MC_WRITE_DATA
|| mc_state >= MC_WRITE_ACK1)
? mc_sector : 0xFFFF;
txn_record_byte(tx_byte, sio_rx_data, mc_cmd, sector_now,