mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
runtime: preserve causality at device deadlines
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user