mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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>
This commit is contained in:
+27
-6
@@ -10,21 +10,42 @@ extern "C" {
|
||||
/* 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.
|
||||
/* Advance SIO timing by `cycles` PSX cycles.
|
||||
*
|
||||
* Phase 1.0a signature change. The cycles arg is reserved for a future
|
||||
* cycle-paced model (SIO_MODEL_CYCLE_PACED=1). With the default macro
|
||||
* setting (SIO_MODEL_CYCLE_PACED=0) the cycles arg is ignored and the
|
||||
* body behaves identically to the prior tick-once-per-call semantics —
|
||||
* fires pending IRQ7 after the transfer delay expires. */
|
||||
* 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 */
|
||||
|
||||
@@ -1262,6 +1262,50 @@ static void handle_mc_status(int id, const char *json)
|
||||
extern void sio_get_card_arm_audit(uint32_t out[3][7]);
|
||||
extern int sio_get_card_arm_countdown_after(void);
|
||||
extern void sio_get_burst_stats(uint64_t out[10]);
|
||||
extern void sio_get_pace_state(uint64_t out[16]);
|
||||
extern volatile int g_sio_timing_active;
|
||||
|
||||
/* Phase 1.0c-v2 telemetry: cycle-paced SIO state snapshot. Read-only.
|
||||
* In 1.0c-v2 the TX path is still synchronous, g_sio_timing_active
|
||||
* stays 0, and all dynamic shifter/ack fields stay zero. */
|
||||
static void handle_pace_state(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
uint64_t s[16];
|
||||
sio_get_pace_state(s);
|
||||
const char *model = s[0] ? "cycle_paced" : "access_legacy";
|
||||
const char *owner = s[7] == 0 ? "none" : s[7] == 1 ? "card"
|
||||
: s[7] == 2 ? "pad" : "unknown";
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
"\"sio_model\":\"%s\","
|
||||
"\"sio_quantum_cycles\":%llu,"
|
||||
"\"timing_active\":%d,"
|
||||
"\"shift_active\":%llu,"
|
||||
"\"shift_remaining\":%llu,"
|
||||
"\"tx_buffered\":%llu,"
|
||||
"\"ack_active\":%llu,"
|
||||
"\"ack_remaining\":%llu,"
|
||||
"\"bus_owner\":\"%s\","
|
||||
"\"bus_byte_index\":%llu,"
|
||||
"\"tx_writes_buffered\":%llu,"
|
||||
"\"tx_writes_dropped_busy\":%llu,"
|
||||
"\"tx_writes_dropped_cross_device\":%llu,"
|
||||
"\"tx_buffer_promoted\":%llu,"
|
||||
"\"tx_buffer_promoted_during_card\":%llu,"
|
||||
"\"pad_byte_processed_in_card_data\":%llu,"
|
||||
"\"cross_device_pad_during_card\":%llu}\n",
|
||||
id, model,
|
||||
(unsigned long long)s[1],
|
||||
g_sio_timing_active,
|
||||
(unsigned long long)s[2], (unsigned long long)s[3],
|
||||
(unsigned long long)s[4], (unsigned long long)s[5],
|
||||
(unsigned long long)s[6],
|
||||
owner,
|
||||
(unsigned long long)s[8], (unsigned long long)s[9],
|
||||
(unsigned long long)s[10], (unsigned long long)s[11],
|
||||
(unsigned long long)s[12], (unsigned long long)s[13],
|
||||
(unsigned long long)s[14], (unsigned long long)s[15]);
|
||||
}
|
||||
|
||||
static void handle_sio_burst_stats(int id, const char *json)
|
||||
{
|
||||
@@ -2833,6 +2877,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "card_buffer_dump", handle_card_buffer_dump },
|
||||
{ "sio_arm_audit", handle_sio_arm_audit },
|
||||
{ "sio_burst_stats", handle_sio_burst_stats },
|
||||
{ "pace_state", handle_pace_state },
|
||||
{ "chain_trace", handle_chain_trace },
|
||||
{ "sio_trace", handle_sio_trace },
|
||||
{ "sio_pc_trace", handle_sio_pc_trace },
|
||||
|
||||
@@ -155,6 +155,16 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
}
|
||||
}
|
||||
if (!in_exception) {
|
||||
#if SIO_MODEL_CYCLE_PACED
|
||||
/* Phase 1.0c-v2: advance cycle-paced SIO bus by one quantum, but
|
||||
* only when something is actually pending. g_sio_timing_active
|
||||
* stays 0 throughout 1.0c-v2 (TX path still synchronous, never
|
||||
* arms shift/ack), so this gate never opens — per-call cost on
|
||||
* this hot path is one volatile load + one branch. */
|
||||
if (g_sio_timing_active) {
|
||||
sio_tick_quantum();
|
||||
}
|
||||
#endif
|
||||
dispatch_count++;
|
||||
if (dispatch_count >= VBLANK_INTERVAL) {
|
||||
/* Defer VBlank while a card SIO transaction is mid-flight.
|
||||
|
||||
+176
-6
@@ -1188,14 +1188,23 @@ void sio_get_burst_stats(uint64_t out[10]) {
|
||||
out[9] = 0;
|
||||
}
|
||||
|
||||
/* Phase 1.0a: SIO_MODEL_CYCLE_PACED defaulting to 0 keeps the legacy
|
||||
* access-paced behavior unchanged. The cycles arg on sio_tick is
|
||||
* reserved for a future cycle-paced model and is ignored under
|
||||
* macro=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 0
|
||||
#define SIO_MODEL_CYCLE_PACED 1
|
||||
#endif
|
||||
|
||||
/* Hot-path active guard. Defined unconditionally (so its address exists
|
||||
* for any caller), but only meaningfully used when macro=1. */
|
||||
volatile int g_sio_timing_active = 0;
|
||||
|
||||
#if SIO_MODEL_CYCLE_PACED
|
||||
/* ---- Phase 1.0b: cycle-paced SIO state (inert) ---------------------------
|
||||
*
|
||||
@@ -1251,8 +1260,139 @@ static uint64_t s_pace_shift_completes;
|
||||
static uint64_t s_pace_ack_fires;
|
||||
#endif /* SIO_MODEL_CYCLE_PACED */
|
||||
|
||||
#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) {
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
if (!(sio_ctrl & SIO_CTRL_ACK_IRQ_EN)) return;
|
||||
sio_stat |= SIO_STAT_IRQ;
|
||||
|
||||
uint32_t i_stat_before = i_stat;
|
||||
i_stat |= (1 << IRQ_SIO0);
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
static void sio_handle_shift_complete(void) {
|
||||
uint8_t b = sio_shift_byte;
|
||||
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;
|
||||
}
|
||||
|
||||
if (sio_tx_buffered) {
|
||||
sio_shift_byte = sio_tx_buffer;
|
||||
sio_shift_active = 1;
|
||||
sio_shift_remaining = SIO_BAUD_CYCLES_DEFAULT;
|
||||
sio_tx_buffered = 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;
|
||||
}
|
||||
}
|
||||
#endif /* SIO_MODEL_CYCLE_PACED */
|
||||
|
||||
void sio_tick(int cycles) {
|
||||
(void)cycles; /* reserved for cycle-paced model (Phase 1.0c+) */
|
||||
#if SIO_MODEL_CYCLE_PACED
|
||||
/* Cycle-paced advance. In 1.0c-v2 sio_shift_active and
|
||||
* sio_pending_ack are never set (TX path still synchronous), so this
|
||||
* loop finds no events and returns immediately. Plus, the dispatch
|
||||
* caller is gated by g_sio_timing_active so this function isn't even
|
||||
* called when nothing is pending. */
|
||||
if (cycles > 0) {
|
||||
int remaining = cycles;
|
||||
int transitions = 0;
|
||||
const int MAX_TRANSITIONS = 8;
|
||||
while (remaining > 0 && transitions < MAX_TRANSITIONS) {
|
||||
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) {
|
||||
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();
|
||||
transitions++;
|
||||
} else if (next_event == 1) {
|
||||
sio_pending_ack = 0;
|
||||
sio_fire_ack_irq();
|
||||
transitions++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#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) {
|
||||
@@ -1287,3 +1427,33 @@ void sio_tick(int cycles) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user