runtime: PS1 main-RAM read wait states — fix MMX6 memcard "invalid"

Model the R3000A having no usable D-cache (its data cache is repurposed as
the scratchpad): every CPU data read from main DRAM costs +6 wait-state
cycles on top of the 1-cycle opcode (DuckStation RAM_READ_TICKS = 6). The
recompiler previously charged 1 cycle/instruction flat, so BIOS/game busy-
wait poll loops ran ~6x too fast in guest time.

Concretely this fixes Mega Man X6's "This data is invalid" memory-card
error: the game's fixed-iteration TestEvent poll (A0(0xAC) directory load)
exhausted at ~43 frames while the VBlank-paced card directory op needs ~72
frames, so the poll timed out mid-operation; the retry re-called A0(0xAC),
A0(0x4F) returned card-busy, cleared A[port]=0, and orphaned the in-flight
sector completion -> permanent stall. With realistic per-read latency the
poll now outlasts the op (op runs once to completion, broken-sector index
reaches 20, terminal event fires), and the card screen shows the correct
"no save data found" message. Verified: empty card + a populated card both
load correctly; gameplay stable.

- memory.c: psx_guest_read_word/half/byte wrappers add +6 cyc for reads
  whose effective physical address is main RAM (keyed on runtime address,
  so relocated kernel code is timed correctly; scratchpad/MMIO/ROM excluded).
- main.cpp: wire cpu.read_word/half/byte to the guest wrappers (covers
  static-recompiled and dirty-RAM-interpreted loads, incl. LWC2).
- debug_server.c: trace_read_word/half use psx_read_* directly so debug
  reads (IO thread) never charge guest cycles or race the emu thread.

Also includes prior uncommitted DualShock SIO config state machine
(sio.c) + dual keyboard/pad P1 input (main.cpp) from the MMX6 bring-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-19 17:50:37 -07:00
parent 9932dabc4c
commit 98aa688928
4 changed files with 150 additions and 71 deletions
+8 -2
View File
@@ -546,24 +546,30 @@ void debug_server_log_restore_event(uint32_t kind, uint32_t target_pc, uint32_t
e->in_exception = (uint8_t)psx_get_in_exception();
}
extern uint16_t psx_read_half(uint32_t addr);
static uint32_t trace_read_word(CPUState *cpu, uint32_t addr)
{
(void)cpu;
uint32_t phys = addr & 0x1FFFFFFFu;
if (phys > 0x001FFFFCu &&
(phys < 0x1F800000u || phys > 0x1F8003FCu)) {
return 0;
}
return cpu ? cpu->read_word(addr) : 0;
/* Use psx_read_word directly (NOT cpu->read_word): debug instrumentation
* must not charge guest main-RAM read wait states, and runs on the IO
* thread where psx_advance_cycles would race the emulation thread. */
return psx_read_word(addr);
}
static uint32_t trace_read_half(CPUState *cpu, uint32_t addr)
{
(void)cpu;
uint32_t phys = addr & 0x1FFFFFFFu;
if (phys > 0x001FFFFEu &&
(phys < 0x1F800000u || phys > 0x1F8003FEu)) {
return 0;
}
return cpu ? cpu->read_half(addr) : 0;
return psx_read_half(addr);
}
static int sreg_trace_focus_func(uint32_t func)
+58 -11
View File
@@ -96,6 +96,12 @@ extern "C" uint16_t psx_read_half(uint32_t addr);
extern "C" void psx_write_half(uint32_t addr, uint16_t val);
extern "C" uint8_t psx_read_byte(uint32_t addr);
extern "C" void psx_write_byte(uint32_t addr, uint8_t val);
/* Guest-side data-read wrappers: same as psx_read_* but charge PS1 main-RAM
* read wait states (R3000A has no D-cache). Wired to cpu->read_* below so the
* timing applies to recompiled + interpreted guest loads, not debug/device reads. */
extern "C" uint32_t psx_guest_read_word(uint32_t addr);
extern "C" uint16_t psx_guest_read_half(uint32_t addr);
extern "C" uint8_t psx_guest_read_byte(uint32_t addr);
/* ---- SDL state ---- */
static SDL_Window* sdl_window;
@@ -1197,6 +1203,18 @@ static bool hybrid_dpad_active(const PlayerInput& p, bool kb_always) {
* (stick -> analog, D-pad -> digital) so the game runs its own analog or
* digital input path exactly as on hardware (mirrors the inline block this
* helper was extracted from for the low-latency re-sample). */
/* Dev builds (PSX_DEBUG_TOOLS — the dev/debug configuration) ALWAYS feed the
* keyboard into Player 1 alongside whatever device the launcher routed to port 1,
* so a tester can drive the game from EITHER the keyboard OR the selected
* controller (e.g. a DualShock) at the same time, with no reconfiguration. The
* keyboard map and the controller are merged (active-low AND) onto the P1 pad
* word. Release builds keep the single launcher-selected device per port. */
#if defined(PSX_DEBUG_TOOLS)
static const bool g_dev_kb_p1 = true;
#else
static const bool g_dev_kb_p1 = false;
#endif
static void sample_pad_into_sio(int override) {
if (override >= 0) {
sio_set_pad_state_slot(0, (uint16_t)override);
@@ -1204,23 +1222,40 @@ static void sample_pad_into_sio(int override) {
}
for (int s = 0; s < 2; s++) {
PlayerInput& p = g_players[s];
if (p.kind == 0) continue; /* no device in this port */
sio_set_pad_state_slot(s, pad_buttons_for(p));
const bool kb_here = (g_dev_kb_p1 && s == 0);
if (p.kind == 0 && !kb_here) continue; /* no device in this port */
/* Resolve the pad type this frame from the player's mode. */
/* Buttons: merge the assigned device with the keyboard (PSX pad word is
* active-low, so AND combines "pressed on either source"). */
uint16_t btn = (p.kind != 0) ? pad_buttons_for(p) : (uint16_t)0xFFFF;
if (kb_here) btn &= pad_from_keyboard();
sio_set_pad_state_slot(s, btn);
/* Resolve the pad type this frame from the player's mode (a port driven
* by the keyboard alone presents as a digital pad). */
const int mode = (p.kind != 0) ? p.mode : (int)PSXRecompV4::PAD_MODE_DIGITAL;
int eff_analog;
uint8_t st[4] = { 0x80, 0x80, 0x80, 0x80 };
if (p.mode == PSXRecompV4::PAD_MODE_DIGITAL) {
if (mode == PSXRecompV4::PAD_MODE_DIGITAL) {
eff_analog = 0;
} else if (p.mode == PSXRecompV4::PAD_MODE_ANALOG) {
} else if (mode == PSXRecompV4::PAD_MODE_ANALOG) {
eff_analog = 1;
pad_sticks_for(p, st, /*fold_dpad=*/true);
} else { /* HYBRID */
if (hybrid_stick_active(p)) p.hybrid_analog = true;
else if (hybrid_dpad_active(p, false)) p.hybrid_analog = false;
if (hybrid_stick_active(p)) p.hybrid_analog = true;
else if (hybrid_dpad_active(p, kb_here)) p.hybrid_analog = false;
eff_analog = p.hybrid_analog ? 1 : 0;
if (eff_analog) pad_sticks_for(p, st, /*fold_dpad=*/false);
}
/* In dev builds also fold the keyboard arrows onto the analog stick, so an
* analog-mode P1 still steers from the keyboard. */
if (kb_here && eff_analog) {
const Uint8* keys = SDL_GetKeyboardState(NULL);
if (keys[SDL_SCANCODE_LEFT]) st[0] = 0x00;
if (keys[SDL_SCANCODE_RIGHT]) st[0] = 0xFF;
if (keys[SDL_SCANCODE_UP]) st[1] = 0x00;
if (keys[SDL_SCANCODE_DOWN]) st[1] = 0xFF;
}
sio_set_pad_analog(s, eff_analog, st[0], st[1], st[2], st[3]);
}
}
@@ -1796,7 +1831,16 @@ int main(int argc, char** argv) {
bool memcard1_enabled = true;
bool memcard2_enabled = true;
/* [controller] device routing (defaults: P1 keyboard/digital, P2 none). */
/* Dev builds default Player 1 to the first connected controller ("auto"):
* combined with the always-on keyboard merge (g_dev_kb_p1) this means the
* selected controller AND the keyboard both drive P1 with no launcher setup.
* If no controller is present, "auto" opens nothing and the keyboard merge
* still drives P1. Release keeps "keyboard" (the launcher assigns devices). */
#if defined(PSX_DEBUG_TOOLS)
std::string p1_device = "auto";
#else
std::string p1_device = "keyboard";
#endif
std::string p2_device = "none";
int p1_mode = PSXRecompV4::PAD_MODE_HYBRID;
int p2_mode = PSXRecompV4::PAD_MODE_HYBRID;
@@ -2212,7 +2256,10 @@ int main(int argc, char** argv) {
set_player_device(g_players[0], p1_device, p1_mode);
set_player_device(g_players[1], p2_device, p2_mode);
for (int s = 0; s < 2; s++) {
sio_set_pad_connected(s, g_players[s].kind != 0 ? 1 : 0);
/* Dev builds keep P1 connected even with no controller so the always-on
* keyboard (g_dev_kb_p1) can drive port 1 standalone. */
const bool kb_p1 = (g_dev_kb_p1 && s == 0);
sio_set_pad_connected(s, (g_players[s].kind != 0 || kb_p1) ? 1 : 0);
sio_set_pad_analog(s, pad_mode_boot_analog(g_players[s].mode), 0x80, 0x80, 0x80, 0x80);
}
/* SPU float-shadow gate must be set before spu_init() (which runs
@@ -2435,11 +2482,11 @@ int main(int argc, char** argv) {
std::memset(&cpu, 0, sizeof(cpu));
/* Wire memory function pointers. */
cpu.read_word = psx_read_word;
cpu.read_word = psx_guest_read_word; /* +6cyc main-RAM read wait states */
cpu.write_word = psx_write_word;
cpu.read_half = psx_read_half;
cpu.read_half = psx_guest_read_half;
cpu.write_half = psx_write_half;
cpu.read_byte = psx_read_byte;
cpu.read_byte = psx_guest_read_byte;
cpu.write_byte = psx_write_byte;
/* Wire the sljit JIT host-helper table (cpu-relative => position-independent
* shards; prerequisite for the persisted sljit shard cache). Harmless when
+25
View File
@@ -744,6 +744,31 @@ uint8_t psx_read_byte(uint32_t addr) {
return 0;
}
/* ---- CPU guest-side data reads: model PS1 main-RAM read latency ----
* The R3000A has no usable D-cache (its data cache is repurposed as the 1 KB
* scratchpad), so every CPU data read from main DRAM costs 1 opcode cycle plus
* 6 wait-state cycles (DuckStation RAM_READ_TICKS = 6; "1 opcode + 6 waitstates
* = 7 cycles"). The base opcode cycle is already charged by the per-block
* psx_advance_cycles(instruction_count); here we add only the +6 wait states,
* and only for main-RAM targets — scratchpad, MMIO, BIOS ROM and open-bus are
* excluded (they have their own access timing). The penalty is keyed on the
* runtime effective physical address (all of KUSEG/KSEG0/KSEG1 alias the same
* DRAM), NOT on the code region, so relocated kernel code is timed correctly.
*
* These wrappers are wired to cpu->read_* (see main.cpp), so the penalty
* applies to BOTH statically-recompiled and dirty-RAM-interpreted guest loads,
* but NOT to debug-server / device reads that call psx_read_* directly. */
#define PSX_RAM_READ_WAIT_CYCLES 6u
extern void psx_advance_cycles(uint32_t cycles);
static inline void charge_main_ram_read(uint32_t addr) {
if ((addr & 0x1FFFFFFFu) < RAM_SIZE) psx_advance_cycles(PSX_RAM_READ_WAIT_CYCLES);
}
uint32_t psx_guest_read_word(uint32_t addr) { charge_main_ram_read(addr); return psx_read_word(addr); }
uint16_t psx_guest_read_half(uint32_t addr) { charge_main_ram_read(addr); return psx_read_half(addr); }
uint8_t psx_guest_read_byte(uint32_t addr) { charge_main_ram_read(addr); return psx_read_byte(addr); }
void psx_write_byte(uint32_t addr, uint8_t val) {
if (sr_ptr && (*sr_ptr & 0x10000u)) return;
+59 -58
View File
@@ -64,6 +64,15 @@ static uint8_t pad_response[8];
static uint8_t pad_response_len = 0;
static uint8_t pad_response_idx = 0;
static uint8_t pad_current_cmd = 0;
/* DualShock config-mode latch, per slot. A real controller only answers the
* config commands (0x44/0x45/0x46/0x47/0x4C/0x4D/0x4F) and reports the config
* ID 0xF3 while it is IN config mode; outside config it reports its normal ID
* (0x41 digital / 0x73 analog) and ignores config commands. Config is entered/
* exited by command 0x43 with the data byte 0x01(enter)/0x00(exit). Faking
* "always in config" (constant 0xF3) wedges games that probe the pad type via
* 0x43 before polling — e.g. Mega Man X6 loops 01 43 00 00 forever and never
* reaches 0x42. (MMX6 ISSUES.md #2.) */
static uint8_t pad_in_config[2] = { 0, 0 };
/* Memory card SIO state machine */
typedef enum {
@@ -591,94 +600,86 @@ static void pad_process_byte(uint8_t tx_byte) {
case PAD_WAIT_ACCESS:
pad_current_cmd = tx_byte;
pad_response_idx = 1;
/* Controller ID reported as the first response byte. Real hardware
* reports the config ID (0xF3) ONLY while in config mode; otherwise the
* normal mode ID (0x41 digital / 0x73 analog). */
{
const uint8_t cur_id = pad_in_config[selected_slot] ? 0xF3
: (pad_analog[selected_slot] ? 0x73 : 0x41);
if (tx_byte == 0x42) {
/* Read poll. Analog (or in-config) uses the 8-byte format with the
* four stick axes; a plain digital pad uses the 4-byte format. */
const uint16_t btn = pad_buttons[selected_slot];
if (pad_analog[selected_slot]) {
/* DualShock analog poll: id 0x73, then buttons, then the four
* stick axes (right X/Y, left X/Y) per the SIO pad protocol. */
pad_response[0] = 0x73;
pad_response[1] = 0x5A;
pad_response[2] = (uint8_t)(btn & 0xFF);
pad_response[3] = (uint8_t)(btn >> 8);
pad_response[0] = cur_id;
pad_response[1] = 0x5A;
pad_response[2] = (uint8_t)(btn & 0xFF);
pad_response[3] = (uint8_t)(btn >> 8);
if (pad_analog[selected_slot] || pad_in_config[selected_slot]) {
pad_response[4] = pad_stick[selected_slot][2]; /* right X */
pad_response[5] = pad_stick[selected_slot][3]; /* right Y */
pad_response[6] = pad_stick[selected_slot][0]; /* left X */
pad_response[7] = pad_stick[selected_slot][1]; /* left Y */
pad_response_len = 8;
} else {
pad_response[0] = 0x41; /* Digital pad poll */
pad_response[1] = 0x5A;
pad_response[2] = (uint8_t)(btn & 0xFF);
pad_response[3] = (uint8_t)(btn >> 8);
pad_response_len = 4;
}
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (tx_byte == 0x43) {
static const uint8_t resp_43[8] = {
0xF3, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
memcpy(pad_response, resp_43, sizeof(resp_43));
pad_response_len = (uint8_t)sizeof(resp_43);
/* Enter/exit config mode. The ID byte reflects the CURRENT mode; the
* enter(0x01)/exit(0x00) flag is the second data byte, latched in
* PAD_SEND_RESPONSE so it takes effect after this transaction. */
pad_response[0] = cur_id;
pad_response[1] = 0x5A;
pad_response[2] = 0x00; pad_response[3] = 0x00;
pad_response[4] = 0x00; pad_response[5] = 0x00;
pad_response[6] = 0x00; pad_response[7] = 0x00;
pad_response_len = 8;
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (tx_byte == 0x45) {
static const uint8_t resp_45[8] = {
0xF3, 0x5A, 0x03, 0x02, 0x01, 0x02, 0x01, 0x00
};
memcpy(pad_response, resp_45, sizeof(resp_45));
pad_response_len = (uint8_t)sizeof(resp_45);
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (tx_byte == 0x46) {
static const uint8_t resp_46[8] = {
0xF3, 0x5A, 0x00, 0x00, 0x01, 0x02, 0x00, 0x0A
};
memcpy(pad_response, resp_46, sizeof(resp_46));
pad_response_len = (uint8_t)sizeof(resp_46);
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (tx_byte == 0x47) {
static const uint8_t resp_47[8] = {
0xF3, 0x5A, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00
};
memcpy(pad_response, resp_47, sizeof(resp_47));
pad_response_len = (uint8_t)sizeof(resp_47);
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (tx_byte == 0x4C) {
static const uint8_t resp_4c[8] = {
0xF3, 0x5A, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00
};
memcpy(pad_response, resp_4c, sizeof(resp_4c));
pad_response_len = (uint8_t)sizeof(resp_4c);
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (tx_byte == 0x4D) {
static const uint8_t resp_4d[8] = {
0xF3, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
memcpy(pad_response, resp_4d, sizeof(resp_4d));
pad_response_len = (uint8_t)sizeof(resp_4d);
} else if (pad_in_config[selected_slot] &&
(tx_byte == 0x44 || tx_byte == 0x45 || tx_byte == 0x46 ||
tx_byte == 0x47 || tx_byte == 0x4C || tx_byte == 0x4D ||
tx_byte == 0x4F)) {
/* Config-only commands (analog/rumble init handshake). Canned
* responses; only valid while in config mode (a digital pad ignores
* them, handled by the else branch below). */
static const uint8_t r_def[8] = { 0xF3,0x5A,0x00,0x00,0x00,0x00,0x00,0x00 };
static const uint8_t r_45[8] = { 0xF3,0x5A,0x03,0x02,0x01,0x02,0x01,0x00 };
static const uint8_t r_46[8] = { 0xF3,0x5A,0x00,0x00,0x01,0x02,0x00,0x0A };
static const uint8_t r_47[8] = { 0xF3,0x5A,0x00,0x00,0x02,0x00,0x01,0x00 };
static const uint8_t r_4c[8] = { 0xF3,0x5A,0x00,0x00,0x00,0x04,0x00,0x00 };
const uint8_t *r = r_def;
if (tx_byte == 0x45) r = r_45;
else if (tx_byte == 0x46) r = r_46;
else if (tx_byte == 0x47) r = r_47;
else if (tx_byte == 0x4C) r = r_4c;
memcpy(pad_response, r, 8);
pad_response_len = 8;
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else {
/* Unknown command, or a config command issued OUTSIDE config mode:
* a real pad does not respond — return hi-z and end the transaction. */
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
sio_rx_data = 0xFF;
}
}
break;
case PAD_SEND_RESPONSE:
/* For 0x43 (enter/exit config), the data byte selecting enter(0x01)/
* exit(0x00) arrives paired with response index 2. Latch the new config
* state; it takes effect from the next transaction (the ID byte already
* reported the mode that was current at the start of this one). */
if (pad_current_cmd == 0x43 && pad_response_idx == 2)
pad_in_config[selected_slot] = (tx_byte == 0x01) ? 1 : 0;
if (pad_response_idx < pad_response_len) {
sio_rx_data = pad_response[pad_response_idx++];
if (pad_response_idx < pad_response_len) {