From fee55358707582f303ac843fac4f92335fc4f2eb Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 9 Jul 2026 23:25:35 -0700 Subject: [PATCH] sio+memory: fix memory-card saves for Tomba 2 (write handshake + RAM-0 store visibility) - sio: echo sector MSB/LSB during the memcard write address phase (new MC_WRITE_LSB_ECHO state), matching hardware/Beetle; the BIOS card-write path validates this echo before scheduling the follow-up directory reads. - memory: treat BIOS trampoline-mirror and delay-loop scratch stores to RAM 0x0..0x10 as non-visible after game start (hardware/Beetle do not expose them), preserving Tomba 2 sector-63 card-management payload passed as buffer=0 to _card_write. - memory: stop the BIOS card-manager cleanup helper (kernel 0x1C5C) from re-arming/consuming the public MARK card event between DeliverEvent and the game TestEvent poll (narrowly keyed to the EvCB layout), which left Tomba 2 stuck at "Checking MEMORY CARD...". - fntrace: clear the low boot scratch at game-start handoff. Fix authored by Codex; validated by user: Tomba 2 save/load PASS, plus cross-title regression gate (Ape Escape, Tomba, Mega Man X6) all PASS, all built against this runtime. Co-Authored-By: Claude Fable 5 --- runtime/src/fntrace.c | 2 ++ runtime/src/memory.c | 57 +++++++++++++++++++++++++++++++++++++++++++ runtime/src/sio.c | 21 +++++++++++++--- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/runtime/src/fntrace.c b/runtime/src/fntrace.c index a50df5cf..c6b1c4eb 100644 --- a/runtime/src/fntrace.c +++ b/runtime/src/fntrace.c @@ -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); } diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 8eb382ef..3a2391b2 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -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()); diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 670938a6..2df0f109 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -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,