diff --git a/runtime/src/dma_gpu_ll.c b/runtime/src/dma_gpu_ll.c index 07c08fd5..e102b03a 100644 --- a/runtime/src/dma_gpu_ll.c +++ b/runtime/src/dma_gpu_ll.c @@ -2,13 +2,12 @@ #include -/* PSX DMA2 linked-list timing used by the clean-room state machine. The header - * read costs eight guest clocks. A non-empty node then has five setup clocks, - * followed by one clock per payload word. Keeping these as separate events is - * important: the guest CPU can change a packet after it starts DMA and before - * DMA reaches that packet. */ -#define DMA_GPU_LL_HEADER_CYCLES 8u -#define DMA_GPU_LL_SETUP_CYCLES 5u +/* Charge one guest clock for each 32-bit word read: one header per node and + * one clock per payload word. There is no separate setup charge. Header and + * payload reads remain separate events so guest writes can affect an active + * transfer before DMA reaches the changed word. */ +#define DMA_GPU_LL_HEADER_CYCLES 1u +#define DMA_GPU_LL_SETUP_CYCLES 0u static uint32_t resolve_address(const DMAGPULinkedListOps *ops, void *opaque, uint32_t address) { @@ -130,6 +129,9 @@ void dma_gpu_ll_advance(DMAGPULinkedList *state, uint32_t cycles, return; } - if (!state->active || cycles == 0) return; + if (!state->active) return; + /* Consume a zero-cost phase transition in this service call. A later + * read still waits for its own non-zero event. */ + if (cycles == 0 && state->cycles_remaining != 0) return; } } diff --git a/runtime/tests/test_dma_gpu_linked_list_timing.c b/runtime/tests/test_dma_gpu_linked_list_timing.c index bac1f050..a7793f15 100644 --- a/runtime/tests/test_dma_gpu_linked_list_timing.c +++ b/runtime/tests/test_dma_gpu_linked_list_timing.c @@ -72,9 +72,7 @@ int main(void) { emitted_count = completed = hit_limit = 0u; dma_gpu_ll_start(&state, 0x40u, 8u); - dma_gpu_ll_advance(&state, 8u, &ops, NULL); - CHECK(emitted_count == 0u); - dma_gpu_ll_advance(&state, 5u, &ops, NULL); + dma_gpu_ll_advance(&state, 1u, &ops, NULL); CHECK(emitted_count == 0u); CHECK(dma_gpu_ll_cycles_to_event(&state) == 1u); dma_gpu_ll_advance(&state, 1u, &ops, NULL); @@ -96,38 +94,58 @@ int main(void) { ram[0x24 / 4] = 0x22222222u; dma_gpu_ll_start(&state, 0x00u, 8u); - CHECK(dma_gpu_ll_cycles_to_event(&state) == 8u); - dma_gpu_ll_advance(&state, 7u, &ops, NULL); + /* Each header and payload word costs one clock. Setup costs nothing. */ + CHECK(dma_gpu_ll_cycles_to_event(&state) == 1u); CHECK(emitted_count == 0u); - /* Header becomes visible at cycle 8; payload waits for setup. */ + /* Setup occurs with the header service. The payload keeps its own event. */ dma_gpu_ll_advance(&state, 1u, &ops, NULL); CHECK(emitted_count == 0u); - CHECK(dma_gpu_ll_cycles_to_event(&state) == 5u); - dma_gpu_ll_advance(&state, 5u, &ops, NULL); - CHECK(emitted_count == 0u); CHECK(dma_gpu_ll_cycles_to_event(&state) == 1u); dma_gpu_ll_advance(&state, 1u, &ops, NULL); CHECK(emitted_count == 1u && emitted[0] == 0x11111111u); - /* This is the regression: the CPU changes the later packet after DMA has - * started. DMA must read the new value when it reaches that packet. */ + /* This is what the sliced walker exists for, and the cost change must not + * weaken it: the CPU changes a later packet after DMA has started, and DMA + * must read the NEW value when it reaches that packet. */ ram[0x24 / 4] = 0xA5A5A5A5u; - CHECK(dma_gpu_ll_cycles_to_event(&state) == 8u); - dma_gpu_ll_advance(&state, 8u, &ops, NULL); - CHECK(emitted_count == 1u); - dma_gpu_ll_advance(&state, 5u, &ops, NULL); + CHECK(dma_gpu_ll_cycles_to_event(&state) == 1u); + dma_gpu_ll_advance(&state, 1u, &ops, NULL); CHECK(emitted_count == 1u); + CHECK(completed == 0u); dma_gpu_ll_advance(&state, 1u, &ops, NULL); CHECK(emitted_count == 2u && emitted[1] == 0xA5A5A5A5u); CHECK(completed == 1u && hit_limit == 0u && !state.active); + /* An empty ordering table costs one header clock per node. */ + { + const uint32_t NODES = 12u; + memset(ram, 0, sizeof(ram)); + for (uint32_t i = 0; i < NODES - 1u; i++) + ram[i] = ((i + 1u) * 4u); /* empty node -> next */ + ram[NODES - 1u] = 0x00FFFFFFu; /* empty terminator */ + + completed = hit_limit = emitted_count = 0u; + dma_gpu_ll_start(&state, 0x00u, 64u); + uint32_t spent = 0u; + while (state.active && spent < 1000u) { + uint32_t d = dma_gpu_ll_cycles_to_event(&state); + dma_gpu_ll_advance(&state, d, &ops, NULL); + spent += d; + } + CHECK(completed == 1u && hit_limit == 0u); + CHECK(state.nodes_processed == NODES); + CHECK(state.total_words == NODES); /* header words only */ + CHECK(spent == NODES); /* nodes + 0 payload words */ + } + /* A malformed cycle is bounded and reports the safety stop. */ + memset(ram, 0, sizeof(ram)); completed = hit_limit = 0u; ram[0x00 / 4] = 0x00000000u; dma_gpu_ll_start(&state, 0x00u, 1u); - dma_gpu_ll_advance(&state, 8u, &ops, NULL); - dma_gpu_ll_advance(&state, 8u, &ops, NULL); + dma_gpu_ll_advance(&state, 1u, &ops, NULL); + dma_gpu_ll_advance(&state, 1u, &ops, NULL); CHECK(completed == 1u && hit_limit == 1u && !state.active); puts("dma_gpu_linked_list_timing_test: PASS");