dma.rsannotateddma.rssource233 lines · 8.1 KB · raw
1use crate::HardwareMode;
2use crate::cartridge::Cartridge;
3use crate::memory::Memory;
4use crate::ppu::{Ppu, PpuMode};
5use bincode::{Decode, Encode};
6use jgenesis_common::num::{GetBit, U16Ext};
7
8const OAM_DMA_M_CYCLES: u8 = 160;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
11enum VramDmaState {
12    #[default]
13    Idle,
14    GpDmaActive,
15    HDmaActive {
16        hblank_bytes_remaining: u8,
17    },
18    HDmaPending,
19}
20
21#[derive(Debug, Clone, Encode, Decode)]
22pub struct DmaUnit {
23    oam_dma_source_address: u16,
24    oam_dma_m_cycles_remaining: u8,
25    oam_dma_pending: bool,
26    oam_dma_running: bool,
27    vram_dma_source_address: u16,
28    vram_dma_destination_address: u16,
29    vram_dma_length: u16,
30    vram_dma_state: VramDmaState,
31    last_ppu_mode: PpuMode,
32}
33
34impl DmaUnit {
35    pub fn new(hardware_mode: HardwareMode) -> Self {
36        Self {
37            oam_dma_source_address: match hardware_mode {
38                HardwareMode::Dmg => 0xFF00,
39                HardwareMode::Cgb => 0x0000,
40            },
41            oam_dma_m_cycles_remaining: 0,
42            oam_dma_pending: false,
43            oam_dma_running: false,
44            vram_dma_source_address: 0,
45            vram_dma_destination_address: 0,
46            vram_dma_length: 0,
47            vram_dma_state: VramDmaState::default(),
48            last_ppu_mode: PpuMode::VBlank,
49        }
50    }
51
52    pub fn read_dma_register(&self) -> u8 {
53        self.oam_dma_source_address.msb()
54    }
55
56    pub fn write_dma_register(&mut self, value: u8) {
57        self.oam_dma_source_address = u16::from_le_bytes([0x00, value]);
58
59        // Writing to DMA register initiates OAM DMA, with a 1 M-cycle delay
60        self.oam_dma_pending = true;
61
62        log::trace!("DMA written: {value:02X}");
63        log::trace!("  OAM DMA source address: {:04X}", self.oam_dma_source_address);
64    }
65
66    pub fn oam_dma_tick_m_cycle(&mut self, cartridge: &Cartridge, memory: &Memory, ppu: &mut Ppu) {
67        if self.oam_dma_pending {
68            self.oam_dma_m_cycles_remaining = OAM_DMA_M_CYCLES;
69            self.oam_dma_pending = false;
70            return;
71        }
72
73        if self.oam_dma_m_cycles_remaining == 0 {
74            self.oam_dma_running = false;
75            return;
76        }
77
78        self.oam_dma_running = true;
79
80        let source_addr = self.oam_dma_source_address;
81        let byte = match source_addr {
82            0x0000..=0x7FFF => cartridge.read_rom(source_addr),
83            0x8000..=0x9FFF => ppu.read_vram(source_addr),
84            0xA000..=0xBFFF => cartridge.read_ram(source_addr),
85            0xC000..=0xDFFF => memory.read_main_ram(source_addr),
86            // OAM, I/O registers, and HRAM are not readable from OAM DMA
87            0xE000..=0xFFFF => 0xFF,
88        };
89        ppu.write_oam_for_dma(source_addr, byte);
90
91        log::trace!(
92            "Copied {byte:02X} to OAM from {source_addr:04X} to $FE{:02X}",
93            source_addr.lsb()
94        );
95
96        self.oam_dma_source_address += 1;
97        self.oam_dma_m_cycles_remaining -= 1;
98    }
99
100    pub fn vram_dma_copy_byte(&mut self, cartridge: &Cartridge, memory: &Memory, ppu: &mut Ppu) {
101        let last_ppu_mode = self.last_ppu_mode;
102        self.last_ppu_mode = ppu.mode();
103
104        match self.vram_dma_state {
105            VramDmaState::Idle => return,
106            VramDmaState::HDmaPending => {
107                if last_ppu_mode != PpuMode::HBlank && ppu.mode() == PpuMode::HBlank {
108                    // Just reached HBlank; halt the CPU and copy 16 bytes
109                    self.vram_dma_state = VramDmaState::HDmaActive { hblank_bytes_remaining: 16 };
110                } else {
111                    // HDMA is still running but the PPU is not in HBlank yet; wait
112                    return;
113                }
114            }
115            _ => {}
116        }
117
118        let source_addr = self.vram_dma_source_address;
119        let byte = match self.vram_dma_source_address {
120            0x0000..=0x7FFF => cartridge.read_rom(source_addr),
121            0xA000..=0xBFFF => cartridge.read_ram(source_addr),
122            0xC000..=0xDFFF => memory.read_main_ram(source_addr),
123            // VRAM, OAM, I/O registers, and HRAM are not accessible from VRAM DMA
124            0x8000..=0x9FFF | 0xE000..=0xFFFF => 0xFF,
125        };
126
127        ppu.write_vram(self.vram_dma_destination_address, byte);
128
129        self.vram_dma_source_address = self.vram_dma_source_address.wrapping_add(1);
130        self.vram_dma_destination_address = self.vram_dma_destination_address.wrapping_add(1);
131        self.vram_dma_length -= 1;
132
133        if let VramDmaState::HDmaActive { hblank_bytes_remaining } = &mut self.vram_dma_state {
134            *hblank_bytes_remaining -= 1;
135            if *hblank_bytes_remaining == 0 {
136                // Finished copying the 16-byte chunk; wait until next HBlank period
137                self.vram_dma_state = VramDmaState::HDmaPending;
138            }
139        }
140
141        // End VRAM DMA when length reaches 0 or destination address overflows to 0
142        if self.vram_dma_length == 0 || self.vram_dma_destination_address == 0x0000 {
143            log::trace!("VRAM DMA complete");
144
145            self.vram_dma_state = VramDmaState::Idle;
146        }
147    }
148
149    pub fn oam_dma_in_progress(&self) -> bool {
150        self.oam_dma_running
151    }
152
153    pub fn vram_dma_active(&self) -> bool {
154        matches!(self.vram_dma_state, VramDmaState::GpDmaActive | VramDmaState::HDmaActive { .. })
155    }
156
157    pub fn write_hdma1(&mut self, value: u8) {
158        // HDMA1: VRAM DMA source address, MSB
159        self.vram_dma_source_address.set_msb(value);
160
161        log::trace!("HDMA1 write, VRAM DMA source address MSB: {value:02X}");
162    }
163
164    pub fn write_hdma2(&mut self, value: u8) {
165        // HDMA2: VRAM DMA source address, LSB
166        // Ignore lowest 4 bits
167        self.vram_dma_source_address.set_lsb(value & 0xF0);
168
169        log::trace!("HDMA2 write, VRAM DMA source address LSB: {value:02X}");
170    }
171
172    pub fn write_hdma3(&mut self, value: u8) {
173        // HDMA3: VRAM DMA destination address, MSB
174        // Highest 3 bits are not used directly since destination is always VRAM, but they are used
175        // for an overflow check
176        self.vram_dma_destination_address.set_msb(value);
177
178        log::trace!("HDMA3 write, VRAM DMA destination address MSB: {value:02X}");
179    }
180
181    pub fn write_hdma4(&mut self, value: u8) {
182        // HDMA4: VRAM DMA destination address, MSB
183        // Ignore lowest 4 bits
184        self.vram_dma_destination_address.set_lsb(value & 0xF0);
185
186        log::trace!("HDMA4 write, VRAM DMA destination address LSB: {value:02X}");
187    }
188
189    pub fn read_hdma5(&self) -> u8 {
190        let length_bits = ((self.vram_dma_length / 16) as u8).wrapping_sub(1) & 0x7F;
191        let status_bit = u8::from(self.vram_dma_state == VramDmaState::Idle);
192        length_bits | (status_bit << 7)
193    }
194
195    pub fn write_hdma5(&mut self, value: u8, ppu_mode: PpuMode) {
196        // HDMA5: VRAM DMA length/mode + initiate VRAM DMA
197        let dma_length = 16 * u16::from((value & 0x7F) + 1);
198
199        if self.vram_dma_state != VramDmaState::Idle {
200            // HDMA5 writes can alter the length of an in-progress HDMA
201            // NASCAR 2000 (with bit 7)
202            // liji32/samesuite/dma/hdma_lcd_off and
203            // liji32/samesuite/dma/hdma_mode0 (without bit 7)
204            // depend on this behavior
205            self.vram_dma_length = dma_length;
206
207            if !value.bit(7) {
208                // Writing HDMA5 with bit 7 clear while an HDMA is in progress immediately cancels it
209                self.vram_dma_state = VramDmaState::Idle;
210            }
211
212            return;
213        }
214
215        self.vram_dma_length = dma_length;
216
217        self.vram_dma_state = if value.bit(7) {
218            // HDMA
219            if ppu_mode == PpuMode::HBlank {
220                VramDmaState::HDmaActive { hblank_bytes_remaining: 16 }
221            } else {
222                VramDmaState::HDmaPending
223            }
224        } else {
225            // GPDMA
226            VramDmaState::GpDmaActive
227        };
228
229        log::trace!("HDMA5 write, VRAM DMA initiated: {value:02X}");
230        log::trace!("  VRAM DMA length: {:04X}", self.vram_dma_length);
231        log::trace!("  VRAM DMA state: {:?}", self.vram_dma_state);
232    }
233}