From e9f834da52a990cb509f0c4dea1fe6eb3f9d5d93 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Thu, 27 Aug 2026 16:26:44 +0300 Subject: [PATCH] cycles: bound idle skip by the SPU sample deadline when IRQ9 is unmasked The SPU sample event was part of the internal device slicer but not of the idle-skip observable-event distance, so a poll loop waiting on SPU IRQ9 could be skipped across several 768-cycle sample boundaries before it could acknowledge and re-arm. Include the sample deadline in devices_cycles_to_next_idle_event() when IRQ_SPU is unmasked (mask-gated like the other sources), expose the distance through psx_idle_cycles_to_next_observable_event() for tests, and extend psx_cycle_event_boundaries_test: masked IRQ9 does not bound the skip; unmasked IRQ9 stops it at the first sample boundary, never across later ones. --- runtime/include/psx_cycles.h | 2 ++ runtime/src/psx_cycles.c | 12 ++++++++ .../tests/test_psx_cycle_event_boundaries.c | 30 +++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/runtime/include/psx_cycles.h b/runtime/include/psx_cycles.h index cc954306..25ba1f3b 100644 --- a/runtime/include/psx_cycles.h +++ b/runtime/include/psx_cycles.h @@ -164,6 +164,8 @@ uint64_t psx_get_cycle_count(void); struct CPUState; void psx_idle_note_check(struct CPUState *cpu, uint32_t check_pc); int psx_idle_skip_is_enabled(void); +/* Cycles until the nearest IRQ-observable device event (mask-aware); the bound an idle skip may not cross. */ +uint32_t psx_idle_cycles_to_next_observable_event(void); extern int g_idle_skip_enabled; extern uint64_t g_idle_skip_count; extern uint64_t g_idle_skip_cycles; diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index c806eec7..1057093f 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -158,10 +158,22 @@ static uint32_t devices_cycles_to_next_idle_event(void) { uint32_t c = cdrom_cycles_to_irq(i_mask); if (c < best) best = c; uint32_t d = dma_cycles_to_deliverable_irq(i_mask); if (d < best) best = d; uint32_t s = sio_cycles_to_irq(i_mask); if (s < best) best = s; + /* SPU IRQ9 is raised by the guest-clock sample scheduler, not by a device + * *_advance(); a poll loop waiting on it must not be skipped across several + * 768-cycle sample boundaries before it can acknowledge and re-arm. Only an + * unmasked IRQ9 is observable, like the other sources above. */ + if (i_mask & (1u << IRQ_SPU)) { + uint32_t a = psx_spu_sample_event_cycles_to_next(); if (a < best) best = a; + } if (best == 0) best = 1; return best; } +/* Test/diagnostic accessor for the idle-skip observation boundary above. */ +uint32_t psx_idle_cycles_to_next_observable_event(void) { + return devices_cycles_to_next_idle_event(); +} + static void psx_devices_recompute_deadline(void) { uint32_t next = devices_cycles_to_next_internal_event(); if (next > PSX_DEADLINE_HARD_CAP) next = PSX_DEADLINE_HARD_CAP; diff --git a/runtime/tests/test_psx_cycle_event_boundaries.c b/runtime/tests/test_psx_cycle_event_boundaries.c index 1202cbd4..83b6e885 100644 --- a/runtime/tests/test_psx_cycle_event_boundaries.c +++ b/runtime/tests/test_psx_cycle_event_boundaries.c @@ -54,7 +54,8 @@ uint32_t dma_cycles_to_deliverable_irq(uint32_t mask) { } uint32_t sio_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; } /* SPU sample-event scheduler (golden 1a973806): psx_cycles.c consults it; stub here. */ -uint32_t psx_spu_sample_event_cycles_to_next(void) { return UINT32_MAX; } +static uint32_t s_spu_next_sample = UINT32_MAX; +uint32_t psx_spu_sample_event_cycles_to_next(void) { return s_spu_next_sample; } void psx_spu_sample_event_service(void) {} int psx_get_in_exception(void) { return 0; } @@ -88,6 +89,31 @@ int main(void) { return 1; } - fprintf(stderr, "PASS cross-device deadline preserves D-1 + 1 causality\n"); + /* Idle-skip observation boundary vs the SPU sample scheduler (mstan/psxrecomp#239 + * review): with IRQ9 unmasked, a wait loop must stop at the FIRST 768-cycle + * sample boundary, not be skipped across several; with IRQ9 masked the + * sample deadline is not observable and must not shorten the skip. */ + s_cd_ready = 1; /* no CD event ahead */ + s_spu_next_sample = 300; /* next sample boundary in 300 cycles */ + i_mask = 0; + if (psx_idle_cycles_to_next_observable_event() == 300) { + fprintf(stderr, "FAIL masked SPU IRQ9 must not bound the idle skip\n"); + return 1; + } + i_mask = 1u << 9; /* IRQ_SPU unmasked */ + if (psx_idle_cycles_to_next_observable_event() != 300) { + fprintf(stderr, "FAIL idle skip must stop at the first SPU sample boundary (300), got %u\n", + psx_idle_cycles_to_next_observable_event()); + return 1; + } + s_spu_next_sample = 768 * 4; /* several boundaries away: still the nearest event */ + if (psx_idle_cycles_to_next_observable_event() != 768 * 4) { + fprintf(stderr, "FAIL idle skip must not cross a later SPU sample boundary either\n"); + return 1; + } + s_spu_next_sample = UINT32_MAX; i_mask = 0; + + fprintf(stderr, "PASS cross-device deadline preserves D-1 + 1 causality; " + "idle skip bounded by the SPU sample deadline\n"); return 0; }