mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
49fe052a3e
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1615 lines
62 KiB
C
1615 lines
62 KiB
C
/* dma.c — PS1 DMA controller simulation (Phase 3).
|
||
*
|
||
* Implements all 7 channel register reads/writes, DPCR, DICR,
|
||
* and transfer execution for:
|
||
* - Ch2 (GPU): block mode and linked-list mode
|
||
* - Ch6 (OTC): ordering table clear
|
||
*
|
||
* Most channels execute synchronously when CHCR start bit is written. MDEC
|
||
* request-mode transfers are advanced from the guest cycle clock so games
|
||
* which synchronize video decode through DMA busy/request state see realistic
|
||
* backpressure.
|
||
* Reference: nocash PSX specs, DuckStation src/core/dma.cpp
|
||
*/
|
||
|
||
#include "dma.h"
|
||
#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"
|
||
#include "overlay_capture.h"
|
||
#include "psx_cycles.h"
|
||
#include "psx_memory.h"
|
||
#include "psx_cycle_freeze.h"
|
||
#include "spu.h"
|
||
#include "audio_trace.h"
|
||
#include "event_ring.h"
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <time.h>
|
||
#ifdef _WIN32
|
||
#ifndef WIN32_LEAN_AND_MEAN
|
||
#define WIN32_LEAN_AND_MEAN
|
||
#endif
|
||
#ifndef NOMINMAX
|
||
#define NOMINMAX
|
||
#endif
|
||
#include <windows.h>
|
||
#endif
|
||
#include "gpu_timeline.h"
|
||
|
||
/* Word-aligned main-RAM offset of a DMA address (MADR / stepped cursor)
|
||
* through the live geometry: retail folds to 2 MiB exactly as the DMAC's
|
||
* 0x1FFFFC decode does; the opt-in 8 MB map addresses its unique high pages. */
|
||
#define DMA_RAM_ADDR(a) (psx_ram_canonical_offset(a) & ~3u)
|
||
/* Memory access — defined in memory.c */
|
||
extern uint32_t psx_read_word(uint32_t addr);
|
||
extern void psx_write_word(uint32_t addr, uint32_t val);
|
||
|
||
/* Interrupt status — defined in memory.c */
|
||
extern uint32_t i_stat;
|
||
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
|
||
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||
extern uint32_t g_debug_current_func_addr;
|
||
extern uint32_t g_debug_last_store_pc;
|
||
extern uint64_t s_frame_count;
|
||
|
||
/* ---- Per-channel registers ---- */
|
||
|
||
typedef struct {
|
||
uint32_t madr; /* +0x00: Memory address */
|
||
uint32_t bcr; /* +0x04: Block control */
|
||
uint32_t chcr; /* +0x08: Channel control */
|
||
} DMAChannel;
|
||
|
||
static DMAChannel channels[7];
|
||
|
||
typedef struct {
|
||
uint8_t active;
|
||
uint8_t debug_started;
|
||
uint32_t total_words;
|
||
uint32_t remaining_words;
|
||
uint32_t cycles_accum;
|
||
uint32_t start_addr; /* madr at transfer start (CD overlay capture) */
|
||
} DMAAsyncChannel;
|
||
|
||
static DMAAsyncChannel mdec_async[2];
|
||
static DMAAsyncChannel cdrom_async;
|
||
static DMAGPULinkedList gpu_linked_list;
|
||
static DMAGpuOtStats gpu_ot_stats;
|
||
static uint64_t gpu_ot_start_cycle;
|
||
static uint32_t gpu_ot_polls_this_walk;
|
||
|
||
/* ---- CD DMA transfer log ---- */
|
||
/* Every forward CH3 DMA that lands below 0x1C0000 (game data region) records
|
||
* (setloc_lba, dest_addr, size). Transfers to 0x1C0000+ are FMV/streaming
|
||
* buffers and are excluded to keep the ring focused on overlay loads.
|
||
* Surfaced via the cd_read_log TCP command; pairs with overlay_dump to map
|
||
* overlay regions back to disc positions for extract_overlays.py. */
|
||
#define CD_DMA_LOG_CAP 65536
|
||
typedef struct { int lba; uint32_t dest; uint32_t size; } CdDmaEntry;
|
||
static CdDmaEntry cd_dma_log[CD_DMA_LOG_CAP];
|
||
static uint32_t cd_dma_log_head = 0;
|
||
static uint32_t cd_dma_log_total = 0;
|
||
|
||
static void cd_dma_log_push(int lba, uint32_t dest, uint32_t size) {
|
||
cd_dma_log[cd_dma_log_head % CD_DMA_LOG_CAP].lba = lba;
|
||
cd_dma_log[cd_dma_log_head % CD_DMA_LOG_CAP].dest = dest;
|
||
cd_dma_log[cd_dma_log_head % CD_DMA_LOG_CAP].size = size;
|
||
cd_dma_log_head = (cd_dma_log_head + 1) % CD_DMA_LOG_CAP;
|
||
cd_dma_log_total++;
|
||
}
|
||
|
||
uint32_t cd_dma_log_get_total(void) { return cd_dma_log_total; }
|
||
void cd_dma_log_get_entry(uint32_t idx, int *lba, uint32_t *dest, uint32_t *size) {
|
||
uint32_t cap = CD_DMA_LOG_CAP;
|
||
uint32_t oldest = cd_dma_log_total > cap ? cd_dma_log_total - cap : 0;
|
||
if (idx < oldest || idx >= cd_dma_log_total) { *lba = -1; return; }
|
||
uint32_t slot = idx % cap;
|
||
*lba = cd_dma_log[slot].lba;
|
||
*dest = cd_dma_log[slot].dest;
|
||
*size = cd_dma_log[slot].size;
|
||
}
|
||
|
||
#define DMA_MDEC_IN_CYCLES_PER_WORD 1u
|
||
#define DMA_MDEC_OUT_CYCLES_PER_WORD 14u
|
||
#define DMA_GPU_CYCLES_PER_WORD 1u
|
||
#define DMA_CDROM_CYCLES_PER_WORD 1u
|
||
/* SPU DMA (ch4) per-word cost. Faithful to the Beetle/mednafen oracle, which
|
||
* charges `extra_cyc_overhead = 47` per word plus the universal 1 cyc/word in
|
||
* RunChannel (mednafen/psx/dma.cpp:267,294,456) => 48 cyc/word. Previously ch4
|
||
* completed in ZERO cycles (instant complete_transfer), so the DMA-completion
|
||
* IRQ fired the same cycle as the kick. Once the I-cache cycle model lands, the
|
||
* BIOS SPU-init loop's instruction timing interleaves with that instant IRQ into
|
||
* an exception re-entry storm (MMX6 boot wedge: kick ch4 -> 0-cyc done -> IRQ ->
|
||
* ack -> re-kick, forever). Deferring completion the faithful ~48 cyc/word breaks
|
||
* the storm and matches hardware (the SPU RAM payload still moves immediately;
|
||
* only the busy-bit clear + completion IRQ are deferred). */
|
||
#define DMA_SPU_CYCLES_PER_WORD 48u
|
||
|
||
/* DMA-execution provenance flags (read by memory.c's psx_write_word d44_note probe
|
||
* for the MMX6 VSync-callback-pointer corruption hunt). g_dma_exec_depth>0 means a
|
||
* DMA is currently moving data through psx_write_word, so a RAM write seen there is
|
||
* DMA-sourced (not a CPU/dirty-interp store, whose g_debug_last_store_pc is accurate).
|
||
* cur_ch/cur_madr/cur_bcr name the in-flight channel + its destination, so a wrong
|
||
* DMA destination clobbering kernel data becomes directly visible. */
|
||
int g_dma_exec_depth = 0;
|
||
int g_dma_cur_ch = -1;
|
||
uint32_t g_dma_cur_madr = 0;
|
||
uint32_t g_dma_cur_bcr = 0;
|
||
/* Guest PC that kicked the in-flight DMA (the CHCR store that set start/busy),
|
||
* so write-provenance (wtrace / parity note_write) attributes DMA-sourced RAM
|
||
* writes to the code that INITIATED the transfer, not to g_debug_last_store_pc
|
||
* (which for an async transfer is a stale, unrelated CPU store). Captured at the
|
||
* kick in trigger_dma_transfer and re-published for async channels whose RAM
|
||
* writes run in a later advance_*() step. 0 = unknown. */
|
||
uint32_t g_dma_initiator_pc = 0;
|
||
static uint32_t s_dma_ch_initiator_pc[7] = {0};
|
||
|
||
typedef struct {
|
||
uint8_t active;
|
||
uint32_t total_words;
|
||
uint32_t cycles_remaining;
|
||
} DMADelayedComplete;
|
||
|
||
static DMADelayedComplete delayed_complete[7];
|
||
|
||
/* ---- Global registers ---- */
|
||
|
||
static uint32_t dpcr; /* 0x1F8010F0: DMA control (enable bits) */
|
||
static uint32_t dicr; /* 0x1F8010F4: DMA interrupt control */
|
||
|
||
#define DICR_WRITE_MASK 0x00FF807Fu
|
||
#define DICR_RESET_MASK 0x7F000000u
|
||
|
||
static DMATraceEntry dma_trace[DMA_TRACE_CAP];
|
||
static uint64_t dma_trace_seq;
|
||
static DMACDROMHistoryEntry cdrom_dma_history[DMA_CDROM_HISTORY_CAP];
|
||
static uint64_t cdrom_dma_history_seq;
|
||
static DMACDROMHistoryEntry cdrom_dma_active_entry;
|
||
static uint8_t cdrom_dma_history_active;
|
||
|
||
static uint32_t dicr_read_value(uint32_t v);
|
||
|
||
static void trace_dma(uint32_t kind, int ch, uint32_t total_words,
|
||
uint32_t dicr_before, uint32_t i_stat_before) {
|
||
DMATraceEntry *e = &dma_trace[dma_trace_seq % DMA_TRACE_CAP];
|
||
e->seq = dma_trace_seq++;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->kind = kind;
|
||
e->channel = (uint32_t)ch;
|
||
e->total_words = total_words;
|
||
e->addr = 0;
|
||
e->val = 0;
|
||
e->mask = 0;
|
||
e->madr = (ch >= 0 && ch < 7) ? channels[ch].madr : 0;
|
||
e->bcr = (ch >= 0 && ch < 7) ? channels[ch].bcr : 0;
|
||
e->chcr = (ch >= 0 && ch < 7) ? channels[ch].chcr : 0;
|
||
e->dpcr = dpcr;
|
||
e->dicr_before = dicr_read_value(dicr_before);
|
||
e->dicr_after = dicr_read_value(dicr);
|
||
e->i_stat_before = i_stat_before;
|
||
e->i_stat_after = i_stat;
|
||
e->func = g_debug_current_func_addr;
|
||
e->pc = g_debug_last_store_pc;
|
||
}
|
||
|
||
static void trace_dma_reg_write(uint32_t addr, uint32_t val, uint32_t mask,
|
||
uint32_t dicr_before,
|
||
uint32_t i_stat_before) {
|
||
DMATraceEntry *e = &dma_trace[dma_trace_seq % DMA_TRACE_CAP];
|
||
e->seq = dma_trace_seq++;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->kind = 'W';
|
||
e->channel = 0xFFFFFFFFu;
|
||
e->total_words = 0;
|
||
e->addr = addr;
|
||
e->val = val;
|
||
e->mask = mask;
|
||
e->madr = 0;
|
||
e->bcr = 0;
|
||
e->chcr = 0;
|
||
e->dpcr = dpcr;
|
||
e->dicr_before = dicr_read_value(dicr_before);
|
||
e->dicr_after = dicr_read_value(dicr);
|
||
e->i_stat_before = i_stat_before;
|
||
e->i_stat_after = i_stat;
|
||
e->func = g_debug_current_func_addr;
|
||
e->pc = g_debug_last_store_pc;
|
||
}
|
||
|
||
static void gpu_ot_record_walk_stats(uint64_t cycles) {
|
||
gpu_ot_stats.nodes_last = gpu_linked_list.nodes_processed;
|
||
gpu_ot_stats.words_last = gpu_linked_list.total_words;
|
||
gpu_ot_stats.cycles_last = cycles;
|
||
if (gpu_ot_stats.nodes_last > gpu_ot_stats.nodes_max)
|
||
gpu_ot_stats.nodes_max = gpu_ot_stats.nodes_last;
|
||
if (gpu_ot_stats.words_last > gpu_ot_stats.words_max)
|
||
gpu_ot_stats.words_max = gpu_ot_stats.words_last;
|
||
if (gpu_ot_stats.cycles_last > gpu_ot_stats.cycles_max)
|
||
gpu_ot_stats.cycles_max = gpu_ot_stats.cycles_last;
|
||
}
|
||
|
||
/* ---- Helpers ---- */
|
||
|
||
static void update_master_irq(void) {
|
||
/* DICR bit 31 (master IRQ flag) is read-only, calculated as:
|
||
* bit31 = bit15 OR (bit23 AND ((bits16-22 AND bits24-30) != 0))
|
||
*
|
||
* We store bits 0-30 in dicr and compute bit 31 on read.
|
||
*
|
||
* I_STAT bit 3 is set on the aggregate DICR bit31 0->1 transition.
|
||
* Per-channel DICR flags are latched by complete_transfer() only
|
||
* when both master IRQ and that channel IRQ are enabled. */
|
||
}
|
||
|
||
static uint32_t dicr_master_flag(uint32_t v) {
|
||
return ((v & (1u << 15)) ||
|
||
((v & (1u << 23)) && (v & DICR_RESET_MASK))) ? (1u << 31) : 0;
|
||
}
|
||
|
||
static uint32_t dicr_read_value(uint32_t v) {
|
||
return (v & ~(1u << 31)) | dicr_master_flag(v);
|
||
}
|
||
|
||
static void raise_dma_irq_on_master_edge(uint32_t before) {
|
||
/* IRQ3 is latched only when DICR's aggregate master flag transitions
|
||
* from 0 to 1. Pending channel flags keep bit 31 high, but do not
|
||
* continuously re-latch I_STAT after software acknowledges IRQ3. */
|
||
if (!dicr_master_flag(before) && dicr_master_flag(dicr)) {
|
||
psx_irq_raise(3, (dicr >> 24) & 0x7Fu); /* detail = DICR per-channel IRQ flags */
|
||
}
|
||
}
|
||
|
||
static void start_cdrom_dma_capture(uint32_t requested_words) {
|
||
CDROMDebugState s;
|
||
cdrom_debug_snapshot(&s);
|
||
|
||
memset(&cdrom_dma_active_entry, 0, sizeof(cdrom_dma_active_entry));
|
||
cdrom_dma_active_entry.seq = cdrom_dma_history_seq++;
|
||
cdrom_dma_active_entry.frame_start = (uint32_t)s_frame_count;
|
||
cdrom_dma_active_entry.start_addr = DMA_RAM_ADDR(channels[3].madr);
|
||
cdrom_dma_active_entry.final_addr = cdrom_dma_active_entry.start_addr;
|
||
cdrom_dma_active_entry.requested_words = requested_words;
|
||
cdrom_dma_active_entry.bcr = channels[3].bcr;
|
||
cdrom_dma_active_entry.chcr = channels[3].chcr;
|
||
cdrom_dma_active_entry.dpcr = dpcr;
|
||
cdrom_dma_active_entry.dicr_start = dicr_read_value(dicr);
|
||
cdrom_dma_active_entry.i_stat_start = i_stat;
|
||
cdrom_dma_active_entry.func = g_debug_current_func_addr;
|
||
cdrom_dma_active_entry.pc = g_debug_last_store_pc;
|
||
cdrom_dma_active_entry.lba = s.last_sector_lba;
|
||
cdrom_dma_active_entry.sector_size =
|
||
(s.sector_size > 0) ? s.sector_size : s.last_sector_size;
|
||
cdrom_dma_active_entry.sector_read_pos_start = s.sector_read_pos;
|
||
cdrom_dma_active_entry.mode =
|
||
s.last_sector_mode ? s.last_sector_mode : s.mode_reg;
|
||
cdrom_dma_active_entry.sector_available_start =
|
||
(uint8_t)(s.sector_available ? 1 : 0);
|
||
cdrom_dma_history_active = 1;
|
||
}
|
||
|
||
static void record_cdrom_dma_word(uint32_t word) {
|
||
if (!cdrom_dma_history_active) return;
|
||
|
||
DMACDROMHistoryEntry *e = &cdrom_dma_active_entry;
|
||
if (e->first_count < DMA_CDROM_HISTORY_WORDS) {
|
||
e->first_words[e->first_count++] = word;
|
||
}
|
||
|
||
if (e->last_count < DMA_CDROM_HISTORY_WORDS) {
|
||
e->last_words[e->last_count++] = word;
|
||
} else {
|
||
memmove(e->last_words, e->last_words + 1,
|
||
sizeof(e->last_words[0]) * (DMA_CDROM_HISTORY_WORDS - 1));
|
||
e->last_words[DMA_CDROM_HISTORY_WORDS - 1] = word;
|
||
}
|
||
|
||
e->moved_words++;
|
||
}
|
||
|
||
static void finish_cdrom_dma_capture(uint32_t final_addr, uint8_t completed) {
|
||
if (!cdrom_dma_history_active) return;
|
||
|
||
CDROMDebugState s;
|
||
cdrom_debug_snapshot(&s);
|
||
|
||
cdrom_dma_active_entry.frame_end = (uint32_t)s_frame_count;
|
||
cdrom_dma_active_entry.final_addr = DMA_RAM_ADDR(final_addr);
|
||
cdrom_dma_active_entry.dicr_end = dicr_read_value(dicr);
|
||
cdrom_dma_active_entry.i_stat_end = i_stat;
|
||
cdrom_dma_active_entry.sector_read_pos_end = s.sector_read_pos;
|
||
cdrom_dma_active_entry.sector_available_end =
|
||
(uint8_t)(s.sector_available ? 1 : 0);
|
||
cdrom_dma_active_entry.completed = completed ? 1 : 0;
|
||
|
||
cdrom_dma_history[
|
||
cdrom_dma_active_entry.seq % DMA_CDROM_HISTORY_CAP] =
|
||
cdrom_dma_active_entry;
|
||
cdrom_dma_history_active = 0;
|
||
}
|
||
|
||
static int channel_enabled(int ch) {
|
||
/* DPCR: each channel has 4 bits, bit 3 of each group is enable.
|
||
* Ch0 = bits 0-3, Ch1 = bits 4-7, etc. Enable = bit (ch*4 + 3). */
|
||
return (dpcr >> (ch * 4 + 3)) & 1;
|
||
}
|
||
|
||
static int channel_irq_flag_armed(int ch) {
|
||
/* DICR channel completion flags (24+n) are latched only when both
|
||
* the per-channel interrupt bit and the master DMA interrupt bit are
|
||
* enabled at completion time. Old flags still contribute to bit31
|
||
* until acknowledged, but masked/master-disabled completions do not
|
||
* create new stale flags. */
|
||
return ((dicr >> (16 + ch)) & 1u) && ((dicr >> 23) & 1u);
|
||
}
|
||
|
||
static uint32_t transfer_word_count(int ch) {
|
||
uint32_t chcr = channels[ch].chcr;
|
||
uint32_t sync_mode = (chcr >> 9) & 3;
|
||
uint32_t block_size = channels[ch].bcr & 0xFFFFu;
|
||
uint32_t block_count = (channels[ch].bcr >> 16) & 0xFFFFu;
|
||
|
||
if (ch == 3 && block_size == 0) {
|
||
return cdrom_dma_sector_word_count();
|
||
}
|
||
|
||
if (sync_mode == 1) {
|
||
if (block_size == 0) block_size = 0x10000u;
|
||
if (block_count == 0) block_count = 1u;
|
||
return block_size * block_count;
|
||
}
|
||
|
||
if (block_size == 0) block_size = 0x10000u;
|
||
return block_size;
|
||
}
|
||
|
||
/* A single DMA kick can never legitimately move more data than the larger
|
||
* of RAM (2 MB) or VRAM (1 MB): 0x80000 words. A bigger count means the
|
||
* MADR/BCR/CHCR the guest programmed are corrupt; executing it would grind
|
||
* through billions of masked-wrap accesses feeding garbage to the device
|
||
* sinks. Halt diagnosably with rings intact instead. Linked-list GPU
|
||
* transfers (ch2 sync mode 2) don't use BCR, so length isn't checked there
|
||
* (the node walker has its own MAX_NODES cycle cap). */
|
||
#define DMA_MAX_TRANSFER_WORDS 0x80000u
|
||
|
||
static void validate_transfer_length(int ch) {
|
||
uint32_t sync_mode = (channels[ch].chcr >> 9) & 3u;
|
||
if (ch == 2 && sync_mode == 2) return;
|
||
uint32_t words = transfer_word_count(ch);
|
||
if (words > DMA_MAX_TRANSFER_WORDS) {
|
||
static char reason[160];
|
||
snprintf(reason, sizeof(reason),
|
||
"DMA ch%d insane transfer length 0x%X words "
|
||
"(MADR=0x%08X BCR=0x%08X CHCR=0x%08X)",
|
||
ch, words, channels[ch].madr, channels[ch].bcr,
|
||
channels[ch].chcr);
|
||
psx_fatal_halt(reason);
|
||
}
|
||
}
|
||
|
||
static void complete_transfer(int ch) {
|
||
uint32_t dicr_before = dicr;
|
||
uint32_t i_stat_before = i_stat;
|
||
channels[ch].chcr &= ~((1u << 24) | (1u << 28));
|
||
if (channel_irq_flag_armed(ch)) {
|
||
dicr |= (1u << (24 + ch));
|
||
raise_dma_irq_on_master_edge(dicr_before);
|
||
}
|
||
trace_dma('C', ch, 0, dicr_before, i_stat_before);
|
||
event_ring_record_aux(EV_DMA_DONE, (uint8_t)ch, channels[ch].chcr);
|
||
event_ring_record_aux(EV_DEQ, (uint8_t)(SRC_DMA0 + ch), channels[ch].chcr);
|
||
}
|
||
|
||
/* ---- Transfer execution ---- */
|
||
|
||
static void cancel_async_transfer(int ch) {
|
||
if (ch >= 0 && ch < 2) {
|
||
mdec_async[ch].active = 0;
|
||
mdec_async[ch].debug_started = 0;
|
||
mdec_async[ch].total_words = 0;
|
||
mdec_async[ch].remaining_words = 0;
|
||
mdec_async[ch].cycles_accum = 0;
|
||
}
|
||
if (ch == 3) {
|
||
finish_cdrom_dma_capture(DMA_RAM_ADDR(channels[3].madr), 0);
|
||
cdrom_async.active = 0;
|
||
cdrom_async.debug_started = 0;
|
||
cdrom_async.total_words = 0;
|
||
cdrom_async.remaining_words = 0;
|
||
cdrom_async.cycles_accum = 0;
|
||
}
|
||
if (ch == 2 && gpu_linked_list.active) {
|
||
uint64_t cycles = psx_cycle_count - gpu_ot_start_cycle;
|
||
gpu_ot_record_walk_stats(cycles);
|
||
DMAGpuOtCancel *c = &gpu_ot_stats.cancel_ring[
|
||
gpu_ot_stats.cancel_ring_count % DMA_GPU_OT_CANCEL_RING];
|
||
c->pc = g_debug_last_store_pc;
|
||
c->chcr = channels[2].chcr;
|
||
c->nodes = gpu_linked_list.nodes_processed;
|
||
c->words = gpu_linked_list.total_words;
|
||
c->cycles = cycles > UINT32_MAX ? UINT32_MAX : (uint32_t)cycles;
|
||
c->polls = gpu_ot_polls_this_walk;
|
||
gpu_ot_stats.cancel_ring_count++;
|
||
gpu_ot_stats.cancels++;
|
||
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;
|
||
delayed_complete[ch].cycles_remaining = 0;
|
||
}
|
||
}
|
||
|
||
static void schedule_delayed_complete(int ch, uint32_t total_words,
|
||
uint32_t cycles_per_word) {
|
||
/* Render passes run in frozen guest time: a delayed completion would
|
||
* never arrive, so finish now (the pass restores DMA state after). */
|
||
if (total_words == 0 || cycles_per_word == 0 || g_psx_render_pass_active) {
|
||
complete_transfer(ch);
|
||
return;
|
||
}
|
||
|
||
uint64_t cycles = (uint64_t)total_words * (uint64_t)cycles_per_word;
|
||
if (cycles > UINT32_MAX) cycles = UINT32_MAX;
|
||
|
||
delayed_complete[ch].active = 1;
|
||
delayed_complete[ch].total_words = total_words;
|
||
delayed_complete[ch].cycles_remaining = (uint32_t)cycles;
|
||
event_ring_record_aux(EV_DMA_SCHED, (uint8_t)ch, channels[ch].chcr);
|
||
}
|
||
|
||
static void advance_delayed_complete(int ch, uint32_t cycles) {
|
||
DMADelayedComplete *d = &delayed_complete[ch];
|
||
if (!d->active) return;
|
||
if (cycles < d->cycles_remaining) {
|
||
d->cycles_remaining -= cycles;
|
||
return;
|
||
}
|
||
|
||
d->active = 0;
|
||
d->total_words = 0;
|
||
d->cycles_remaining = 0;
|
||
complete_transfer(ch);
|
||
}
|
||
|
||
static void start_async_mdec_transfer(int ch) {
|
||
DMAAsyncChannel *a = &mdec_async[ch];
|
||
if (a->active) return;
|
||
|
||
a->active = 1;
|
||
a->debug_started = 0;
|
||
a->total_words = transfer_word_count(ch);
|
||
a->remaining_words = a->total_words;
|
||
a->cycles_accum = 0;
|
||
|
||
if (ch == 0) {
|
||
mdec_debug_dma_in_start(DMA_RAM_ADDR(channels[0].madr), a->remaining_words);
|
||
} else {
|
||
mdec_debug_dma_out_start(DMA_RAM_ADDR(channels[1].madr), a->remaining_words);
|
||
}
|
||
a->debug_started = 1;
|
||
}
|
||
|
||
static void finish_async_mdec_transfer(int ch, uint32_t final_addr, uint32_t total_words) {
|
||
if (ch == 0) {
|
||
mdec_debug_dma_in_end(final_addr, total_words);
|
||
} else {
|
||
mdec_debug_dma_out_end(final_addr, total_words);
|
||
}
|
||
cancel_async_transfer(ch);
|
||
complete_transfer(ch);
|
||
}
|
||
|
||
static void start_async_cdrom_transfer(void) {
|
||
DMAAsyncChannel *a = &cdrom_async;
|
||
if (a->active) return;
|
||
|
||
a->active = 1;
|
||
a->debug_started = 0;
|
||
a->total_words = transfer_word_count(3);
|
||
a->remaining_words = a->total_words;
|
||
a->cycles_accum = 0;
|
||
a->start_addr = DMA_RAM_ADDR(channels[3].madr);
|
||
start_cdrom_dma_capture(a->total_words);
|
||
|
||
if (a->total_words == 0) {
|
||
finish_cdrom_dma_capture(DMA_RAM_ADDR(channels[3].madr), 1);
|
||
cancel_async_transfer(3);
|
||
complete_transfer(3);
|
||
}
|
||
}
|
||
|
||
int dma_host_cdrom_write(uint32_t dest, const uint8_t *src, uint32_t bytes, int lba) {
|
||
if ((dest & 3u) || (bytes & 3u) || (!src && bytes)) return 0;
|
||
const uint32_t addr = DMA_RAM_ADDR(dest);
|
||
const uint32_t live = psx_ram_live_bytes();
|
||
if (addr >= live || bytes > live - addr) return 0;
|
||
if (!bytes) return 1;
|
||
/* Same capture window as the real ch3 path: game data, not FMV buffers. */
|
||
const int capture = addr < 0x1C0000u || addr >= PSX_MAIN_RAM_RETAIL_BYTES;
|
||
const int saved_ch = g_dma_cur_ch;
|
||
const uint32_t saved_madr = g_dma_cur_madr, saved_bcr = g_dma_cur_bcr;
|
||
g_dma_exec_depth++;
|
||
g_dma_cur_ch = 3;
|
||
g_dma_cur_bcr = bytes / 4u;
|
||
if (capture) overlay_capture_before_dma(addr, bytes);
|
||
for (uint32_t i = 0; i < bytes; i += 4u) {
|
||
uint32_t word;
|
||
memcpy(&word, src + i, 4);
|
||
g_dma_cur_madr = addr + i;
|
||
psx_write_word(addr + i, word);
|
||
}
|
||
dirty_ram_mark_executable_range(addr, bytes);
|
||
if (capture) {
|
||
if (lba >= 0) cd_dma_log_push(lba, addr, bytes);
|
||
extern uint8_t *memory_get_ram_ptr(void);
|
||
overlay_capture_on_dma(addr, bytes, memory_get_ram_ptr() + addr);
|
||
}
|
||
g_dma_cur_ch = saved_ch;
|
||
g_dma_cur_madr = saved_madr;
|
||
g_dma_cur_bcr = saved_bcr;
|
||
g_dma_exec_depth--;
|
||
return 1;
|
||
}
|
||
|
||
static void finish_async_cdrom_transfer(uint32_t final_addr) {
|
||
finish_cdrom_dma_capture(final_addr, 1);
|
||
DMAAsyncChannel *a = &cdrom_async;
|
||
uint32_t step = (channels[3].chcr >> 1) & 1u;
|
||
uint32_t load_start = a->start_addr;
|
||
uint32_t size = a->total_words * 4u;
|
||
|
||
/* Log and capture game-data transfers only.
|
||
* Transfers to 0x1C0000+ are FMV/streaming buffers; skip them. */
|
||
if (!step && size > 0 &&
|
||
(load_start < 0x1C0000u || load_start >= PSX_MAIN_RAM_RETAIL_BYTES)) {
|
||
/* The DMA word loop wraps inside the live RAM mask; the capture
|
||
* path below takes a flat ram+offset span and must not follow the
|
||
* wrap past the end of host RAM. */
|
||
if (size > psx_ram_live_bytes() - load_start)
|
||
size = psx_ram_live_bytes() - load_start;
|
||
int lba = cdrom_get_setloc_lba();
|
||
if (lba >= 0) cd_dma_log_push(lba, load_start, size);
|
||
|
||
/* B-1: capture overlay bytes into the write-once capture set.
|
||
* overlay_capture_on_dma auto-activates after game handoff and is a
|
||
* no-op unless the overlay cache is enabled in config. */
|
||
extern uint8_t *memory_get_ram_ptr(void);
|
||
uint8_t *ram = memory_get_ram_ptr();
|
||
overlay_capture_on_dma(load_start, size, ram + load_start);
|
||
}
|
||
|
||
channels[3].madr = final_addr;
|
||
cancel_async_transfer(3);
|
||
complete_transfer(3);
|
||
}
|
||
|
||
static void advance_mdec_channel(int ch, uint32_t cycles) {
|
||
DMAAsyncChannel *a = &mdec_async[ch];
|
||
if (!a->active) return;
|
||
if (!((channels[ch].chcr >> 24) & 1u) || !channel_enabled(ch)) return;
|
||
|
||
uint32_t chcr = channels[ch].chcr;
|
||
uint32_t direction = chcr & 1u;
|
||
uint32_t step = (chcr >> 1) & 1u;
|
||
int32_t addr_step = step ? -4 : 4;
|
||
uint32_t cycles_per_word = (ch == 0) ? DMA_MDEC_IN_CYCLES_PER_WORD : DMA_MDEC_OUT_CYCLES_PER_WORD;
|
||
|
||
if ((ch == 0 && direction == 0) || (ch == 1 && direction != 0)) {
|
||
uint32_t words = a->total_words;
|
||
uint32_t addr = DMA_RAM_ADDR(channels[ch].madr);
|
||
finish_async_mdec_transfer(ch, addr, words);
|
||
return;
|
||
}
|
||
|
||
if (ch == 0) {
|
||
if (!mdec_dma_write_ready()) return;
|
||
} else {
|
||
if (!mdec_dma_read_ready()) return;
|
||
}
|
||
|
||
if (cycles > UINT32_MAX - a->cycles_accum) {
|
||
a->cycles_accum = UINT32_MAX;
|
||
} else {
|
||
a->cycles_accum += cycles;
|
||
}
|
||
|
||
uint32_t words_budget = a->cycles_accum / cycles_per_word;
|
||
if (words_budget == 0) return;
|
||
|
||
uint32_t addr = DMA_RAM_ADDR(channels[ch].madr);
|
||
uint32_t moved = 0;
|
||
/* ch1 (MDEC→RAM) writes guest RAM here in a deferred step; mark it as
|
||
* DMA-sourced so write-provenance tags these writes with ch1 + the kick PC
|
||
* (ch0 only reads RAM, so it needs no marking). Mirrors the CDROM async. */
|
||
int mdec_writes_ram = (ch == 1);
|
||
if (mdec_writes_ram) {
|
||
g_dma_exec_depth++;
|
||
g_dma_cur_ch = 1; g_dma_cur_bcr = channels[1].bcr;
|
||
g_dma_initiator_pc = s_dma_ch_initiator_pc[1];
|
||
}
|
||
/* Contiguous +4 ch0 (RAM→MDEC): feed via LE burst helper. Guest halfwords
|
||
* + decode triggers match the per-word loop; wraps / decrementing MADR
|
||
* and ch1 (needs psx_write_word watchers) stay on the word path. */
|
||
if (ch == 0 && addr_step == 4) {
|
||
extern uint8_t *memory_get_ram_ptr(void);
|
||
uint8_t *ram = memory_get_ram_ptr();
|
||
while (a->remaining_words > 0 && words_budget > 0) {
|
||
if (!mdec_dma_write_ready()) break;
|
||
uint32_t n = a->remaining_words < words_budget
|
||
? a->remaining_words : words_budget;
|
||
uint32_t max_by_ram = (psx_ram_live_bytes() - addr) / 4u;
|
||
if (max_by_ram == 0) break;
|
||
if (n > max_by_ram) n = max_by_ram;
|
||
uint32_t got =
|
||
mdec_dma_write_words((const uint32_t *)(ram + addr), n);
|
||
if (got == 0) break;
|
||
addr = DMA_RAM_ADDR((addr + got * 4u));
|
||
a->remaining_words -= got;
|
||
words_budget -= got;
|
||
moved += got;
|
||
}
|
||
} else {
|
||
while (a->remaining_words > 0 && words_budget > 0) {
|
||
if (ch == 0) {
|
||
if (!mdec_dma_write_ready()) break;
|
||
mdec_dma_write_word(psx_read_word(addr));
|
||
} else {
|
||
if (!mdec_dma_read_ready()) break;
|
||
g_dma_cur_madr = addr;
|
||
psx_write_word(addr, mdec_dma_read_word());
|
||
}
|
||
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
a->remaining_words--;
|
||
words_budget--;
|
||
moved++;
|
||
}
|
||
}
|
||
if (mdec_writes_ram) { g_dma_cur_ch = -1; g_dma_exec_depth--; }
|
||
|
||
if (moved == 0) return;
|
||
|
||
a->cycles_accum -= moved * cycles_per_word;
|
||
channels[ch].madr = addr;
|
||
|
||
if (a->remaining_words == 0) {
|
||
finish_async_mdec_transfer(ch, addr, a->total_words);
|
||
}
|
||
}
|
||
|
||
static void execute_ch0_mdec_in(void) {
|
||
uint32_t chcr = channels[0].chcr;
|
||
uint32_t direction = chcr & 1; /* 1=from RAM to MDEC */
|
||
uint32_t step = (chcr >> 1) & 1;
|
||
uint32_t total_words = transfer_word_count(0);
|
||
uint32_t addr = DMA_RAM_ADDR(channels[0].madr);
|
||
int32_t addr_step = step ? -4 : 4;
|
||
|
||
if (direction != 0) {
|
||
mdec_debug_dma_in_start(addr, total_words);
|
||
if (addr_step == 4 && total_words > 0 &&
|
||
addr + total_words * 4u <= psx_ram_live_bytes()) {
|
||
extern uint8_t *memory_get_ram_ptr(void);
|
||
uint32_t got = mdec_dma_write_words(
|
||
(const uint32_t *)(memory_get_ram_ptr() + addr), total_words);
|
||
addr = DMA_RAM_ADDR((addr + got * 4u));
|
||
/* If the FIFO stalled mid-burst, finish any remainder word-wise. */
|
||
for (uint32_t i = got; i < total_words; i++) {
|
||
mdec_dma_write_word(psx_read_word(addr));
|
||
addr = DMA_RAM_ADDR((addr + 4u));
|
||
}
|
||
} else {
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
mdec_dma_write_word(psx_read_word(addr));
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
}
|
||
mdec_debug_dma_in_end(addr, total_words);
|
||
channels[0].madr = addr;
|
||
}
|
||
|
||
complete_transfer(0);
|
||
}
|
||
|
||
static void execute_ch1_mdec_out(void) {
|
||
uint32_t chcr = channels[1].chcr;
|
||
uint32_t direction = chcr & 1; /* 0=from MDEC to RAM */
|
||
uint32_t step = (chcr >> 1) & 1;
|
||
uint32_t total_words = transfer_word_count(1);
|
||
uint32_t addr = DMA_RAM_ADDR(channels[1].madr);
|
||
int32_t addr_step = step ? -4 : 4;
|
||
|
||
if (direction == 0) {
|
||
mdec_debug_dma_out_start(addr, total_words);
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
psx_write_word(addr, mdec_dma_read_word());
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
mdec_debug_dma_out_end(addr, total_words);
|
||
channels[1].madr = addr;
|
||
}
|
||
|
||
complete_transfer(1);
|
||
}
|
||
|
||
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 void gpu_ll_observe_header(void *opaque, uint32_t addr,
|
||
uint32_t header) {
|
||
(void)opaque;
|
||
gpu_ws_validate_linked_list_header(addr, header);
|
||
}
|
||
|
||
static int gpu_ll_begin_node(void *opaque, uint32_t addr, uint32_t num_words) {
|
||
(void)opaque;
|
||
|
||
gpu_ws_validate_linked_list_node(addr, num_words);
|
||
gpu_set_gp0_linked_list_node(addr, num_words);
|
||
return 1;
|
||
}
|
||
|
||
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;
|
||
gpu_ot_stats.completes++;
|
||
gpu_ot_record_walk_stats(psx_cycle_count - gpu_ot_start_cycle);
|
||
gpu_timeline_note(GTL_LL_END, gpu_linked_list.nodes_processed,
|
||
gpu_linked_list.total_words);
|
||
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_observe_header,
|
||
gpu_ll_begin_node,
|
||
gpu_ll_emit_word,
|
||
gpu_ll_complete
|
||
};
|
||
|
||
static void start_async_gpu_linked_list(void) {
|
||
if (gpu_linked_list.active) {
|
||
gpu_ot_stats.starts_dropped++;
|
||
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);
|
||
gpu_timeline_note(GTL_LL_START, start_addr, 0);
|
||
event_ring_record_aux(EV_DMA_SCHED, 2u, channels[2].chcr);
|
||
gpu_ot_stats.starts++;
|
||
gpu_ot_start_cycle = psx_cycle_count;
|
||
gpu_ot_polls_this_walk = 0;
|
||
}
|
||
|
||
/* PSX-SPX "CPU Operation during DMA": DMA outruns CPU memory access, and a CPU
|
||
* read of RAM or I/O waits while DMA moves data. So the kicking store of a GPU
|
||
* linked-list transfer returns only when the walk ends: guest code that
|
||
* rewrites a packet right after DrawOTag (the retail SCPH1001 intro re-copies
|
||
* its text packets from a template, clearing the link) cannot reach it before
|
||
* the walk does. A cyclic list still ends at the walker's node limit. The hold
|
||
* also ends if an advance moves no list word. */
|
||
/* Always-on hold observability (dma_hold_stats / dma_hold_ring). Every held
|
||
* kick is recorded with its guest cost, its host wall time and whether it was
|
||
* made from inside an exception handler: a kick from an IRQ callback runs the
|
||
* whole walk (and the GPU work it feeds) with in_exception set, which is what
|
||
* the phase profiler's exc_share then measures. */
|
||
extern int psx_get_in_exception(void);
|
||
static DMAGpuHoldEntry s_hold_ring[DMA_GPU_HOLD_RING_CAP];
|
||
static DMAGpuHoldStats s_hold_stats;
|
||
|
||
static uint64_t hold_host_ticks(void) {
|
||
#ifdef _WIN32
|
||
LARGE_INTEGER c; QueryPerformanceCounter(&c); return (uint64_t)c.QuadPart;
|
||
#else
|
||
struct timespec ts; timespec_get(&ts, TIME_UTC); /* C11; diagnostics only */
|
||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||
#endif
|
||
}
|
||
|
||
static uint64_t hold_host_ticks_per_sec(void) {
|
||
#ifdef _WIN32
|
||
LARGE_INTEGER f; QueryPerformanceFrequency(&f); return (uint64_t)f.QuadPart;
|
||
#else
|
||
return 1000000000ull;
|
||
#endif
|
||
}
|
||
|
||
static void hold_cpu_for_gpu_linked_list(void) {
|
||
if (psx_in_device_service || g_ls_replay_active || g_psx_guest_time_frozen)
|
||
return;
|
||
const uint64_t cyc0 = psx_cycle_count;
|
||
const uint64_t host0 = hold_host_ticks();
|
||
const uint32_t words0 = gpu_linked_list.total_words;
|
||
uint32_t steps = 0;
|
||
while (gpu_linked_list.active && ((channels[2].chcr >> 24) & 1u) &&
|
||
channel_enabled(2)) {
|
||
const uint32_t moved = gpu_linked_list.total_words;
|
||
psx_advance_cycles(dma_gpu_ll_cycles_to_event(&gpu_linked_list));
|
||
psx_devices_service_to_now();
|
||
steps++;
|
||
if (gpu_linked_list.active && gpu_linked_list.total_words == moved) break;
|
||
}
|
||
const uint64_t host_us = (hold_host_ticks() - host0) * 1000000ull /
|
||
hold_host_ticks_per_sec();
|
||
const int in_exc = psx_get_in_exception() != 0;
|
||
DMAGpuHoldEntry *e = &s_hold_ring[s_hold_stats.count & (DMA_GPU_HOLD_RING_CAP - 1u)];
|
||
e->seq = s_hold_stats.count;
|
||
e->cycle = cyc0;
|
||
e->held_cycles = (uint32_t)(psx_cycle_count - cyc0);
|
||
e->words = gpu_linked_list.total_words - words0;
|
||
e->steps = steps;
|
||
e->host_us = (uint32_t)(host_us > 0xFFFFFFFFull ? 0xFFFFFFFFull : host_us);
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->kick_pc = g_debug_last_store_pc;
|
||
e->madr = channels[2].madr;
|
||
e->in_exception = (uint8_t)in_exc;
|
||
e->still_active = (uint8_t)(gpu_linked_list.active != 0);
|
||
s_hold_stats.count++;
|
||
s_hold_stats.held_cycles += e->held_cycles;
|
||
s_hold_stats.host_us += host_us;
|
||
s_hold_stats.words += e->words;
|
||
s_hold_stats.steps += steps;
|
||
if (in_exc) {
|
||
s_hold_stats.exc_count++;
|
||
s_hold_stats.exc_held_cycles += e->held_cycles;
|
||
s_hold_stats.exc_host_us += host_us;
|
||
}
|
||
}
|
||
|
||
void dma_debug_get_gpu_hold(DMAGpuHoldStats *out_stats,
|
||
const DMAGpuHoldEntry **out_ring) {
|
||
if (out_stats) *out_stats = s_hold_stats;
|
||
if (out_ring) *out_ring = s_hold_ring;
|
||
}
|
||
|
||
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) */
|
||
uint32_t step = (chcr >> 1) & 1; /* 0=forward(+4), 1=backward(-4) */
|
||
uint32_t sync_mode = (chcr >> 9) & 3; /* 0=burst, 1=block, 2=linked-list */
|
||
uint32_t actual_words = 0;
|
||
|
||
if (direction == 0) {
|
||
/* GPU → RAM (VRAM read): read pixel data via GPUREAD.
|
||
* A prior GP0(C0h) command must have set up the VRAM read region. */
|
||
if (sync_mode == 1) {
|
||
uint32_t block_size = channels[2].bcr & 0xFFFF;
|
||
uint32_t block_count = (channels[2].bcr >> 16) & 0xFFFF;
|
||
uint32_t total_words = block_size * block_count;
|
||
uint32_t addr = DMA_RAM_ADDR(channels[2].madr);
|
||
int32_t addr_step = step ? -4 : 4;
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
uint32_t pixel_data = gpu_read_gpuread();
|
||
psx_write_word(addr, pixel_data);
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
channels[2].madr = addr;
|
||
actual_words = total_words;
|
||
}
|
||
return actual_words;
|
||
}
|
||
|
||
/* direction == 1: RAM → GPU */
|
||
if (sync_mode == 1) {
|
||
/* Block mode: BCR bits 0-15 = block size (words), bits 16-31 = block count */
|
||
uint32_t block_size = channels[2].bcr & 0xFFFF;
|
||
uint32_t block_count = (channels[2].bcr >> 16) & 0xFFFF;
|
||
uint32_t total_words = block_size * block_count;
|
||
uint32_t addr = DMA_RAM_ADDR(channels[2].madr); /* mask to RAM, word-aligned */
|
||
int32_t addr_step = step ? -4 : 4;
|
||
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
uint32_t word = psx_read_word(addr);
|
||
gpu_set_gp0_source(addr);
|
||
gpu_write_gp0(word);
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
channels[2].madr = addr;
|
||
actual_words = total_words;
|
||
} else if (sync_mode == 2) {
|
||
/* 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;
|
||
if (word_count == 0) word_count = 0x10000; /* 0 means 0x10000 */
|
||
uint32_t addr = DMA_RAM_ADDR(channels[2].madr);
|
||
int32_t addr_step = step ? -4 : 4;
|
||
|
||
for (uint32_t i = 0; i < word_count; i++) {
|
||
uint32_t word = psx_read_word(addr);
|
||
gpu_set_gp0_source(addr);
|
||
gpu_write_gp0(word);
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
channels[2].madr = addr;
|
||
actual_words = word_count;
|
||
}
|
||
|
||
return actual_words;
|
||
}
|
||
|
||
static void execute_ch3_cdrom(void) {
|
||
uint32_t chcr = channels[3].chcr;
|
||
uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM */
|
||
|
||
if (direction != 0) {
|
||
channels[3].chcr &= ~((1u << 24) | (1u << 28));
|
||
return;
|
||
}
|
||
|
||
start_async_cdrom_transfer();
|
||
}
|
||
|
||
/* Returns the number of words moved so the caller can schedule a faithful
|
||
* delayed completion (DMA_SPU_CYCLES_PER_WORD). The payload moves immediately
|
||
* (SPU RAM is correct the instant this returns); only the busy-bit clear and
|
||
* completion IRQ are deferred by schedule_delayed_complete. */
|
||
static uint32_t execute_ch4_spu(void) {
|
||
uint32_t chcr = channels[4].chcr;
|
||
uint32_t direction = chcr & 1; /* 1=from RAM to SPU, 0=SPU to RAM */
|
||
uint32_t step = (chcr >> 1) & 1;
|
||
uint32_t total_words = transfer_word_count(4);
|
||
uint32_t addr = DMA_RAM_ADDR(channels[4].madr);
|
||
int32_t addr_step = step ? -4 : 4;
|
||
|
||
if (direction != 0) {
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
spu_dma_write(psx_read_word(addr));
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
/* One aggregated event per SPU-bound transfer (per-word would flood
|
||
* the ring: a full sound-bank upload is ~128k words). */
|
||
audio_trace_event(AUDIO_EV_DMA_WRITE, total_words,
|
||
DMA_RAM_ADDR(channels[4].madr));
|
||
} else {
|
||
/* SPU RAM -> CPU RAM. This direction previously zero-filled the
|
||
* destination, which is not a transfer at all: SPU RAM is readable
|
||
* memory and games do read it back. Titles that carry state through
|
||
* SPU RAM across an Exec boundary (a checksummed block surviving an
|
||
* EXE swap, since SPU RAM is one of the few regions main RAM's reload
|
||
* does not touch) got zeros, failed their own integrity check, and
|
||
* fell back to a cold-boot path. */
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
psx_write_word(addr, spu_dma_read());
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
audio_trace_event(AUDIO_EV_DMA_READ, total_words,
|
||
DMA_RAM_ADDR(channels[4].madr));
|
||
}
|
||
|
||
channels[4].madr = addr;
|
||
return total_words;
|
||
}
|
||
|
||
static void execute_ch6_otc(void) {
|
||
/* OTC (Ordering Table Clear): writes a backward-linked list to RAM.
|
||
* Node N = address of node N-1, node 0 = 0xFFFFFF (end marker).
|
||
* Direction is always to-RAM, step is always backward.
|
||
* BCR bits 0-15 = number of entries. */
|
||
uint32_t num_entries = channels[6].bcr & 0xFFFF;
|
||
if (num_entries == 0) num_entries = 0x10000;
|
||
uint32_t addr = DMA_RAM_ADDR(channels[6].madr);
|
||
|
||
for (uint32_t i = 0; i < num_entries; i++) {
|
||
uint32_t val;
|
||
if (i == num_entries - 1) {
|
||
/* Last entry (first in memory): end marker */
|
||
val = 0x00FFFFFFu;
|
||
} else {
|
||
/* Points to the previous entry (addr - 4) */
|
||
val = (addr - 4) & 0x00FFFFFFu;
|
||
}
|
||
psx_write_word(addr, val);
|
||
addr = DMA_RAM_ADDR((addr - 4));
|
||
}
|
||
|
||
complete_transfer(6);
|
||
}
|
||
|
||
static void execute_ch5_pio(void) {
|
||
/* PIO (Parallel I/O) — used for expansion port / parallel port transfers.
|
||
* Very simple: just move words directly to/from RAM with no device interaction.
|
||
* Direction: 0 = to RAM (read from device), 1 = from RAM (write to device).
|
||
* For now, we just complete the transfer immediately as PIO devices are
|
||
* typically slow and the game handles timing via busy-wait on the port. */
|
||
uint32_t chcr = channels[5].chcr;
|
||
uint32_t direction = chcr & 1u;
|
||
uint32_t step = (chcr >> 1) & 1u;
|
||
uint32_t total_words = transfer_word_count(5);
|
||
uint32_t addr = DMA_RAM_ADDR(channels[5].madr);
|
||
int32_t addr_step = step ? -4 : 4;
|
||
|
||
if (direction == 1) {
|
||
/* from RAM to device: just read and discard */
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
(void)psx_read_word(addr);
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
} else {
|
||
/* to RAM from device: write zeros (device not emulated) */
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
psx_write_word(addr, 0);
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
}
|
||
}
|
||
channels[5].madr = addr;
|
||
complete_transfer(5);
|
||
}
|
||
|
||
static void try_execute(int ch) {
|
||
uint32_t chcr = channels[ch].chcr;
|
||
|
||
/* Transfer starts when bit 24 (start/busy) is set AND channel is enabled in DPCR */
|
||
if (!((chcr >> 24) & 1)) return;
|
||
if (!channel_enabled(ch)) return;
|
||
|
||
channels[ch].chcr &= ~(1u << 28);
|
||
trace_dma('S', ch, transfer_word_count(ch), dicr, i_stat);
|
||
event_ring_record_aux(EV_DMA_KICK, (uint8_t)ch, channels[ch].chcr);
|
||
event_ring_record_aux(EV_ENQ, (uint8_t)(SRC_DMA0 + ch), transfer_word_count(ch));
|
||
|
||
/* After the kick is in the rings, refuse corrupt-length transfers. */
|
||
validate_transfer_length(ch);
|
||
|
||
/* Capture the kick PC (this CHCR store) so both the immediate (sync) writes
|
||
* below and any deferred async writes for this channel attribute to it. */
|
||
s_dma_ch_initiator_pc[ch] = g_debug_last_store_pc;
|
||
g_dma_initiator_pc = g_debug_last_store_pc;
|
||
g_dma_exec_depth++;
|
||
g_dma_cur_ch = ch; g_dma_cur_madr = channels[ch].madr; g_dma_cur_bcr = channels[ch].bcr;
|
||
switch (ch) {
|
||
case 0:
|
||
start_async_mdec_transfer(0);
|
||
break;
|
||
case 1:
|
||
start_async_mdec_transfer(1);
|
||
break;
|
||
case 2:
|
||
if ((channels[2].chcr & 1u) != 0u &&
|
||
((channels[2].chcr >> 9) & 3u) == 2u) {
|
||
start_async_gpu_linked_list();
|
||
/* Render pass: devices never advance in frozen time, so walk
|
||
* the whole list now. The walk is the same event-driven code,
|
||
* only with an unlimited budget; completion flags and the IRQ
|
||
* latch it raises are rolled back with the pass. */
|
||
while (g_psx_render_pass_active && gpu_linked_list.active)
|
||
dma_gpu_ll_advance(&gpu_linked_list, UINT32_MAX,
|
||
&gpu_ll_ops, NULL);
|
||
hold_cpu_for_gpu_linked_list();
|
||
} else {
|
||
schedule_delayed_complete(2, execute_ch2_gpu(),
|
||
DMA_GPU_CYCLES_PER_WORD);
|
||
}
|
||
break;
|
||
case 3:
|
||
execute_ch3_cdrom();
|
||
break;
|
||
case 4:
|
||
schedule_delayed_complete(4, execute_ch4_spu(),
|
||
DMA_SPU_CYCLES_PER_WORD);
|
||
break;
|
||
case 5:
|
||
execute_ch5_pio();
|
||
break;
|
||
case 6:
|
||
execute_ch6_otc();
|
||
break;
|
||
default: {
|
||
/* Other channels not implemented yet — fatal if transfer is triggered */
|
||
static char reason[96];
|
||
snprintf(reason, sizeof(reason),
|
||
"DMA ch%d: transfer triggered but not implemented (CHCR=0x%08X)",
|
||
ch, chcr);
|
||
psx_fatal_halt(reason);
|
||
}
|
||
}
|
||
g_dma_exec_depth--;
|
||
g_dma_cur_ch = -1;
|
||
}
|
||
|
||
/* ---- Public interface ---- */
|
||
|
||
uint32_t dma_get_dicr(void) { return dicr_read_value(dicr); }
|
||
/* A GPU linked-list walk is in flight (render passes refuse to start then). */
|
||
int dma_gpu_linked_list_active(void) { return gpu_linked_list.active != 0; }
|
||
uint32_t dma_get_dpcr(void) { return dpcr; }
|
||
int dma_cdrom_transfer_active(void) {
|
||
return cdrom_async.active &&
|
||
((channels[3].chcr >> 24) & 1u) &&
|
||
channel_enabled(3) &&
|
||
((channels[3].chcr & 1u) == 0);
|
||
}
|
||
|
||
/* Cycle-budgeted precise event slicing: guest CPU cycles until DMA raises a
|
||
* DELIVERABLE IRQ (bit3 unmasked in i_mask). UINT32_MAX if none. Conservative
|
||
* under-estimate: for async channels uses a 1-cycle/word floor minus cycles
|
||
* already accumulated (always <= the true remaining, since per-word cost >= 1),
|
||
* and the exact countdown for delayed-complete channels. Over-slicing on a
|
||
* channel whose DICR completion is masked is safe. See PRECISE_IRQ_SLICE.md. */
|
||
uint32_t dma_cycles_to_irq(uint32_t i_mask) {
|
||
if (!(i_mask & (1u << 3))) return 0xFFFFFFFFu; /* IRQ_DMA masked */
|
||
uint32_t best = 0xFFFFFFFFu;
|
||
const DMAAsyncChannel *async_ch[3] = { &mdec_async[0], &mdec_async[1], &cdrom_async };
|
||
for (int i = 0; i < 3; i++) {
|
||
const DMAAsyncChannel *a = async_ch[i];
|
||
if (!a->active || a->remaining_words == 0) continue;
|
||
/* floor: rw*per_word - accum >= rw - accum (per_word >= 1). Clamp >=0. */
|
||
uint32_t est = a->remaining_words > a->cycles_accum
|
||
? (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;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
uint32_t dma_cycles_to_internal_event(void) {
|
||
uint32_t best = 0xFFFFFFFFu;
|
||
|
||
/* Async MDEC channels move one word whenever their cycle accumulator
|
||
* reaches the channel cost. Completion-only scheduling batches many RAM
|
||
* writes at a later service boundary, which is observably different when
|
||
* the CPU polls the destination buffer. */
|
||
for (int ch = 0; ch < 2; ch++) {
|
||
const DMAAsyncChannel *a = &mdec_async[ch];
|
||
if (!a->active || a->remaining_words == 0 ||
|
||
!((channels[ch].chcr >> 24) & 1u) || !channel_enabled(ch))
|
||
continue;
|
||
uint32_t direction = channels[ch].chcr & 1u;
|
||
if ((ch == 0 && direction == 0) || (ch == 1 && direction != 0))
|
||
return 1u; /* invalid route is completed on the next DMA tick */
|
||
if ((ch == 0 && !mdec_dma_write_ready()) ||
|
||
(ch == 1 && !mdec_dma_read_ready()))
|
||
continue;
|
||
uint32_t cpw = ch == 0 ? DMA_MDEC_IN_CYCLES_PER_WORD
|
||
: DMA_MDEC_OUT_CYCLES_PER_WORD;
|
||
uint32_t d = a->cycles_accum < cpw ? cpw - a->cycles_accum : 1u;
|
||
if (d < best) best = d;
|
||
}
|
||
|
||
/* CD-ROM DMA writes guest RAM incrementally. Expose each word on time;
|
||
* waiting only for the channel-complete IRQ can delay hundreds of writes. */
|
||
if (cdrom_async.active && cdrom_async.remaining_words != 0 &&
|
||
((channels[3].chcr >> 24) & 1u) && channel_enabled(3)) {
|
||
if ((channels[3].chcr & 1u) != 0) {
|
||
return 1u; /* unsupported RAM->CD direction cancels next tick */
|
||
}
|
||
if (cdrom_dma_ready()) {
|
||
uint32_t d = cdrom_async.cycles_accum < DMA_CDROM_CYCLES_PER_WORD
|
||
? DMA_CDROM_CYCLES_PER_WORD - cdrom_async.cycles_accum
|
||
: 1u;
|
||
if (d < best) best = d;
|
||
}
|
||
}
|
||
|
||
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)
|
||
best = delayed_complete[ch].cycles_remaining;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask) {
|
||
if (!(i_mask & (1u << 3))) return 0xFFFFFFFFu;
|
||
uint32_t best = 0xFFFFFFFFu;
|
||
const int async_num[3] = { 0, 1, 3 };
|
||
const DMAAsyncChannel *async_ch[3] = {
|
||
&mdec_async[0], &mdec_async[1], &cdrom_async
|
||
};
|
||
for (int i = 0; i < 3; i++) {
|
||
const DMAAsyncChannel *a = async_ch[i];
|
||
if (!channel_irq_flag_armed(async_num[i]) ||
|
||
!a->active || a->remaining_words == 0) continue;
|
||
uint32_t est = a->remaining_words > a->cycles_accum
|
||
? (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)
|
||
best = delayed_complete[ch].cycles_remaining;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
void dma_advance(uint32_t cycles) {
|
||
if (cycles == 0) return;
|
||
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;
|
||
uint32_t direction = chcr & 1u;
|
||
uint32_t step = (chcr >> 1) & 1u;
|
||
int32_t addr_step = step ? -4 : 4;
|
||
|
||
if (direction != 0) {
|
||
cancel_async_transfer(3);
|
||
channels[3].chcr &= ~((1u << 24) | (1u << 28));
|
||
} else if (cdrom_dma_ready()) {
|
||
if (cycles > UINT32_MAX - a->cycles_accum) {
|
||
a->cycles_accum = UINT32_MAX;
|
||
} else {
|
||
a->cycles_accum += cycles;
|
||
}
|
||
|
||
uint32_t words_budget = a->cycles_accum / DMA_CDROM_CYCLES_PER_WORD;
|
||
uint32_t addr = DMA_RAM_ADDR(channels[3].madr);
|
||
uint32_t moved = 0;
|
||
g_dma_cur_ch = 3; g_dma_cur_bcr = channels[3].bcr;
|
||
g_dma_initiator_pc = s_dma_ch_initiator_pc[3]; /* deferred: restore kick PC */
|
||
/* Snapshot outgoing executed code at the last possible coherent
|
||
* moment: after the async wait, immediately before the first RAM
|
||
* word. Scheduling-time capture was too early because guest code
|
||
* can continue executing while the CD device is not ready. */
|
||
if (a->remaining_words == a->total_words && words_budget > 0 &&
|
||
(addr < 0x1C0000u || addr >= PSX_MAIN_RAM_RETAIL_BYTES)) {
|
||
uint32_t bytes = a->total_words * 4u;
|
||
if (bytes > psx_ram_live_bytes() - addr)
|
||
bytes = psx_ram_live_bytes() - addr;
|
||
overlay_capture_before_dma(addr, bytes);
|
||
}
|
||
while (a->remaining_words > 0 && words_budget > 0 && cdrom_dma_ready()) {
|
||
uint32_t word = cdrom_dma_read();
|
||
g_dma_cur_madr = addr;
|
||
psx_write_word(addr, word);
|
||
record_cdrom_dma_word(word);
|
||
dirty_ram_mark_executable_range(addr, 4);
|
||
addr = DMA_RAM_ADDR((addr + addr_step));
|
||
a->remaining_words--;
|
||
words_budget--;
|
||
moved++;
|
||
}
|
||
|
||
if (moved > 0) {
|
||
a->cycles_accum -= moved * DMA_CDROM_CYCLES_PER_WORD;
|
||
channels[3].madr = addr;
|
||
if (a->remaining_words == 0) {
|
||
finish_async_cdrom_transfer(addr);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/* Drive every delayed-complete channel (ch2 GPU + ch4 SPU today; any future
|
||
* delayed channel is covered automatically — inactive slots no-op). */
|
||
for (int ch = 0; ch < 7; ch++)
|
||
advance_delayed_complete(ch, cycles);
|
||
g_dma_cur_ch = -1;
|
||
g_dma_exec_depth--;
|
||
}
|
||
|
||
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;
|
||
dma_debug_clear_trace();
|
||
dma_debug_clear_cdrom_history();
|
||
}
|
||
|
||
uint32_t dma_read(uint32_t addr) {
|
||
/* DPCR */
|
||
if (addr == 0x1F8010F0u) return dpcr;
|
||
/* DICR */
|
||
if (addr == 0x1F8010F4u) {
|
||
return dma_get_dicr();
|
||
}
|
||
|
||
/* Per-channel registers: 0x1F801080 + ch*0x10 + offset */
|
||
if (addr >= 0x1F801080u && addr <= 0x1F8010EFu) {
|
||
uint32_t offset = addr - 0x1F801080u;
|
||
int ch = offset / 0x10;
|
||
int reg = offset % 0x10;
|
||
|
||
if (ch > 6) goto bad;
|
||
switch (reg) {
|
||
case 0x00: return channels[ch].madr;
|
||
case 0x04: return channels[ch].bcr;
|
||
case 0x08:
|
||
if (ch == 2) {
|
||
gpu_ot_stats.chcr_reads_total++;
|
||
if (gpu_linked_list.active) {
|
||
gpu_ot_stats.chcr_reads_in_walk++;
|
||
gpu_ot_polls_this_walk++;
|
||
}
|
||
}
|
||
return channels[ch].chcr;
|
||
case 0x0C: return 0;
|
||
default: goto bad;
|
||
}
|
||
}
|
||
|
||
bad:
|
||
/* Unmapped words inside the DMA register block (0x1F8010F8/0xFC, channel
|
||
* reg offset 0x0C variants): real hardware open-buses them and Tomba2's
|
||
* late-attract wild I/O sweep (BIOS bzero/read over a 0xDF80xxxx pointer)
|
||
* reads straight through here. Beetle parity: return 0, no fault. */
|
||
{
|
||
extern uint64_t g_io_openbus_reads;
|
||
g_io_openbus_reads++;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) {
|
||
/* DPCR */
|
||
if (addr == 0x1F8010F0u) {
|
||
dpcr = (dpcr & ~mask) | (val & mask);
|
||
return;
|
||
}
|
||
/* DICR: selected low/control bits are writable; bits 24-30 are write-1-to-acknowledge. */
|
||
if (addr == 0x1F8010F4u) {
|
||
/* Bits 0-5: unknown/unused but writable */
|
||
/* Bits 6-14: unused/read-only */
|
||
/* Bit 15: bus-error flag */
|
||
/* Bits 16-22: per-channel IRQ enable */
|
||
/* Bit 23: master IRQ enable */
|
||
/* Bits 24-30: IRQ flags, write 1 to clear */
|
||
/* Bit 31: master flag, read-only (computed) */
|
||
uint32_t dicr_before = dicr;
|
||
uint32_t i_stat_before = i_stat;
|
||
uint32_t write_mask = DICR_WRITE_MASK & mask;
|
||
uint32_t reset_mask = DICR_RESET_MASK & mask;
|
||
dicr = (dicr & ~write_mask) | (val & write_mask);
|
||
dicr &= ~(val & reset_mask);
|
||
raise_dma_irq_on_master_edge(dicr_before);
|
||
trace_dma_reg_write(addr, val, mask, dicr_before, i_stat_before);
|
||
return;
|
||
}
|
||
|
||
/* Per-channel registers */
|
||
if (addr >= 0x1F801080u && addr <= 0x1F8010EFu) {
|
||
uint32_t offset = addr - 0x1F801080u;
|
||
int ch = offset / 0x10;
|
||
int reg = offset % 0x10;
|
||
|
||
if (ch > 6) goto bad;
|
||
switch (reg) {
|
||
case 0x00:
|
||
channels[ch].madr = (channels[ch].madr & ~mask) | (val & mask);
|
||
return;
|
||
case 0x04:
|
||
channels[ch].bcr = (channels[ch].bcr & ~mask) | (val & mask);
|
||
return;
|
||
case 0x08:
|
||
channels[ch].chcr = (channels[ch].chcr & ~mask) | (val & mask);
|
||
if ((mask & (1u << 24)) && !((channels[ch].chcr >> 24) & 1u)) {
|
||
cancel_async_transfer(ch);
|
||
}
|
||
/* Writing CHCR's start bit set triggers transfer. */
|
||
if ((mask & (1u << 24)) && ((channels[ch].chcr >> 24) & 1)) {
|
||
try_execute(ch);
|
||
}
|
||
return;
|
||
case 0x0C:
|
||
return;
|
||
default:
|
||
goto bad;
|
||
}
|
||
}
|
||
|
||
bad:
|
||
/* See the read-side note: open-bus, Beetle parity. */
|
||
{
|
||
extern uint64_t g_io_openbus_writes;
|
||
g_io_openbus_writes++;
|
||
}
|
||
}
|
||
|
||
void dma_write(uint32_t addr, uint32_t val) {
|
||
dma_write_masked(addr, val, 0xFFFFFFFFu);
|
||
}
|
||
|
||
void dma_debug_get_gpu_ot_stats(DMAGpuOtStats* out) {
|
||
if (!out) return;
|
||
*out = gpu_ot_stats;
|
||
out->initiator_pc = s_dma_ch_initiator_pc[2];
|
||
out->active = gpu_linked_list.active;
|
||
}
|
||
|
||
uint64_t dma_debug_get_trace(const DMATraceEntry** out_entries) {
|
||
if (out_entries) *out_entries = dma_trace;
|
||
return dma_trace_seq;
|
||
}
|
||
|
||
void dma_debug_clear_trace(void) {
|
||
memset(dma_trace, 0, sizeof(dma_trace));
|
||
dma_trace_seq = 0;
|
||
}
|
||
|
||
uint64_t dma_debug_get_cdrom_history(const DMACDROMHistoryEntry** out_entries) {
|
||
if (out_entries) *out_entries = cdrom_dma_history;
|
||
return cdrom_dma_history_seq;
|
||
}
|
||
|
||
void dma_debug_clear_cdrom_history(void) {
|
||
memset(cdrom_dma_history, 0, sizeof(cdrom_dma_history));
|
||
memset(&cdrom_dma_active_entry, 0, sizeof(cdrom_dma_active_entry));
|
||
cdrom_dma_history_seq = 0;
|
||
cdrom_dma_history_active = 0;
|
||
}
|
||
|
||
void dma_debug_get_state(DMADebugState* out) {
|
||
if (!out) return;
|
||
out->dpcr = dpcr;
|
||
out->dicr = dma_get_dicr();
|
||
for (int i = 0; i < 7; i++) {
|
||
out->channels[i].madr = channels[i].madr;
|
||
out->channels[i].bcr = channels[i].bcr;
|
||
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 - gpu_linked_list.payload_index :
|
||
(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;
|
||
}
|
||
}
|
||
|
||
/* ---- boot snapshot: complete DMA hardware state (LE field wire; see boot_state.h) ---- */
|
||
#include "pst_wire.h"
|
||
|
||
/* 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 + (10u * 4u)) /* 44 */
|
||
#define DMA_SNAP_WIRE_BYTES ( \
|
||
(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) &&
|
||
pst_w_u32(w, a->total_words) && pst_w_u32(w, a->remaining_words) &&
|
||
pst_w_u32(w, a->cycles_accum) && pst_w_u32(w, a->start_addr);
|
||
}
|
||
static int dma_r_async(PstR *r, DMAAsyncChannel *a) {
|
||
return pst_r_u8(r, &a->active) && pst_r_u8(r, &a->debug_started) &&
|
||
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->payload_index) &&
|
||
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) {
|
||
int ok = 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->payload_index) &&
|
||
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);
|
||
return ok && s->payload_index <= s->word_count &&
|
||
s->phase <= DMA_GPU_LL_PHASE_PAYLOAD;
|
||
}
|
||
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);
|
||
}
|
||
static int dma_r_delay(PstR *r, DMADelayedComplete *d) {
|
||
return pst_r_u8(r, &d->active) && pst_r_u32(r, &d->total_words) &&
|
||
pst_r_u32(r, &d->cycles_remaining);
|
||
}
|
||
|
||
uint32_t dma_snapshot_bytes(void) { return DMA_SNAP_WIRE_BYTES; }
|
||
|
||
void dma_snapshot_write(uint8_t *p) {
|
||
PstW w;
|
||
pst_w_init(&w, p, DMA_SNAP_WIRE_BYTES);
|
||
for (int i = 0; i < 7; i++) {
|
||
pst_w_u32(&w, channels[i].madr);
|
||
pst_w_u32(&w, channels[i].bcr);
|
||
pst_w_u32(&w, channels[i].chcr);
|
||
}
|
||
pst_w_u32(&w, dpcr);
|
||
pst_w_u32(&w, dicr);
|
||
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++) {
|
||
if (!pst_r_u32(&r, &channels[i].madr) || !pst_r_u32(&r, &channels[i].bcr) ||
|
||
!pst_r_u32(&r, &channels[i].chcr))
|
||
return 0;
|
||
}
|
||
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_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;
|
||
}
|