runtime: restore DMA2 word-cost timing

This commit is contained in:
Alex Vanderveen
2026-09-02 18:01:15 -04:00
committed by Alexandros Mandravillis
parent 99f775c839
commit bc88c06c5f
2 changed files with 45 additions and 25 deletions
+10 -8
View File
@@ -2,13 +2,12 @@
#include <string.h>
/* 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;
}
}
+35 -17
View File
@@ -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");