mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
3bd62b3099
Rebased onto upstream after #362: the Ape Escape card-repair hunks are
dropped with the removed block, and the pace walk keeps upstream's uncapped
deadline consumption plus the source ACK-pulse edge.
(cherry picked from commit 8235a7700f)
2040 lines
83 KiB
C
2040 lines
83 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 "source_gpu_runtime.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 "spu.h"
|
||
#include "audio_trace.h"
|
||
#include "event_ring.h"
|
||
#include "psx_cycles.h"
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
/* 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;
|
||
|
||
/* Optional source-core experiment. This deliberately models Octoshock 2.2.2
|
||
* OTC service granularity and whole-CPU waiting, not PS1 bus arbitration.
|
||
* Facts consulted: original dma.cpp RunChannelI/DMA_Update/DMA_Write.
|
||
* No reference implementation text is incorporated. */
|
||
static int otc_source_model;
|
||
static struct {
|
||
uint32_t remaining, address;
|
||
uint64_t last_cycle, next_cycle;
|
||
} otc_source;
|
||
/* Optional manual CD DMA timing only. Register conventions remain native.
|
||
* Source facts: 64 initial clocks, 9 clocks/word, 128-clock service and CPU
|
||
* halt. These are original-core compatibility rules, not bus measurements. */
|
||
static int cd_source_model;
|
||
static struct {
|
||
int32_t budget;
|
||
uint64_t last_cycle, next_cycle;
|
||
} cd_source;
|
||
/* Optional source request timing for an already active VRAM upload only. */
|
||
static int gpu_upload_source_model;
|
||
static int gpu_ll_source_model;
|
||
static struct {
|
||
uint32_t active, address, remaining, nodes;
|
||
int32_t budget;
|
||
uint64_t last_cycle, next_cycle;
|
||
} gpu_ll_source;
|
||
uint32_t g_dma_cpu_read_wait;
|
||
static uint32_t gpu_upload_live_read_wait;
|
||
static int cpu_read_wait_latched;
|
||
static struct {
|
||
uint32_t remaining, block_size, in_block, address;
|
||
int32_t budget;
|
||
uint64_t last_cycle, next_cycle;
|
||
} gpu_upload_source;
|
||
/* Source SPU request DMA:64 initial clocks,48 per word, global128-clock
|
||
* updates. Payload and register progress belong to those service calls. */
|
||
static struct {
|
||
uint32_t remaining, block_size, in_block, address, total_words, start_addr;
|
||
int32_t budget;
|
||
uint64_t last_cycle, next_cycle;
|
||
} spu_source;
|
||
|
||
/* ---- 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 = channels[3].madr & 0x1FFFFCu;
|
||
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 = final_addr & 0x1FFFFCu;
|
||
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) {
|
||
if (cd_source_model) return 0x10000u;
|
||
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));
|
||
/* Source channel4 latches its enabled completion flag even when the
|
||
* master IRQ output is disabled; the master gate belongs to IRQ output. */
|
||
if ((ch==4 && source_gpu_runtime_active()) ?
|
||
((dicr>>(16+ch))&1u) : 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(channels[3].madr & 0x1FFFFCu, 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) {
|
||
if (total_words == 0 || cycles_per_word == 0) {
|
||
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);
|
||
/* The MMIO pre-barrier cached deadlines before this channel was armed.
|
||
* Refresh through the scheduler owner so a later cached event cannot hide
|
||
* this completion. This publishes no new CPU cycles; nested service is
|
||
* guarded and its outer pass recomputes the deadline. */
|
||
psx_devices_service_to_now();
|
||
}
|
||
|
||
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(channels[0].madr & 0x1FFFFCu, a->remaining_words);
|
||
} else {
|
||
mdec_debug_dma_out_start(channels[1].madr & 0x1FFFFCu, 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 = channels[3].madr & 0x1FFFFCu;
|
||
start_cdrom_dma_capture(a->total_words);
|
||
|
||
if (a->total_words == 0) {
|
||
finish_cdrom_dma_capture(channels[3].madr & 0x1FFFFCu, 1);
|
||
cancel_async_transfer(3);
|
||
complete_transfer(3);
|
||
}
|
||
}
|
||
|
||
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) {
|
||
/* The DMA word loop wraps inside the 2 MB 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 > 0x200000u - load_start)
|
||
size = 0x200000u - 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 = channels[ch].madr & 0x1FFFFCu;
|
||
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 = channels[ch].madr & 0x1FFFFCu;
|
||
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 = (0x200000u - 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 = (addr + got * 4u) & 0x1FFFFCu;
|
||
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 = (addr + addr_step) & 0x1FFFFCu;
|
||
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 = channels[0].madr & 0x1FFFFCu;
|
||
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 <= 0x200000u) {
|
||
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 = (addr + got * 4u) & 0x1FFFFCu;
|
||
/* 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 = (addr + 4u) & 0x1FFFFCu;
|
||
}
|
||
} else {
|
||
for (uint32_t i = 0; i < total_words; i++) {
|
||
mdec_dma_write_word(psx_read_word(addr));
|
||
addr = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
}
|
||
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 = channels[1].madr & 0x1FFFFCu;
|
||
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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
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);
|
||
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);
|
||
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;
|
||
}
|
||
|
||
static void source_gpu_upload_words(void) {
|
||
int queued=source_gpu_runtime_active();
|
||
unsigned to_gpu=channels[2].chcr&1u;
|
||
g_dma_cur_ch=2;g_dma_initiator_pc=s_dma_ch_initiator_pc[2];
|
||
g_dma_cur_madr=channels[2].madr;g_dma_cur_bcr=channels[2].bcr;
|
||
while(gpu_upload_source.remaining && gpu_upload_source.budget>0) {
|
||
if(!queued && gpu_dma_vram_upload_words()<gpu_upload_source.remaining) {
|
||
fprintf(stderr,"[dma-model] source GPU upload changed during transfer\n");exit(2);
|
||
}
|
||
if(!gpu_upload_source.in_block) {
|
||
/* Request feedback is sampled at block boundaries, including
|
||
* when the packet header is still queued behind a draw. */
|
||
if(queued && to_gpu && gpu_dma_source_ll_ready()!=1)break;
|
||
gpu_upload_source.in_block=gpu_upload_source.block_size;
|
||
gpu_upload_source.address=channels[2].madr&0xfffffcu;
|
||
channels[2].bcr-=1u<<16;
|
||
gpu_upload_source.budget-=7;
|
||
}
|
||
if(gpu_upload_source.address&0x800000u) {
|
||
fprintf(stderr,"[dma-model] unsupported source GPU request payload address\n");exit(2);
|
||
}
|
||
uint32_t at=gpu_upload_source.address&0x1ffffcu;
|
||
if(to_gpu) {
|
||
uint32_t value=psx_read_word(at);
|
||
gpu_set_gp0_source(at);gpu_write_gp0(value);
|
||
} else psx_write_word(at,gpu_read_gpuread());
|
||
gpu_upload_source.address=(gpu_upload_source.address+((channels[2].chcr&2u)?-4u:4u))&0xffffffu;
|
||
gpu_upload_source.in_block--;gpu_upload_source.remaining--;
|
||
gpu_upload_source.budget--;
|
||
if(!gpu_upload_source.in_block)channels[2].madr=gpu_upload_source.address;
|
||
if(!gpu_upload_source.remaining)complete_transfer(2);
|
||
}
|
||
if(gpu_upload_source.budget>0)gpu_upload_source.budget=0;
|
||
uint32_t n=gpu_upload_source.block_size?gpu_upload_source.block_size-1u:0u;
|
||
int can= !queued || !to_gpu || gpu_dma_source_ll_ready()==1;
|
||
gpu_upload_live_read_wait=gpu_upload_source.remaining && can?(n>200u?200u:n):0u;
|
||
if(!cpu_read_wait_latched)g_dma_cpu_read_wait=gpu_upload_live_read_wait;
|
||
}
|
||
static void start_source_gpu_upload(void) {
|
||
uint32_t bs=channels[2].bcr&0xffffu,bc=channels[2].bcr>>16;
|
||
uint64_t total=(uint64_t)bs*bc;
|
||
unsigned mode=channels[2].chcr&0x00ffffffu;
|
||
int queued=source_gpu_runtime_active();
|
||
if((queued?(mode&~3u)!=0x200u:mode!=0x201u) || !bs || !bc ||
|
||
(channels[2].madr&0x800000u) || (!queued && total>gpu_dma_vram_upload_words())) {
|
||
fprintf(stderr,"[dma-model] unsupported source GPU request: CHCR=%08X BCR=%08X MADR=%08X upload=%u\n",channels[2].chcr,channels[2].bcr,channels[2].madr,gpu_dma_vram_upload_words());exit(2);
|
||
}
|
||
gpu_upload_source.remaining=(uint32_t)total;
|
||
gpu_upload_source.block_size=bs;gpu_upload_source.in_block=0;
|
||
gpu_upload_source.budget=64;
|
||
gpu_upload_source.last_cycle=psx_cycle_count;
|
||
gpu_upload_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
|
||
source_gpu_upload_words();
|
||
}
|
||
static void advance_source_gpu(void) {
|
||
while(gpu_upload_source.remaining && psx_cycle_count>=gpu_upload_source.next_cycle) {
|
||
gpu_upload_source.budget+=(int32_t)(gpu_upload_source.next_cycle-gpu_upload_source.last_cycle);
|
||
gpu_upload_source.last_cycle=gpu_upload_source.next_cycle;
|
||
gpu_upload_source.next_cycle+=128;
|
||
source_gpu_upload_words();
|
||
}
|
||
}
|
||
/* Bounded source compatibility experiment. Independently implemented from
|
||
* observed original-core facts: 64 startup clocks, 15/10 per header, one per
|
||
* payload, ready checked at a node boundary, 128-clock subsequent service.
|
||
* Only NOP/environment payloads and the qualified parser-ready states are
|
||
* admitted. General draw/FIFO timing, forced stop and restore are excluded. */
|
||
static void source_gpu_ll_words(void) {
|
||
g_dma_cur_ch=2;g_dma_initiator_pc=s_dma_ch_initiator_pc[2];
|
||
while(gpu_ll_source.active && gpu_ll_source.budget>0) {
|
||
if(!gpu_ll_source.remaining) {
|
||
int ready=gpu_dma_source_ll_ready();
|
||
if(ready<0) {
|
||
fprintf(stderr,"[dma-model] unsupported source linked-list GPU parser state\n");exit(2);
|
||
}
|
||
if(!ready)break;
|
||
if(channels[2].madr&0x800000u) {
|
||
fprintf(stderr,"[dma-model] unsupported source linked-list address %08X\n",channels[2].madr);exit(2);
|
||
}
|
||
uint32_t at=channels[2].madr&0x1ffffcu;
|
||
uint32_t header=psx_read_word(at);
|
||
gpu_ll_source.remaining=header>>24;
|
||
gpu_ll_source.address=(channels[2].madr+4u)&0xffffffu;
|
||
channels[2].madr=header&0xffffffu;
|
||
gpu_set_gp0_linked_list_node(at,gpu_ll_source.nodes++);
|
||
gpu_ll_source.budget-=gpu_ll_source.remaining?15:10;
|
||
} else {
|
||
if(gpu_ll_source.address&0x800000u) {
|
||
fprintf(stderr,"[dma-model] unsupported source linked-list payload address\n");exit(2);
|
||
}
|
||
uint32_t at=gpu_ll_source.address&0x1ffffcu;
|
||
uint32_t value=psx_read_word(at),opcode=value>>24;
|
||
if(!source_gpu_runtime_active() && (gpu_dma_source_ll_ready()!=1 ||
|
||
!(opcode==0 || opcode==1 || (opcode>=0xe1 && opcode<=0xe6)))) {
|
||
fprintf(stderr,"[dma-model] unsupported source linked-list payload %08X at %08X\n",value,at);exit(2);
|
||
}
|
||
gpu_set_gp0_source(at);gpu_write_gp0(value);
|
||
gpu_ll_source.address=(gpu_ll_source.address+4u)&0xffffffu;
|
||
gpu_ll_source.remaining--;gpu_ll_source.budget--;
|
||
}
|
||
if(!gpu_ll_source.remaining && channels[2].madr==0xffffffu) {
|
||
gpu_ll_source.active=0;complete_transfer(2);
|
||
}
|
||
}
|
||
/* Source discards unused positive clocks at a ready stall/completion. */
|
||
if(gpu_ll_source.budget>0)gpu_ll_source.budget=0;
|
||
}
|
||
static void start_source_gpu_ll(void) {
|
||
if((channels[2].chcr&0x00ffffffu)!=0x401u) {
|
||
fprintf(stderr,"[dma-model] unsupported source linked-list CHCR=%08X\n",channels[2].chcr);exit(2);
|
||
}
|
||
memset(&gpu_ll_source,0,sizeof(gpu_ll_source));
|
||
gpu_ll_source.active=1;gpu_ll_source.budget=64;
|
||
gpu_ll_source.last_cycle=psx_cycle_count;
|
||
gpu_ll_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
|
||
source_gpu_ll_words();
|
||
psx_devices_service_to_now();
|
||
}
|
||
static void advance_source_gpu_ll(void) {
|
||
while(gpu_ll_source.active && psx_cycle_count>=gpu_ll_source.next_cycle) {
|
||
gpu_ll_source.budget+=(int32_t)(gpu_ll_source.next_cycle-gpu_ll_source.last_cycle);
|
||
gpu_ll_source.last_cycle=gpu_ll_source.next_cycle;
|
||
gpu_ll_source.next_cycle+=128u;source_gpu_ll_words();
|
||
}
|
||
}
|
||
static void advance_source_cdrom(void);
|
||
static void source_cdrom_words(void);
|
||
static void advance_source_otc(void);
|
||
static void advance_source_otc_words(uint32_t budget);
|
||
static void advance_source_spu(void);
|
||
static void source_spu_words(void);
|
||
void dma_source_gpu_service_at(uint64_t cycle) {
|
||
if(cycle!=psx_cycle_count) {
|
||
fprintf(stderr,"[dma-model] source GPU service outside scheduler cycle\n");exit(2);
|
||
}
|
||
g_dma_exec_depth++;
|
||
advance_source_gpu();
|
||
if(gpu_upload_source.remaining && cycle>gpu_upload_source.last_cycle) {
|
||
gpu_upload_source.budget+=(int32_t)(cycle-gpu_upload_source.last_cycle);
|
||
gpu_upload_source.last_cycle=cycle;source_gpu_upload_words();
|
||
}
|
||
advance_source_gpu_ll();
|
||
if(gpu_ll_source.active && cycle>gpu_ll_source.last_cycle) {
|
||
gpu_ll_source.budget+=(int32_t)(cycle-gpu_ll_source.last_cycle);
|
||
gpu_ll_source.last_cycle=cycle;source_gpu_ll_words();
|
||
}
|
||
/* Original DMA_Update services every channel on register writes and
|
||
* frontend returns, including the fraction since the last 128-clock
|
||
* event. Preserve that global event phase and charge each interval once. */
|
||
advance_source_cdrom();
|
||
if(cd_source_model && cdrom_async.active && cycle>cd_source.last_cycle) {
|
||
cd_source.budget+=(int32_t)(cycle-cd_source.last_cycle);
|
||
cd_source.last_cycle=cycle;source_cdrom_words();
|
||
}
|
||
advance_source_spu();
|
||
if(spu_source.remaining && cycle>spu_source.last_cycle) {
|
||
spu_source.budget+=(int32_t)(cycle-spu_source.last_cycle);
|
||
spu_source.last_cycle=cycle;source_spu_words();
|
||
}
|
||
advance_source_otc();
|
||
if(otc_source_model && otc_source.remaining && cycle>otc_source.last_cycle) {
|
||
uint32_t elapsed=(uint32_t)(cycle-otc_source.last_cycle);
|
||
otc_source.last_cycle=cycle;advance_source_otc_words(elapsed);
|
||
}
|
||
g_dma_exec_depth--;
|
||
}
|
||
uint32_t dma_cpu_read_penalty(void) {
|
||
return g_dma_cpu_read_wait;
|
||
}
|
||
void dma_cpu_read_wait_boundary(void) {
|
||
/* Source RunReal does not service an event crossed by fetch/base cycles
|
||
* until the instruction's memory access (MMIO) or retirement (RAM/ROM).
|
||
* Its ReadMemory therefore sees the DMA steal from instruction entry.
|
||
* Our scheduler may already have completed DMA during the fetch refill;
|
||
* preserve that entry wait, including for compiled RAM fast-path routing. */
|
||
cpu_read_wait_latched=1;
|
||
g_dma_cpu_read_wait=gpu_upload_live_read_wait;
|
||
}
|
||
|
||
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 = channels[2].madr & 0x1FFFFCu;
|
||
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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
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 = channels[2].madr & 0x1FFFFCu; /* 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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
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 = channels[2].madr & 0x1FFFFCu;
|
||
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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
channels[2].madr = addr;
|
||
actual_words = word_count;
|
||
}
|
||
|
||
return actual_words;
|
||
}
|
||
|
||
static void source_cdrom_words(void) {
|
||
DMAAsyncChannel *a=&cdrom_async;
|
||
uint32_t addr=channels[3].madr&0x1FFFFCu;
|
||
int32_t step=(channels[3].chcr&2u)?-4:4;
|
||
if(a->remaining_words==a->total_words) {
|
||
uint32_t bytes=a->total_words*4u;
|
||
if(bytes>0x200000u-addr)bytes=0x200000u-addr;
|
||
if(addr<0x1C0000u)overlay_capture_before_dma(addr,bytes);
|
||
}
|
||
while(a->active && a->remaining_words && cd_source.budget>0) {
|
||
uint32_t word=cdrom_dma_read_padded();
|
||
g_dma_cur_ch=3;g_dma_cur_madr=addr;g_dma_cur_bcr=channels[3].bcr;
|
||
g_dma_initiator_pc=s_dma_ch_initiator_pc[3];
|
||
psx_write_word(addr,word);record_cdrom_dma_word(word);
|
||
dirty_ram_mark_executable_range(addr,4);
|
||
addr=(addr+step)&0x1FFFFCu;
|
||
a->remaining_words--;cd_source.budget-=9;
|
||
channels[3].madr=addr;
|
||
if(!a->remaining_words)finish_async_cdrom_transfer(addr);
|
||
}
|
||
}
|
||
|
||
static void start_source_cdrom(void) {
|
||
cd_source.budget=64;cd_source.last_cycle=psx_cycle_count;
|
||
cd_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
|
||
source_cdrom_words();
|
||
}
|
||
|
||
static void advance_source_cdrom(void) {
|
||
while(cd_source_model && cdrom_async.active && psx_cycle_count>=cd_source.next_cycle) {
|
||
cd_source.budget+=(int32_t)(cd_source.next_cycle-cd_source.last_cycle);
|
||
cd_source.last_cycle=cd_source.next_cycle;cd_source.next_cycle+=128u;
|
||
source_cdrom_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(cd_source_model && ((chcr&0x601u)!=0 || (channels[3].madr&0x800000u))) {
|
||
fprintf(stderr,"[dma-model] source CD timing requires valid manual to-RAM transfer: frame=%llu cycle=%llu CHCR=%08X MADR=%08X BCR=%08X\n",
|
||
(unsigned long long)s_frame_count,(unsigned long long)psx_cycle_count,
|
||
chcr,channels[3].madr,channels[3].bcr);
|
||
exit(2);
|
||
}
|
||
if (direction != 0) {
|
||
channels[3].chcr &= ~((1u << 24) | (1u << 28));
|
||
return;
|
||
}
|
||
|
||
start_async_cdrom_transfer();
|
||
if(cd_source_model) {
|
||
start_source_cdrom();
|
||
/* Source's CPU owns fetch attempts during a manual DMA halt, just as
|
||
* for OTC. Let the CHCR store retire before that boundary runs.
|
||
* Standalone controller callers retain their synchronous contract. */
|
||
if(source_gpu_runtime_active())return;
|
||
while(cdrom_async.active && !(chcr&0x100u)) {
|
||
uint32_t wait=cd_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(cd_source.next_cycle-psx_cycle_count):1u;
|
||
psx_advance_cycles(wait);
|
||
/* The MMIO pre-barrier can cache a later device deadline before
|
||
* this transfer is armed. Service each new DMA boundary even when
|
||
* that cached deadline has not expired, as the OTC halt does. */
|
||
psx_devices_service_to_now();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 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 = channels[4].madr & 0x1FFFFCu;
|
||
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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
/* 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,
|
||
channels[4].madr & 0x1FFFFCu);
|
||
} 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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
audio_trace_event(AUDIO_EV_DMA_READ, total_words,
|
||
channels[4].madr & 0x1FFFFCu);
|
||
}
|
||
|
||
channels[4].madr = addr;
|
||
return total_words;
|
||
}
|
||
|
||
static void source_spu_words(void) {
|
||
g_dma_cur_ch=4;g_dma_initiator_pc=s_dma_ch_initiator_pc[4];
|
||
while(spu_source.remaining && spu_source.budget>0) {
|
||
if(!spu_source.in_block) {
|
||
spu_source.in_block=spu_source.block_size;
|
||
spu_source.address=channels[4].madr&0xffffffu;
|
||
channels[4].bcr-=1u<<16;
|
||
}
|
||
if(spu_source.address&0x800000u) {
|
||
fprintf(stderr,"[dma-model] source SPU request payload address unsupported\n");exit(2);
|
||
}
|
||
uint32_t at=spu_source.address&0x1ffffcu;
|
||
g_dma_cur_madr=spu_source.address;g_dma_cur_bcr=channels[4].bcr;
|
||
if(channels[4].chcr&1u)spu_dma_write(psx_read_word(at));
|
||
else psx_write_word(at,spu_dma_read());
|
||
spu_source.address=(spu_source.address+((channels[4].chcr&2u)?-4u:4u))&0xffffffu;
|
||
spu_source.budget-=48;spu_source.in_block--;spu_source.remaining--;
|
||
if(!spu_source.in_block)channels[4].madr=spu_source.address;
|
||
if(!spu_source.remaining) {
|
||
audio_trace_event((channels[4].chcr&1u)?AUDIO_EV_DMA_WRITE:AUDIO_EV_DMA_READ,
|
||
spu_source.total_words,spu_source.start_addr);
|
||
complete_transfer(4);
|
||
}
|
||
}
|
||
if(spu_source.budget>0)spu_source.budget=0;
|
||
}
|
||
static void start_source_spu(void) {
|
||
uint32_t bs=channels[4].bcr&0xffffu,bc=channels[4].bcr>>16;
|
||
if((channels[4].chcr&0x00fffffcu)!=0x200u || !bs || !bc ||
|
||
(channels[4].madr&0x800000u)) {
|
||
fprintf(stderr,"[dma-model] source SPU request unsupported CHCR=%08X BCR=%08X MADR=%08X\n",
|
||
channels[4].chcr,channels[4].bcr,channels[4].madr);exit(2);
|
||
}
|
||
spu_source.remaining=spu_source.total_words=bs*bc;
|
||
spu_source.block_size=bs;spu_source.in_block=0;spu_source.budget=64;
|
||
spu_source.start_addr=channels[4].madr&0x1ffffcu;
|
||
spu_source.last_cycle=psx_cycle_count;
|
||
spu_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
|
||
source_spu_words();
|
||
}
|
||
static void advance_source_spu(void) {
|
||
while(spu_source.remaining && psx_cycle_count>=spu_source.next_cycle) {
|
||
spu_source.budget+=(int32_t)(spu_source.next_cycle-spu_source.last_cycle);
|
||
spu_source.last_cycle=spu_source.next_cycle;spu_source.next_cycle+=128;
|
||
source_spu_words();
|
||
}
|
||
}
|
||
|
||
static void advance_source_otc_words(uint32_t budget) {
|
||
while (budget-- && otc_source.remaining) {
|
||
if (otc_source.address & 0x800000u) {
|
||
otc_source.remaining = 0;
|
||
channels[6].chcr &= ~((1u << 24) | (1u << 28));
|
||
uint32_t before = dicr;
|
||
dicr |= 0x8000u;
|
||
raise_dma_irq_on_master_edge(before);
|
||
return;
|
||
}
|
||
uint32_t value = otc_source.remaining == 1u ? 0xFFFFFFu :
|
||
(otc_source.address - 4u) & 0xFFFFFFu;
|
||
g_dma_cur_ch = 6; g_dma_cur_madr = otc_source.address;
|
||
g_dma_cur_bcr = channels[6].bcr;
|
||
g_dma_initiator_pc = s_dma_ch_initiator_pc[6];
|
||
psx_write_word(otc_source.address & 0x1FFFFCu, value);
|
||
otc_source.address = (otc_source.address - 4u) & 0xFFFFFFu;
|
||
if (--otc_source.remaining == 0) complete_transfer(6);
|
||
}
|
||
}
|
||
|
||
static void start_source_otc(void) {
|
||
otc_source.remaining = channels[6].bcr & 0xFFFFu;
|
||
if (!otc_source.remaining) otc_source.remaining = 65536u;
|
||
otc_source.address = channels[6].madr & 0xFFFFFCu;
|
||
otc_source.last_cycle = psx_cycle_count;
|
||
otc_source.next_cycle = psx_cycle_count + 128u - (psx_cycle_count & 127u);
|
||
advance_source_otc_words(64u);
|
||
}
|
||
|
||
static void advance_source_otc(void) {
|
||
while (otc_source.remaining && psx_cycle_count >= otc_source.next_cycle) {
|
||
uint32_t elapsed = (uint32_t)(otc_source.next_cycle - otc_source.last_cycle);
|
||
otc_source.last_cycle = otc_source.next_cycle;
|
||
otc_source.next_cycle += 128u;
|
||
advance_source_otc_words(elapsed);
|
||
}
|
||
}
|
||
|
||
static void execute_ch6_otc(void) {
|
||
if (otc_source_model) {
|
||
start_source_otc();
|
||
/* The source CPU fetches while halted. Its functional instruction
|
||
* boundary owns that overlap when the source CPU/GPU profile is on.
|
||
* Standalone controller callers retain the synchronous contract. */
|
||
if(source_gpu_runtime_active())return;
|
||
/* The ordinary CHCR store does not return to guest execution until
|
||
* the source-style halt ends. All devices keep advancing. Source's
|
||
* next instruction fetch may overlap this halt; this adapter does
|
||
* not claim fetch/cache phase or physical write-queue equivalence. */
|
||
while (otc_source.remaining) {
|
||
uint32_t wait = otc_source.next_cycle > psx_cycle_count ?
|
||
(uint32_t)(otc_source.next_cycle - psx_cycle_count) : 1u;
|
||
psx_advance_cycles(wait);
|
||
psx_devices_service_to_now();
|
||
}
|
||
return;
|
||
}
|
||
/* 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 = channels[6].madr & 0x1FFFFCu;
|
||
|
||
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 = (addr - 4) & 0x1FFFFCu;
|
||
}
|
||
|
||
complete_transfer(6);
|
||
}
|
||
|
||
int dma_cpu_otc_halted(void) {
|
||
return otc_source_model && otc_source.remaining!=0u;
|
||
}
|
||
|
||
int dma_cpu_source_halted(void) {
|
||
return dma_cpu_otc_halted() ||
|
||
(cd_source_model && cdrom_async.active &&
|
||
!(channels[3].chcr&0x100u));
|
||
}
|
||
|
||
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 = channels[5].madr & 0x1FFFFCu;
|
||
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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
} 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 = (addr + addr_step) & 0x1FFFFCu;
|
||
}
|
||
}
|
||
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;
|
||
|
||
/* Source CD and SPU retain the trigger in their live registers until
|
||
* completion, observable while the CPU is allowed to keep running.
|
||
* Default and standalone controller callers retain their old image. */
|
||
if(!((ch==4 || (ch==3 && cd_source_model)) && source_gpu_runtime_active()))
|
||
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(gpu_upload_source_model && ((channels[2].chcr>>9)&3u)==1u)
|
||
start_source_gpu_upload();
|
||
else if(gpu_ll_source_model && ((channels[2].chcr>>9)&3u)==2u)
|
||
start_source_gpu_ll();
|
||
else 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();
|
||
break;
|
||
case 4:
|
||
if(source_gpu_runtime_active())start_source_spu();
|
||
else 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); }
|
||
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. */
|
||
static uint32_t source_gpu_dma_irq_bound(void) {
|
||
uint32_t best=0xFFFFFFFFu;
|
||
if(gpu_ll_source.active)
|
||
best=gpu_ll_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(gpu_ll_source.next_cycle-psx_cycle_count):1u;
|
||
if(gpu_upload_source.remaining) {
|
||
uint32_t d=gpu_upload_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(gpu_upload_source.next_cycle-psx_cycle_count):1u;
|
||
if(d<best)best=d;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
uint32_t dma_cycles_to_irq(uint32_t i_mask) {
|
||
if (!(i_mask & (1u << 3))) return 0xFFFFFFFFu; /* IRQ_DMA masked */
|
||
/* Source GPU transfers complete during periodic DMA service, without a
|
||
* delayed_complete entry. A compiled block must stop before that service
|
||
* can raise its completion IRQ. This is a conservative bound, not a new
|
||
* transfer duration: readiness can postpone completion beyond the event. */
|
||
uint32_t best = source_gpu_dma_irq_bound();
|
||
if(spu_source.remaining) {
|
||
uint32_t d=spu_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
|
||
if(d<best)best=d;
|
||
}
|
||
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;
|
||
if(gpu_ll_source.active) {
|
||
best=gpu_ll_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(gpu_ll_source.next_cycle-psx_cycle_count):1u;
|
||
}
|
||
if(gpu_upload_source.remaining) {
|
||
best=gpu_upload_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(gpu_upload_source.next_cycle-psx_cycle_count):1u;
|
||
}
|
||
if (otc_source.remaining) {
|
||
uint32_t d = otc_source.next_cycle > psx_cycle_count ?
|
||
(uint32_t)(otc_source.next_cycle - psx_cycle_count) : 1u;
|
||
if(d<best)best=d;
|
||
}
|
||
if(cd_source_model && cdrom_async.active) {
|
||
uint32_t d=cd_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(cd_source.next_cycle-psx_cycle_count):1u;
|
||
if(d<best)best=d;
|
||
}
|
||
if(spu_source.remaining) {
|
||
uint32_t d=spu_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
|
||
if(d<best)best=d;
|
||
}
|
||
|
||
/* 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 (!cd_source_model && 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 = channel_irq_flag_armed(2)?source_gpu_dma_irq_bound():0xFFFFFFFFu;
|
||
if(spu_source.remaining && channel_irq_flag_armed(4)) {
|
||
uint32_t d=spu_source.next_cycle>psx_cycle_count?
|
||
(uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
|
||
if(d<best)best=d;
|
||
}
|
||
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_source_otc();
|
||
advance_source_cdrom();
|
||
advance_source_gpu();
|
||
advance_source_gpu_ll();
|
||
advance_mdec_channel(0, cycles);
|
||
advance_source_spu();
|
||
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 (!cd_source_model && 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 = channels[3].madr & 0x1FFFFCu;
|
||
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) {
|
||
uint32_t bytes = a->total_words * 4u;
|
||
if (bytes > 0x200000u - addr) bytes = 0x200000u - 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 = (addr + addr_step) & 0x1FFFFCu;
|
||
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) {
|
||
g_dma_cpu_read_wait=0;
|
||
gpu_upload_live_read_wait=0;cpu_read_wait_latched=0;
|
||
memset(&gpu_ll_source,0,sizeof(gpu_ll_source));
|
||
memset(&gpu_upload_source,0,sizeof(gpu_upload_source));
|
||
memset(&spu_source,0,sizeof(spu_source));
|
||
const char *gpu_model=getenv("PSX_GPU_DMA_MODEL");
|
||
gpu_upload_source_model=gpu_model && *gpu_model;
|
||
gpu_ll_source_model=gpu_model && (!strcmp(gpu_model,"octoshock-2.2.2-bounded-linked-list") || !strcmp(gpu_model,"octoshock-2.2.2-bounded-quad"));
|
||
if(gpu_upload_source_model && ((!gpu_ll_source_model && strcmp(gpu_model,"octoshock-2.2.2-vram-upload")) || !getenv("PSX_INPUT_ROUTE_FILE"))) {
|
||
fprintf(stderr,"[dma-model] invalid source GPU upload model or missing route\n");exit(2);
|
||
}
|
||
memset(&otc_source, 0, sizeof(otc_source));
|
||
memset(&cd_source,0,sizeof(cd_source));
|
||
const char *cd_model=getenv("PSX_CD_DMA_MODEL");
|
||
cd_source_model=cd_model && *cd_model;
|
||
if(cd_source_model && (strcmp(cd_model,"octoshock-2.2.2") || !getenv("PSX_INPUT_ROUTE_FILE"))) {
|
||
fprintf(stderr,"[dma-model] invalid source CD model or missing route\n");exit(2);
|
||
}
|
||
const char *model = getenv("PSX_INPUT_ROUTE_DMA_MODEL");
|
||
otc_source_model = model && *model;
|
||
if (otc_source_model &&
|
||
(strcmp(model, "octoshock-2.2.2-otc") || !getenv("PSX_INPUT_ROUTE_FILE"))) {
|
||
fprintf(stderr, "[dma-model] invalid source OTC model or missing input route\n");
|
||
exit(2);
|
||
}
|
||
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));
|
||
/* Original2.2.2 Power clears DMAControl. Keep that source-profile cold
|
||
* value distinct from the default hardware-style priority reset image. */
|
||
dpcr = gpu_ll_source_model ? 0u : 0x07654321u;
|
||
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) {
|
||
source_gpu_runtime_dma_write();
|
||
if(spu_source.remaining &&
|
||
((addr>=0x1f8010c0u && addr<=0x1f8010cbu) ||
|
||
(addr==0x1f8010f0u && ((dpcr^val)&mask&0xf0000u)))) {
|
||
fprintf(stderr,"[dma-model] active source SPU request register replacement unsupported\n");exit(2);
|
||
}
|
||
if(gpu_ll_source.active) {
|
||
advance_source_gpu_ll();
|
||
if(gpu_ll_source.active && psx_cycle_count>gpu_ll_source.last_cycle) {
|
||
gpu_ll_source.budget+=(int32_t)(psx_cycle_count-gpu_ll_source.last_cycle);
|
||
gpu_ll_source.last_cycle=psx_cycle_count;source_gpu_ll_words();
|
||
}
|
||
/* Source DMA_Write services elapsed work first. With no payload left,
|
||
* clearing start stops before the next header: no GPU FIFO flush, no
|
||
* completion IRQ, and no retained positive DMA credit. Other active
|
||
* register changes and partial-payload cancellation remain guarded. */
|
||
if(gpu_ll_source.active && !gpu_ll_source.remaining &&
|
||
addr==0x1f8010a8u && mask==0xffffffffu &&
|
||
channels[2].chcr==0x01000401u && val==0x00000401u) {
|
||
gpu_ll_source.active=0;
|
||
gpu_ll_source.budget=0;
|
||
}
|
||
if(gpu_ll_source.active &&
|
||
((addr>=0x1f8010a0u && addr<=0x1f8010abu) ||
|
||
(addr==0x1f8010f0u && ((dpcr^val)&mask&0xf00u)))) {
|
||
SourceGPUCommandProjection state;source_gpu_runtime_copy(0,&state);
|
||
fprintf(stderr,"[dma-model] active source linked-list register replacement unsupported at %llu addr=%08X value=%08X mask=%08X pc=%08X MADR=%08X BCR=%08X CHCR=%08X payload=%08X remaining=%u nodes=%u budget=%d last=%llu next=%llu GPU=%d,%u,%u,%u,%08X\n",
|
||
(unsigned long long)psx_cycle_count,addr,val,mask,g_debug_last_store_pc,
|
||
channels[2].madr,channels[2].bcr,channels[2].chcr,gpu_ll_source.address,
|
||
gpu_ll_source.remaining,gpu_ll_source.nodes,gpu_ll_source.budget,
|
||
(unsigned long long)gpu_ll_source.last_cycle,(unsigned long long)gpu_ll_source.next_cycle,
|
||
state.budget,state.phase,state.count,state.command,state.count?state.queue[0]:0);
|
||
exit(2);
|
||
}
|
||
}
|
||
if(gpu_upload_source.remaining) {
|
||
/* The source synchronizes elapsed DMA work before changing any DMA
|
||
* register. In particular, completion at this timestamp must see the
|
||
* previous DICR enables. Reads do not perform this partial service. */
|
||
advance_source_gpu();
|
||
if(gpu_upload_source.remaining && psx_cycle_count>gpu_upload_source.last_cycle) {
|
||
gpu_upload_source.budget+=(int32_t)(psx_cycle_count-gpu_upload_source.last_cycle);
|
||
gpu_upload_source.last_cycle=psx_cycle_count;
|
||
source_gpu_upload_words();
|
||
}
|
||
}
|
||
if(gpu_upload_source.remaining && addr==0x1f8010f0u && ((dpcr^val)&mask&0xf00u)) {
|
||
fprintf(stderr,"[dma-model] active source GPU channel control change unsupported\n");exit(2);
|
||
}
|
||
/* 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;
|
||
}
|
||
|
||
if(gpu_upload_source.remaining && addr>=0x1f8010a0u && addr<=0x1f8010abu) {
|
||
fprintf(stderr,"[dma-model] source GPU upload register replacement unsupported\n");exit(2);
|
||
}
|
||
/* 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) {
|
||
if(spu_source.remaining) {
|
||
fprintf(stderr,"[dma-model] active source SPU request capture unsupported\n");exit(2);
|
||
}
|
||
if(gpu_ll_source.active) {
|
||
fprintf(stderr,"[dma-model] active source linked-list capture unsupported\n");exit(2);
|
||
}
|
||
if(gpu_upload_source.remaining) {
|
||
fprintf(stderr,"[dma-model] active source GPU upload capture unsupported\n");exit(2);
|
||
}
|
||
if(cd_source_model && cdrom_async.active) {
|
||
fprintf(stderr,"[dma-model] state capture during source CD transfer is unsupported\n");exit(2);
|
||
}
|
||
if (otc_source.remaining) {
|
||
fprintf(stderr, "[dma-model] state capture during experimental OTC transfer is unsupported\n");
|
||
exit(2);
|
||
}
|
||
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) {
|
||
if(gpu_upload_source_model) {
|
||
fprintf(stderr,"[dma-model] source GPU upload timing requires cold boot\n");return 0;
|
||
}
|
||
if(cd_source_model) {
|
||
fprintf(stderr,"[dma-model] source CD timing requires cold boot\n");return 0;
|
||
}
|
||
if (otc_source_model) {
|
||
fprintf(stderr, "[dma-model] experimental source OTC model requires cold boot\n");
|
||
return 0;
|
||
}
|
||
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;
|
||
}
|