Files
psxrecomp/runtime/include/sio.h
T
Matthew Stanley 0d15e2bd4e phase1.0c-v2: cycle-paced sio_tick + dispatch quantum behind active-guard
Adds the cycle-paced sio_tick body, the sio_tick_quantum() wrapper, and
the pace_state TCP telemetry handler. Crucial change vs. failed 1.0c:
the dispatch-loop quantum tick is gated by g_sio_timing_active (volatile
int), so on the psx_check_interrupts hot path the cost is one load + one
branch when no SIO time is pending — not a full function call into the
cycle-paced loop.

In 1.0c-v2 the TX path is still synchronous and nothing arms shift_active
or pending_ack, so g_sio_timing_active stays 0. Phase 1.0d will set it
when arming the shifter and clear it when the shifter+buffer+ack are all
empty.

Files:
- sio.h: SIO_MODEL_CYCLE_PACED moved to header (default 1); declares
  g_sio_timing_active (extern volatile int) and sio_tick_quantum()
- sio.c: defines g_sio_timing_active=0; adds cycle-paced helpers
  (sio_consume_ack_event, sio_fire_ack_irq, sio_handle_shift_complete);
  sio_tick gains a cycle-paced loop that runs only if shift_active or
  pending_ack is set (currently never), then falls through to the legacy
  IRQ countdown body which remains the live IRQ delivery path; adds
  sio_get_pace_state(uint64_t out[16]) accessor
- interrupts.c: in psx_check_interrupts !in_exception block, calls
  sio_tick_quantum only if g_sio_timing_active is set
- debug_server.c: new pace_state TCP handler exposing the snapshot

Validation against 64e0194 baseline:
- t=20s/30s TCP responsive; pad arm audit 2840/2840 no_ack=0
- 4x CROSS succeeds
- card txn distribution unchanged (10 txns reach bytes=15 ts=10)
- txn 0 still reads "MC" magic at bytes 10-11 (0x4D 0x43)
- pace_state: timing_active=0, all dynamic fields 0 throughout

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:04:14 -07:00

186 lines
8.0 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_V4_SIO_H
#define PSXRECOMP_V4_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 by future 1.0d code when sio_shift or
* sio_pending_ack arms; cleared back to 0 when both shifter and buffer
* and ACK are empty. The dispatch loop reads this BEFORE calling
* sio_tick_quantum, so when no SIO time is pending the per-call cost is
* one load + one branch. In 1.0c-v2 nothing sets this — it stays 0. */
extern volatile int g_sio_timing_active;
/* 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 */
void sio_set_pad_state(uint16_t buttons);
/* Connect a pad to a slot (0=port1, 1=port2). By default no pads are
* connected during initial BIOS boot. */
void sio_connect_pad(int slot);
/* Return current pad button state (for debug server). */
uint16_t sio_get_pad_buttons(void);
/* ---- 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);
#ifdef __cplusplus
}
#endif
#endif /* PSXRECOMP_V4_SIO_H */