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