runtime: slice GPU linked-list DMA

This commit is contained in:
Alexandros Mandravillis
2026-08-30 19:22:01 +03:00
parent 47bda8172e
commit 8af48ae986
8 changed files with 448 additions and 108 deletions
+7
View File
@@ -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
+52
View File
@@ -0,0 +1,52 @@
#ifndef PSXRECOMP_DMA_GPU_LL_H
#define PSXRECOMP_DMA_GPU_LL_H
#include <stdint.h>
#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
+1
View File
@@ -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
+1
View File
@@ -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
+155 -108
View File
@@ -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;
}
+131
View File
@@ -0,0 +1,131 @@
#include "dma_gpu_ll.h"
#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
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);
}
+4
View File
@@ -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;
@@ -0,0 +1,97 @@
#include "dma_gpu_ll.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
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;
}