Files
psxrecomp/runtime/src/sio.c
T
2026-09-26 08:03:39 -07:00

2949 lines
121 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* sio.c -- PS1 Serial I/O (SIO0) controller
*
* Handles controller (pad) and memory card communication.
* SIO0 registers at 0x1F801040-0x1F80104E.
*
* Pure hardware simulation. No BIOS state, no HLE, no stubs.
*
* Ported from v3 with audit:
* - Removed psx_runtime.h dependency (unused)
* - Removed all fprintf (CLAUDE.md rule #3)
* - IRQ delivery via i_stat bit-set (same as timers/dma)
*/
#include "sio.h"
#include "psx_bss.h"
#include "memcard.h"
#include "debug_server.h"
#include "event_ring.h"
#include <stdlib.h>
#include <string.h>
/* I_STAT is owned by memory.c */
extern uint32_t i_stat;
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
/* IRQ bit for SIO0 */
#define IRQ_SIO0 7
/* SIO registers */
static uint8_t sio_tx_data;
static uint8_t sio_rx_data;
static uint16_t sio_stat;
static uint16_t sio_mode;
static uint16_t sio_ctrl;
static uint16_t sio_baud;
static uint32_t sio_debug_poll_counter;
static void sio_debug_poll_maybe(void) {
if ((++sio_debug_poll_counter & 0x3FFu) == 0) {
debug_server_poll();
}
}
/* Pad state: 0=pressed, 1=released (PS1 convention). Indexed by LOGICAL pad
* 0 .. PSX_MAX_PLAYERS-1 (not physical SIO slot). */
static uint16_t pad_buttons[PSX_MAX_PLAYERS] = { PSX_PAD_INIT(0xFFFF) };
/* Per-logical-pad type + analog stick state. analog: 0=digital pad (poll id
* 0x41), 1=DualShock/analog (poll id 0x73). Sticks are 0..255, 0x80 centred. */
static PSX_BSS uint8_t pad_analog[PSX_MAX_PLAYERS];
static uint8_t pad_stick[PSX_MAX_PLAYERS][4] = {
PSX_PAD_INIT({ 0x80, 0x80, 0x80, 0x80 })
}; /* lx,ly,rx,ry */
/* DualShock command 0x4D maps the six writable bytes in a 0x42 poll onto the
* two motors: 0x00 = small/high-frequency, 0x01 = large/low-frequency,
* 0xFF = unused. The map powers up unassigned and is echoed back while a new
* map is latched, matching the physical pad/Mednafen protocol. */
static uint8_t pad_rumble_map[PSX_MAX_PLAYERS][6] = {
PSX_PAD_INIT({ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })
};
static PSX_BSS uint8_t pad_rumble_small[PSX_MAX_PLAYERS];
static PSX_BSS uint8_t pad_rumble_large[PSX_MAX_PLAYERS];
/* Analog-mode lock, per logical pad. A real DualShock's config command 0x44
* 0x..02/0x03 locks/unlocks the mode (dualshock.cpp:714-725); a locked pad
* ignores the physical analog button (dualshock.cpp:203). We emulate the
* analog button via the host hybrid heuristic (pad_type_req), so when a game
* LOCKS the mode the hybrid auto-flip must not override it — else the type
* flips underneath a game that pinned DualShock, the exact desync the
* deferred-request machinery cannot otherwise prevent. */
static PSX_BSS uint8_t analog_mode_locked[PSX_MAX_PLAYERS];
/* Which logical pads have devices connected (bit i = pad i). Fits 5 pads. */
static uint8_t pad_connected = 0;
/* Host-side SCPH-1070 enable. Only meaningful when PSX_MAX_PLAYERS >= 5. */
static int sio_multitap_enabled = 0;
/* Physical SIO port hosting the tap: 0 = console Port 1, 1 = Port 2. */
static int sio_multitap_port = 0;
/* Opt-in DualShock on tap seats (default off — faithful digital taps). */
static int sio_multitap_analog_hack = 0;
/* Pad communication state machine */
typedef enum {
PAD_IDLE,
PAD_WAIT_ACCESS, /* received 0x01 (pad access) */
PAD_SEND_RESPONSE, /* sending command response bytes */
} PadState;
/* Multitap 0x42 bulk: ID(0x80)+0x5A + 4×8 pad status bytes. */
#define PAD_RESPONSE_MAX 34
/* What the next 0x42 on a multitap port returns (psx-spx TAP/REQ latch).
* Indexed by physical SIO port so dual multitap keeps independent FSMs. */
typedef enum {
MTAP_NEXT_SLOT_A = 0,
MTAP_NEXT_BULK,
MTAP_NEXT_GARBAGE,
} MtapNextMode;
static PadState pad_state = PAD_IDLE;
static int selected_slot = 0; /* physical SIO slot (CTRL bit13): 0 or 1 */
static int pad_active_logical = 0; /* logical pad for single-pad / config cmds */
static PSX_BSS uint8_t pad_response[PAD_RESPONSE_MAX];
static uint8_t pad_response_len = 0;
static uint8_t pad_response_idx = 0;
static uint8_t pad_current_cmd = 0;
/* Address byte that opened this pad txn (01h=Slot A / bulk, 02h..04h=B..D). */
static uint8_t pad_mtap_addr = 0x01;
static int mtap_next_mode[2] = { MTAP_NEXT_SLOT_A, MTAP_NEXT_SLOT_A };
static int mtap_req_this[2]; /* TAP==1 seen on current 0x42 txn */
static int mtap_returned[2] = { MTAP_NEXT_SLOT_A, MTAP_NEXT_SLOT_A };
/* DualShock config-mode latch, per logical pad. 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 PSX_BSS uint8_t pad_in_config[PSX_MAX_PLAYERS];
/* Whether the pad on a logical slot is a config-capable DualShock (1) or a
* plain digital controller (0). A real SCPH-1080 digital pad (poll id 0x41)
* does NOT answer the config-mode commands (0x43/0x44/.../0x4F): it returns
* hi-z and the transaction ends. A game's pad driver that probes with 0x43 to
* detect a DualShock therefore classifies a digital pad as digital-only and
* just polls it with 0x42. Tomba 2's driver probes this way every frame; when
* the SM (wrongly) answered 0x43 for its digital pad it went down the
* DualShock config path and read the 0x00 config-response bytes as buttons ->
* phantom "all pressed" input. Default 1 keeps analog/hybrid pads unchanged;
* main.cpp sets 0 for PAD_MODE_DIGITAL. */
static uint8_t pad_supports_config[PSX_MAX_PLAYERS] = {
PSX_PAD_INIT(1)
};
/* Coherent-DualShock model (Tomba phantom-input fix). A real controller never
* changes its reported type (0x41 digital <-> 0x73 analog) in the middle of a
* transaction, nor while the host is mid config-handshake: the type only flips
* at a clean boundary, driven by the physical ANALOG button or a game 0x44
* set-mode command. The launcher "hybrid" mode emulates that analog button, so
* it must NOT slam pad_analog[] every frame (which could flip the reported type
* underneath an in-flight poll or a multi-transaction config handshake and
* desync the game's pad driver -> phantom/garbage button reads). Instead the
* host REQUESTS a type via pad_type_req[] and the change is applied atomically
* only when the bus is idle (PAD_IDLE) and the pad is NOT in config mode. A
* request raised during config is held until config exits. -1 = no request. */
static int8_t pad_type_req[PSX_MAX_PLAYERS] = {
PSX_PAD_INIT(-1)
};
/* ---- Logical pad ↔ physical SIO port mapping ----
*
* Multitap off (default / PSX_MAX_PLAYERS==2):
* physical 0 → logical 0, physical 1 → logical 1
* Single multitap (PSX_MAX_PLAYERS 5..7) on Port 1 (sio_multitap_port==0):
* physical 0 → multitap (pads A–D = logical 0–3; Slot A path = 0)
* physical 1 → logical 4
* Single multitap on Port 2 (sio_multitap_port==1):
* physical 0 → logical 0
* physical 1 → multitap (pads A–D = logical 1–4; Slot A path = 1)
* Dual multitap (PSX_MAX_PLAYERS >= 8):
* physical 0 → multitap pads A–D = logical 0–3
* physical 1 → multitap pads A–D = logical 4–7
*/
static int sio_multitap_active(void) {
#if PSX_MAX_PLAYERS >= 5
return sio_multitap_enabled;
#else
return 0;
#endif
}
/* Dual SCPH-1070 when the build can host 8 pads. */
static int sio_dual_multitap(void) {
#if PSX_MAX_PLAYERS >= 8
return sio_multitap_enabled;
#else
return 0;
#endif
}
static int mtap_slot_a_logical(void) {
if (sio_dual_multitap())
return (selected_slot == 0) ? 0 : 4;
return (sio_multitap_port == 0) ? 0 : 1;
}
static int mtap_standalone_logical(void) {
if (sio_dual_multitap())
return -1; /* no lone opposite-port pad */
return (sio_multitap_port == 0) ? 4 : 0;
}
static int pad_logical_for_port(int phys_port) {
if (phys_port < 0 || phys_port > 1) return -1;
if (sio_multitap_active()) {
if (sio_dual_multitap())
return (phys_port == 0) ? 0 : 4;
if (phys_port == sio_multitap_port)
return (sio_multitap_port == 0) ? 0 : 1;
return mtap_standalone_logical();
}
return phys_port;
}
/* Physical port answers when a device is present. Multitap itself is present
* whenever enabled (individual tap slots may still be empty). */
static int pad_port_has_device(int phys_port) {
if (phys_port < 0 || phys_port > 1) return 0;
if (sio_multitap_active()) {
if (sio_dual_multitap()) return 1;
if (phys_port == sio_multitap_port) return 1;
return (pad_connected & (1u << mtap_standalone_logical())) ? 1 : 0;
}
return (pad_connected & (1u << phys_port)) ? 1 : 0;
}
static int selected_is_mtap_port(void) {
if (!sio_multitap_active()) return 0;
if (sio_dual_multitap()) return (selected_slot == 0 || selected_slot == 1);
return selected_slot == sio_multitap_port;
}
/* After a completed 0x42 on the multitap port, arm the next response mode
* from the REQ bit seen this transfer and what we just returned (psx-spx). */
static void mtap_finish_42(void) {
int p;
if (!selected_is_mtap_port() || pad_current_cmd != 0x42 || pad_mtap_addr != 0x01)
return;
p = selected_slot;
if (p < 0 || p > 1) return;
if (!mtap_req_this[p]) {
mtap_next_mode[p] = MTAP_NEXT_SLOT_A;
} else if (mtap_returned[p] == MTAP_NEXT_SLOT_A) {
mtap_next_mode[p] = MTAP_NEXT_BULK;
} else if (mtap_returned[p] == MTAP_NEXT_BULK) {
mtap_next_mode[p] = MTAP_NEXT_GARBAGE;
} else {
mtap_next_mode[p] = MTAP_NEXT_BULK;
}
}
/* Fill 8-byte per-pad status block used in multitap bulk 0x42 responses.
* Disconnected → all 0xFF. Digital → 0x41 0x5A btnL btnH + 0xFF pad.
* Analog/config → 0x73/0xF3 0x5A btn + stick bytes. */
static void pad_fill_status8(int logical, uint8_t out[8]) {
if (logical < 0 || logical >= PSX_MAX_PLAYERS ||
!(pad_connected & (1u << logical))) {
memset(out, 0xFF, 8);
return;
}
const uint8_t id = pad_in_config[logical] ? 0xF3
: (pad_analog[logical] ? 0x73 : 0x41);
const uint16_t btn = pad_buttons[logical];
out[0] = id;
out[1] = 0x5A;
out[2] = (uint8_t)(btn & 0xFF);
out[3] = (uint8_t)(btn >> 8);
if (pad_analog[logical] || pad_in_config[logical]) {
out[4] = pad_stick[logical][2]; /* right X */
out[5] = pad_stick[logical][3]; /* right Y */
out[6] = pad_stick[logical][0]; /* left X */
out[7] = pad_stick[logical][1]; /* left Y */
} else {
out[4] = out[5] = out[6] = out[7] = 0xFF;
}
}
/* Memory card SIO state machine */
typedef enum {
MC_IDLE,
MC_CMD,
MC_ID1,
MC_ID2,
MC_ADDR_MSB,
MC_ADDR_LSB,
/* Read states */
MC_READ_ACK1,
MC_READ_ACK2,
MC_READ_MSB_ECHO, /* rx = sector_msb (cmd ack 1 echo per no$psx) */
MC_READ_LSB_ECHO, /* rx = sector_lsb (cmd ack 2 echo per no$psx) */
MC_READ_DATA,
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,
MC_WRITE_ACK2,
MC_WRITE_END,
/* Get ID states */
MC_GETID_1,
MC_GETID_2,
MC_GETID_3,
MC_GETID_4,
} McState;
/* Per-slot card state. On real hardware each card controller is a
* separate physical device that retains its protocol state independently.
* When the BIOS alternates between slot 0 and slot 1 (by changing the
* SLOT bit in SIO_CTRL), the in-flight card on one slot must not lose
* its state because the other slot is being probed. */
typedef struct {
McState state;
uint8_t cmd;
uint16_t sector;
uint8_t sector_msb;
uint8_t sector_lsb;
uint8_t data[128];
int data_idx;
uint8_t checksum;
uint8_t flag; /* 0x08=new data, 0x00=normal */
} McSlotState;
static McSlotState mc_slots[2] = {
{ MC_IDLE, 0, 0, 0, 0, {0}, 0, 0, 0x08 },
{ MC_IDLE, 0, 0, 0, 0, {0}, 0, 0, 0x08 },
};
/* Active slot's state — copied from mc_slots[selected_slot] before each
* byte exchange and copied back after. The mc_process_byte function
* operates on these "working" variables for simplicity. */
static McState mc_state = MC_IDLE;
static int mc_slot = 0;
static uint8_t mc_cmd = 0;
static uint16_t mc_sector = 0;
static uint8_t mc_sector_msb = 0;
static uint8_t mc_sector_lsb = 0;
static uint8_t mc_data[128];
static int mc_data_idx = 0;
static uint8_t mc_checksum = 0;
static uint8_t mc_flag = 0x08; /* 0x08=new data, 0x00=normal */
typedef enum {
DEV_NONE,
DEV_PAD,
DEV_MEMCARD,
} ActiveDevice;
static ActiveDevice active_device = DEV_NONE;
/* Save working mc_ vars back to the slot state for the given slot. */
static void mc_save_slot(int slot) {
if (slot < 0 || slot > 1) return;
McSlotState *s = &mc_slots[slot];
s->state = mc_state;
s->cmd = mc_cmd;
s->sector = mc_sector;
s->sector_msb = mc_sector_msb;
s->sector_lsb = mc_sector_lsb;
memcpy(s->data, mc_data, sizeof(mc_data));
s->data_idx = mc_data_idx;
s->checksum = mc_checksum;
s->flag = mc_flag;
}
/* Load slot state into working mc_ vars. */
static void mc_load_slot(int slot) {
if (slot < 0 || slot > 1) return;
const McSlotState *s = &mc_slots[slot];
mc_state = s->state;
mc_cmd = s->cmd;
mc_sector = s->sector;
mc_sector_msb = s->sector_msb;
mc_sector_lsb = s->sector_lsb;
memcpy(mc_data, s->data, sizeof(mc_data));
mc_data_idx = s->data_idx;
mc_checksum = s->checksum;
mc_flag = s->flag;
}
/* Card probe diagnostic counters */
static int sio_mc_probe_count = 0; /* times 0x81 written to TX */
static int sio_mc_ack_count = 0; /* times card sent ACK */
static int sio_mc_cmd_count = 0; /* times card reached CMD state */
static int sio_mc_read_count = 0; /* times 0x52 (read) cmd received */
static int sio_mc_read_done = 0; /* times read protocol completed */
static uint32_t sio_mc_last_caller = 0; /* func that last sent 0x81 */
static int sio_mc_abort_count = 0; /* times mc_state reset from non-IDLE */
static int sio_mc_abort_state = 0; /* mc_state at last abort */
static uint16_t sio_mc_abort_ctrl = 0; /* CTRL value that caused last abort */
static int sio_mc_max_state = 0; /* highest mc_state reached */
static int sio_tx_writes = 0; /* ANY write to SIO_TX_DATA */
static int sio_tx_gated = 0; /* writes gated by missing TX_EN */
static uint16_t sio_last_ctrl_on_tx = 0; /* CTRL at last TX write */
/* ---- SIO byte-level trace ring buffer ---- */
static PSX_BSS SioTraceEntry sio_trace_buf[SIO_TRACE_CAP];
static int sio_trace_idx = 0; /* next write position */
static uint32_t sio_trace_seq = 0; /* monotonic sequence number */
uint32_t sio_get_trace(const SioTraceEntry **buf_out, int *write_idx_out) {
if (buf_out) *buf_out = sio_trace_buf;
if (write_idx_out) *write_idx_out = sio_trace_idx;
return sio_trace_seq;
}
uint32_t sio_get_seq(void) {
return sio_trace_seq;
}
int sio_card_protocol_active(void) {
/* Active if either slot has an in-flight protocol, or the working
* mc_state is non-idle. */
if (mc_state != MC_IDLE) return 1;
if (mc_slots[0].state != MC_IDLE) return 1;
if (mc_slots[1].state != MC_IDLE) return 1;
return 0;
}
#if SIO_MODEL_CYCLE_PACED
static void sio_fire_ack_irq(void);
static void sio_handle_shift_complete(void);
#endif
/* ---- Post-probe SIO / interrupt-mask handoff ring ---- */
#define CARD_HANDOFF_CAP 256
static SioCardHandoffEntry s_card_handoff[CARD_HANDOFF_CAP];
static int s_card_handoff_idx = 0;
static int s_card_handoff_count = 0;
static int s_card_handoff_armed = 0; /* set after LOAD-style probe abort */
static void card_handoff_push(uint8_t kind, uint8_t byte) {
extern uint32_t g_debug_current_func_addr;
extern uint32_t g_debug_last_store_pc;
extern uint32_t i_mask;
extern uint64_t psx_get_cycle_count(void);
SioCardHandoffEntry *e = &s_card_handoff[s_card_handoff_idx];
e->kind = kind;
e->byte = byte;
e->imask = (uint16_t)(i_mask & 0x7FFu);
e->pc = g_debug_last_store_pc;
e->func = g_debug_current_func_addr;
e->ctrl = sio_ctrl;
e->stat = sio_stat;
e->card_state = (uint32_t)mc_state;
e->cyc = psx_get_cycle_count();
s_card_handoff_idx = (s_card_handoff_idx + 1) % CARD_HANDOFF_CAP;
s_card_handoff_count++;
}
/* Diagnostic only: guest writes own the interrupt mask. */
void sio_card_handoff_on_imask(uint32_t old_mask, uint32_t new_mask) {
if (!s_card_handoff_armed) return;
if (!(old_mask & 0x80u) && (new_mask & 0x80u))
card_handoff_push(2, 0);
else if ((old_mask & 0x80u) && !(new_mask & 0x80u))
card_handoff_push(3, 0);
}
const SioCardHandoffEntry *sio_get_card_handoff(int *idx_out, int *count_out) {
if (idx_out) *idx_out = s_card_handoff_idx;
if (count_out) *count_out = s_card_handoff_count;
return s_card_handoff;
}
int sio_card_handoff_cap(void) { return CARD_HANDOFF_CAP; }
int sio_card_handoff_armed(void) { return s_card_handoff_armed; }
int sio_hold_present_for_card(void) {
/* NTSC VBlank period — matches interrupts.c VBLANK_CYCLES. */
enum { SIO_PRESENT_HOLD_STALE_VB = 10 };
static const uint64_t stale_cycles =
564480ull * (uint64_t)SIO_PRESENT_HOLD_STALE_VB;
static uint32_t s_hold_seq;
static uint64_t s_hold_progress_cyc;
static int s_hold_armed;
uint32_t seq;
uint64_t now;
if (!sio_card_protocol_active()) {
s_hold_armed = 0;
return 0;
}
extern uint64_t psx_get_cycle_count(void);
seq = sio_get_seq();
now = psx_get_cycle_count();
if (!s_hold_armed || seq != s_hold_seq) {
s_hold_seq = seq;
s_hold_progress_cyc = now;
s_hold_armed = 1;
}
if (now - s_hold_progress_cyc >= stale_cycles)
return 0; /* stale: allow present drain */
return 1;
}
/* Forward decl: defined below sio_get_freeze_diag. */
static int sio_card_burst_drain(int max_iters);
extern int psx_get_in_exception(void);
extern uint8_t psx_read_byte(uint32_t addr);
/* ---- Phase 1.0e-e2 cycle-paced SIO state (lifted above sio_write) ----
*
* Pad and card share a single shifter / one-byte buffer / ACK pipeline.
* No device-specific paths. Macro is in sio.h; defaults to 1. */
volatile int g_sio_timing_active = 0;
#if SIO_MODEL_CYCLE_PACED
#define SIO_BAUD_CYCLES_DEFAULT 1088
#define SIO_ACK_CYCLES_DEFAULT 170
static int sio_tick_quantum_cycles = 64;
static int sio_shift_active = 0;
static uint8_t sio_shift_byte = 0;
static int sio_shift_remaining = 0;
static int sio_tx_buffered = 0;
static uint8_t sio_tx_buffer = 0;
static int sio_shift_ack_irq_en = 0;
static int sio_tx_buffer_ack_irq_en = 0;
static int sio_pending_ack = 0;
static int sio_ack_remaining = 0;
static int sio_pending_ack_irq_en = 0;
typedef enum {
SIO_OWNER_NONE = 0, SIO_OWNER_CARD = 1, SIO_OWNER_PAD = 2, SIO_OWNER_UNKNOWN = 3
} SioBusOwner;
static SioBusOwner sio_bus_owner = SIO_OWNER_NONE;
static uint32_t sio_bus_byte_index = 0;
static uint64_t s_pace_tx_writes_buffered;
static uint64_t s_pace_tx_writes_dropped_busy;
static uint64_t s_pace_tx_writes_dropped_cross_device;
static uint64_t s_pace_cross_device_pad_during_card;
static uint64_t s_pace_tx_buffer_promoted;
static uint64_t s_pace_tx_buffer_promoted_during_card;
static uint64_t s_pace_pad_byte_processed_in_card_data;
static uint64_t s_pace_shift_completes;
static uint64_t s_pace_ack_fires;
#endif
/* ---- Starvation-ring helper -------------------------------------------
* Fill in current SIO state at every recorded event. */
#include "starvation_ring.h"
static void sr_record(uint8_t kind, uint8_t tx, uint8_t rx) {
#if SIO_MODEL_CYCLE_PACED
starvation_ring_record(kind, tx, rx,
sio_ctrl, sio_stat,
sio_shift_active, sio_shift_remaining,
sio_tx_buffered, sio_pending_ack,
sio_ack_remaining,
(uint8_t)sio_bus_owner, sio_bus_byte_index,
(uint8_t)active_device, (uint8_t)mc_state,
(uint8_t)pad_state, (uint8_t)selected_slot,
g_sio_timing_active);
#else
starvation_ring_record(kind, tx, rx, sio_ctrl, sio_stat,
0, 0, 0, 0, 0, 0, 0,
(uint8_t)active_device, (uint8_t)mc_state,
(uint8_t)pad_state, (uint8_t)selected_slot, 0);
#endif
}
/* ---- Card transaction ring buffer ---- */
static PSX_BSS SioTxnEntry sio_txn_buf[SIO_TXN_CAP];
static int sio_txn_idx = 0; /* next-write slot */
static uint32_t sio_txn_seq = 0; /* monotonic id of next-to-close */
static int sio_txn_open = 0;
static SioTxnEntry sio_txn_cur; /* in-progress txn, flushed on close */
uint32_t sio_get_card_txns(const SioTxnEntry **buf_out, int *write_idx_out, int *open_out) {
if (buf_out) *buf_out = sio_txn_buf;
if (write_idx_out) *write_idx_out = sio_txn_idx;
if (open_out) *open_out = sio_txn_open;
return sio_txn_seq;
}
const SioTxnEntry *sio_get_card_txn_live(void) {
return sio_txn_open ? &sio_txn_cur : NULL;
}
/* Open a new txn. Caller must have ensured none is currently open. */
static void txn_open(uint8_t slot, uint32_t start_byte_seq, uint32_t func) {
memset(&sio_txn_cur, 0, sizeof(sio_txn_cur));
sio_txn_cur.txn_seq = sio_txn_seq;
sio_txn_cur.start_byte_seq = start_byte_seq;
sio_txn_cur.start_func = func;
sio_txn_cur.slot = slot;
sio_txn_cur.sector = 0xFFFF;
sio_txn_cur.terminal_state = MC_IDLE;
sio_txn_cur.end_reason = SIO_TXN_END_OPEN;
sio_txn_open = 1;
}
/* Append the just-processed byte to the current txn. */
static void txn_record_byte(uint8_t tx, uint8_t rx,
uint8_t cmd_after, uint16_t sector_after,
int got_ack, uint32_t byte_seq) {
if (!sio_txn_open) return;
if (sio_txn_cur.byte_count < SIO_TXN_MAX_BYTES) {
sio_txn_cur.tx[sio_txn_cur.byte_count] = tx;
sio_txn_cur.rx[sio_txn_cur.byte_count] = rx;
}
sio_txn_cur.byte_count++;
sio_txn_cur.end_byte_seq = byte_seq;
if (cmd_after && !sio_txn_cur.cmd) sio_txn_cur.cmd = cmd_after;
if (sector_after != 0xFFFF) sio_txn_cur.sector = sector_after;
if (got_ack) sio_txn_cur.ack_count++;
}
/* Close the current txn into the ring. Safe to call when no txn open. */
static void txn_close(uint8_t end_reason, uint8_t terminal_state, uint32_t func) {
if (!sio_txn_open) return;
sio_txn_cur.end_reason = end_reason;
sio_txn_cur.terminal_state = terminal_state;
sio_txn_cur.end_func = func;
/* Short read-presence probe: 81 52 00 + SELECT abort. Record the
* following interrupt-mask and card traffic without altering it. */
if (end_reason == SIO_TXN_END_ABORT_OTHER &&
sio_txn_cur.byte_count == 3 &&
sio_txn_cur.tx[0] == 0x81 && sio_txn_cur.tx[1] == 0x52) {
s_card_handoff_armed = 1;
card_handoff_push(1, 0x52);
}
sio_txn_buf[sio_txn_idx] = sio_txn_cur;
sio_txn_idx = (sio_txn_idx + 1) % SIO_TXN_CAP;
sio_txn_seq++;
sio_txn_open = 0;
}
/* ---- SIO IRQ #7 delivery ring ---- */
static PSX_BSS SioIrqEntry sio_irq_buf[SIO_IRQ_RING_CAP];
static int sio_irq_idx = 0;
static uint32_t sio_irq_seq = 0;
/* Pending-IRQ context — captured when the countdown is armed, used when it fires. */
static uint8_t sio_irq_pending_source = SIO_IRQ_SRC_UNKNOWN;
static uint8_t sio_irq_pending_slot = 0;
static uint8_t sio_irq_pending_delay = 0;
static uint8_t sio_irq_pending_mc_state = 0;
static uint32_t sio_irq_pending_byte_seq = 0;
uint32_t sio_get_irq_ring(const SioIrqEntry **buf_out, int *write_idx_out) {
if (buf_out) *buf_out = sio_irq_buf;
if (write_idx_out) *write_idx_out = sio_irq_idx;
return sio_irq_seq;
}
/* ---- Card IRQ-arm audit -----------------------------------------------
* Per-call counters that record what happened at the IRQ-arm decision
* point in sio_write SIO_TX_DATA, partitioned by active_device==DEV_MEMCARD
* (card path) vs not (pad/none). For each card-path call we further
* record:
* - "tx_card": total card TX writes that reached this point
* - "armed_card": cases where (ACK && ACK_IRQ_EN) — countdown was set
* - "no_ack": cases where SIO_STAT_ACK was clear (state machine didn't ACK)
* - "no_ackirqen": cases where ACK was set but ACK_IRQ_EN bit was clear
* - "ctrl_last": last ctrl seen at decision time (for sanity)
* - "stat_pre_last", "stat_post_last": last seen sio_stat
* - "countdown_after_last": value of sio_irq_countdown after the if-block
* Same fields tracked for pad path so we can compare. Always-on; cheap. */
typedef struct {
uint32_t tx_total;
uint32_t armed;
uint32_t no_ack;
uint32_t no_ackirqen;
uint16_t ctrl_last;
uint16_t stat_pre_last;
uint16_t stat_post_last;
int32_t countdown_after_last;
} CardArmAudit;
static CardArmAudit s_card_arm_audit_card;
static CardArmAudit s_card_arm_audit_pad;
static CardArmAudit s_card_arm_audit_none;
void sio_card_arm_audit_record(int dev, uint16_t ctrl_pre,
uint16_t stat_pre, uint16_t stat_post,
int armed, int countdown_after) {
CardArmAudit *a = (dev == DEV_MEMCARD) ? &s_card_arm_audit_card
: (dev == DEV_PAD) ? &s_card_arm_audit_pad
: &s_card_arm_audit_none;
a->tx_total++;
if (armed) {
a->armed++;
} else {
/* SIO_STAT_ACK = (1<<7) = 0x0080;
* SIO_CTRL_ACK_IRQ_EN = (1<<12) = 0x1000.
* Constants are #define'd later in this file; re-state them here
* to avoid forward-decl ordering issues. */
if (!(stat_post & 0x0080u)) a->no_ack++;
else if (!(ctrl_pre & 0x1000u)) a->no_ackirqen++;
}
a->ctrl_last = ctrl_pre;
a->stat_pre_last = stat_pre;
a->stat_post_last = stat_post;
a->countdown_after_last = countdown_after;
}
void sio_get_card_arm_audit(uint32_t out[3][7]) {
/* row 0 = card, row 1 = pad, row 2 = none.
* cols: tx_total, armed, no_ack, no_ackirqen, ctrl_last, stat_pre, stat_post */
const CardArmAudit *src[3] = {
&s_card_arm_audit_card, &s_card_arm_audit_pad, &s_card_arm_audit_none };
for (int i = 0; i < 3; i++) {
out[i][0] = src[i]->tx_total;
out[i][1] = src[i]->armed;
out[i][2] = src[i]->no_ack;
out[i][3] = src[i]->no_ackirqen;
out[i][4] = (uint32_t)src[i]->ctrl_last;
out[i][5] = (uint32_t)src[i]->stat_pre_last;
out[i][6] = (uint32_t)src[i]->stat_post_last;
}
}
int sio_get_card_arm_countdown_after(void) {
return s_card_arm_audit_card.countdown_after_last;
}
int sio_get_mc_probe_count(void) { return sio_mc_probe_count; }
int sio_get_mc_ack_count(void) { return sio_mc_ack_count; }
int sio_get_mc_cmd_count(void) { return sio_mc_cmd_count; }
int sio_get_mc_read_count(void) { return sio_mc_read_count; }
int sio_get_mc_read_done(void) { return sio_mc_read_done; }
uint32_t sio_get_mc_last_caller(void) { return sio_mc_last_caller; }
int sio_get_mc_abort_count(void) { return sio_mc_abort_count; }
int sio_get_mc_abort_state(void) { return sio_mc_abort_state; }
uint16_t sio_get_mc_abort_ctrl(void) { return sio_mc_abort_ctrl; }
int sio_get_mc_max_state(void) { return sio_mc_max_state; }
int sio_get_tx_writes(void) { return sio_tx_writes; }
int sio_get_tx_gated(void) { return sio_tx_gated; }
uint16_t sio_get_last_ctrl_on_tx(void) { return sio_last_ctrl_on_tx; }
/* Delayed IRQ mechanism.
*
* On real PS1, each SIO byte transfer takes BAUD*8 cycles (~1088 for memcard)
* and the ACK fires ~170 cycles later. The BIOS card detection sequence
* depends on this timing:
* 1. Write TX byte
* 2. Delay loop (~50-100 instructions)
* 3. Clear JOY_STAT.INTR and I_STAT.IRQ7
* 4. Check if IRQ7 was re-set -> if yes, device present
*
* If we fire IRQ7 instantly on step 1, step 3 clears it, and step 4 sees
* nothing -> BIOS thinks no device is connected.
*
* Fix: process the byte immediately (RX data available) but delay the ACK
* and IRQ7 by SIO_IRQ_DELAY ticks. */
/* In v4, this counts SIO register accesses (not interpreter steps).
* The BIOS card detection does: TX write, STAT read, RX read, CTRL
* write (clear IRQ), then checks I_STAT. We need the IRQ to fire
* AFTER the CTRL clear. 4 accesses covers the typical sequence. */
/* IRQ delay for pad detection — short, just enough for the BIOS
* "write-clear-check" card detection sequence. */
#define SIO_IRQ_DELAY_PAD 4
/* IRQ delay for active card transfers — moderate value to keep TX_RDY
* cleared briefly during a byte transfer. Per-slot state already
* prevents pad bytes from corrupting card state, so we don't need a
* long delay — we just need the IRQ to fire SOON so the SIO chain
* walker can dispatch the next card byte before the next timer tick
* clears the protocol counter. */
#define SIO_IRQ_DELAY_CARD 8
static int sio_irq_pending = 0;
static int sio_irq_countdown = 0;
static int sio_ack_visible_reads = 0;
/* SIO status register bits */
#define SIO_STAT_TX_RDY (1 << 0)
#define SIO_STAT_RX_RDY (1 << 1)
#define SIO_STAT_TX_EMPTY (1 << 2)
#define SIO_STAT_ACK (1 << 7)
#define SIO_STAT_IRQ (1 << 9)
/* SIO control register bits */
#define SIO_CTRL_TX_EN (1 << 0)
#define SIO_CTRL_SELECT (1 << 1)
#define SIO_CTRL_RX_EN (1 << 2)
#define SIO_CTRL_ACK (1 << 4)
#define SIO_CTRL_RESET (1 << 6)
#define SIO_CTRL_RX_IRQ_EN (1 << 11)
#define SIO_CTRL_ACK_IRQ_EN (1 << 12)
#define SIO_CTRL_SLOT (1 << 13)
void sio_init(void) {
sio_tx_data = 0;
sio_rx_data = 0xFF;
sio_stat = SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
sio_mode = 0;
sio_ctrl = 0;
sio_baud = 0;
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
pad_active_logical = 0;
memset(pad_rumble_map, 0xFF, sizeof(pad_rumble_map));
memset(pad_rumble_small, 0, sizeof(pad_rumble_small));
memset(pad_rumble_large, 0, sizeof(pad_rumble_large));
for (int i = 0; i < PSX_MAX_PLAYERS; i++) {
pad_buttons[i] = 0xFFFF;
pad_analog[i] = 0;
pad_stick[i][0] = pad_stick[i][1] = pad_stick[i][2] = pad_stick[i][3] = 0x80;
pad_in_config[i] = 0;
pad_type_req[i] = -1;
analog_mode_locked[i] = 0;
pad_supports_config[i] = 1;
}
pad_connected = 0;
/* Multitap enable/port are host preferences — leave them alone across
* sio_init so a soft reset does not drop the tap configuration. */
mc_state = MC_IDLE;
for (int i = 0; i < 2; i++) {
mc_slots[i].state = MC_IDLE;
mc_slots[i].cmd = 0;
mc_slots[i].sector = 0;
mc_slots[i].sector_msb = 0;
mc_slots[i].sector_lsb = 0;
memset(mc_slots[i].data, 0, sizeof(mc_slots[i].data));
mc_slots[i].data_idx = 0;
mc_slots[i].checksum = 0;
mc_slots[i].flag = 0x08;
}
active_device = DEV_NONE;
sio_irq_pending = 0;
sio_irq_countdown = 0;
sio_ack_visible_reads = 0;
#if SIO_MODEL_CYCLE_PACED
sio_shift_active = 0;
sio_shift_remaining = 0;
sio_tx_buffered = 0;
sio_shift_ack_irq_en = 0;
sio_tx_buffer_ack_irq_en = 0;
sio_pending_ack = 0;
sio_ack_remaining = 0;
sio_pending_ack_irq_en = 0;
sio_bus_owner = SIO_OWNER_NONE;
sio_bus_byte_index = 0;
g_sio_timing_active = 0;
#endif
sio_txn_open = 0;
/* Note: sio_txn_buf, sio_txn_idx, sio_txn_seq deliberately persist
* across sio_init so post-reset diagnostics can still inspect prior
* transactions. Boot path zero-inits them via BSS. */
}
/* Cycle-budgeted precise event slicing: guest CPU cycles until SIO raises a
* DELIVERABLE IRQ (bit7 unmasked in i_mask). UINT32_MAX if none. Returns the
* nearest armed countdown: shift-complete, pending ack, or the pad/card IRQ
* delivery countdown. See PRECISE_IRQ_SLICE.md. */
uint32_t sio_cycles_to_irq(uint32_t i_mask) {
if (!(i_mask & (1u << 7))) return 0xFFFFFFFFu; /* IRQ_SIO0 masked */
uint32_t best = 0xFFFFFFFFu;
if (sio_irq_pending && sio_irq_countdown > 0 && (uint32_t)sio_irq_countdown < best)
best = (uint32_t)sio_irq_countdown;
#if SIO_MODEL_CYCLE_PACED
if (g_sio_timing_active) {
if (sio_pending_ack && sio_ack_remaining > 0 && (uint32_t)sio_ack_remaining < best)
best = (uint32_t)sio_ack_remaining;
if (sio_shift_active && sio_shift_remaining > 0 && (uint32_t)sio_shift_remaining < best)
best = (uint32_t)sio_shift_remaining;
}
#endif
return best;
}
void sio_set_multitap(int enabled) {
#if PSX_MAX_PLAYERS >= 5
sio_multitap_enabled = enabled ? 1 : 0;
if (!enabled) {
mtap_next_mode[0] = mtap_next_mode[1] = MTAP_NEXT_SLOT_A;
mtap_req_this[0] = mtap_req_this[1] = 0;
mtap_returned[0] = mtap_returned[1] = MTAP_NEXT_SLOT_A;
}
#else
(void)enabled;
sio_multitap_enabled = 0;
#endif
}
int sio_get_multitap(void) {
return sio_multitap_active();
}
void sio_set_multitap_port(int phys_port) {
#if PSX_MAX_PLAYERS >= 5
sio_multitap_port = (phys_port == 1) ? 1 : 0;
mtap_next_mode[0] = mtap_next_mode[1] = MTAP_NEXT_SLOT_A;
mtap_req_this[0] = mtap_req_this[1] = 0;
mtap_returned[0] = mtap_returned[1] = MTAP_NEXT_SLOT_A;
#else
(void)phys_port;
sio_multitap_port = 0;
#endif
}
int sio_get_multitap_port(void) {
#if PSX_MAX_PLAYERS >= 5
return sio_multitap_port;
#else
return 0;
#endif
}
void sio_set_multitap_analog(int enabled) {
sio_multitap_analog_hack = enabled ? 1 : 0;
}
int sio_get_multitap_analog(void) {
return sio_multitap_analog_hack;
}
int sio_pad_on_multitap(int logical_slot) {
#if PSX_MAX_PLAYERS >= 5
if (!sio_multitap_active()) return 0;
if (logical_slot < 0 || logical_slot >= PSX_MAX_PLAYERS) return 0;
/* Dual tap: every seat is a tap pad. Single tap: opposite-port lone pad
* may stay DualShock; every other slot is on the tap. */
if (sio_dual_multitap()) return 1;
return (logical_slot == mtap_standalone_logical()) ? 0 : 1;
#else
(void)logical_slot;
return 0;
#endif
}
/* Tap seat that must stay digital unless the analog hack is armed. */
static int sio_tap_force_digital(int logical_slot) {
return sio_pad_on_multitap(logical_slot) && !sio_multitap_analog_hack;
}
void sio_connect_pad(int slot) {
if (slot >= 0 && slot < PSX_MAX_PLAYERS)
pad_connected |= (uint8_t)(1u << slot);
}
void sio_netplay_canonicalize_session_pads(int slot_count)
{
int i;
if (slot_count < 2)
slot_count = 2;
if (slot_count > PSX_MAX_PLAYERS)
slot_count = PSX_MAX_PLAYERS;
if (slot_count >= 3)
sio_set_multitap(1);
else
sio_set_multitap(0);
/* Idle the pad bus — dig0 snaps otherwise capture mid-poll FSM / response
* bytes that differ when peers finish present dig at different host times. */
pad_state = PAD_IDLE;
selected_slot = 0;
pad_active_logical = 0;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
pad_mtap_addr = 0x01;
memset(pad_response, 0, sizeof(pad_response));
mtap_next_mode[0] = mtap_next_mode[1] = MTAP_NEXT_SLOT_A;
mtap_returned[0] = mtap_returned[1] = MTAP_NEXT_SLOT_A;
mtap_req_this[0] = mtap_req_this[1] = 0;
if (active_device == DEV_PAD)
active_device = DEV_NONE;
for (i = 0; i < PSX_MAX_PLAYERS; i++) {
if (i < slot_count) {
sio_connect_pad(i);
/* Immediate digital — sio_request_pad_type is deferred and left
* rematch hosts (local DualShock seed) analog=1 through dig0. */
sio_set_pad_analog(i, 0, 0x80, 0x80, 0x80, 0x80);
sio_set_pad_state_slot(i, 0xFFFFu);
{
const int force_dig =
sio_pad_on_multitap(i) && !sio_get_multitap_analog();
sio_set_pad_config_capable(i, force_dig ? 0 : 1);
}
pad_in_config[i] = 0;
analog_mode_locked[i] = 0;
pad_type_req[i] = -1;
memset(pad_rumble_map[i], 0xFF, sizeof(pad_rumble_map[i]));
pad_rumble_small[i] = 0;
pad_rumble_large[i] = 0;
} else {
sio_set_pad_connected(i, 0);
sio_set_pad_analog(i, 0, 0x80, 0x80, 0x80, 0x80);
sio_set_pad_state_slot(i, 0xFFFFu);
pad_in_config[i] = 0;
pad_type_req[i] = -1;
}
}
}
void sio_set_pad_connected(int slot, int connected) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (connected) {
pad_connected |= (uint8_t)(1u << slot);
} else {
pad_connected &= (uint8_t)~(1u << slot);
pad_rumble_small[slot] = 0;
pad_rumble_large[slot] = 0;
}
}
void sio_set_pad_config_capable(int slot, int capable) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_tap_force_digital(slot)) capable = 0;
pad_supports_config[slot] = capable ? 1 : 0;
/* A plain digital pad can never be in config mode; clear any stale latch so
* the next poll reports the digital id (0x41), not the config id (0xF3). */
if (!capable) {
pad_in_config[slot] = 0;
pad_rumble_small[slot] = 0;
pad_rumble_large[slot] = 0;
}
}
void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large) {
uint8_t s = 0, l = 0;
if (slot >= 0 && slot < PSX_MAX_PLAYERS && (pad_connected & (1u << slot))) {
s = pad_rumble_small[slot];
l = pad_rumble_large[slot];
}
if (small) *small = s;
if (large) *large = l;
}
void sio_set_pad_state(uint16_t buttons) {
pad_buttons[0] = buttons;
}
void sio_set_pad_state_slot(int slot, uint16_t buttons) {
if (slot >= 0 && slot < PSX_MAX_PLAYERS) pad_buttons[slot] = buttons;
}
/* Direct set of pad type + sticks. Used at boot/hotplug (refresh_player_devices)
* to establish the initial pinned type; safe there because the bus is idle and
* no handshake is in flight. Per-frame input must NOT use this for the type —
* use sio_set_pad_sticks + sio_request_pad_type so the type change is applied
* coherently (see pad_type_req[] above). */
void sio_set_pad_analog(int slot, int enabled,
uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_tap_force_digital(slot)) {
enabled = 0;
lx = ly = rx = ry = 0x80;
}
pad_analog[slot] = enabled ? 1 : 0;
pad_type_req[slot] = -1; /* explicit set supersedes any pending request */
pad_stick[slot][0] = lx; pad_stick[slot][1] = ly;
pad_stick[slot][2] = rx; pad_stick[slot][3] = ry;
}
/* Per-frame stick update (does not touch the reported pad type). */
void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
pad_stick[slot][0] = lx; pad_stick[slot][1] = ly;
pad_stick[slot][2] = rx; pad_stick[slot][3] = ry;
}
/* Per-frame type request (the emulated DualShock "analog button"). The flip is
* deferred and applied atomically at the next idle, non-config boundary, so it
* can never split a poll or a config handshake. A no-op if already that type. */
void sio_request_pad_type(int slot, int analog) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_tap_force_digital(slot)) analog = 0;
int want = analog ? 1 : 0;
pad_type_req[slot] = (pad_analog[slot] == want) ? -1 : (int8_t)want;
}
uint16_t sio_get_pad_buttons(void) {
return pad_buttons[0];
}
uint16_t sio_get_pad_buttons_slot(int slot) {
return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? pad_buttons[slot] : 0xFFFF;
}
int sio_get_pad_connected(int slot) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return 0;
return (pad_connected & (1u << slot)) ? 1 : 0;
}
int sio_get_pad_analog(int slot) {
return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? pad_analog[slot] : 0;
}
void sio_get_pad_sticks(int slot, uint8_t out[4]) {
if (!out) return;
if (slot < 0 || slot >= PSX_MAX_PLAYERS) {
out[0] = out[1] = out[2] = out[3] = 0x80;
return;
}
out[0] = pad_stick[slot][0]; out[1] = pad_stick[slot][1];
out[2] = pad_stick[slot][2]; out[3] = pad_stick[slot][3];
}
/* ── LEGACY pad-config compatibility (Tomba "Hybrid" controller) ─────────────
*
* Why this exists, and why it is explicitly LEGACY:
*
* This is the descendant of our FIRST controller implementation. It was built
* for Tomba, to reproduce the seamless analog/digital feel of Tomba: Special
* Edition — the launcher "Hybrid" mode flips the emulated pad's reported TYPE
* between digital (poll id 0x41) and DualShock/analog (poll id 0x73) as the
* player moves between the d-pad and the stick. In that first implementation
* the SIO pad answered the DualShock config-mode commands trivially (it always
* reported the config id 0xF3), and Tomba's Hybrid flip worked.
*
* We then matured the pad against Mega Man X6. MMX6's libpad probes the pad
* type via config mode (01 43 00 00 ...) BEFORE it ever polls, and the trivial
* "always 0xF3" answer WEDGED it: it looped the probe forever, never reached
* the 0x42 poll, and had no input. The fix (commit 98aa688) was a REAL
* DualShock config-mode state machine — report 0xF3 only while actually in
* config, track 0x43 enter/exit, answer the capability queries (0x45/0x46/
* 0x47/0x4C) like the real pad. That "modern" SM is the correct behaviour, is
* what MMX6 and every other title needs, and is the default.
*
* But post-MMX6, under the modern SM, Tomba's Hybrid flip regressed. Any type
* change makes libpad re-run findpad / re-detect the pad; under the modern SM
* that re-detect manufactures a one-frame "pad unplugged" (buf[0] = 0xFF).
* Tomba reads it as a controller disconnect and unpauses the menu / drops
* input; MMX6 reads the held direction as released-then-re-pressed and fires a
* phantom dash. We could NOT, with the modern SM, keep the flip benign. Rather
* than block Tomba's Hybrid feature, we kept the original behaviour available
* as a per-game opt-in — this flag.
*
* What the flag does:
* g_pad_legacy_cfg == 0 (default) -> modern DualShock config state machine.
* Required by MMX6; correct for every title.
* g_pad_legacy_cfg != 0 -> the pre-98aa688 behaviour: config commands
* always answer the config id 0xF3, with no
* enter/exit tracking. Tomba's libpad was
* written against exactly this, so its Hybrid
* flip re-detect is benign.
*
* Driven per-game by [controller] legacy_pad_config in game.toml (applied via
* sio_set_legacy_cfg() at boot). ONLY Tomba opts in. Because the default is 0,
* the modern path in pad_process_byte() below is byte-for-byte unchanged when
* the flag is off — no other title is affected. The `pad_cfg` debug command can
* also flip it live for A/B testing.
*
* THIS IS LEGACY — remove it once the behavioural mechanism is right. A real
* DualShock tolerates unlimited analog-button presses (type changes) with no
* disconnect, which proves the Hybrid flip CAN be benign under a correct state
* machine for every game, with no per-game compatibility branch. When that
* findpad/re-detect refactor lands, DELETE this whole legacy feature set — this
* flag, the g_pad_legacy_cfg-gated branches in pad_process_byte(), the
* legacy_pad_config config field, and the per-game game.toml opt-in — and let
* Tomba ride the modern SM like everything else. */
volatile int g_pad_legacy_cfg = 0;
int sio_get_legacy_cfg(void) { return g_pad_legacy_cfg; }
void sio_set_legacy_cfg(int v) {
g_pad_legacy_cfg = v ? 1 : 0;
/* Clear any in-flight config latch so a mid-session toggle can't carry a
* stale 0xF3/8-byte poll into the other mode's dispatch. */
for (int s = 0; s < PSX_MAX_PLAYERS; s++)
pad_in_config[s] = 0;
}
static void pad_process_byte(uint8_t tx_byte) {
/* Apply any pending host type change (the emulated analog button) ONLY while
* the bus is idle and the pad is not in config mode. This guarantees the
* reported type (0x41/0x73) is stable for the whole of any poll or config
* handshake — a hybrid stick/d-pad flip can never desync the game's driver
* mid-transaction. A request raised during config stays pending until exit. */
if (pad_state == PAD_IDLE) {
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
/* A game-LOCKED analog mode (0x44 ..03) ignores the physical analog
* button — and our hybrid auto-flip IS that button — so a locked slot
* drops the pending host request instead of applying it. */
if (pad_type_req[s] >= 0 && !pad_in_config[s] && !analog_mode_locked[s]) {
pad_analog[s] = (uint8_t)pad_type_req[s];
pad_type_req[s] = -1;
}
}
}
switch (pad_state) {
case PAD_IDLE:
/* Standard address 01h selects Slot A (or the standalone pad). With a
* multitap, 02h..04h select pads B–D on that port (psx-spx method 2). */
if (tx_byte == 0x01 && pad_port_has_device(selected_slot)) {
pad_active_logical = pad_logical_for_port(selected_slot);
pad_mtap_addr = 0x01;
pad_state = PAD_WAIT_ACCESS;
sio_rx_data = 0xFF;
sio_stat |= SIO_STAT_ACK;
} else if (selected_is_mtap_port() && tx_byte >= 0x02 && tx_byte <= 0x04) {
pad_active_logical = mtap_slot_a_logical() + (int)(tx_byte - 1);
pad_mtap_addr = tx_byte;
pad_state = PAD_WAIT_ACCESS;
sio_rx_data = 0xFF;
sio_stat |= SIO_STAT_ACK;
} else {
sio_rx_data = 0xFF;
}
break;
case PAD_WAIT_ACCESS:
pad_current_cmd = tx_byte;
pad_response_idx = 1;
if (selected_slot >= 0 && selected_slot <= 1)
mtap_req_this[selected_slot] = 0;
/* SCPH-1070 method 1: only when address was 01h AND a prior transfer
* latched REQ=1. Otherwise Slot A (or garbage) — never force bulk on
* every 0x42 (that breaks P1 when the tap is present with one pad). */
if (selected_is_mtap_port() && pad_mtap_addr == 0x01 && tx_byte == 0x42 &&
mtap_next_mode[selected_slot] == MTAP_NEXT_BULK) {
const int base = mtap_slot_a_logical();
pad_response[0] = 0x80;
pad_response[1] = 0x5A;
for (int i = 0; i < 4; i++)
pad_fill_status8(base + i, &pad_response[2 + i * 8]);
pad_response_len = PAD_RESPONSE_MAX;
mtap_returned[selected_slot] = MTAP_NEXT_BULK;
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
break;
}
if (selected_is_mtap_port() && pad_mtap_addr == 0x01 && tx_byte == 0x42 &&
mtap_next_mode[selected_slot] == MTAP_NEXT_GARBAGE) {
/* HiZ,80h,5Ah,LSB(Slot A id) then abort (psx-spx). */
const int a = mtap_slot_a_logical();
const uint8_t id = (!(pad_connected & (1u << a))) ? 0xFFu
: (pad_in_config[a] ? 0xF3u
: (pad_analog[a] ? 0x73u : 0x41u));
pad_response[0] = 0x80;
pad_response[1] = 0x5A;
pad_response[2] = id;
pad_response_len = 3;
mtap_returned[selected_slot] = MTAP_NEXT_GARBAGE;
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
break;
}
if (selected_is_mtap_port() && pad_mtap_addr == 0x01 && tx_byte == 0x42)
mtap_returned[selected_slot] = MTAP_NEXT_SLOT_A;
/* Single-pad path (standalone port / Slot A / method-2 pad / non-0x42). */
{
const int lp = pad_active_logical;
if (lp < 0 || lp >= PSX_MAX_PLAYERS || !(pad_connected & (1u << lp))) {
/* No pad on this logical slot (e.g. empty multitap A during a
* non-bulk command): hi-z, end transaction. */
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
sio_rx_data = 0xFF;
break;
}
/* 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[lp] ? 0xF3
: (pad_analog[lp] ? 0x73 : 0x41);
/* A plain digital controller (SCPH-1080) answers ONLY the 0x42 poll; it
* ignores every config-mode command (returns hi-z, no ACK). A driver
* that probes with 0x43 to detect a DualShock then classifies it as
* digital-only and just polls. Gate all config branches on this so a
* digital-mode pad behaves like real hardware (see pad_supports_config). */
const int ds = pad_supports_config[lp];
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[lp];
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[lp] || pad_in_config[lp]) {
pad_response[4] = pad_stick[lp][2]; /* right X */
pad_response[5] = pad_stick[lp][3]; /* right Y */
pad_response[6] = pad_stick[lp][0]; /* left X */
pad_response[7] = pad_stick[lp][1]; /* left Y */
pad_response_len = 8;
} else {
pad_response_len = 4;
}
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (ds && tx_byte == 0x43) {
/* 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. */
const uint16_t btn = pad_buttons[lp];
pad_response[1] = 0x5A;
if (g_pad_legacy_cfg) {
/* LEGACY (pre-98aa688): always config ID 0xF3, zero frame, no
* enter/exit tracking. */
pad_response[0] = 0xF3;
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;
} else if (!pad_in_config[lp]) {
/* ENTER attempt (normal mode): a real DualShock transmits the LIVE
* poll frame here — identical framing to 0x42 (dualshock.cpp:471-490)
* — and only latches config entry from the 0x01 data byte AFTERWARD.
* The old all-zero frame fed any driver that reads the 0x43 frame as
* input (most do; 0x43-with-data is a poll on the wire) a phantom
* "all pressed"/centered-stick garbage — the hybrid phantom-input
* mechanism (axis5_sio_controller.md D4). */
pad_response[0] = cur_id;
pad_response[2] = (uint8_t)(btn & 0xFF);
pad_response[3] = (uint8_t)(btn >> 8);
if (pad_analog[lp]) {
pad_response[4] = pad_stick[lp][2]; /* right X */
pad_response[5] = pad_stick[lp][3]; /* right Y */
pad_response[6] = pad_stick[lp][0]; /* left X */
pad_response[7] = pad_stick[lp][1]; /* left Y */
pad_response_len = 8;
} else {
pad_response_len = 4;
}
} else {
/* EXIT attempt (in config): config-mode 0x43 returns the zero frame
* (dualshock.cpp:660-674); cur_id is 0xF3 while in config. */
pad_response[0] = cur_id;
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 (ds && g_pad_legacy_cfg &&
(tx_byte == 0x45 || tx_byte == 0x46 || tx_byte == 0x47 ||
tx_byte == 0x4C || tx_byte == 0x4D)) {
/* LEGACY config answers (pre-98aa688): canned 0xF3 responses given
* UNCONDITIONALLY (no config-mode gating). 0x44/0x4F had no handler
* then, so they fall through to the hi-z "no response" else below. */
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 };
static const uint8_t r_4d[8] = { 0xF3,0x5A,0x00,0x00,0x00,0x00,0x00,0x00 };
const uint8_t *r = r_4d;
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);
/* 0x45 status byte must report the LIVE analog mode, not a fixed
* analog-on (dualshock.cpp:743) — see fix below for the modern path. */
if (tx_byte == 0x45)
pad_response[3] = pad_analog[lp] ? 0x01 : 0x00;
pad_response_len = 8;
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
sio_stat |= SIO_STAT_ACK;
} else if (ds && !g_pad_legacy_cfg && pad_in_config[lp] &&
(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);
/* 0x45 reports the analog-status byte: a driver polling 0x45 to learn
* the live mode must read the CURRENT analog state, not a hard-coded
* analog-on (dualshock.cpp:743 transmit_buffer[1]=analog_mode?1:0).
* Reporting "analog" while we present digital (or vice-versa) makes the
* driver mis-parse the poll frame length → off-by-frame garbage buttons
* (axis5_sio_controller.md D8). */
if (tx_byte == 0x45)
pad_response[3] = pad_analog[lp] ? 0x01 : 0x00;
/* 0x4D returns the previous six-byte motor map while latching the
* replacement bytes later in this same transaction. */
if (tx_byte == 0x4D)
memcpy(&pad_response[2], pad_rumble_map[lp], 6);
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:
/* TAP/REQ (third command byte, paired with idhi/5Ah at idx==1): does not
* change *this* response; it arms the next 0x42 on the multitap port. */
if (selected_is_mtap_port() && pad_current_cmd == 0x42 &&
pad_mtap_addr == 0x01 && pad_response_idx == 1 &&
selected_slot >= 0 && selected_slot <= 1)
mtap_req_this[selected_slot] = (tx_byte == 0x01) ? 1 : 0;
/* DualShock rumble: index by the active logical pad (multitap-aware). */
{
const int rs = (pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS)
? pad_active_logical
: selected_slot;
if (rs >= 0 && rs < PSX_MAX_PLAYERS) {
/* The six data bytes after 0x42's leading 0x00 occupy response
* indexes 2..7. Route each through the map negotiated by 0x4D. */
if (pad_current_cmd == 0x42 &&
pad_response_idx >= 2 && pad_response_idx < 8) {
const unsigned map_index = (unsigned)pad_response_idx - 2u;
const uint8_t motor = pad_rumble_map[rs][map_index];
if (motor == 0x00)
pad_rumble_small[rs] = tx_byte;
else if (motor == 0x01)
pad_rumble_large[rs] = tx_byte;
}
/* 0x4D uses the same six wire positions. pad_response[] was
* populated with the old values before the transaction, so
* updating the live map here preserves echo-before-write. */
if (pad_current_cmd == 0x4D &&
pad_response_idx >= 2 && pad_response_idx < 8) {
const unsigned map_index = (unsigned)pad_response_idx - 2u;
pad_rumble_map[rs][map_index] = tx_byte;
}
}
}
/* 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 (!g_pad_legacy_cfg && pad_current_cmd == 0x43 && pad_response_idx == 2 &&
pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS)
pad_in_config[pad_active_logical] = (tx_byte == 0x01) ? 1 : 0;
/* 0x44 set-mode (game owns the analog/digital mode): the mode byte rides
* in the same slot as 0x43's enter/exit flag (data position 3). 0x01 =>
* analog (0x73), 0x00 => digital (0x41). Honouring it makes the pad
* coherent — the type the game just selected is the type it then polls,
* instead of the host hybrid silently winning. Drop any stale host
* request so it can't immediately undo the game's choice. */
if (!g_pad_legacy_cfg && pad_current_cmd == 0x44 && pad_response_idx == 2 &&
pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS) {
pad_analog[pad_active_logical] = (tx_byte == 0x01) ? 1 : 0;
pad_type_req[pad_active_logical] = -1;
}
/* 0x44 lock byte (data position 4, the byte after the mode byte): 0x03 =>
* lock analog mode, 0x02 => unlock (dualshock.cpp:714-725). A locked slot
* ignores the host hybrid auto-flip (see analog_mode_locked). */
if (!g_pad_legacy_cfg && pad_current_cmd == 0x44 && pad_response_idx == 3 &&
pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS) {
if (tx_byte == 0x03) analog_mode_locked[pad_active_logical] = 1;
else if (tx_byte == 0x02) analog_mode_locked[pad_active_logical] = 0;
}
if (pad_response_idx < pad_response_len) {
sio_rx_data = pad_response[pad_response_idx++];
if (pad_response_idx < pad_response_len) {
sio_stat |= SIO_STAT_ACK;
} else {
mtap_finish_42();
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
}
} else {
mtap_finish_42();
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
sio_rx_data = 0xFF;
}
break;
default:
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
sio_rx_data = 0xFF;
break;
}
sio_stat |= SIO_STAT_RX_RDY;
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
static void mc_process_byte(uint8_t tx_byte) {
if ((int)mc_state > sio_mc_max_state) sio_mc_max_state = (int)mc_state;
switch (mc_state) {
case MC_IDLE:
mc_slot = selected_slot;
if (memcard_is_present(mc_slot)) {
mc_state = MC_CMD;
sio_rx_data = 0xFF;
sio_stat |= SIO_STAT_ACK;
sio_mc_ack_count++;
} else {
sio_rx_data = 0xFF;
}
break;
case MC_CMD:
mc_cmd = tx_byte;
sio_mc_cmd_count++;
if (tx_byte == 0x52 || tx_byte == 0x57 || tx_byte == 0x53) {
if (tx_byte == 0x52) sio_mc_read_count++;
mc_state = MC_ID1;
sio_rx_data = mc_flag;
sio_stat |= SIO_STAT_ACK;
/* FLAG.3 survives reads and ID queries. Original cards clear it
* only after a write; BIOS card initialization normally performs
* a dummy write to sector 003Fh for exactly that purpose. */
} else {
mc_state = MC_IDLE;
sio_rx_data = 0xFF;
}
break;
case MC_ID1:
mc_state = MC_ID2;
sio_rx_data = 0x5A;
sio_stat |= SIO_STAT_ACK;
break;
case MC_ID2:
if (mc_cmd == 0x53) {
mc_state = MC_GETID_1;
sio_rx_data = 0x5D;
sio_stat |= SIO_STAT_ACK;
} else {
mc_state = MC_ADDR_MSB;
sio_rx_data = 0x5D;
sio_stat |= SIO_STAT_ACK;
}
break;
case MC_GETID_1:
mc_state = MC_GETID_2;
sio_rx_data = 0x04;
sio_stat |= SIO_STAT_ACK;
break;
case MC_GETID_2:
mc_state = MC_GETID_3;
sio_rx_data = 0x00;
sio_stat |= SIO_STAT_ACK;
break;
case MC_GETID_3:
mc_state = MC_GETID_4;
sio_rx_data = 0x00;
sio_stat |= SIO_STAT_ACK;
break;
case MC_GETID_4:
mc_state = MC_IDLE;
sio_rx_data = 0x80;
break;
case MC_ADDR_MSB:
mc_sector_msb = tx_byte;
mc_state = MC_ADDR_LSB;
sio_rx_data = 0x00;
sio_stat |= SIO_STAT_ACK;
break;
case MC_ADDR_LSB:
mc_sector_lsb = tx_byte;
mc_sector = ((uint16_t)mc_sector_msb << 8) | mc_sector_lsb;
if (mc_cmd == 0x52) {
if (mc_sector < MEMCARD_SECTORS) {
memcard_read_sector(mc_slot, mc_sector, mc_data);
} else {
memset(mc_data, 0xFF, 128);
}
mc_data_idx = 0;
mc_checksum = mc_sector_msb ^ mc_sector_lsb;
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_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_stat |= SIO_STAT_ACK;
break;
/* ---- READ states ---- */
case MC_READ_ACK1:
mc_state = MC_READ_ACK2;
sio_rx_data = 0x5C;
sio_stat |= SIO_STAT_ACK;
break;
case MC_READ_ACK2:
mc_state = MC_READ_MSB_ECHO;
sio_rx_data = 0x5D;
sio_stat |= SIO_STAT_ACK;
break;
case MC_READ_MSB_ECHO:
/* Per no$psx: card echoes the requested sector address back to host
* as a confirm-the-address handshake, BEFORE sending the data bytes. */
mc_state = MC_READ_LSB_ECHO;
sio_rx_data = mc_sector_msb;
sio_stat |= SIO_STAT_ACK;
break;
case MC_READ_LSB_ECHO:
mc_state = MC_READ_DATA;
mc_data_idx = 0;
sio_rx_data = mc_sector_lsb;
sio_stat |= SIO_STAT_ACK;
break;
case MC_READ_DATA:
sio_rx_data = mc_data[mc_data_idx++];
if (mc_data_idx >= 128) {
mc_state = MC_READ_CHK;
}
sio_stat |= SIO_STAT_ACK;
break;
case MC_READ_CHK:
mc_state = MC_READ_END;
sio_rx_data = mc_checksum;
sio_stat |= SIO_STAT_ACK;
break;
case MC_READ_END: {
mc_state = MC_IDLE;
sio_mc_read_done++;
extern void card_read_summary_record(uint8_t slot, uint8_t cmd,
uint16_t sector,
uint8_t checksum,
uint8_t data_idx,
const uint8_t *data128);
card_read_summary_record(mc_slot, mc_cmd, mc_sector,
mc_checksum, mc_data_idx, mc_data);
if (mc_sector < MEMCARD_SECTORS) {
sio_rx_data = 0x47; /* 'G' = Good */
} else {
sio_rx_data = 0xFF;
}
break;
}
/* ---- 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;
if (mc_data_idx >= 128) {
mc_state = MC_WRITE_CHK;
}
sio_stat |= SIO_STAT_ACK;
break;
case MC_WRITE_CHK: {
uint8_t expected = mc_sector_msb ^ mc_sector_lsb;
for (int i = 0; i < 128; i++)
expected ^= mc_data[i];
mc_state = MC_WRITE_ACK1;
sio_rx_data = 0x00;
sio_stat |= SIO_STAT_ACK;
if (tx_byte == expected && mc_sector < MEMCARD_SECTORS) {
memcard_write_sector(mc_slot, mc_sector, mc_data);
memcard_flush(mc_slot);
mc_checksum = 0x47; /* Good */
} else if (mc_sector >= MEMCARD_SECTORS) {
mc_checksum = 0xFF; /* Bad sector */
} else {
mc_checksum = 0x4E; /* 'N' = bad checksum */
}
break;
}
case MC_WRITE_ACK1:
mc_state = MC_WRITE_ACK2;
sio_rx_data = 0x5C;
sio_stat |= SIO_STAT_ACK;
break;
case MC_WRITE_ACK2:
mc_state = MC_WRITE_END;
sio_rx_data = 0x5D;
sio_stat |= SIO_STAT_ACK;
break;
case MC_WRITE_END:
mc_state = MC_IDLE;
sio_rx_data = mc_checksum;
mc_flag = 0x00; /* Clear "new data" flag after first write */
break;
default:
mc_state = MC_IDLE;
sio_rx_data = 0xFF;
break;
}
sio_stat |= SIO_STAT_RX_RDY;
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
static void sio_process_byte(uint8_t tx_byte) {
/* ---- Trace: capture pre-state ---- */
extern uint32_t g_debug_current_func_addr;
uint8_t trace_mc_pre = (uint8_t)mc_state;
uint8_t trace_dev_pre = (uint8_t)active_device;
uint8_t trace_irq_cd = (uint8_t)(sio_irq_countdown > 255 ? 255 : sio_irq_countdown);
int trace_abort_before = sio_mc_abort_count;
int trace_ack_before = sio_mc_ack_count;
/* ---- Card transaction tracking ----
* Decide whether this byte (a) closes a prior txn before processing,
* (b) opens a new txn before processing, (c) belongs to an existing
* txn, or (d) is unrelated to the card protocol. The actual record
* happens AFTER mc_process_byte runs so post-state is accurate. */
int txn_was_card_byte = 0;
uint8_t txn_pre_state = (uint8_t)mc_state;
if (active_device == DEV_NONE) {
selected_slot = (sio_ctrl & SIO_CTRL_SLOT) ? 1 : 0;
if (tx_byte == 0x01) {
/* Pad select. Save any in-flight card state back to its slot
* so it survives pad polling. Don't touch mc_state — we need
* it per-slot, and mc_load_slot will restore it when the card
* slot is selected again. */
if (mc_state != MC_IDLE) {
mc_save_slot(mc_slot);
mc_state = MC_IDLE; /* working vars idle while pad talks */
}
active_device = DEV_PAD;
pad_process_byte(tx_byte);
} else if (tx_byte == 0x81) {
/* Card select. Load this slot's state. If the slot already
* has an in-flight protocol, the 0x81 restarts it (abort). */
mc_load_slot(selected_slot);
if (mc_state != MC_IDLE) {
sio_mc_abort_count++;
sio_mc_abort_state = (int)mc_state;
sio_mc_abort_ctrl = sio_ctrl;
/* Old txn (still open) gets force-closed before the new
* 0x81 starts its own. */
txn_close(SIO_TXN_END_ABORT_RESELECT, mc_state, g_debug_current_func_addr);
mc_state = MC_IDLE;
}
active_device = DEV_MEMCARD;
mc_slot = selected_slot;
sio_mc_probe_count++;
sio_mc_last_caller = g_debug_current_func_addr;
if (!sio_txn_open) {
txn_open((uint8_t)selected_slot, sio_trace_seq, g_debug_current_func_addr);
}
txn_pre_state = (uint8_t)mc_state;
txn_was_card_byte = 1;
mc_process_byte(tx_byte);
} else {
/* Non-select byte with DEV_NONE — only resume a card protocol
* if the CTRL slot matches the slot that started the card
* transaction. Otherwise this is stray data (e.g. pad bytes
* that leaked through). */
mc_load_slot(selected_slot);
if (mc_state != MC_IDLE && selected_slot == mc_slot) {
active_device = DEV_MEMCARD;
/* Continuation of an in-flight card txn across deselect/
* reselect. Re-open ring entry if we don't already have
* one (rare: pad polling closed it via mc_save_slot). */
if (!sio_txn_open) {
txn_open((uint8_t)mc_slot, sio_trace_seq, g_debug_current_func_addr);
}
txn_pre_state = (uint8_t)mc_state;
txn_was_card_byte = 1;
mc_process_byte(tx_byte);
} else {
if (mc_state != MC_IDLE) {
/* Slot-mismatch reset: the slot's saved card state is
* abandoned because the BIOS aimed at a different slot
* with a non-select byte. Close the txn as ABORT_SLOT. */
txn_close(SIO_TXN_END_ABORT_SLOT, mc_state, g_debug_current_func_addr);
}
mc_state = MC_IDLE;
sio_rx_data = 0xFF;
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
}
} else if (active_device == DEV_PAD) {
pad_process_byte(tx_byte);
} else if (active_device == DEV_MEMCARD) {
if (mc_state == MC_IDLE) {
/* Card protocol finished or was aborted. Only start a new
* card transaction if this is actually 0x81 (card select).
* Any other byte means the BIOS moved on to a different
* device (pad polling sends 0x01 here). */
if (tx_byte == 0x81) {
/* New txn after a previous one closed naturally. */
if (!sio_txn_open) {
txn_open((uint8_t)mc_slot, sio_trace_seq, g_debug_current_func_addr);
}
txn_pre_state = (uint8_t)mc_state;
txn_was_card_byte = 1;
mc_process_byte(tx_byte);
} else {
active_device = DEV_NONE;
selected_slot = (sio_ctrl & SIO_CTRL_SLOT) ? 1 : 0;
if (tx_byte == 0x01) {
active_device = DEV_PAD;
pad_process_byte(tx_byte);
} else {
sio_rx_data = 0xFF;
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
}
} else {
txn_pre_state = (uint8_t)mc_state;
txn_was_card_byte = 1;
mc_process_byte(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_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,
got_ack, sio_trace_seq);
/* Natural close: mc_state went IDLE this byte from non-IDLE. */
if (mc_state == MC_IDLE && txn_pre_state != MC_IDLE) {
uint8_t reason;
switch (txn_pre_state) {
case MC_READ_END:
case MC_WRITE_END:
case MC_GETID_4:
reason = SIO_TXN_END_SUCCESS;
break;
case MC_CMD:
/* Bad cmd byte: mc_process_byte resets to IDLE. */
reason = SIO_TXN_END_ABORT_BAD_CMD;
break;
default:
reason = SIO_TXN_END_ABORT_OTHER;
break;
}
txn_close(reason, txn_pre_state, g_debug_current_func_addr);
}
}
/* ---- Trace: capture post-state and record entry ---- */
{
SioTraceEntry *e = &sio_trace_buf[sio_trace_idx];
e->seq = sio_trace_seq;
e->tx = tx_byte;
e->rx = sio_rx_data;
e->mc_state_pre = trace_mc_pre;
e->mc_state_post = (uint8_t)mc_state;
e->dev_pre = trace_dev_pre;
e->dev_post = (uint8_t)active_device;
e->ctrl = sio_ctrl;
e->func_addr = g_debug_current_func_addr;
{ extern uint32_t memory_get_sr(void);
e->cop0_sr = memory_get_sr(); }
e->was_abort = (sio_mc_abort_count > trace_abort_before) ? 1 : 0;
e->irq_countdown = trace_irq_cd;
{ extern int psx_get_in_exception(void);
e->in_exception = (uint8_t)psx_get_in_exception(); }
{ extern uint8_t psx_read_byte(uint32_t addr);
/* Read card counter 0x7514 — low byte only for trace */
e->counter_7514 = psx_read_byte(0x7514); }
e->slot0_state = (uint8_t)mc_slots[0].state;
e->slot1_state = (uint8_t)mc_slots[1].state;
sio_trace_idx = (sio_trace_idx + 1) % SIO_TRACE_CAP;
sio_trace_seq++;
}
}
uint32_t sio_read(uint32_t addr) {
sio_debug_poll_maybe();
/* Advance delayed IRQ on every SIO register access.
* In v4, recompiled functions run as native C without per-instruction
* stepping. sio_tick() from the dispatch loop won't advance during
* a tight BIOS polling loop within a single function. Advancing on
* register access ensures the IRQ fires in time for the BIOS's
* "clear, delay, check" card detection sequence. */
sio_tick(0);
switch (addr) {
case 0x1F801040: /* SIO_RX_DATA */ {
uint8_t b = sio_rx_data;
sio_stat &= ~SIO_STAT_RX_RDY;
sr_record(SR_EVT_RX_DATA_READ, 0, b);
if (active_device == DEV_MEMCARD &&
mc_state >= MC_READ_DATA && mc_state <= MC_READ_END) {
extern void card_data_writes_arm(uint8_t value,
uint16_t mc_state,
uint8_t mc_data_idx,
uint8_t slot);
card_data_writes_arm(b, (uint16_t)mc_state,
mc_data_idx, (uint8_t)mc_slot);
}
return b;
}
case 0x1F801044: { /* SIO_STAT — record only on value transitions */
static uint16_t s_last_stat_observed = 0xFFFF;
uint16_t observed = sio_stat;
if (sio_stat != s_last_stat_observed) {
s_last_stat_observed = sio_stat;
sr_record(SR_EVT_STAT_READ, 0, 0);
}
if (sio_ack_visible_reads > 0 && --sio_ack_visible_reads == 0) {
sio_stat &= ~SIO_STAT_ACK;
}
return observed;
}
case 0x1F801048: /* SIO_MODE */
return sio_mode;
case 0x1F80104A: /* SIO_CTRL */
return sio_ctrl;
case 0x1F80104E: /* SIO_BAUD */
return sio_baud;
default:
return 0;
}
}
/* Side-effect-FREE SIO register peeks for the debug/observability path ONLY.
* sio_read() deliberately has guest-visible side effects (sio_tick advances the
* delayed-IRQ state machine; reading SIO_STAT decrements sio_ack_visible_reads
* and clears the ACK bit; reading SIO_RX_DATA pops the RX FIFO / clears RX_RDY).
* Those are correct for the GUEST bus, but the TCP debug server reading SIO every
* vblank (record_frame) or per query (sio_state) MUST NOT perturb the SIO/pad
* handshake — that is the observer corrupting the observed, and it desynced the
* DualShock handshake the Mega Man X engine depends on (dev builds skipped the
* boot FMV; release, which never records frames, played it). Read raw state. */
uint16_t sio_peek_stat(void) { return sio_stat; }
uint16_t sio_peek_ctrl(void) { return sio_ctrl; }
uint8_t sio_peek_rx_data(void) { return sio_rx_data; }
void sio_write(uint32_t addr, uint32_t value) {
sio_debug_poll_maybe();
/* Advance delayed IRQ AFTER write processing (not before).
* Ticking before a CTRL ACK write would fire the pending IRQ
* right before the CTRL clears it — wrong order. */
/* Capture writing PC (set by recompiler before every store) into the
* SIO PC tracer ring before the write actually takes effect. */
debug_server_log_sio_write(addr, value, 4);
switch (addr) {
case 0x1F801040: /* SIO_TX_DATA */
sio_tx_data = (uint8_t)value;
sio_tx_writes++;
sio_last_ctrl_on_tx = sio_ctrl;
{
uint8_t hb = (uint8_t)value;
/* Arm handoff on any card select so probe ACKs are visible too. */
if (hb == 0x81 && (sio_ctrl & SIO_CTRL_SELECT))
s_card_handoff_armed = 1;
if (s_card_handoff_armed &&
(hb == 0x81 || hb == 0x52 || hb == 0x57 || hb == 0x53))
card_handoff_push(4, hb);
}
if (!(sio_ctrl & SIO_CTRL_TX_EN)) {
sio_tx_gated++;
sr_record(SR_EVT_TX_DATA_WRITE, (uint8_t)value, 0);
break;
}
#if SIO_MODEL_CYCLE_PACED
/* Cycle-paced TX. Pad and card share single bus. */
{
uint8_t b = (uint8_t)value;
/* FAITHFUL pad RX pacing (fixes dead pads under OpenBIOS). The pad
* "fast path" below processes the byte SYNCHRONOUSLY at the TX
* write: RX_RDY asserts instantly, while the ACK IRQ is scheduled
* BAUD+ACK (1088+170) cycles out. Real hardware asserts RX_RDY only
* when the byte has SHIFTED (~1088 cycles); the /ACK pulse lands
* ~170 cycles after that. A driver that waits on RXRDY (absorbing
* the shift time) and then spins a SHORT bounded loop on I_STAT.7 —
* OpenBIOS readPad's 0x50-iteration wait — starts that spin ~1000
* cycles early against the synchronous model and TIMES OUT: every
* poll aborts with padBuffer[0]=0xFF and the game sees a dead pad
* (byte 1 survived only by its extra busyloops; byte 2 aborted —
* sio_trace showed exactly 0x01,0x42 then abandon, one ack/poll).
* Default: route pads through the same cycle-paced shifter the
* memcard uses (RX_RDY at shift-complete, ack +170 after). Total
* ack-IRQ latency is UNCHANGED (1088+170), so the SCPH1001
* pad-detect phase the constants were tuned for is preserved.
* A/B: PSX_SIO_PAD_SYNC_RX=1 restores the legacy synchronous path. */
static int s_pad_sync_rx = -1;
if (s_pad_sync_rx < 0) {
const char *e = getenv("PSX_SIO_PAD_SYNC_RX");
s_pad_sync_rx = (e && e[0] == '1') ? 1 : 0;
}
int pad_fast_path = s_pad_sync_rx &&
active_device != DEV_MEMCARD &&
(active_device == DEV_PAD ||
sio_bus_owner == SIO_OWNER_PAD ||
b == 0x01);
if (pad_fast_path) {
uint16_t arm_dbg_ctrl_pre = sio_ctrl;
uint16_t arm_dbg_stat_pre = sio_stat;
sio_shift_active = 0;
sio_shift_remaining = 0;
sio_tx_buffered = 0;
sio_shift_ack_irq_en = 0;
sio_tx_buffer_ack_irq_en = 0;
sio_pending_ack = 0;
sio_ack_remaining = 0;
sio_pending_ack_irq_en = 0;
sio_bus_owner = SIO_OWNER_PAD;
sio_bus_byte_index++;
g_sio_timing_active = 0;
sr_record(SR_EVT_TX_DATA_WRITE, b, 0);
sio_process_byte(b);
uint8_t arm_dbg_dev_at_decision = (uint8_t)active_device;
int armed_now = 0;
if ((sio_stat & SIO_STAT_ACK) && (sio_ctrl & SIO_CTRL_ACK_IRQ_EN)) {
sio_stat &= ~(SIO_STAT_ACK | SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY);
/* FAITHFUL pad ACK timing (faithful-core, replaces the
* access-paced sio_irq_countdown=SIO_IRQ_DELAY_PAD hack).
* On real hardware the controller's DSR/ACK pulse arrives
* ~one byte-shift (baud) + ack-pulse after the TX byte —
* a GUEST-CYCLE delay, independent of how many SIO register
* reads the CPU manages to issue while it waits. The old
* countdown decremented once per SIO register access
* (sio_tick(0)), so a faithful/faster CPU (load=4) fired the
* pad IRQ at the wrong guest-cycle phase relative to the
* cycle-paced timers/VBLANK, diverging the BIOS pad-detect
* state machine (load=4 Tomba2 boot wedge). Route through the
* same cycle-paced ack scheduler the card path uses, driven
* by sio_advance() from psx_advance_cycles(). */
sio_pending_ack = 1;
sio_ack_remaining = SIO_BAUD_CYCLES_DEFAULT + SIO_ACK_CYCLES_DEFAULT;
sio_pending_ack_irq_en = 1;
g_sio_timing_active = 1;
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_SIO, (uint32_t)sio_ack_remaining);
sio_irq_pending_source = SIO_IRQ_SRC_PAD_ACK;
sio_irq_pending_slot = (uint8_t)selected_slot;
sio_irq_pending_delay = (uint8_t)SIO_ACK_CYCLES_DEFAULT;
sio_irq_pending_mc_state = (uint8_t)mc_state;
sio_irq_pending_byte_seq = sio_trace_seq;
armed_now = 1;
} else {
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
sio_card_arm_audit_record(arm_dbg_dev_at_decision, arm_dbg_ctrl_pre,
arm_dbg_stat_pre, sio_stat,
armed_now, sio_irq_countdown);
break;
}
sr_record(SR_EVT_TX_DATA_WRITE, b, 0);
if (sio_bus_byte_index == 0) {
sio_bus_owner = (b == 0x81) ? SIO_OWNER_CARD
: (b == 0x01) ? SIO_OWNER_PAD
: SIO_OWNER_UNKNOWN;
}
sio_bus_byte_index++;
if (!sio_shift_active) {
sio_shift_byte = b;
sio_shift_active = 1;
sio_shift_remaining = SIO_BAUD_CYCLES_DEFAULT;
sio_shift_ack_irq_en = (sio_ctrl & SIO_CTRL_ACK_IRQ_EN) ? 1 : 0;
sio_stat &= ~(SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY);
g_sio_timing_active = 1;
sr_record(SR_EVT_SHIFT_START, b, 0);
} else if (!sio_tx_buffered) {
sio_tx_buffer = b;
sio_tx_buffered = 1;
sio_tx_buffer_ack_irq_en = (sio_ctrl & SIO_CTRL_ACK_IRQ_EN) ? 1 : 0;
sio_stat &= ~SIO_STAT_TX_EMPTY;
s_pace_tx_writes_buffered++;
sr_record(SR_EVT_BUFFER_LOAD, b, 0);
} else {
s_pace_tx_writes_dropped_busy++;
sr_record(SR_EVT_TX_DROPPED, b, 0);
}
}
#else
{
uint16_t arm_dbg_ctrl_pre = sio_ctrl;
uint16_t arm_dbg_stat_pre = sio_stat;
sio_process_byte(sio_tx_data);
uint8_t arm_dbg_dev_at_decision = (uint8_t)active_device;
sio_stat &= ~(SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY);
extern void sio_card_arm_audit_record(int dev, uint16_t ctrl_pre,
uint16_t stat_pre, uint16_t stat_post,
int armed, int countdown_after);
int armed_now = 0;
if ((sio_stat & SIO_STAT_ACK) && (sio_ctrl & SIO_CTRL_ACK_IRQ_EN)) {
sio_stat &= ~SIO_STAT_ACK;
sio_irq_pending = 1;
sio_irq_countdown = (active_device == DEV_MEMCARD)
? SIO_IRQ_DELAY_CARD : SIO_IRQ_DELAY_PAD;
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_SIO, (uint32_t)sio_irq_countdown);
sio_irq_pending_source = (active_device == DEV_MEMCARD)
? SIO_IRQ_SRC_CARD_ACK : SIO_IRQ_SRC_PAD_ACK;
sio_irq_pending_slot = (uint8_t)selected_slot;
sio_irq_pending_delay = (uint8_t)sio_irq_countdown;
sio_irq_pending_mc_state = (uint8_t)mc_state;
sio_irq_pending_byte_seq = sio_trace_seq;
armed_now = 1;
}
sio_card_arm_audit_record(arm_dbg_dev_at_decision, arm_dbg_ctrl_pre,
arm_dbg_stat_pre, sio_stat,
armed_now, sio_irq_countdown);
}
#endif
break;
case 0x1F801048: /* SIO_MODE */
sio_mode = (uint16_t)value;
sr_record(SR_EVT_MODE_WRITE, (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF));
break;
case 0x1F80104A: /* SIO_CTRL */ {
uint16_t old_ctrl = sio_ctrl;
sio_ctrl = (uint16_t)value;
sr_record(SR_EVT_CTRL_WRITE, (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF));
if (value & SIO_CTRL_ACK) {
sio_stat &= ~SIO_STAT_IRQ;
sio_stat &= ~SIO_STAT_ACK;
sio_ack_visible_reads = 0;
}
if (value & SIO_CTRL_RESET) {
if (mc_state != MC_IDLE) {
sio_mc_abort_count++;
sio_mc_abort_state = (int)mc_state;
sio_mc_abort_ctrl = (uint16_t)value;
}
if (sio_txn_open) {
extern uint32_t g_debug_current_func_addr;
txn_close(SIO_TXN_END_ABORT_RESET, mc_state, g_debug_current_func_addr);
}
sio_stat = SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
sio_mode = 0;
sio_ctrl = 0;
sio_baud = 0;
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
mc_state = MC_IDLE;
mc_slots[0].state = MC_IDLE;
mc_slots[1].state = MC_IDLE;
active_device = DEV_NONE;
sio_irq_pending = 0;
sio_irq_countdown = 0;
sio_ack_visible_reads = 0;
#if SIO_MODEL_CYCLE_PACED
sio_shift_active = 0; sio_shift_remaining = 0;
sio_tx_buffered = 0;
sio_shift_ack_irq_en = 0; sio_tx_buffer_ack_irq_en = 0;
sio_pending_ack = 0; sio_ack_remaining = 0;
sio_pending_ack_irq_en = 0;
sio_bus_owner = SIO_OWNER_NONE; sio_bus_byte_index = 0;
g_sio_timing_active = 0;
#endif
sr_record(SR_EVT_RESET, 0, 0);
}
#if SIO_MODEL_CYCLE_PACED
if (!(old_ctrl & SIO_CTRL_SELECT) && (value & SIO_CTRL_SELECT)) {
sio_bus_owner = SIO_OWNER_NONE;
sio_bus_byte_index = 0;
sr_record(SR_EVT_SELECT_ASSERT, 0, 0);
}
/* TX_EN 1→0 transition kills any in-flight shifter the same
* way SELECT-deassert does. Restore TX_RDY/TX_EMPTY so the
* SIO returns to an idle status word, matching the recovery
* the SELECT-deassert path now performs. */
if ((old_ctrl & SIO_CTRL_TX_EN) && !(value & SIO_CTRL_TX_EN)) {
if (sio_shift_active || sio_tx_buffered || sio_pending_ack) {
sio_shift_active = 0; sio_shift_remaining = 0;
sio_tx_buffered = 0;
sio_shift_ack_irq_en = 0; sio_tx_buffer_ack_irq_en = 0;
sio_pending_ack = 0; sio_ack_remaining = 0;
sio_pending_ack_irq_en = 0;
if (!(value & SIO_CTRL_SELECT)) {
sio_bus_owner = SIO_OWNER_NONE;
sio_bus_byte_index = 0;
}
g_sio_timing_active = 0;
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
}
#endif
/* On SELECT deassert: reset all volatile protocol state.
*
* Earlier model preserved mc_state into mc_slots[] so a card
* protocol could resume across brief SELECT drops (e.g. CTRL
* ACK writes during pad polling). Audit confirmed this caused
* EVERY new 0x81 after a SELECT-deassert to land on a saved
* non-IDLE state, triggering abort_reselect bookkeeping. The
* abort path itself resets mc_state, so the BIOS-visible RX is
* still correct — but the simulated card retaining sector/cmd/
* counter context across what BIOS treats as independent
* transactions is wrong per the no$psx model: real cards
* restart on every fresh 0x81. SELECT is a bus-wide signal;
* both physical cards see the deassert and reset their state
* machines simultaneously.
*
* Cleared: state, cmd, sector, sector_msb, sector_lsb,
* data_idx, checksum (volatile protocol state).
* Preserved: flag (persistent "new card" metadata, only cleared
* after first read/write completes; carries across
* SELECT drops on real hardware) and data (sector
* buffer; harmless to retain since the next read
* overwrites it). */
if ((old_ctrl & SIO_CTRL_SELECT) && !(value & SIO_CTRL_SELECT)) {
/* SELECT deassertion cancels the device's pending DSR pulse.
* It cannot create an ACK or preserve one for later delivery. */
if (active_device == DEV_MEMCARD && mc_state != MC_IDLE && sio_txn_open) {
extern uint32_t g_debug_current_func_addr;
txn_close(SIO_TXN_END_ABORT_OTHER, mc_state, g_debug_current_func_addr);
}
/* Persist mc_flag back to the active slot before resetting. mc_flag
* was loaded from mc_slots[mc_slot].flag on the 0x81 select and
* cleared to 0x00 in MC_CMD on the first 0x52/0x57/0x53. Without
* writing it back here, every reselect re-loads the stale 0x08
* "new card" value, and the BIOS aborts every multi-sector read
* after the first one (sees FLAG=0x08 → "card was just inserted,
* re-init" → 2-byte 0x81-0x52 abort). */
if (active_device == DEV_MEMCARD && mc_slot >= 0 && mc_slot <= 1) {
mc_slots[mc_slot].flag = mc_flag;
}
mc_state = MC_IDLE;
for (int i = 0; i < 2; i++) {
mc_slots[i].state = MC_IDLE;
mc_slots[i].cmd = 0;
mc_slots[i].sector = 0;
mc_slots[i].sector_msb = 0;
mc_slots[i].sector_lsb = 0;
mc_slots[i].data_idx = 0;
mc_slots[i].checksum = 0;
/* keep mc_slots[i].flag, mc_slots[i].data */
}
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
active_device = DEV_NONE;
#if SIO_MODEL_CYCLE_PACED
sio_shift_active = 0; sio_shift_remaining = 0;
sio_tx_buffered = 0;
sio_shift_ack_irq_en = 0; sio_tx_buffer_ack_irq_en = 0;
sio_pending_ack = 0; sio_ack_remaining = 0;
sio_pending_ack_irq_en = 0;
sio_bus_owner = SIO_OWNER_NONE; sio_bus_byte_index = 0;
g_sio_timing_active = 0;
/* Killing an in-flight shifter mid-cycle leaves sio_stat
* with TX_RDY/TX_EMPTY clear (they were masked by
* SHIFT_START and only sio_handle_shift_complete restores
* them). Restore here so the next handler sees an idle
* SIO instead of busy-waiting forever on TX_RDY=0. */
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
#endif
sr_record(SR_EVT_SELECT_DEASS, 0, 0);
}
break;
}
case 0x1F80104E: /* SIO_BAUD */
sio_baud = (uint16_t)value;
sr_record(SR_EVT_BAUD_WRITE, (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF));
break;
default:
break;
}
/* Advance delayed IRQ after write processing (not before).
* This ensures CTRL ACK writes clear the old IRQ before the
* pending one fires. */
sio_tick(0);
}
void sio_get_freeze_diag(int *out_irq_pending, int *out_irq_countdown,
uint16_t *out_sio_stat, uint16_t *out_sio_ctrl,
int *out_card_active) {
if (out_irq_pending) *out_irq_pending = sio_irq_pending;
if (out_irq_countdown) *out_irq_countdown = sio_irq_countdown;
if (out_sio_stat) *out_sio_stat = sio_stat;
if (out_sio_ctrl) *out_sio_ctrl = sio_ctrl;
if (out_card_active) *out_card_active = sio_card_protocol_active();
}
/* ---- Bounded SIO IRQ burst drain (active card data phase only) ---------
* Approved as the FIRST step on the path away from "1 byte/VBlank". This
* is NOT the cycle-paced rewrite. It is a transient measure that, while
* a card transfer is in-flight, drains the access-paced countdown so the
* IRQ that would have stalled until the next pad poll instead fires now.
*
* Invariants:
* - Only invoked from sio_write SIO_TX_DATA, after the IRQ-arm decision.
* - Caller guarantees active_device==DEV_MEMCARD and !in_exception.
* - Hard cap on iterations (default 128).
* - Break early if (a) no IRQ pending and countdown idle, OR
* (b) mc_state == MC_IDLE (transaction finished/aborted).
* - Each iteration calls sio_tick() ONCE. sio_tick already handles "fire
* when countdown==0" and updates i_stat / sio_stat / IRQ ring atomically.
* - sio_tick does not enter the BIOS exception. The BIOS exception fires
* later in the dispatch loop when (i_stat & i_mask & !in_exception).
*
* Stats (always-on, exposed via sio_burst_stats TCP cmd) so we can see if
* the bound is being hit, what the typical iter count is, and which break
* reason dominates. */
typedef struct {
uint64_t calls; /* total burst invocations */
uint64_t iters_total; /* total sio_tick iterations across all calls */
uint32_t iter_max_seen; /* max iters in a single call */
uint64_t break_idle; /* broke because pending=0 && countdown=0 */
uint64_t break_mode_clear; /* broke because mc_state==MC_IDLE */
uint64_t break_capped; /* broke at max_iters cap */
uint32_t fires_in_burst; /* IRQ fires that occurred during a burst */
uint32_t last_iters; /* iterations on most recent call */
uint8_t last_break_reason;/* 1=idle, 2=mode_clear, 3=capped, 0=skip */
} SioBurstStats;
static SioBurstStats s_burst_stats;
static uint32_t s_irq_seq_at_burst_start = 0;
extern uint8_t psx_read_byte(uint32_t addr);
extern int psx_get_in_exception(void);
static int sio_card_burst_drain(int max_iters) {
if (max_iters <= 0) max_iters = 128;
if (max_iters > 1024) max_iters = 1024;
s_burst_stats.calls++;
uint32_t fires_before = sio_irq_seq;
int iters = 0;
uint8_t reason = 1; /* default: idle break */
for (; iters < max_iters; iters++) {
/* Idle check (do BEFORE the tick — if there's nothing pending and
* countdown is 0, ticking is a no-op anyway). */
if (!sio_irq_pending && sio_irq_countdown == 0) {
reason = 1;
break;
}
/* Card protocol returned to IDLE — transaction finished or aborted.
* mc_state is the authoritative "transfer in flight" signal; the
* BIOS-RAM word [0x75C0] is only set during the data-byte phase
* (after the address handshake), so checking it here would have
* blocked bursting through the SELECT/CMD/ADDR setup bytes that
* precede the data phase. */
if (mc_state == MC_IDLE) {
reason = 2;
break;
}
sio_tick(0);
}
if (iters == max_iters) reason = 3;
s_burst_stats.iters_total += (uint64_t)iters;
if ((uint32_t)iters > s_burst_stats.iter_max_seen) s_burst_stats.iter_max_seen = (uint32_t)iters;
if (reason == 1) s_burst_stats.break_idle++;
else if (reason == 2) s_burst_stats.break_mode_clear++;
else if (reason == 3) s_burst_stats.break_capped++;
s_burst_stats.last_iters = (uint32_t)iters;
s_burst_stats.last_break_reason = reason;
s_burst_stats.fires_in_burst += (uint32_t)(sio_irq_seq - fires_before);
return iters;
}
void sio_get_burst_stats(uint64_t out[10]) {
out[0] = s_burst_stats.calls;
out[1] = s_burst_stats.iters_total;
out[2] = (uint64_t)s_burst_stats.iter_max_seen;
out[3] = s_burst_stats.break_idle;
out[4] = s_burst_stats.break_mode_clear;
out[5] = s_burst_stats.break_capped;
out[6] = (uint64_t)s_burst_stats.fires_in_burst;
out[7] = (uint64_t)s_burst_stats.last_iters;
out[8] = (uint64_t)s_burst_stats.last_break_reason;
out[9] = 0;
}
/* Phase 1.0c-v2: flip default to 1, but gate the dispatch-loop quantum
* tick by g_sio_timing_active so the per-call cost on the
* psx_check_interrupts hot path is one load + one branch when no SIO
* cycle-paced work is pending. TX path remains synchronous in 1.0c-v2
* — nothing sets g_sio_timing_active, so the gate stays 0 and
* sio_tick_quantum is never invoked. Phase 1.0d will reroute TX into
* the shifter and arm the guard.
*
* Macro=0 still available to revert to legacy entirely. */
#ifndef SIO_MODEL_CYCLE_PACED
#define SIO_MODEL_CYCLE_PACED 1
#endif
/* Cycle-paced SIO state moved above sio_write (after forward decls). */
#if SIO_MODEL_CYCLE_PACED
/* ---- Phase 1.0c-v2: cycle-paced sio_tick helpers (inert in 1.0c-v2) ---
*
* In 1.0c-v2, the TX path is still synchronous and nothing arms
* sio_shift_active or sio_pending_ack. Therefore these helpers exist
* but are never reached at runtime: sio_tick_quantum is gated by
* g_sio_timing_active (which stays 0), and even if called the cycle-
* paced loop in sio_tick would find no events to fire. */
static int sio_consume_ack_event(void) {
int acked = (sio_stat & SIO_STAT_ACK) ? 1 : 0;
sio_stat &= ~SIO_STAT_ACK;
return acked;
}
static void sio_fire_ack_irq(void) {
/* I_STAT sources are owned per device: an SIO0 card ACK may only raise
* bit 7. It must never clear SPU (bit 9) — a pending SPU IRQ stays
* pending until the guest performs the SPU/INTC acknowledgement. The
* prior netplay-parity experiment that dropped bit 9 here consumed the
* SPU interrupt during card scans and silenced SPU-IRQ-driven audio. */
int card_ack = (sio_irq_pending_source == SIO_IRQ_SRC_CARD_ACK ||
active_device == DEV_MEMCARD);
sio_stat |= SIO_STAT_ACK;
sio_ack_visible_reads = 2;
sr_record(SR_EVT_ACK_FIRE, 0, 0);
int irq_enabled = sio_pending_ack_irq_en
|| ((sio_ctrl & SIO_CTRL_ACK_IRQ_EN) ? 1 : 0);
if (!irq_enabled) {
sio_pending_ack_irq_en = 0;
return;
}
/* The device IRQ latch is independent of INTC acknowledgement. Do not
* queue an ACK for replay when the guest later clears I_STAT.7. */
sio_pending_ack_irq_en = 0;
sio_stat |= SIO_STAT_IRQ;
uint32_t i_stat_before = i_stat;
psx_irq_raise(IRQ_SIO0, 0); /* SIO ACK IRQ */
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_SIO, 0u); /* SIO ACK IRQ fired */
if (card_ack) {
if (s_card_handoff_armed)
card_handoff_push(5, 0);
}
extern uint32_t g_debug_current_func_addr;
extern uint8_t psx_read_byte(uint32_t addr);
SioIrqEntry *e = &sio_irq_buf[sio_irq_idx];
e->seq = sio_irq_seq;
e->byte_seq = sio_irq_pending_byte_seq;
e->i_stat_before = i_stat_before;
e->i_stat_after = i_stat;
e->mc_state = (uint32_t)sio_irq_pending_mc_state;
e->active_device = (uint32_t)active_device;
e->ctrl = (uint32_t)sio_ctrl;
e->func_addr = g_debug_current_func_addr;
e->counter_7514 = (uint32_t)psx_read_byte(0x7514);
e->source = sio_irq_pending_source;
e->slot = sio_irq_pending_slot;
e->delay_applied = sio_irq_pending_delay;
sio_irq_idx = (sio_irq_idx + 1) % SIO_IRQ_RING_CAP;
sio_irq_seq++;
s_pace_ack_fires++;
if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack) {
g_sio_timing_active = 0;
}
}
static void sio_handle_shift_complete(void) {
uint8_t b = sio_shift_byte;
sr_record(SR_EVT_SHIFT_DONE, b, 0);
if (b == 0x01 && mc_state >= MC_READ_DATA && mc_state <= MC_READ_END) {
s_pace_pad_byte_processed_in_card_data++;
}
sio_process_byte(b);
int acked = sio_consume_ack_event();
sio_shift_active = 0;
sio_stat |= SIO_STAT_RX_RDY;
s_pace_shift_completes++;
sio_irq_pending_source = (active_device == DEV_MEMCARD)
? SIO_IRQ_SRC_CARD_ACK : SIO_IRQ_SRC_PAD_ACK;
sio_irq_pending_slot = (uint8_t)selected_slot;
sio_irq_pending_delay = (uint8_t)SIO_ACK_CYCLES_DEFAULT;
sio_irq_pending_mc_state = (uint8_t)mc_state;
sio_irq_pending_byte_seq = sio_trace_seq;
if (acked) {
sio_pending_ack = 1;
sio_ack_remaining = SIO_ACK_CYCLES_DEFAULT;
sio_pending_ack_irq_en = sio_shift_ack_irq_en;
}
if (sio_tx_buffered) {
sio_shift_byte = sio_tx_buffer;
sio_shift_active = 1;
sio_shift_remaining = SIO_BAUD_CYCLES_DEFAULT;
sio_shift_ack_irq_en = sio_tx_buffer_ack_irq_en;
sio_tx_buffered = 0;
sio_tx_buffer_ack_irq_en = 0;
sio_stat |= SIO_STAT_TX_EMPTY;
sio_bus_byte_index++;
s_pace_tx_buffer_promoted++;
if (sio_bus_owner == SIO_OWNER_CARD)
s_pace_tx_buffer_promoted_during_card++;
} else {
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack) {
g_sio_timing_active = 0;
}
}
/* Phase 1.0e-e1: peripheral-only advance.
*
* Drives the cycle-paced shifter+ack scheduler ONLY. Does NOT touch the
* legacy sio_irq_pending/sio_irq_countdown — that's the unconditional-
* decrement that broke pad timing when called from psx_advance_cycles
* in earlier slices. Until the TX path is rerouted (1.0e-e2), nothing
* arms the shifter, so this body returns immediately at the
* g_sio_timing_active==0 gate without doing any work. */
static uint64_t s_sio_advance_called = 0;
static uint64_t s_sio_advance_with_work = 0;
uint64_t sio_get_advance_called(void) { return s_sio_advance_called; }
uint64_t sio_get_advance_with_work(void) { return s_sio_advance_with_work; }
/* Consume every elapsed device deadline. Device time cannot stop after one
* transition merely to space interrupts for a particular guest handler. */
static void sio_pace_walk(int cycles) {
int remaining = cycles;
while (remaining > 0 ||
(sio_shift_active && sio_shift_remaining <= 0) ||
(sio_pending_ack && sio_ack_remaining <= 0)) {
int dt = remaining;
int next_event = -1;
if (sio_shift_active && sio_shift_remaining > 0
&& sio_shift_remaining <= dt) {
dt = sio_shift_remaining;
next_event = 0;
}
if (sio_pending_ack && sio_ack_remaining > 0
&& (sio_ack_remaining < dt ||
(sio_ack_remaining == dt && next_event < 0))) {
dt = sio_ack_remaining;
next_event = 1;
}
if (sio_shift_active && sio_shift_remaining <= 0) {
dt = 0; next_event = 0;
} else if (sio_pending_ack && sio_ack_remaining <= 0) {
dt = 0; next_event = 1;
}
if (sio_shift_active) sio_shift_remaining -= dt;
if (sio_pending_ack) sio_ack_remaining -= dt;
remaining -= dt;
if (next_event == 0) {
sio_handle_shift_complete();
} else if (next_event == 1) {
sio_pending_ack = 0;
sio_fire_ack_irq();
} else {
break;
}
}
}
void sio_advance(uint32_t cycles) {
if (cycles == 0) return;
s_sio_advance_called++;
if (!g_sio_timing_active) return;
s_sio_advance_with_work++;
sio_pace_walk((int)cycles);
}
#else
/* Macro=0: stubs for ABI symmetry. */
void sio_advance(uint32_t cycles) { (void)cycles; }
uint64_t sio_get_advance_called(void) { return 0; }
uint64_t sio_get_advance_with_work(void) { return 0; }
#endif /* SIO_MODEL_CYCLE_PACED */
void sio_tick(int cycles) {
#if SIO_MODEL_CYCLE_PACED
/* Access-paced callers (I_STAT / SIO MMIO) pass 0. Master never walked
* the cycle-paced shifter on those edges. Flushing overdue shift/ack
* from sio_tick(0) can raise the next card ACK inside the ISR that is
* still clearing the previous one.
* EXPERIMENT: was netplay-only MMIO flush (MotK peer parity); always
* skip tick(0) flush here for TM4 netplay desync testing. */
if (cycles > 0)
sio_pace_walk(cycles);
#else
(void)cycles;
#endif
/* Legacy access-paced IRQ countdown. Always live. In 1.0c-v2 this
* remains the actual IRQ delivery path because the TX path arms it.
* Phase 1.0d will stop arming this and rely on the cycle-paced
* shifter+ack path instead. */
if (sio_irq_pending && sio_irq_countdown > 0) {
sio_irq_countdown--;
if (sio_irq_countdown == 0) {
sio_irq_pending = 0;
sio_stat |= SIO_STAT_ACK;
sio_ack_visible_reads = 2;
sio_stat |= SIO_STAT_IRQ;
/* Restore TX_RDY — the byte transfer is complete.
* This unblocks the BIOS pad polling loop that spins
* on TX_RDY before writing the next byte. */
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
uint32_t i_stat_before = i_stat;
psx_irq_raise(IRQ_SIO0, 1); /* SIO shift IRQ */
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_SIO, 1u); /* SIO shift IRQ fired */
/* SIO IRQ ring capture. */
extern uint32_t g_debug_current_func_addr;
extern uint8_t psx_read_byte(uint32_t addr);
SioIrqEntry *e = &sio_irq_buf[sio_irq_idx];
e->seq = sio_irq_seq;
e->byte_seq = sio_irq_pending_byte_seq;
e->i_stat_before = i_stat_before;
e->i_stat_after = i_stat;
e->mc_state = (uint32_t)sio_irq_pending_mc_state;
e->active_device = (uint32_t)active_device;
e->ctrl = (uint32_t)sio_ctrl;
e->func_addr = g_debug_current_func_addr;
e->counter_7514 = (uint32_t)psx_read_byte(0x7514);
e->source = sio_irq_pending_source;
e->slot = sio_irq_pending_slot;
e->delay_applied = sio_irq_pending_delay;
sio_irq_idx = (sio_irq_idx + 1) % SIO_IRQ_RING_CAP;
sio_irq_seq++;
}
}
}
void sio_tick_quantum(void) {
#if SIO_MODEL_CYCLE_PACED
sio_tick(sio_tick_quantum_cycles);
#endif
}
/* Telemetry accessor for pace_state TCP probe. Read-only. */
void sio_get_pace_state(uint64_t out[16]) {
#if SIO_MODEL_CYCLE_PACED
out[ 0] = 1; /* sio_model = cycle_paced */
out[ 1] = (uint64_t)sio_tick_quantum_cycles;
out[ 2] = (uint64_t)sio_shift_active;
out[ 3] = (uint64_t)sio_shift_remaining;
out[ 4] = (uint64_t)sio_tx_buffered;
out[ 5] = (uint64_t)sio_pending_ack;
out[ 6] = (uint64_t)sio_ack_remaining;
out[ 7] = (uint64_t)sio_bus_owner;
out[ 8] = (uint64_t)sio_bus_byte_index;
out[ 9] = s_pace_tx_writes_buffered;
out[10] = s_pace_tx_writes_dropped_busy;
out[11] = s_pace_tx_writes_dropped_cross_device;
out[12] = s_pace_tx_buffer_promoted;
out[13] = s_pace_tx_buffer_promoted_during_card;
out[14] = s_pace_pad_byte_processed_in_card_data;
out[15] = s_pace_cross_device_pad_during_card;
#else
for (int i = 0; i < 16; i++) out[i] = 0;
#endif
}
/* ---- boot snapshot (LE field wire; McSlotState has host padding) ---- */
#include "pst_wire.h"
static int sio_w_mcslot(PstW *w, const McSlotState *s) {
return pst_w_u32(w, (uint32_t)s->state) && pst_w_u8(w, s->cmd) &&
pst_w_u16(w, s->sector) && pst_w_u8(w, s->sector_msb) &&
pst_w_u8(w, s->sector_lsb) && pst_w_bytes(w, s->data, 128) &&
pst_w_i32(w, (int32_t)s->data_idx) && pst_w_u8(w, s->checksum) &&
pst_w_u8(w, s->flag);
}
static int sio_r_mcslot(PstR *r, McSlotState *s) {
uint32_t st = 0;
int32_t di = 0;
if (!pst_r_u32(r, &st) || !pst_r_u8(r, &s->cmd) || !pst_r_u16(r, &s->sector) ||
!pst_r_u8(r, &s->sector_msb) || !pst_r_u8(r, &s->sector_lsb) ||
!pst_r_bytes(r, s->data, 128) || !pst_r_i32(r, &di) ||
!pst_r_u8(r, &s->checksum) || !pst_r_u8(r, &s->flag))
return 0;
s->state = (McState)st;
s->data_idx = (int)di;
return 1;
}
/* Snapshot wire is emitted in four sections so netplay diags can CRC each
* independently (regs / pads / memcard / shift+irq FSM) — a single SIO digest
* could not say WHICH subsystem forked a resim. Order and bytes unchanged. */
static int sio_snap_emit_regs(PstW *w) {
return pst_w_u8(w, sio_tx_data) && pst_w_u8(w, sio_rx_data) &&
pst_w_u16(w, sio_stat) && pst_w_u16(w, sio_mode) &&
pst_w_u16(w, sio_ctrl) && pst_w_u16(w, sio_baud);
}
static int sio_snap_emit_pads(PstW *w) {
/* Pad arrays sized by PSX_MAX_PLAYERS. Default MAX=2 keeps the historical
* 2-pad snap layout byte-identical. pad_response runtime buffer is larger
* for multitap bulk (34); snap still stores the first 8 bytes. */
if (!pst_w_bytes(w, pad_analog, PSX_MAX_PLAYERS) || !pst_w_u8(w, pad_connected) ||
!pst_w_u32(w, (uint32_t)pad_state) || !pst_w_i32(w, (int32_t)selected_slot) ||
!pst_w_bytes(w, pad_response, 8) || !pst_w_u8(w, pad_response_len) ||
!pst_w_u8(w, pad_response_idx) || !pst_w_u8(w, pad_current_cmd) ||
!pst_w_bytes(w, pad_in_config, PSX_MAX_PLAYERS))
return 0;
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
if (!pst_w_i16(w, (int16_t)pad_type_req[s]))
return 0;
}
return 1;
}
static int sio_snap_emit_mc(PstW *w) {
if (!pst_w_u32(w, (uint32_t)mc_state) || !pst_w_i32(w, (int32_t)mc_slot) ||
!pst_w_u8(w, mc_cmd) || !pst_w_u16(w, mc_sector) ||
!pst_w_u8(w, mc_sector_msb) || !pst_w_u8(w, mc_sector_lsb) ||
!pst_w_bytes(w, mc_data, 128) || !pst_w_i32(w, (int32_t)mc_data_idx) ||
!pst_w_u8(w, mc_checksum) || !pst_w_u8(w, mc_flag))
return 0;
if (!sio_w_mcslot(w, &mc_slots[0]) || !sio_w_mcslot(w, &mc_slots[1]))
return 0;
return pst_w_u32(w, (uint32_t)active_device);
}
/* Load-bearing pacing state (guest-visible IRQ timing). */
static int sio_snap_emit_fsm_pace(PstW *w) {
#if SIO_MODEL_CYCLE_PACED
if (!pst_w_i32(w, (int32_t)g_sio_timing_active) ||
!pst_w_i32(w, (int32_t)sio_shift_active) || !pst_w_u8(w, sio_shift_byte) ||
!pst_w_i32(w, (int32_t)sio_shift_remaining) ||
!pst_w_i32(w, (int32_t)sio_tx_buffered) || !pst_w_u8(w, sio_tx_buffer) ||
!pst_w_i32(w, (int32_t)sio_shift_ack_irq_en) ||
!pst_w_i32(w, (int32_t)sio_tx_buffer_ack_irq_en) ||
!pst_w_i32(w, (int32_t)sio_pending_ack) ||
!pst_w_i32(w, (int32_t)sio_ack_remaining) ||
!pst_w_i32(w, (int32_t)sio_pending_ack_irq_en) ||
!pst_w_u32(w, (uint32_t)sio_bus_owner) || !pst_w_u32(w, sio_bus_byte_index))
return 0;
#endif
return pst_w_i32(w, (int32_t)sio_irq_pending) &&
pst_w_i32(w, (int32_t)sio_irq_countdown) &&
pst_w_i32(w, (int32_t)sio_ack_visible_reads);
}
/* Audit/diag metadata — must not drive netplay digests (byte_seq tracks the
* host-local sio_trace_seq counter, which is not itself a guest register). */
static int sio_snap_emit_fsm_meta(PstW *w) {
return pst_w_u8(w, sio_irq_pending_source) && pst_w_u8(w, sio_irq_pending_slot) &&
pst_w_u8(w, sio_irq_pending_delay) && pst_w_u8(w, sio_irq_pending_mc_state) &&
pst_w_u32(w, sio_irq_pending_byte_seq);
}
/* DualShock rumble map/motors — appended after meta so pre-rumble snapshots
* remain loadable and netplay digests that stop at fsm_pace stay stable. */
static int sio_snap_emit_rumble(PstW *w) {
return pst_w_bytes(w, pad_rumble_map, sizeof(pad_rumble_map)) &&
pst_w_bytes(w, pad_rumble_small, sizeof(pad_rumble_small)) &&
pst_w_bytes(w, pad_rumble_large, sizeof(pad_rumble_large));
}
static int sio_snap_emit_fsm(PstW *w) {
return sio_snap_emit_fsm_pace(w) && sio_snap_emit_fsm_meta(w);
}
static int sio_snap_emit(PstW *w) {
return sio_snap_emit_regs(w) && sio_snap_emit_pads(w) &&
sio_snap_emit_mc(w) && sio_snap_emit_fsm(w) && sio_snap_emit_rumble(w);
}
/* Cumulative section end offsets in the snapshot wire:
* out[0]=regs, out[1]=pads, out[2]=memcard, out[3]=fsm_pace (netplay),
* out[4]=full including fsm_meta + rumble (== sio_snapshot_bytes()). */
void sio_snapshot_section_ends(uint32_t out[5]) {
PstW w;
pst_w_init(&w, NULL, 0);
(void)sio_snap_emit_regs(&w);
out[0] = (uint32_t)w.written;
(void)sio_snap_emit_pads(&w);
out[1] = (uint32_t)w.written;
(void)sio_snap_emit_mc(&w);
out[2] = (uint32_t)w.written;
(void)sio_snap_emit_fsm_pace(&w);
out[3] = (uint32_t)w.written;
(void)sio_snap_emit_fsm_meta(&w);
(void)sio_snap_emit_rumble(&w);
out[4] = (uint32_t)w.written;
}
static int sio_snap_parse(PstR *r) {
uint32_t u;
int32_t i;
int16_t tr;
if (!pst_r_u8(r, &sio_tx_data) || !pst_r_u8(r, &sio_rx_data) ||
!pst_r_u16(r, &sio_stat) || !pst_r_u16(r, &sio_mode) ||
!pst_r_u16(r, &sio_ctrl) || !pst_r_u16(r, &sio_baud))
return 0;
if (!pst_r_bytes(r, pad_analog, PSX_MAX_PLAYERS) || !pst_r_u8(r, &pad_connected) ||
!pst_r_u32(r, &u) || !pst_r_i32(r, &i) ||
!pst_r_bytes(r, pad_response, 8) || !pst_r_u8(r, &pad_response_len) ||
!pst_r_u8(r, &pad_response_idx) || !pst_r_u8(r, &pad_current_cmd) ||
!pst_r_bytes(r, pad_in_config, PSX_MAX_PLAYERS))
return 0;
pad_state = (PadState)u;
selected_slot = (int)i;
pad_active_logical = pad_logical_for_port(selected_slot);
/* Multitap bulk responses are 34 bytes; snap only stores 8. Abort an
* in-flight bulk restore rather than feed a truncated frame. */
if (pad_response_len > 8) {
pad_state = PAD_IDLE;
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
}
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
if (!pst_r_i16(r, &tr))
return 0;
pad_type_req[s] = (int8_t)tr;
}
if (!pst_r_u32(r, &u) || !pst_r_i32(r, &i) || !pst_r_u8(r, &mc_cmd) ||
!pst_r_u16(r, &mc_sector) || !pst_r_u8(r, &mc_sector_msb) ||
!pst_r_u8(r, &mc_sector_lsb) || !pst_r_bytes(r, mc_data, 128))
return 0;
mc_state = (McState)u;
mc_slot = (int)i;
if (!pst_r_i32(r, &i) || !pst_r_u8(r, &mc_checksum) || !pst_r_u8(r, &mc_flag))
return 0;
mc_data_idx = (int)i;
if (!sio_r_mcslot(r, &mc_slots[0]) || !sio_r_mcslot(r, &mc_slots[1]))
return 0;
if (!pst_r_u32(r, &u)) return 0;
active_device = (ActiveDevice)u;
#if SIO_MODEL_CYCLE_PACED
if (!pst_r_i32(r, &i)) return 0;
g_sio_timing_active = (int)i;
if (!pst_r_i32(r, &i) || !pst_r_u8(r, &sio_shift_byte)) return 0;
sio_shift_active = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_shift_remaining = (int)i;
if (!pst_r_i32(r, &i) || !pst_r_u8(r, &sio_tx_buffer)) return 0;
sio_tx_buffered = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_shift_ack_irq_en = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_tx_buffer_ack_irq_en = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_pending_ack = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_ack_remaining = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_pending_ack_irq_en = (int)i;
if (!pst_r_u32(r, &u) || !pst_r_u32(r, &sio_bus_byte_index)) return 0;
sio_bus_owner = (SioBusOwner)u;
#endif
if (!pst_r_i32(r, &i)) return 0;
sio_irq_pending = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_irq_countdown = (int)i;
if (!pst_r_i32(r, &i)) return 0;
sio_ack_visible_reads = (int)i;
if (!pst_r_u8(r, &sio_irq_pending_source) || !pst_r_u8(r, &sio_irq_pending_slot) ||
!pst_r_u8(r, &sio_irq_pending_delay) || !pst_r_u8(r, &sio_irq_pending_mc_state) ||
!pst_r_u32(r, &sio_irq_pending_byte_seq))
return 0;
/* sio_trace_seq is host-local and not on the wire; reseat it from the
* restored byte_seq so the next TX does not stamp a peer-divergent
* seq into sio_irq_pending_byte_seq (was forking fsm digests alone). */
sio_trace_seq = sio_irq_pending_byte_seq;
/* Pre-rumble snapshots end here. A mid-game legacy state cannot tell us
* the negotiated map, so assume the standard 0x00/0x01 layout used by
* commercial DualShock games while leaving both motors stopped. */
if (r->p == r->end) {
memset(pad_rumble_map, 0xFF, sizeof(pad_rumble_map));
memset(pad_rumble_small, 0, sizeof(pad_rumble_small));
memset(pad_rumble_large, 0, sizeof(pad_rumble_large));
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
pad_rumble_map[s][0] = 0x00;
pad_rumble_map[s][1] = 0x01;
}
return 1;
}
if (!pst_r_bytes(r, pad_rumble_map, sizeof(pad_rumble_map)) ||
!pst_r_bytes(r, pad_rumble_small, sizeof(pad_rumble_small)) ||
!pst_r_bytes(r, pad_rumble_large, sizeof(pad_rumble_large)))
return 0;
if (r->p != r->end) return 0;
return 1;
}
uint32_t sio_snapshot_bytes(void) {
PstW w;
pst_w_init(&w, NULL, 0);
(void)sio_snap_emit(&w);
return (uint32_t)w.written;
}
void sio_snapshot_write(uint8_t *p) {
PstW w;
uint32_t n = sio_snapshot_bytes();
pst_w_init(&w, p, n);
(void)sio_snap_emit(&w);
}
/* Non-mutating pre-check for boot_state's two-pass load: a section that passes
* cannot fail sio_snapshot_read (the parser only reads fixed-width fields, and
* both accepted lengths end exactly where one of its two layouts ends). */
int sio_snapshot_validate(const uint8_t *p, uint32_t len) {
const uint32_t current = sio_snapshot_bytes();
const uint32_t rumble_bytes = (uint32_t)(sizeof(pad_rumble_map) +
sizeof(pad_rumble_small) +
sizeof(pad_rumble_large));
return p && (len == current || (len < current && len + rumble_bytes == current));
}
int sio_snapshot_read(const uint8_t *p, uint32_t len) {
PstR r;
if (!sio_snapshot_validate(p, len))
return 0;
pst_r_init(&r, p, len);
return sio_snap_parse(&r);
}