diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 2494923a..0f73d6b0 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -167,6 +167,13 @@ if(BUILD_TESTING) add_test(NAME mod_gpu_dma_aperture_test COMMAND mod_gpu_dma_aperture_test) + add_executable(dma_gpu_linked_list_timing_test + tests/test_dma_gpu_linked_list_timing.c + src/dma_gpu_ll.c) + target_include_directories(dma_gpu_linked_list_timing_test PRIVATE include) + add_test(NAME dma_gpu_linked_list_timing_test + COMMAND dma_gpu_linked_list_timing_test) + # Clean-room SPU DSP fidelity (issue #103): SPU IRQ on every RAM access # class, reverb (write gating + work-area wrap + CD-only path), noise # generator, volume sweeps / direct decode, capture buffers, and diff --git a/runtime/include/dma_gpu_ll.h b/runtime/include/dma_gpu_ll.h new file mode 100644 index 00000000..cd0a8f31 --- /dev/null +++ b/runtime/include/dma_gpu_ll.h @@ -0,0 +1,52 @@ +#ifndef PSXRECOMP_DMA_GPU_LL_H +#define PSXRECOMP_DMA_GPU_LL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + DMA_GPU_LL_PHASE_IDLE = 0, + DMA_GPU_LL_PHASE_HEADER = 1, + DMA_GPU_LL_PHASE_PAYLOAD = 2, + DMA_GPU_LL_PHASE_COMPLETE = 3 +}; + +typedef struct { + uint8_t active; + uint8_t phase; + uint8_t emit_node; + uint8_t hit_limit; + uint32_t start_addr; + uint32_t current_addr; + uint32_t next_addr; + uint32_t word_count; + uint32_t cycles_remaining; + uint32_t nodes_processed; + uint32_t max_nodes; + uint32_t total_words; + uint32_t empty_rank; +} DMAGPULinkedList; + +typedef struct { + uint32_t (*resolve_address)(void *opaque, uint32_t address); + uint32_t (*read_word)(void *opaque, uint32_t address); + int (*begin_node)(void *opaque, uint32_t address, uint32_t word_count); + void (*emit_word)(void *opaque, uint32_t address, uint32_t word); + void (*complete)(void *opaque, int hit_limit); +} DMAGPULinkedListOps; + +void dma_gpu_ll_start(DMAGPULinkedList *state, uint32_t start_addr, + uint32_t max_nodes); +void dma_gpu_ll_cancel(DMAGPULinkedList *state); +uint32_t dma_gpu_ll_cycles_to_event(const DMAGPULinkedList *state); +void dma_gpu_ll_advance(DMAGPULinkedList *state, uint32_t cycles, + const DMAGPULinkedListOps *ops, void *opaque); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 9abfd3e8..67f71f6a 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -350,6 +350,7 @@ void gpu_ws_set_nw_left_hud_packet_range(uint32_t lo, uint32_t hi); void gpu_ws_begin_linked_list(void); void gpu_ws_end_linked_list(void); void gpu_ws_prepass_linked_list(uint32_t start_addr); +void gpu_ws_restore_linked_list_rank(uint32_t rank); /* Native-wide full-frame 2D backdrop stretch ([widescreen] nw_backdrop): * stretch a screen-space quad that covers the whole 4:3 framebuffer (sky * gradient / backdrop image) to fill the wide frame, so it no longer diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 2265c1ee..e9b9aa7c 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -318,6 +318,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/gpu_render.c ${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c ${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c + ${PSXRECOMP_ROOT}/runtime/src/dma_gpu_ll.c ${PSXRECOMP_ROOT}/runtime/src/dma.c ${PSXRECOMP_ROOT}/runtime/src/mdec.c ${PSXRECOMP_ROOT}/runtime/src/timers.c diff --git a/runtime/src/dma.c b/runtime/src/dma.c index 2bb30e50..7d9ce988 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -16,6 +16,7 @@ #include "cdrom.h" #include "crash_trace.h" #include "dirty_ram_interp.h" +#include "dma_gpu_ll.h" #include "gpu.h" #include "mdec.h" #include "mod_memory.h" @@ -61,6 +62,7 @@ typedef struct { static DMAAsyncChannel mdec_async[2]; static DMAAsyncChannel cdrom_async; +static DMAGPULinkedList gpu_linked_list; /* ---- CD DMA transfer log ---- */ /* Every forward CH3 DMA that lands below 0x1C0000 (game data region) records @@ -389,6 +391,10 @@ static void cancel_async_transfer(int ch) { cdrom_async.remaining_words = 0; cdrom_async.cycles_accum = 0; } + if (ch == 2 && gpu_linked_list.active) { + gpu_ws_end_linked_list(); + dma_gpu_ll_cancel(&gpu_linked_list); + } if (ch >= 0 && ch < 7) { delayed_complete[ch].active = 0; delayed_complete[ch].total_words = 0; @@ -653,6 +659,89 @@ static void execute_ch1_mdec_out(void) { complete_transfer(1); } +static int gpu_ll_nd_flap_last(void) { + static int enabled = -1; + if (enabled < 0) { + const char *e = getenv("PSX_ND_SIB_FLAP_LAST"); + if (!e || !*e) e = getenv("PSX_ND_OT_OPAQUE_LAST"); + enabled = (e && *e && *e != '0') ? 1 : 0; + if (enabled) + fprintf(stdout, "psxrecomp: PSX_ND_SIB_FLAP_LAST enabled\n"); + } + return enabled; +} + +static uint32_t gpu_ll_resolve_address(void *opaque, uint32_t address) { + (void)opaque; + return psx_mod_gpu_dma_resolve_address(address); +} + +static uint32_t gpu_ll_read_word(void *opaque, uint32_t address) { + (void)opaque; + return psx_read_word(address); +} + +static int gpu_ll_begin_node(void *opaque, uint32_t addr, uint32_t num_words) { + (void)opaque; + int emit = 1; + uint32_t ot_rank = gpu_linked_list.empty_rank; + + if (gpu_ll_nd_flap_last() && num_words >= 6u && + ot_rank >= 1600u && ot_rank < 2100u) { + uint32_t word_addr = psx_mod_gpu_dma_resolve_address(addr + 4u); + uint32_t cmd = psx_read_word(word_addr); + uint32_t op = (cmd >> 24) & 0xFFu; + if (op == 0x36u) { + int32_t sx_max = -0x8000; + for (uint32_t vi = 1; vi <= 5; vi += 2) { + uint32_t xy = psx_read_word(psx_mod_gpu_dma_resolve_address( + word_addr + vi * 4u)); + int32_t sx = (int32_t)(xy & 0x7FFu); + if (sx & 0x400) sx -= 0x800; + if (sx > sx_max) sx_max = sx; + } + if (sx_max >= 280) emit = 0; + } + } + + if (emit) gpu_set_gp0_linked_list_node(addr, num_words); + return emit; +} + +static void gpu_ll_emit_word(void *opaque, uint32_t address, uint32_t word) { + (void)opaque; + gpu_set_gp0_source(address); + gpu_write_gp0(word); +} + +static void gpu_ll_complete(void *opaque, int hit_limit) { + (void)opaque; + channels[2].madr = hit_limit ? gpu_linked_list.current_addr + : 0x00FFFFFFu; + gpu_ws_end_linked_list(); + complete_transfer(2); +} + +static const DMAGPULinkedListOps gpu_ll_ops = { + gpu_ll_resolve_address, + gpu_ll_read_word, + gpu_ll_begin_node, + gpu_ll_emit_word, + gpu_ll_complete +}; + +static void start_async_gpu_linked_list(void) { + if (gpu_linked_list.active) return; + uint32_t start_addr = psx_mod_gpu_dma_resolve_address(channels[2].madr); + gpu_ws_begin_linked_list(); + /* This scan only prepares optional widescreen grouping. It does not send + * packets to GP0. The event-driven walker below performs every guest-visible + * header and payload read at its consumption boundary. */ + gpu_ws_prepass_linked_list(start_addr); + dma_gpu_ll_start(&gpu_linked_list, start_addr, 0x40000u); + event_ring_record_aux(EV_DMA_SCHED, 2u, channels[2].chcr); +} + static uint32_t execute_ch2_gpu(void) { uint32_t chcr = channels[2].chcr; uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM (to device) */ @@ -698,110 +787,9 @@ static uint32_t execute_ch2_gpu(void) { channels[2].madr = addr; actual_words = total_words; } else if (sync_mode == 2) { - /* Linked-list mode: follow ordering table in RAM. - * Each node: bits 24-31 = number of words following header, - * bits 0-23 = next node address (0xFFFFFF = end). - * The words following the header are sent to GP0. - * - * PSX_ND_SIB_FLAP_LAST=1 (opt-in): skip additive PolyGT3 (GP0 0x36) - * whose signed 11-bit SX max is >= 280 AND whose OT rank is in the ND - * digit-rain band (~1832). Was a flap-overpaint workaround; after the - * AVSZ3 MAC0 fix it shreds the crate glow fountain — leave unset/0. - * - * OT-rank gate is mandatory: the same SX filter matches menu/char-select - * 0x36 (e.g. ot~1066) and would strip Crash face/trophy semis. Rank is - * the empty-node count used by gpu_set_gp0_linked_list_node. - * - * SX must be parsed as signed 11-bit (gpu.c parse_vertex). Legacy alias: - * PSX_ND_OT_OPAQUE_LAST. */ - static int s_nd_flap_last = -1; - if (s_nd_flap_last < 0) { - const char *e = getenv("PSX_ND_SIB_FLAP_LAST"); - if (!e || !*e) - e = getenv("PSX_ND_OT_OPAQUE_LAST"); - s_nd_flap_last = (e && *e && *e != '0') ? 1 : 0; - if (s_nd_flap_last) - fprintf(stdout, "psxrecomp: PSX_ND_SIB_FLAP_LAST enabled\n"); - } - - gpu_ws_begin_linked_list(); - uint32_t start_addr = - psx_mod_gpu_dma_resolve_address(channels[2].madr); - gpu_ws_prepass_linked_list(start_addr); - const uint32_t MAX_NODES = 0x40000; /* prevent infinite loops */ - uint32_t last_addr = start_addr; - int hit_limit = 0; - /* Mirror gpu.c empty-node OT rank (0 after first empty header). */ - uint32_t ot_rank = 0xffffffffu; - - uint32_t addr = start_addr; - uint32_t safety = 0; - for (;;) { - if (safety++ > MAX_NODES) { - hit_limit = 1; - last_addr = addr; - break; - } - - uint32_t header = psx_read_word(addr); - uint32_t num_words = (header >> 24) & 0xFF; - uint32_t word_addr = - psx_mod_gpu_dma_resolve_address(addr + 4u); - - if (num_words == 0u) - ot_rank = (ot_rank == 0xffffffffu) ? 0u : (ot_rank + 1u); - - int emit = 1; - if (s_nd_flap_last && num_words >= 6u && - ot_rank >= 1600u && ot_rank < 2100u) { - uint32_t cmd = psx_read_word(word_addr); - uint32_t op = (cmd >> 24) & 0xFFu; - if (op == 0x36u) { - /* Match gpu.c parse_vertex: signed 11-bit SX. Digit-rain - * packets often stash junk in the high bits of the XY word; - * int16 SX mis-classifies them. Skip glow whose s11 bbox - * reaches the sibling right-flap band (sx_max >= 280). */ - int32_t sx_max = -0x8000; - for (uint32_t vi = 1; vi <= 5; vi += 2) { - uint32_t xy = psx_read_word( - psx_mod_gpu_dma_resolve_address(word_addr + - vi * 4u)); - int32_t sx = (int32_t)(xy & 0x7FFu); - if (sx & 0x400) - sx -= 0x800; - if (sx > sx_max) - sx_max = sx; - } - if (sx_max >= 280) - emit = 0; - } - } - - if (emit) { - gpu_set_gp0_linked_list_node(addr, num_words); - actual_words += 1u; - for (uint32_t i = 0; i < num_words; i++) { - uint32_t word = psx_read_word(word_addr); - gpu_set_gp0_source(word_addr); - gpu_write_gp0(word); - word_addr = - psx_mod_gpu_dma_resolve_address(word_addr + 4u); - } - actual_words += num_words; - } - - uint32_t next = header & 0xFFFFFFu; - if (next == 0xFFFFFFu) { - last_addr = 0x00FFFFFFu; - break; - } - addr = psx_mod_gpu_dma_resolve_address(next); - last_addr = addr; - } - (void)hit_limit; - - channels[2].madr = last_addr; - gpu_ws_end_linked_list(); + /* Linked-list mode is started by try_execute() and advanced from the + * guest cycle clock. It must never be drained synchronously here. */ + return 0; } else { /* Burst mode (sync_mode == 0) */ uint32_t word_count = channels[2].bcr & 0xFFFF; @@ -959,8 +947,13 @@ static void try_execute(int ch) { start_async_mdec_transfer(1); break; case 2: - schedule_delayed_complete(2, execute_ch2_gpu(), - DMA_GPU_CYCLES_PER_WORD); + if ((channels[2].chcr & 1u) != 0u && + ((channels[2].chcr >> 9) & 3u) == 2u) { + start_async_gpu_linked_list(); + } else { + schedule_delayed_complete(2, execute_ch2_gpu(), + DMA_GPU_CYCLES_PER_WORD); + } break; case 3: execute_ch3_cdrom(); @@ -1017,6 +1010,10 @@ uint32_t dma_cycles_to_irq(uint32_t i_mask) { ? (a->remaining_words - a->cycles_accum) : 0u; if (est < best) best = est; } + if (gpu_linked_list.active) { + uint32_t d = dma_gpu_ll_cycles_to_event(&gpu_linked_list); + 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; @@ -1063,6 +1060,12 @@ uint32_t dma_cycles_to_internal_event(void) { } } + if (gpu_linked_list.active && ((channels[2].chcr >> 24) & 1u) && + channel_enabled(2)) { + uint32_t d = dma_gpu_ll_cycles_to_event(&gpu_linked_list); + if (d < best) best = d; + } + for (int ch = 0; ch < 7; ch++) { if (delayed_complete[ch].active && delayed_complete[ch].cycles_remaining < best) @@ -1086,6 +1089,10 @@ uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask) { ? (a->remaining_words - a->cycles_accum) : 0u; if (est < best) best = est; } + if (channel_irq_flag_armed(2) && gpu_linked_list.active) { + uint32_t d = dma_gpu_ll_cycles_to_event(&gpu_linked_list); + if (d < best) best = d; + } for (int ch = 0; ch < 7; ch++) { if (channel_irq_flag_armed(ch) && delayed_complete[ch].active && delayed_complete[ch].cycles_remaining < best) @@ -1099,6 +1106,14 @@ void dma_advance(uint32_t cycles) { g_dma_exec_depth++; /* async to-RAM DMA writes below run through psx_write_word */ advance_mdec_channel(0, cycles); advance_mdec_channel(1, cycles); + if (gpu_linked_list.active && ((channels[2].chcr >> 24) & 1u) && + channel_enabled(2)) { + g_dma_cur_ch = 2; + g_dma_cur_madr = gpu_linked_list.current_addr; + g_dma_cur_bcr = channels[2].bcr; + g_dma_initiator_pc = s_dma_ch_initiator_pc[2]; + dma_gpu_ll_advance(&gpu_linked_list, cycles, &gpu_ll_ops, NULL); + } DMAAsyncChannel *a = &cdrom_async; if (dma_cdrom_transfer_active()) { uint32_t chcr = channels[3].chcr; @@ -1164,6 +1179,7 @@ void dma_init(void) { memset(channels, 0, sizeof(channels)); memset(mdec_async, 0, sizeof(mdec_async)); memset(&cdrom_async, 0, sizeof(cdrom_async)); + memset(&gpu_linked_list, 0, sizeof(gpu_linked_list)); memset(delayed_complete, 0, sizeof(delayed_complete)); dpcr = 0x07654321u; /* default: priorities set, no channels enabled */ dicr = 0; @@ -1308,14 +1324,17 @@ void dma_debug_get_state(DMADebugState* out) { out->channels[i].chcr = channels[i].chcr; out->channels[i].active = ((i < 2) ? mdec_async[i].active : 0) || + ((i == 2) ? gpu_linked_list.active : 0) || ((i == 3) ? cdrom_async.active : 0) || delayed_complete[i].active; out->channels[i].remaining_words = (i < 2 && mdec_async[i].active) ? mdec_async[i].remaining_words : + (i == 2 && gpu_linked_list.active) ? gpu_linked_list.word_count : (i == 3 && cdrom_async.active) ? cdrom_async.remaining_words : delayed_complete[i].total_words; out->channels[i].cycles_accum = (i < 2 && mdec_async[i].active) ? mdec_async[i].cycles_accum : + (i == 2 && gpu_linked_list.active) ? gpu_linked_list.cycles_remaining : (i == 3 && cdrom_async.active) ? cdrom_async.cycles_accum : delayed_complete[i].cycles_remaining; } @@ -1327,8 +1346,10 @@ void dma_debug_get_state(DMADebugState* out) { /* DMAChannel = 3×u32 (no pad). Async/delayed structs have host padding — field LE. */ #define DMA_ASYNC_WIRE (1u + 1u + 4u + 4u + 4u + 4u) /* 18 */ #define DMA_DELAY_WIRE (1u + 4u + 4u) /* 9 */ +#define DMA_GPU_LL_WIRE (4u + (9u * 4u)) /* 40 */ #define DMA_SNAP_WIRE_BYTES ( \ - (7u * 12u) + 4u + 4u + (2u * DMA_ASYNC_WIRE) + DMA_ASYNC_WIRE + (7u * DMA_DELAY_WIRE)) + (7u * 12u) + 4u + 4u + (2u * DMA_ASYNC_WIRE) + DMA_ASYNC_WIRE + \ + DMA_GPU_LL_WIRE + (7u * DMA_DELAY_WIRE)) static int dma_w_async(PstW *w, const DMAAsyncChannel *a) { return pst_w_u8(w, a->active) && pst_w_u8(w, a->debug_started) && @@ -1340,6 +1361,24 @@ static int dma_r_async(PstR *r, DMAAsyncChannel *a) { pst_r_u32(r, &a->total_words) && pst_r_u32(r, &a->remaining_words) && pst_r_u32(r, &a->cycles_accum) && pst_r_u32(r, &a->start_addr); } +static int dma_w_gpu_ll(PstW *w, const DMAGPULinkedList *s) { + return pst_w_u8(w, s->active) && pst_w_u8(w, s->phase) && + pst_w_u8(w, s->emit_node) && pst_w_u8(w, s->hit_limit) && + pst_w_u32(w, s->start_addr) && pst_w_u32(w, s->current_addr) && + pst_w_u32(w, s->next_addr) && pst_w_u32(w, s->word_count) && + pst_w_u32(w, s->cycles_remaining) && + pst_w_u32(w, s->nodes_processed) && pst_w_u32(w, s->max_nodes) && + pst_w_u32(w, s->total_words) && pst_w_u32(w, s->empty_rank); +} +static int dma_r_gpu_ll(PstR *r, DMAGPULinkedList *s) { + return pst_r_u8(r, &s->active) && pst_r_u8(r, &s->phase) && + pst_r_u8(r, &s->emit_node) && pst_r_u8(r, &s->hit_limit) && + pst_r_u32(r, &s->start_addr) && pst_r_u32(r, &s->current_addr) && + pst_r_u32(r, &s->next_addr) && pst_r_u32(r, &s->word_count) && + pst_r_u32(r, &s->cycles_remaining) && + pst_r_u32(r, &s->nodes_processed) && pst_r_u32(r, &s->max_nodes) && + pst_r_u32(r, &s->total_words) && pst_r_u32(r, &s->empty_rank); +} static int dma_w_delay(PstW *w, const DMADelayedComplete *d) { return pst_w_u8(w, d->active) && pst_w_u32(w, d->total_words) && pst_w_u32(w, d->cycles_remaining); @@ -1364,12 +1403,14 @@ void dma_snapshot_write(uint8_t *p) { dma_w_async(&w, &mdec_async[0]); dma_w_async(&w, &mdec_async[1]); dma_w_async(&w, &cdrom_async); + dma_w_gpu_ll(&w, &gpu_linked_list); for (int i = 0; i < 7; i++) dma_w_delay(&w, &delayed_complete[i]); } int dma_snapshot_read(const uint8_t *p, uint32_t len) { PstR r; + int gpu_ll_was_active = gpu_linked_list.active != 0; if (len != DMA_SNAP_WIRE_BYTES) return 0; pst_r_init(&r, p, len); for (int i = 0; i < 7; i++) { @@ -1379,9 +1420,15 @@ int dma_snapshot_read(const uint8_t *p, uint32_t len) { } if (!pst_r_u32(&r, &dpcr) || !pst_r_u32(&r, &dicr)) return 0; if (!dma_r_async(&r, &mdec_async[0]) || !dma_r_async(&r, &mdec_async[1]) || - !dma_r_async(&r, &cdrom_async)) + !dma_r_async(&r, &cdrom_async) || !dma_r_gpu_ll(&r, &gpu_linked_list)) return 0; for (int i = 0; i < 7; i++) if (!dma_r_delay(&r, &delayed_complete[i])) return 0; + if (gpu_ll_was_active) gpu_ws_end_linked_list(); + if (gpu_linked_list.active) { + gpu_ws_begin_linked_list(); + gpu_ws_prepass_linked_list(gpu_linked_list.start_addr); + gpu_ws_restore_linked_list_rank(gpu_linked_list.empty_rank); + } return 1; } diff --git a/runtime/src/dma_gpu_ll.c b/runtime/src/dma_gpu_ll.c new file mode 100644 index 00000000..7882b273 --- /dev/null +++ b/runtime/src/dma_gpu_ll.c @@ -0,0 +1,131 @@ +#include "dma_gpu_ll.h" + +#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 + +static uint32_t resolve_address(const DMAGPULinkedListOps *ops, void *opaque, + uint32_t address) { + return ops->resolve_address ? ops->resolve_address(opaque, address) : address; +} + +static void finish(DMAGPULinkedList *state, + const DMAGPULinkedListOps *ops, void *opaque) { + int hit_limit = state->hit_limit != 0; + state->active = 0; + state->phase = DMA_GPU_LL_PHASE_IDLE; + state->cycles_remaining = 0; + if (ops->complete) ops->complete(opaque, hit_limit); +} + +void dma_gpu_ll_start(DMAGPULinkedList *state, uint32_t start_addr, + uint32_t max_nodes) { + memset(state, 0, sizeof(*state)); + state->active = 1; + state->phase = DMA_GPU_LL_PHASE_HEADER; + state->start_addr = start_addr; + state->current_addr = start_addr; + state->cycles_remaining = DMA_GPU_LL_HEADER_CYCLES; + state->max_nodes = max_nodes; + state->empty_rank = UINT32_MAX; +} + +void dma_gpu_ll_cancel(DMAGPULinkedList *state) { + memset(state, 0, sizeof(*state)); +} + +uint32_t dma_gpu_ll_cycles_to_event(const DMAGPULinkedList *state) { + if (!state->active) return UINT32_MAX; + return state->cycles_remaining ? state->cycles_remaining : 1u; +} + +void dma_gpu_ll_advance(DMAGPULinkedList *state, uint32_t cycles, + const DMAGPULinkedListOps *ops, void *opaque) { + if (!state->active || cycles == 0 || !ops || !ops->read_word) return; + if (cycles < state->cycles_remaining) { + state->cycles_remaining -= cycles; + return; + } + + /* The runtime's deadline scheduler lands on each advertised boundary. If a + * caller reaches us late, process one boundary only. This preserves the + * same one-event-per-service contract as SIO, CD-ROM, and MDEC. */ + state->cycles_remaining = 0; + + if (state->phase == DMA_GPU_LL_PHASE_HEADER) { + if (state->nodes_processed >= state->max_nodes) { + state->hit_limit = 1; + finish(state, ops, opaque); + return; + } + + state->current_addr = resolve_address(ops, opaque, state->current_addr); + uint32_t header = ops->read_word(opaque, state->current_addr); + state->word_count = header >> 24; + state->next_addr = header & 0x00FFFFFFu; + state->nodes_processed++; + state->total_words++; + if (state->word_count == 0) + state->empty_rank = state->empty_rank == UINT32_MAX + ? 0u : state->empty_rank + 1u; + if (state->word_count != 0) { + state->emit_node = 1u; + state->phase = DMA_GPU_LL_PHASE_PAYLOAD; + state->cycles_remaining = DMA_GPU_LL_SETUP_CYCLES; + return; + } + + state->emit_node = ops->begin_node + ? (uint8_t)(ops->begin_node(opaque, state->current_addr, 0u) != 0) + : 1u; + + if (state->next_addr == 0x00FFFFFFu) { + finish(state, ops, opaque); + return; + } + state->current_addr = resolve_address(ops, opaque, state->next_addr); + state->cycles_remaining = DMA_GPU_LL_HEADER_CYCLES; + return; + } + + if (state->phase == DMA_GPU_LL_PHASE_PAYLOAD) { + uint32_t word_addr = resolve_address( + ops, opaque, state->current_addr + 4u); + state->emit_node = ops->begin_node + ? (uint8_t)(ops->begin_node(opaque, state->current_addr, + state->word_count) != 0) + : 1u; + if (state->emit_node) { + for (uint32_t i = 0; i < state->word_count; i++) { + uint32_t word = ops->read_word(opaque, word_addr); + if (ops->emit_word) ops->emit_word(opaque, word_addr, word); + word_addr = resolve_address(ops, opaque, word_addr + 4u); + } + } + state->total_words += state->word_count; + + if (state->next_addr == 0x00FFFFFFu) { + state->phase = DMA_GPU_LL_PHASE_COMPLETE; + state->cycles_remaining = state->word_count; + return; + } + state->phase = DMA_GPU_LL_PHASE_HEADER; + state->current_addr = resolve_address(ops, opaque, state->next_addr); + state->cycles_remaining = state->word_count + DMA_GPU_LL_HEADER_CYCLES; + return; + } + + if (state->phase == DMA_GPU_LL_PHASE_COMPLETE) { + finish(state, ops, opaque); + return; + } + + state->hit_limit = 1; + finish(state, ops, opaque); +} diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 13b4245b..3c4309f9 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2560,6 +2560,10 @@ void gpu_ws_end_linked_list(void) { gp0_ot_rank = 0xFFFFu; } +void gpu_ws_restore_linked_list_rank(uint32_t rank) { + gp0_ot_rank = rank == UINT32_MAX ? 0xFFFFu : (uint16_t)rank; +} + /* Horizontal display range (GP1(06h)) */ static uint32_t h_display_x1; diff --git a/runtime/tests/test_dma_gpu_linked_list_timing.c b/runtime/tests/test_dma_gpu_linked_list_timing.c new file mode 100644 index 00000000..37961c90 --- /dev/null +++ b/runtime/tests/test_dma_gpu_linked_list_timing.c @@ -0,0 +1,97 @@ +#include "dma_gpu_ll.h" + +#include +#include +#include + +static uint32_t ram[64]; +static uint32_t emitted[16]; +static uint32_t emitted_count; +static uint32_t completed; +static uint32_t hit_limit; + +static uint32_t resolve(void *opaque, uint32_t address) { + (void)opaque; + return address & 0xFCu; +} + +static uint32_t read_word(void *opaque, uint32_t address) { + (void)opaque; + return ram[(address & 0xFCu) >> 2]; +} + +static int begin_node(void *opaque, uint32_t address, uint32_t word_count) { + (void)opaque; + (void)address; + (void)word_count; + return 1; +} + +static void emit_word(void *opaque, uint32_t address, uint32_t word) { + (void)opaque; + (void)address; + emitted[emitted_count++] = word; +} + +static void complete(void *opaque, int limited) { + (void)opaque; + completed++; + hit_limit = (uint32_t)limited; +} + +static const DMAGPULinkedListOps ops = { + resolve, read_word, begin_node, emit_word, complete +}; + +#define CHECK(expr) do { \ + if (!(expr)) { \ + fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \ + return 1; \ + } \ +} while (0) + +int main(void) { + DMAGPULinkedList state; + memset(ram, 0, sizeof(ram)); + + /* Node 0 at 0x00 points to node 1 at 0x20. Node 1 terminates. */ + ram[0x00 / 4] = 0x01000020u; + ram[0x04 / 4] = 0x11111111u; + ram[0x20 / 4] = 0x01FFFFFFu; + 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); + CHECK(emitted_count == 0u); + + /* Header becomes visible at cycle 8; payload waits for setup. */ + 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 == 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. */ + ram[0x24 / 4] = 0xA5A5A5A5u; + CHECK(dma_gpu_ll_cycles_to_event(&state) == 9u); + dma_gpu_ll_advance(&state, 9u, &ops, NULL); + CHECK(emitted_count == 1u); + dma_gpu_ll_advance(&state, 5u, &ops, NULL); + CHECK(emitted_count == 2u && emitted[1] == 0xA5A5A5A5u); + CHECK(completed == 0u); + dma_gpu_ll_advance(&state, 1u, &ops, NULL); + CHECK(completed == 1u && hit_limit == 0u && !state.active); + + /* A malformed cycle is bounded and reports the safety stop. */ + 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); + CHECK(completed == 1u && hit_limit == 1u && !state.active); + + puts("dma_gpu_linked_list_timing_test: PASS"); + return 0; +}