/* 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: PSX-SPX, page dmachannels (DMA Channels). */ #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 #include #include #include #include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #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; }