diff --git a/runtime/include/dma.h b/runtime/include/dma.h index 98b5d58e..dccddd3d 100644 --- a/runtime/include/dma.h +++ b/runtime/include/dma.h @@ -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); diff --git a/runtime/src/dma.c b/runtime/src/dma.c index f1ea9df6..e5ed38ec 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -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; diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 8e051272..cf1f9a9a 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -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(); } diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 0b5058c0..dc70668b 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -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; } diff --git a/runtime/tests/test_psx_cycle_event_boundaries.c b/runtime/tests/test_psx_cycle_event_boundaries.c new file mode 100644 index 00000000..ce29ec05 --- /dev/null +++ b/runtime/tests/test_psx_cycle_event_boundaries.c @@ -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 +#include + +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; +} diff --git a/runtime/tests/test_sio_card_protocol.c b/runtime/tests/test_sio_card_protocol.c index 7fe64395..607286dd 100644 --- a/runtime/tests/test_sio_card_protocol.c +++ b/runtime/tests/test_sio_card_protocol.c @@ -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;