mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
d22ebac9a4
psx_mod_render_pass_plan() / psx_mod_render_pass() let a trusted plugin redraw its game between two logic ticks with the game's own draw code. A pass is a sandboxed nested guest call: - time is frozen: cycles are counted (GTE and mult/div deadlines keep working) but no device advances, no VBlank or device event fires and no interrupt is delivered; GPU linked-list DMA and delayed completions finish synchronously (psx_cycles freeze, interrupts, dma); - stores go straight to RAM/scratchpad, bypassing code-page tracking, overlay watch and write traces; MMIO is an allow-list (GP0, GP1 DMA mode / info, GPU and OTC DMA, I_STAT/I_MASK) and SPU, CD, timer and other device stores are dropped and counted (memory.c); - afterwards CPU (with the GTE), RAM, scratchpad, I-cache tags, interrupt, timer, DMA and GPU registers are restored exactly (render_pass.c, a GPU register checkpoint without savestate side effects), with a guest-cycle watchdog that rolls a runaway pass back; - on OpenGL the declared display rect (hr colour + mask stencil, raw mirror, native-wide band, CPU VRAM rows) is backed up, drawn, captured and rolled back; writes outside it are journaled and restored, or refused. The presenter keeps two generations of images (the game's own frame plus one image per pass phase), promotes one when the flip-aware source sees the display flip to its rect, and shows the newest image at each output deadline's phase, crossfading across gaps when passes were shed. Presents that fall due while passes run are made between them. The plan predicts the output deadlines of the coming frame and sheds passes to fit a host-time budget learnt from the presenter's idle time (PSX_RENDER_PASS_BUDGET). PSX_MOD_FRAME_INTERPOLATION_HOLD repeats the newest frame where no pass image applies, so a pass-driven title never shows anything later than stock. Default off: nothing runs unless a plugin calls the API, and the plan refuses in netplay, rollback, rewind, turbo, self-check resimulation, while suspended (FMV) or without GL flip-aware interpolation. Pass writes are kept out of frame_fingerprint. Debug: render_pass_stats, render_pass_dump, PSX_RENDER_PASS_VERIFY=1 (state + VRAM hashes around every pass). The freeze API lives in the runtime-only psx_cycle_freeze.h. psx_cycles.h is part of the overlay codegen hash and stays byte-identical, so titles that never use passes keep their overlay caches and savestates. Tests: render_pass_plan_test (deadline-exact phases per rate, shedding, selection), render_pass_freeze_test (10^6 frozen cycles, exact restore, watchdog), render_pass_guards (sandbox choke points; the codegen-hashed headers stay free of the pass API). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
5.0 KiB
C
168 lines
5.0 KiB
C
/* dma.h — PS1 DMA controller simulation (Phase 3).
|
|
*
|
|
* 7 DMA channels:
|
|
* Ch0: MDEC in 0x1F801080
|
|
* Ch1: MDEC out 0x1F801090
|
|
* Ch2: GPU 0x1F8010A0
|
|
* Ch3: CDROM 0x1F8010B0
|
|
* Ch4: SPU 0x1F8010C0
|
|
* Ch5: PIO 0x1F8010D0
|
|
* Ch6: OTC 0x1F8010E0
|
|
*
|
|
* Global:
|
|
* DPCR: 0x1F8010F0 (DMA control — enable bits per channel)
|
|
* DICR: 0x1F8010F4 (DMA interrupt control)
|
|
*/
|
|
|
|
#ifndef PSXRECOMP_DMA_H
|
|
#define PSXRECOMP_DMA_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
void dma_init(void);
|
|
uint32_t dma_read(uint32_t addr);
|
|
void dma_write(uint32_t addr, uint32_t val);
|
|
void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask);
|
|
void dma_advance(uint32_t cycles);
|
|
/* Cycle-budgeted precise event slicing: guest CPU cycles until a DELIVERABLE
|
|
* DMA IRQ (bit3 unmasked in i_mask). UINT32_MAX if none. */
|
|
uint32_t dma_cycles_to_irq(uint32_t i_mask);
|
|
/* Mask-blind scheduler boundary: next DMA word movement or delayed completion,
|
|
* not merely the final IRQ. RAM-producing DMA must become visible at its
|
|
* per-word due cycle even when guest code polls ordinary RAM instead of DICR. */
|
|
uint32_t dma_cycles_to_internal_event(void);
|
|
/* Strict deliverability form for wait-loop elision: unlike the conservative
|
|
* scheduler bound, ignores channel completions whose DICR IRQ is disabled. */
|
|
uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask);
|
|
uint32_t dma_get_dicr(void);
|
|
uint32_t dma_get_dpcr(void);
|
|
int dma_cdrom_transfer_active(void);
|
|
int dma_gpu_linked_list_active(void);
|
|
|
|
typedef struct DMAChannelDebugState {
|
|
uint32_t madr;
|
|
uint32_t bcr;
|
|
uint32_t chcr;
|
|
uint32_t active;
|
|
uint32_t remaining_words;
|
|
uint32_t cycles_accum;
|
|
} DMAChannelDebugState;
|
|
|
|
typedef struct DMADebugState {
|
|
uint32_t dpcr;
|
|
uint32_t dicr;
|
|
DMAChannelDebugState channels[7];
|
|
} DMADebugState;
|
|
|
|
typedef struct DMATraceEntry {
|
|
uint64_t seq;
|
|
uint32_t frame;
|
|
uint32_t kind;
|
|
uint32_t channel;
|
|
uint32_t total_words;
|
|
uint32_t addr;
|
|
uint32_t val;
|
|
uint32_t mask;
|
|
uint32_t madr;
|
|
uint32_t bcr;
|
|
uint32_t chcr;
|
|
uint32_t dpcr;
|
|
uint32_t dicr_before;
|
|
uint32_t dicr_after;
|
|
uint32_t i_stat_before;
|
|
uint32_t i_stat_after;
|
|
uint32_t func;
|
|
uint32_t pc;
|
|
} DMATraceEntry;
|
|
|
|
#define DMA_TRACE_CAP (1 << 14)
|
|
#define DMA_CDROM_HISTORY_CAP (1 << 13)
|
|
#define DMA_CDROM_HISTORY_WORDS 16
|
|
|
|
typedef struct DMACDROMHistoryEntry {
|
|
uint64_t seq;
|
|
uint32_t frame_start;
|
|
uint32_t frame_end;
|
|
uint32_t start_addr;
|
|
uint32_t final_addr;
|
|
uint32_t requested_words;
|
|
uint32_t moved_words;
|
|
uint32_t bcr;
|
|
uint32_t chcr;
|
|
uint32_t dpcr;
|
|
uint32_t dicr_start;
|
|
uint32_t dicr_end;
|
|
uint32_t i_stat_start;
|
|
uint32_t i_stat_end;
|
|
uint32_t func;
|
|
uint32_t pc;
|
|
int lba;
|
|
int sector_size;
|
|
int sector_read_pos_start;
|
|
int sector_read_pos_end;
|
|
uint8_t mode;
|
|
uint8_t sector_available_start;
|
|
uint8_t sector_available_end;
|
|
uint8_t completed;
|
|
uint8_t first_count;
|
|
uint8_t last_count;
|
|
uint32_t first_words[DMA_CDROM_HISTORY_WORDS];
|
|
uint32_t last_words[DMA_CDROM_HISTORY_WORDS];
|
|
} DMACDROMHistoryEntry;
|
|
|
|
/* GPU (CH2) linked-list / ordering-table walk observability.
|
|
*
|
|
* These counters make DMA2 geometry loss countable without screenshots:
|
|
* - starts_dropped: the guest asked for a new OT transfer while the previous
|
|
* walk was still running. start_async_gpu_linked_list() returns early in
|
|
* that case, so a whole ordering table is never sent to GP0.
|
|
* - chcr_reads_in_walk: guest polls of CHCR(2) while the linked-list walk is
|
|
* still active, useful for distinguishing timeout loops from blind aborts.
|
|
*/
|
|
typedef struct {
|
|
uint32_t pc; /* guest PC of the CHCR store that aborted the walk */
|
|
uint32_t chcr; /* CHCR value after that store */
|
|
uint32_t nodes; /* ordering-table nodes walked before the abort */
|
|
uint32_t words; /* DMA words read before the abort, including headers */
|
|
uint32_t cycles; /* guest cycles the walk had been running */
|
|
uint32_t polls; /* guest reads of CHCR(2) during this walk */
|
|
} DMAGpuOtCancel;
|
|
|
|
#define DMA_GPU_OT_CANCEL_RING 8
|
|
|
|
typedef struct {
|
|
uint64_t starts;
|
|
uint64_t starts_dropped;
|
|
uint64_t completes;
|
|
uint64_t cancels;
|
|
uint32_t nodes_last;
|
|
uint32_t nodes_max;
|
|
uint32_t words_last;
|
|
uint32_t words_max;
|
|
uint64_t cycles_last;
|
|
uint64_t cycles_max;
|
|
uint8_t active;
|
|
uint64_t chcr_reads_total;
|
|
uint64_t chcr_reads_in_walk;
|
|
uint32_t initiator_pc;
|
|
uint32_t cancel_ring_count;
|
|
DMAGpuOtCancel cancel_ring[DMA_GPU_OT_CANCEL_RING];
|
|
} DMAGpuOtStats;
|
|
|
|
uint64_t dma_debug_get_trace(const DMATraceEntry** out_entries);
|
|
void dma_debug_clear_trace(void);
|
|
void dma_debug_get_state(DMADebugState* out);
|
|
void dma_debug_get_gpu_ot_stats(DMAGpuOtStats* out);
|
|
uint64_t dma_debug_get_cdrom_history(const DMACDROMHistoryEntry** out_entries);
|
|
void dma_debug_clear_cdrom_history(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSXRECOMP_DMA_H */
|