Persist BIOS memory-card copies + dev/prod memcard layout

The COPY/DELETE flows were silently no-oping the SIO write protocol:
60 dispatches per copy were missing the 3-instruction predicate fn at
0xBFC0989C (the WRITE-command sibling of 0xBFC09890), so the BIOS's
write-handler fork test returned junk via uninitialized v0 and the
BIOS skipped sending 0x57 over SIO entirely. The in-memory card cache
appeared updated (visual UI worked, leave/re-enter showed copies) but
nothing reached memcard_write_sector or the .mcd files.

Also reorganize memcard directory resolution: production builds put
cards next to the exe; dev builds (CMake build tree detected) keep
them in the project root next to bios/ so a runtime rebuild doesn't
delete them. --memcard-dir still overrides.
This commit is contained in:
Matthew Stanley
2026-05-09 13:15:33 -07:00
parent 9b332ba5be
commit 696ffbd1f6
4 changed files with 56 additions and 22 deletions
+5 -3
View File
@@ -1,7 +1,7 @@
/* AUTO-GENERATED by psxrecomp-bios --emit-full. DO NOT EDIT.
*
* BIOS SHA256: 71af94d1e47a68c11e8fdb9f8368040601514a42a5a399cda48c7d3bff1e99d3
* Dispatch entries: 4390
* Dispatch entries: 4391
*/
#include "cpu_state.h"
@@ -1176,6 +1176,7 @@ extern void func_1FC09720_cont_BFC09834(CPUState* cpu);
extern void func_1FC09720_cont_BFC09848(CPUState* cpu);
extern void func_1FC09720_cont_BFC0985C(CPUState* cpu);
extern void func_1FC09890(CPUState* cpu);
extern void func_1FC0989C(CPUState* cpu);
extern void func_1FC098A8(CPUState* cpu);
extern void func_1FC098A8_cont_BFC098C0(CPUState* cpu);
extern void func_1FC098A8_cont_BFC098E0(CPUState* cpu);
@@ -4416,7 +4417,7 @@ typedef struct {
PsxRecompFunc func;
} DispatchEntry;
static const DispatchEntry dispatch_table[4390] = {
static const DispatchEntry dispatch_table[4391] = {
{ 0x00000500u, func_00000500 },
{ 0x00000510u, func_00000510 },
{ 0x00000520u, func_00000520 },
@@ -5575,6 +5576,7 @@ static const DispatchEntry dispatch_table[4390] = {
{ 0x1FC09848u, func_1FC09720_cont_BFC09848 },
{ 0x1FC0985Cu, func_1FC09720_cont_BFC0985C },
{ 0x1FC09890u, func_1FC09890 },
{ 0x1FC0989Cu, func_1FC0989C },
{ 0x1FC098A8u, func_1FC098A8 },
{ 0x1FC098C0u, func_1FC098A8_cont_BFC098C0 },
{ 0x1FC098E0u, func_1FC098A8_cont_BFC098E0 },
@@ -8839,7 +8841,7 @@ void psx_dispatch(CPUState* cpu, uint32_t addr) {
fntrace_record(cpu, addr);
cpu->pc = 0;
uint32_t phys = normalize(addr);
int lo = 0, hi = 4390 - 1;
int lo = 0, hi = 4391 - 1;
int found = 0;
while (lo <= hi) {
int mid = (lo + hi) / 2;
+22 -1
View File
@@ -1,7 +1,7 @@
/* AUTO-GENERATED by psxrecomp-bios --emit-full. DO NOT EDIT.
*
* BIOS SHA256: 71af94d1e47a68c11e8fdb9f8368040601514a42a5a399cda48c7d3bff1e99d3
* Functions: 1275
* Functions: 1276
*/
#include "cpu_state.h"
@@ -187,6 +187,7 @@ void func_1FC09540(CPUState* cpu);
void func_1FC09598(CPUState* cpu);
void func_1FC09720(CPUState* cpu);
void func_1FC09890(CPUState* cpu);
void func_1FC0989C(CPUState* cpu);
void func_1FC098A8(CPUState* cpu);
void func_1FC09914(CPUState* cpu);
void func_1FC0994C(CPUState* cpu);
@@ -43585,6 +43586,26 @@ label_BFC09890:
}
}
void func_1FC0989C(CPUState* cpu) {
debug_server_log_call_entry(0x1FC0989Cu);
int psx_delay_BFC098A0 = 0;
label_BFC0989C:
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_advance_cycles(3u);
#endif
psx_check_interrupts(cpu);
/* 0xBFC0989C: 38820051 xori $v0, $a0, 0x51 */
cpu->gpr[2] = cpu->gpr[4] ^ 0x51u;
/* 0xBFC098A0: 03E00008 jr $ra */
psx_delay_BFC098A0 = 1;
/* 0xBFC098A4: 0002102B sltu $v0, $zero, $v0 */
cpu->gpr[2] = (uint32_t)(cpu->gpr[0] < cpu->gpr[2] ? 1 : 0);
if (psx_delay_BFC098A0) {
psx_delay_BFC098A0 = 0;
return;
}
}
void func_1FC098A8(CPUState* cpu) {
debug_server_log_call_entry(0x1FC098A8u);
int psx_delay_BFC098B8 = 0;
+7 -2
View File
@@ -796,8 +796,13 @@
},
{
"address": "0xBFC09890",
"label": "card_copy_dispatch_miss_2026_05_05_safe",
"rationale": "Runtime dispatch miss: 120 hits during COPY. BANDAID per user permission. Bisect 2026-05-05 confirmed safe alone, but pair with 0xBFC0989C causes freeze and 0xBFC0989C alone causes crash (misaligned dispatch from caller 0xBFC09790). Suspected mid-function label, not true function entry. NOT seeding 0xBFC0989C."
"label": "card_cmd_is_not_read",
"rationale": "3-instruction predicate function: xori $v0,$a0,0x52; jr $ra; sltu $v0,$0,$v0. Returns ($a0 != 0x52 READ). Sibling of 0xBFC0989C."
},
{
"address": "0xBFC0989C",
"label": "card_cmd_is_not_write_2026_05_09",
"rationale": "3-instruction predicate function: xori $v0,$a0,0x51; jr $ra; sltu $v0,$0,$v0. Returns ($a0 != 0x51 WRITE). Sibling of 0xBFC09890. Discovery missed it because it sits between 0xBFC09898 (end of 0xBFC09890) and 0xBFC098A8 (next discovered fn). Called via JALR at 0xBFC09788 in func_1FC09598 — without this seed, JALR dispatches to unknown, write predicate returns junk via uninitialized v0, BIOS COPY silently no-ops the SIO write protocol (60 misses per copy, no 0x57 ever sent). Earlier bisect (2026-05-05) said this crashed; that was before the predicate-init fix and dispatch-miss class fixes. Retry 2026-05-09."
},
{
"address": "0xBFC098A8",
+22 -16
View File
@@ -92,27 +92,33 @@ static std::filesystem::path resolve_memcard_dir(const char* requested,
const char* argv0) {
namespace fs = std::filesystem;
std::error_code ec;
// 1. Explicit --memcard-dir always wins.
if (requested && requested[0]) return fs::path(requested);
fs::path absolute_bios = fs::absolute(bios_path, ec);
if (ec) absolute_bios = bios_path;
fs::path project_root = absolute_bios.parent_path().parent_path();
if (fs::exists(project_root / "card1.mcd", ec) ||
fs::exists(project_root / "card2.mcd", ec))
return project_root;
fs::path cwd = fs::current_path();
if (fs::exists(cwd / "card1.mcd", ec) || fs::exists(cwd / "card2.mcd", ec))
return cwd;
// 2. Locate exe directory (production layout anchor).
fs::path exe_dir;
if (argv0 && argv0[0]) {
fs::path exe_dir = fs::absolute(argv0, ec).parent_path();
if (fs::exists(exe_dir / "card1.mcd", ec) ||
fs::exists(exe_dir / "card2.mcd", ec))
return exe_dir;
exe_dir = fs::absolute(argv0, ec).parent_path();
if (ec) exe_dir.clear();
}
if (exe_dir.empty()) exe_dir = fs::current_path();
// 3. Dev-build detection: exe lives inside a CMake build tree. Keep
// cards in the project root (next to bios/) so a runtime rebuild
// never blows them away.
bool is_dev_build = fs::exists(exe_dir / "CMakeFiles", ec) ||
fs::exists(exe_dir / "CMakeCache.txt", ec) ||
fs::exists(exe_dir.parent_path() / "CMakeCache.txt", ec);
if (is_dev_build) {
fs::path absolute_bios = fs::absolute(bios_path, ec);
if (ec) absolute_bios = bios_path;
fs::path project_root = absolute_bios.parent_path().parent_path();
return project_root;
}
return cwd;
// 4. Production: cards sit next to the exe.
return exe_dir;
}
static void shutdown_runtime(void) {