Files
psxrecomp/runtime/include/sio.h
T
Matthew Stanley af303f3454 runtime: Tomba "Hybrid" legacy pad-config compatibility (per-game opt-in)
The launcher Hybrid controller mode (built first, for Tomba, to mimic Tomba:
Special Edition) flips the emulated pad's reported type 0x41<->0x73 on
d-pad<->stick. After the real DualShock config state machine was added for MMX6
(98aa688), that flip regressed every game's libpad re-detect into a 1-frame
"pad unplugged": Tomba unpauses its menu / drops input, MMX6 phantom-dashes.

The modern config SM is correct and required by MMX6, so instead of weakening it
globally, restore the pre-98aa688 "config commands always answer 0xF3" behaviour
as a per-game opt-in: [controller] legacy_pad_config (default OFF). Only Tomba
sets it; with the flag off the modern pad path is byte-for-byte unchanged, so no
other title is affected. A/B verified live: legacy = clean Tomba flip / MMX6
input; modern = Tomba disconnect + MMX6 dash.

Explicitly LEGACY: a correct findpad/re-detect refactor would make the flip
benign for every game with no per-game branch (real hardware tolerates unlimited
analog-button presses); when that lands this whole feature set is to be removed.
Full rationale in sio.c (g_pad_legacy_cfg).

Also includes the deferred coherent pad-type apply (pad_type_req, applied only at
an idle/non-config bus boundary so a flip can't split a poll/handshake) and a
`pad_cfg` debug command + tools/padcfg.py + tools/sio_flip_trace.py used to A/B
the config SM live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 13:43:34 -07:00

221 lines
9.7 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.
#ifndef PSXRECOMP_SIO_H
#define PSXRECOMP_SIO_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* SIO0 register base: 0x1F801040 */
#define SIO_BASE 0x1F801040
/* Phase 1.0c-v2: cycle-paced SIO model. Default 1 enables the dispatch-
* loop quantum tick (gated by g_sio_timing_active). Set to 0 to revert
* to legacy access-paced behavior across all callers. */
#ifndef SIO_MODEL_CYCLE_PACED
#define SIO_MODEL_CYCLE_PACED 1
#endif
void sio_init(void);
/* MMIO read/write (0x1F801040-0x1F80104E) */
uint32_t sio_read(uint32_t addr);
void sio_write(uint32_t addr, uint32_t value);
/* Advance SIO timing by `cycles` PSX cycles.
*
* In cycle-paced mode (SIO_MODEL_CYCLE_PACED=1, default in 1.0c+): walks
* shift-complete and ACK-fire events scheduled in cycles ahead. In 1.0c
* the shifter is never armed (TX path still synchronous), so the body
* is effectively inert. Phase 1.0d will route TX through the shifter.
*
* The legacy access-paced IRQ countdown body always runs and is the
* actual IRQ delivery mechanism in 1.0c. */
void sio_tick(int cycles);
/* Convenience: advance SIO by the dispatch-loop quantum. Called from
* psx_check_interrupts inside !in_exception, gated by g_sio_timing_active
* (no-op when no SIO cycle-paced work is pending). No-op under macro=0. */
void sio_tick_quantum(void);
/* Hot-path active guard. Set to 1 when sio_shift / sio_pending_ack /
* buffer arms; cleared when all three empty. */
extern volatile int g_sio_timing_active;
/* Phase 1.0e-e1: peripheral-only cycle advance. Drives shifter+ack
* scheduler. Does NOT touch the legacy sio_irq_pending/countdown. Safe
* to call from psx_advance_cycles per emitted block. No-op when
* g_sio_timing_active is 0. */
void sio_advance(uint32_t cycles);
/* Telemetry counters for sio_advance. */
uint64_t sio_get_advance_called(void);
uint64_t sio_get_advance_with_work(void);
/* Update pad button state. Buttons use PS1 convention: 0=pressed, 1=released.
Bit layout: SELECT, L3, R3, START, UP, RIGHT, DOWN, LEFT,
L2, R2, L1, R1, TRIANGLE, CIRCLE, CROSS, SQUARE
sio_set_pad_state targets port 1 (slot 0); the _slot form targets either. */
void sio_set_pad_state(uint16_t buttons);
void sio_set_pad_state_slot(int slot, uint16_t buttons);
/* Set the analog stick state + pad type for a slot. enabled selects the
* emulated pad: 0 = digital (poll id 0x41), 1 = DualShock/analog (poll id
* 0x73, with the four 0..255 stick axes appended; 0x80 = centred). */
void sio_set_pad_analog(int slot, int enabled,
uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry);
/* Per-frame input plumbing for the coherent-DualShock model. Update the sticks
* every frame with sio_set_pad_sticks; change the reported pad type with
* sio_request_pad_type (the emulated "analog button"), which is applied
* atomically at the next idle, non-config bus boundary so a hybrid stick/d-pad
* flip can never split a poll or a config handshake. Do NOT call
* sio_set_pad_analog every frame for the type — that is for boot/hotplug only. */
void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry);
void sio_request_pad_type(int slot, int analog);
/* Connect / disconnect a pad on a slot (0=port1, 1=port2). By default no pads
* are connected during initial BIOS boot. */
void sio_connect_pad(int slot);
void sio_set_pad_connected(int slot, int connected);
/* Return current pad button state (for debug server). _slot targets either. */
uint16_t sio_get_pad_buttons(void);
uint16_t sio_get_pad_buttons_slot(int slot);
/* Debug accessors: is a pad connected on the slot, and is it in analog mode. */
int sio_get_pad_connected(int slot);
int sio_get_pad_analog(int slot);
/* ---- SIO byte-level trace ring buffer ----
* 1M entries × ~28 B ≈ 32 MB. At ~600 byte/sec that's ~30 min of history. */
#define SIO_TRACE_CAP (1 << 20)
typedef struct {
uint32_t seq; /* monotonic sequence number */
uint8_t tx; /* byte written to TX */
uint8_t rx; /* byte produced in RX */
uint8_t mc_state_pre; /* mc_state BEFORE processing */
uint8_t mc_state_post; /* mc_state AFTER processing */
uint8_t dev_pre; /* active_device BEFORE (0=NONE,1=PAD,2=MC) */
uint8_t dev_post; /* active_device AFTER */
uint16_t ctrl; /* CTRL register value */
uint32_t func_addr; /* g_debug_current_func_addr */
uint8_t was_abort; /* 1 if this byte caused mc_state abort */
uint8_t irq_countdown; /* sio_irq_countdown at entry */
uint8_t in_exception; /* psx_get_in_exception() at time of byte */
uint8_t counter_7514; /* RAM[0x7514] at time of byte */
uint32_t cop0_sr; /* COP0 SR at time of byte (IEc=bit0, IM2=bit10) */
/* Saved slot states captured AT TRACE TIME (after the byte was processed
* and any mc_save/load_slot has run). Lets the audit show whether a
* SELECT-deassert preserved state into the next 0x81. */
uint8_t slot0_state;
uint8_t slot1_state;
} SioTraceEntry;
/* Get pointer to ring buffer and current write index.
* Returns number of entries ever written (seq of next write). */
uint32_t sio_get_trace(const SioTraceEntry **buf_out, int *write_idx_out);
/* Current SIO byte sequence — used by SIO PC tracer for cross-referencing. */
uint32_t sio_get_seq(void);
/* Returns 1 if a memcard protocol exchange is in progress on either
* slot. Callers (notably the VBlank scheduler) use this to defer
* interrupt delivery so the BIOS's pad polling routine — which fires
* from the VBlank handler — doesn't preempt an in-flight card
* transaction by issuing 0x01 on the SIO bus mid-read. */
int sio_card_protocol_active(void);
/* Snapshot SIO IRQ-pacing internals for the freeze_check diagnostic. */
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);
/* ---- Card transaction ring buffer ----
*
* One entry per card protocol transaction (0x81 → terminal state / abort).
* Always-on. 64K entries × ~544 B ≈ 35 MB — holds tens of minutes of card
* activity at typical detection-cycle rates. */
#define SIO_TXN_CAP (1 << 16)
#define SIO_TXN_MAX_BYTES 256
typedef enum {
SIO_TXN_END_OPEN = 0, /* still in progress (only on the live txn) */
SIO_TXN_END_SUCCESS = 1, /* mc_state reached IDLE via natural progression */
SIO_TXN_END_ABORT_RESELECT= 2, /* 0x81 received while txn already non-IDLE */
SIO_TXN_END_ABORT_RESET = 3, /* CTRL.RESET written */
SIO_TXN_END_ABORT_SLOT = 4, /* slot mismatch reset (DEV_NONE non-0x81 path) */
SIO_TXN_END_ABORT_BAD_CMD = 5, /* CMD byte not 0x52/0x57/0x53 */
SIO_TXN_END_ABORT_OTHER = 6, /* state machine fell through */
} SioTxnEndReason;
typedef struct {
uint32_t txn_seq; /* monotonic transaction id */
uint32_t start_byte_seq; /* SIO byte ring seq at first byte of txn */
uint32_t end_byte_seq; /* SIO byte ring seq at last byte (inclusive) */
uint32_t start_func; /* g_debug_current_func_addr at first byte */
uint32_t end_func; /* g_debug_current_func_addr at last byte */
uint8_t slot; /* mc_slot when txn opened */
uint8_t cmd; /* 0x52/0x57/0x53 once observed; 0 before */
uint16_t sector; /* 0xFFFF if address phase not reached */
uint16_t ack_count; /* SIO ACKs returned during txn */
uint8_t terminal_state; /* mc_state at termination */
uint8_t end_reason; /* SioTxnEndReason */
uint16_t byte_count; /* total bytes processed (may exceed MAX_BYTES) */
uint8_t tx[SIO_TXN_MAX_BYTES];
uint8_t rx[SIO_TXN_MAX_BYTES];
} SioTxnEntry;
/* Returns ring buffer + next-write index + whether a live txn is currently
* open. The live txn (when open_out=1) is NOT yet in the ring; query
* sio_get_card_txn_live() if you want it too. Total seq = number of CLOSED
* transactions ever recorded. */
uint32_t sio_get_card_txns(const SioTxnEntry **buf_out, int *write_idx_out, int *open_out);
/* Returns pointer to live (open) txn, or NULL if none. Lifetime: pointer
* is valid until the next sio_process_byte call. */
const SioTxnEntry *sio_get_card_txn_live(void);
/* ---- SIO IRQ #7 delivery ring ----
*
* Captures every time IRQ #7 (SIO0) is raised into I_STAT. Always-on.
* 1M entries × ~64 B ≈ 64 MB — holds tens of minutes of IRQ history. */
#define SIO_IRQ_RING_CAP (1 << 20)
typedef enum {
SIO_IRQ_SRC_UNKNOWN = 0,
SIO_IRQ_SRC_CARD_ACK = 1, /* IRQ from card-side ACK after card byte */
SIO_IRQ_SRC_PAD_ACK = 2, /* IRQ from pad-side ACK after pad byte */
} SioIrqSource;
typedef struct {
uint32_t seq;
uint32_t byte_seq; /* corresponding sio_trace_seq when this IRQ was scheduled */
uint32_t i_stat_before; /* I_STAT just before raising bit 7 */
uint32_t i_stat_after; /* I_STAT immediately after */
uint32_t mc_state; /* mc_state at time of fire */
uint32_t active_device; /* DEV_NONE/PAD/MEMCARD at time of fire */
uint32_t ctrl; /* sio_ctrl at fire */
uint32_t func_addr; /* g_debug_current_func_addr at fire */
uint32_t counter_7514; /* card chain counter byte at fire */
uint8_t source; /* SioIrqSource */
uint8_t slot; /* selected_slot at fire */
uint8_t delay_applied; /* the SIO_IRQ_DELAY_* value used */
uint8_t pad;
} SioIrqEntry;
uint32_t sio_get_irq_ring(const SioIrqEntry **buf_out, int *write_idx_out);
/* Phantom-input regression A/B instrument: select pre/post-98aa688 pad config
* behavior at runtime (0 = new state machine, 1 = legacy always-0xF3). */
int sio_get_legacy_cfg(void);
void sio_set_legacy_cfg(int v);
#ifdef __cplusplus
}
#endif
#endif /* PSXRECOMP_SIO_H */