runtime: preserve causality at device deadlines

This commit is contained in:
Matthew Stanley
2026-07-19 13:40:25 -07:00
parent d73dc5f36e
commit ffda40934b
6 changed files with 223 additions and 8 deletions
+4
View File
@@ -31,6 +31,10 @@ void dma_advance(uint32_t cycles);
/* Cycle-budgeted precise event slicing: guest CPU cycles until a DELIVERABLE
* DMA IRQ (bit3 unmasked in i_mask). UINT32_MAX if none. */
uint32_t dma_cycles_to_irq(uint32_t i_mask);
/* Mask-blind scheduler boundary: next DMA word movement or delayed completion,
* not merely the final IRQ. RAM-producing DMA must become visible at its
* per-word due cycle even when guest code polls ordinary RAM instead of DICR. */
uint32_t dma_cycles_to_internal_event(void);
/* Strict deliverability form for wait-loop elision: unlike the conservative
* scheduler bound, ignores channel completions whose DICR IRQ is disabled. */
uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask);
+47
View File
@@ -881,6 +881,53 @@ uint32_t dma_cycles_to_irq(uint32_t i_mask) {
return best;
}
uint32_t dma_cycles_to_internal_event(void) {
uint32_t best = 0xFFFFFFFFu;
/* Async MDEC channels move one word whenever their cycle accumulator
* reaches the channel cost. Completion-only scheduling batches many RAM
* writes at a later service boundary, which is observably different when
* the CPU polls the destination buffer. */
for (int ch = 0; ch < 2; ch++) {
const DMAAsyncChannel *a = &mdec_async[ch];
if (!a->active || a->remaining_words == 0 ||
!((channels[ch].chcr >> 24) & 1u) || !channel_enabled(ch))
continue;
uint32_t direction = channels[ch].chcr & 1u;
if ((ch == 0 && direction == 0) || (ch == 1 && direction != 0))
return 1u; /* invalid route is completed on the next DMA tick */
if ((ch == 0 && !mdec_dma_write_ready()) ||
(ch == 1 && !mdec_dma_read_ready()))
continue;
uint32_t cpw = ch == 0 ? DMA_MDEC_IN_CYCLES_PER_WORD
: DMA_MDEC_OUT_CYCLES_PER_WORD;
uint32_t d = a->cycles_accum < cpw ? cpw - a->cycles_accum : 1u;
if (d < best) best = d;
}
/* CD-ROM DMA writes guest RAM incrementally. Expose each word on time;
* waiting only for the channel-complete IRQ can delay hundreds of writes. */
if (cdrom_async.active && cdrom_async.remaining_words != 0 &&
((channels[3].chcr >> 24) & 1u) && channel_enabled(3)) {
if ((channels[3].chcr & 1u) != 0) {
return 1u; /* unsupported RAM->CD direction cancels next tick */
}
if (cdrom_dma_ready()) {
uint32_t d = cdrom_async.cycles_accum < DMA_CDROM_CYCLES_PER_WORD
? DMA_CDROM_CYCLES_PER_WORD - cdrom_async.cycles_accum
: 1u;
if (d < best) best = d;
}
}
for (int ch = 0; ch < 7; ch++) {
if (delayed_complete[ch].active &&
delayed_complete[ch].cycles_remaining < best)
best = delayed_complete[ch].cycles_remaining;
}
return best;
}
uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask) {
if (!(i_mask & (1u << 3))) return 0xFFFFFFFFu;
uint32_t best = 0xFFFFFFFFu;
+13 -1
View File
@@ -108,7 +108,7 @@ static uint32_t devices_cycles_to_next_internal_event(void) {
uint32_t best = interrupts_cycles_to_vblank(); /* frame pacing always */
uint32_t t = timers_cycles_to_irq(0xFFFFFFFFu); if (t < best) best = t;
uint32_t c = cdrom_cycles_to_irq(0xFFFFFFFFu); if (c < best) best = c;
uint32_t d = dma_cycles_to_irq(0xFFFFFFFFu); if (d < best) best = d;
uint32_t d = dma_cycles_to_internal_event(); if (d < best) best = d;
uint32_t s = sio_cycles_to_irq(0xFFFFFFFFu); if (s < best) best = s;
if (best == 0) best = 1; /* due/overdue: process within one cycle */
return best;
@@ -153,6 +153,18 @@ static void psx_devices_service_to_now(void) {
uint32_t to_ev = devices_cycles_to_next_internal_event();
if (to_ev > 0 && to_ev < step) step = to_ev;
if (step == 0) step = 1;
/* Preserve interval causality at an advertised event boundary.
* Devices are advanced in a fixed dependency order (CD before DMA,
* MDEC input before output). Passing the whole D-cycle interval to
* each device lets a state created by an earlier device at cycle D
* be treated by a later device as though it existed for all D
* cycles. Advance the quiet prefix first, then the boundary cycle:
* this is exactly the conservative timeline with O(events), not
* O(cycles), host calls. */
if (step > 1u && to_ev == step) {
advance_devices(step - 1u);
step = 1u;
}
advance_devices(step);
interrupts_service_scheduled_events();
}
+14 -6
View File
@@ -2006,16 +2006,20 @@ void sio_advance(uint32_t cycles) {
int remaining = (int)cycles;
int transitions = 0;
const int MAX_TRANSITIONS = 8;
while (remaining > 0 && transitions < MAX_TRANSITIONS) {
while (transitions < MAX_TRANSITIONS &&
(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) {
&& 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 ||
(sio_ack_remaining == dt && next_event < 0))) {
dt = sio_ack_remaining;
next_event = 1;
}
@@ -2057,16 +2061,20 @@ void sio_tick(int cycles) {
int remaining = cycles;
int transitions = 0;
const int MAX_TRANSITIONS = 8;
while (remaining > 0 && transitions < MAX_TRANSITIONS) {
while (transitions < MAX_TRANSITIONS &&
(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) {
&& 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 ||
(sio_ack_remaining == dt && next_event < 0))) {
dt = sio_ack_remaining;
next_event = 1;
}
@@ -0,0 +1,86 @@
/*
* Pin cross-device causality at a scheduler deadline.
*
* Build from runtime/tests:
* gcc -std=c99 -Wall -Wextra -ffunction-sections -fdata-sections \
* -Wl,--gc-sections -I../include -o test_psx_cycle_event_boundaries \
* test_psx_cycle_event_boundaries.c ../src/psx_cycles.c
*/
#include "psx_cycles.h"
#include <stdint.h>
#include <stdio.h>
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
int g_psx_call_bail = 0;
uint32_t i_mask = 0;
uint64_t g_guest_store_count = 0;
uint64_t g_mmio_access_count = 0;
static uint32_t s_cd_cycles_remaining = 5;
static int s_cd_ready = 0;
static uint32_t s_dma_ready_cycles = 0;
void sio_advance(uint32_t cycles) { (void)cycles; }
void cdrom_advance(uint32_t cycles) {
if (s_cd_ready) return;
if (cycles >= s_cd_cycles_remaining) {
s_cd_cycles_remaining = 0;
s_cd_ready = 1;
} else {
s_cd_cycles_remaining -= cycles;
}
}
void dma_advance(uint32_t cycles) {
if (s_cd_ready) s_dma_ready_cycles += cycles;
}
void timers_advance(uint32_t cycles) { (void)cycles; }
void interrupts_advance_cycles(uint32_t cycles) { (void)cycles; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) { return UINT32_MAX; }
uint32_t timers_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
uint32_t cdrom_cycles_to_irq(uint32_t mask) {
(void)mask;
return s_cd_ready ? UINT32_MAX : s_cd_cycles_remaining;
}
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t mask) {
(void)mask;
return UINT32_MAX;
}
uint32_t sio_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
int psx_get_in_exception(void) { return 0; }
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
int main(void) {
psx_cycles_resync_after_restore();
psx_advance_cycles(5);
if (psx_get_cycle_count() != 5) {
fprintf(stderr, "FAIL cycle count: expected 5 got %llu\n",
(unsigned long long)psx_get_cycle_count());
return 1;
}
if (!s_cd_ready) {
fprintf(stderr, "FAIL CD event did not fire at cycle 5\n");
return 1;
}
if (s_dma_ready_cycles != 1) {
fprintf(stderr,
"FAIL retroactive DMA credit: expected 1 boundary cycle got %u\n",
s_dma_ready_cycles);
return 1;
}
fprintf(stderr, "PASS cross-device deadline preserves D-1 + 1 causality\n");
return 0;
}
+59 -1
View File
@@ -66,6 +66,13 @@ void card_data_writes_arm(uint8_t value, uint16_t mc_state,
uint8_t mc_data_idx, uint8_t slot) {
(void)value; (void)mc_state; (void)mc_data_idx; (void)slot;
}
void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux) {
(void)kind; (void)detail; (void)aux;
}
void psx_irq_raise(uint32_t bit, uint32_t detail) {
(void)detail;
i_stat |= 1u << bit;
}
/* ---- Test harness ---- */
@@ -356,9 +363,54 @@ static void test_directory_frame_layout(void) {
* Just check that read succeeded. */
}
int main(void) {
/* Exact scheduler deadlines must fire on the call that reaches the boundary.
* The old strict-inequality loops subtracted the countdown to zero without
* processing the event, after which sio_cycles_to_irq() no longer advertised
* it and the event could be delayed until an unrelated scheduler wake. */
static void test_cycle_paced_exact_boundaries(void) {
const uint32_t irq_sio0 = 1u << 7;
const uint16_t ctrl = CTRL_TX_EN | CTRL_SELECT | CTRL_ACK_IRQ_EN;
sio_init();
memcard_init("test_dir");
i_stat = 0;
sio_write(SIO_CTRL, ctrl);
sio_write(SIO_TX_DATA, 0x81);
EXPECT_EQ("paced.shift.initial", 1088, sio_cycles_to_irq(irq_sio0));
sio_advance(1087);
EXPECT_EQ("paced.shift.remaining", 1, sio_cycles_to_irq(irq_sio0));
EXPECT_EQ("paced.shift.no_irq", 0, i_stat & irq_sio0);
sio_advance(1);
EXPECT_EQ("paced.ack.initial", 170, sio_cycles_to_irq(irq_sio0));
EXPECT_EQ("paced.ack.no_irq", 0, i_stat & irq_sio0);
sio_advance(169);
EXPECT_EQ("paced.ack.remaining", 1, sio_cycles_to_irq(irq_sio0));
sio_advance(1);
EXPECT_EQ("paced.ack.irq", irq_sio0, i_stat & irq_sio0);
/* sio_tick() retains the same event walker for access-paced callers. */
sio_init();
memcard_init("test_dir");
i_stat = 0;
sio_write(SIO_CTRL, ctrl);
sio_write(SIO_TX_DATA, 0x81);
sio_tick(1088);
EXPECT_EQ("paced.tick.ack", 170, sio_cycles_to_irq(irq_sio0));
sio_tick(170);
EXPECT_EQ("paced.tick.irq", irq_sio0, i_stat & irq_sio0);
}
int main(int argc, char **argv) {
fprintf(stderr, "=== sio card protocol tests ===\n");
if (argc == 2 && strcmp(argv[1], "exact-boundaries") == 0) {
test_cycle_paced_exact_boundaries();
fprintf(stderr, "test_cycle_paced_exact_boundaries: %d/%d ok\n",
g_checks - g_failures, g_checks);
return g_failures > 0 ? 1 : 0;
}
test_no_card_present();
fprintf(stderr, "test_no_card_present: %d/%d ok\n",
g_checks - g_failures, g_checks);
@@ -399,6 +451,12 @@ int main(void) {
(g_checks - prev_checks) - (g_failures - prev_fails),
g_checks - prev_checks);
prev_checks = g_checks; prev_fails = g_failures;
test_cycle_paced_exact_boundaries();
fprintf(stderr, "test_cycle_paced_exact_boundaries: %d/%d ok\n",
(g_checks - prev_checks) - (g_failures - prev_fails),
g_checks - prev_checks);
fprintf(stderr, "=== TOTAL: %d checks, %d failures ===\n",
g_checks, g_failures);
return g_failures > 0 ? 1 : 0;