vdp.rsannotatedvdp.rssource2130 lines · 78.4 KB · raw

Genesis VDP (video display processor)

3mod colors;
4mod cramdots;
5pub mod debug;
6mod fifo;
7mod registers;
8mod render;
9mod sprites;
11#[cfg(test)]
12mod tests;
13
14use crate::vdp::colors::ColorTables;
15use crate::vdp::cramdots::CramDotBuffer;
16use crate::vdp::fifo::{VdpFifo, VdpFifoEntry, VramWriteSize};
17use crate::vdp::registers::{
18    DebugRegister, DmaMode, H40_LEFT_BORDER, HorizontalDisplaySize, HorizontalScrollMode,
19    InterlacingMode, NTSC_BOTTOM_BORDER, NTSC_TOP_BORDER, PAL_V28_BOTTOM_BORDER,
20    PAL_V28_TOP_BORDER, PAL_V30_BOTTOM_BORDER, PAL_V30_TOP_BORDER, RIGHT_BORDER, Registers,
21    VerticalDisplaySize, VerticalScrollMode, VramSizeKb,
22};
23use crate::vdp::render::RasterLine;
24use crate::vdp::sprites::{SpriteBuffers, SpriteState};
25use bincode::{Decode, Encode};
26use jgenesis_common::boxedarray::{BoxedByteArray, BoxedColorArray, BoxedWordArray};
27use jgenesis_common::frontend::{
28    Color, CompositeParams, FrameSize, SamplesPerColorCycle, TimingMode,
29};
30use jgenesis_common::num::{GetBit, U16Ext};
31use jgenesis_proc_macros::EnumAll;
32use std::collections::VecDeque;
33use std::ops::Range;
34use std::{array, cmp};
35use z80_emu::traits::InterruptLine;
36
37pub use crate::vdp::colors::ColorModifier;
38
39const VRAM_LEN: usize = 64 * 1024;
40const CRAM_LEN_WORDS: usize = 64;
41const VSRAM_LEN_WORDS: usize = 40;
42
43const MAX_SCREEN_WIDTH: usize = 320 + H40_LEFT_BORDER as usize + RIGHT_BORDER as usize;
44const MAX_SCREEN_HEIGHT: usize = 240 + PAL_V30_TOP_BORDER as usize + PAL_V30_BOTTOM_BORDER as usize;

Double screen height to account for interlaced 2x mode

47pub const FRAME_BUFFER_LEN: usize = MAX_SCREEN_WIDTH * MAX_SCREEN_HEIGHT * 2;
49pub const MCLK_CYCLES_PER_SCANLINE: u64 = 3420;
50pub const ACTIVE_MCLK_CYCLES_PER_SCANLINE: u64 = 2560;
51pub const NTSC_SCANLINES_PER_FRAME: u16 = 262;
52pub const PAL_SCANLINES_PER_FRAME: u16 = 313;
53
54const MAX_SPRITES_PER_FRAME: usize = 80;
55
56macro_rules! new_bool256 {
57    ($($value:literal),* $(,)?) => {
58        {
59            let mut bools = [false; 256];
60            $(
61                bools[$value] = true;
62            )*
63            bools
64        }
65    }
66}

Adapted from https://gendev.spritesmind.net/forum/viewtopic.php?t=851 and modified so that 0 is at H=0x000 rather than the H scroll fetch

70const H32_ACCESS_SLOTS: &[bool; 256] =
71    &new_bool256![5, 13, 21, 37, 45, 53, 69, 77, 85, 101, 109, 117, 132, 133, 147, 161];
72const H40_ACCESS_SLOTS: &[bool; 256] =
73    &new_bool256![6, 14, 22, 38, 46, 54, 70, 78, 86, 102, 110, 118, 134, 142, 150, 165, 166, 190];

Adapted from https://gendev.spritesmind.net/forum/viewtopic.php?p=20921#p20921 TODO H32 refresh slot locations are probably not accurate

77const H32_BLANK_REFRESH_SLOTS: &[bool; 256] = &new_bool256![1, 33, 65, 97, 129];
78const H40_BLANK_REFRESH_SLOTS: &[bool; 256] = &new_bool256![26, 58, 90, 122, 154, 204];

Most H values sourced from https://gendev.spritesmind.net/forum/viewtopic.php?p=17683#p17683

81impl HorizontalDisplaySize {
82    // Total number of slots minus number of refresh slots
83    const fn access_slots_per_blank_line(self) -> u16 {
84        match self {
85            Self::ThirtyTwoCell => 171 - 5,
86            Self::FortyCell => 210 - 6,
87        }
88    }
89
90    // H range during which the status HBlank flag is _not_ set
91    const fn hblank_flag_clear_h_range(self) -> Range<u16> {
92        match self {
93            Self::ThirtyTwoCell => 0x00A..0x126,
94            Self::FortyCell => 0x00B..0x166,
95        }
96    }
97
98    // H value at which the VDP increments the H interrupt counter, increments the V counter, and
99    // potentially sets HINT pending
100    const fn h_interrupt_h(self) -> u16 {
101        match self {
102            Self::ThirtyTwoCell => 0x10A,
103            Self::FortyCell => 0x14A,
104        }
105    }
106
107    const fn h_interrupt_scanline_mclk(self) -> u64 {
108        (self.h_interrupt_h() as u64) * self.active_display_mclk_divider()
109    }
110
111    // H value at which the VDP sets VINT pending on line 224/240
112    const fn v_interrupt_h(self) -> u16 {
113        match self {
114            Self::ThirtyTwoCell => 0x001,
115            Self::FortyCell => 0x002,
116        }
117    }
118
119    const fn v_interrupt_scanline_mclk(self) -> u64 {
120        (self.v_interrupt_h() as u64) * self.active_display_mclk_divider()
121    }
122
123    // Range when the VDP is actively displaying pixels
124    const fn active_display_h_range(self) -> Range<u16> {
125        match self {
126            Self::ThirtyTwoCell => 0x018..0x118,
127            Self::FortyCell => 0x01A..0x15A,
128        }
129    }
130
131    const fn read_h_scroll_h(self) -> u16 {
132        // Actually -26 but that overflows in H32 mode
133        self.active_display_h_range().start - 24
134    }
135
136    const fn rendering_begin_h(self) -> u16 {
137        self.active_display_h_range().start - 16
138    }
139
140    // H at which to execute sprite processing phase 2 (fetch sprite attributes)
141    const fn fetch_sprite_attributes_h(self) -> u16 {
142        // Chaekopon demo by Limp Ninja is sensitive to when phase 2 is executed
143        // This demo sometimes modifies the sprite attribute table address shortly before HINT,
144        // seemingly just after attributes are fetched for the last sprite scanned in phase 1
145        self.active_display_h_range().end - 16 - 8
146    }
147
148    // H where HBlank begins (should line up with the two consecutive external access slots)
149    const fn hblank_begin_h(self) -> u16 {
150        self.active_display_h_range().end - 16
151    }
152
153    // H by which VDP register latching for the next line is completed
154    const fn latch_registers_h(self) -> u16 {
155        // Estimated based on latching taking place within 36 CPU cycles of HINT
156        // TODO this is probably inaccurate for either H32 or H40 mode
157        match self {
158            Self::ThirtyTwoCell => 0x121,
159            Self::FortyCell => 0x169,
160        }
161    }
162
163    const fn active_display_mclk_divider(self) -> u64 {
164        match self {
165            Self::ThirtyTwoCell => 10,
166            Self::FortyCell => 8,
167        }
168    }
169}
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
172pub enum ControlWriteFlag {
173    First,
174    Second,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
178pub enum DataPortMode {
179    Read,
180    Write,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
184pub enum DataPortLocation {
185    Vram,
186    Vram8Bit,
187    Cram,
188    Vsram,
189    Invalid,
190}
191
192#[derive(Debug, Clone, Encode, Decode)]
193struct ControlPort {
194    mode: DataPortMode,
195    location_bits: u8,
196    location: DataPortLocation,
197    control_address: u32,
198    data_port_address: u32,
199    write_flag: ControlWriteFlag,
200    dma_active: bool,
201}
202
203impl ControlPort {
204    fn new() -> Self {
205        Self {
206            mode: DataPortMode::Read,
207            location_bits: 0,
208            location: DataPortLocation::Vram,
209            control_address: 0,
210            data_port_address: 0,
211            write_flag: ControlWriteFlag::First,
212            dma_active: false,
213        }
214    }
215
216    fn write_first_command(&mut self, value: u16) {
217        // First command word: Lowest 14 bits of address and lowest 2 bits of code
218        self.control_address = (self.control_address & !0x3FFF) | u32::from(value & 0x3FFF);
219        self.data_port_address = self.control_address;
220
221        self.mode = if value.bit(14) { DataPortMode::Write } else { DataPortMode::Read };
222        self.location_bits = (self.location_bits & !1) | (value >> 15) as u8;
223        self.location = parse_location_bits(self.location_bits, self.mode);
224    }
225
226    fn write_second_command(&mut self, value: u16, registers: &Registers) {
227        // Second command word: Highest 3 bits of address (A14-A16) and highest 4 bits of code (CD2-CD5)
228        self.control_address = (self.control_address & 0x3FFF) | (u32::from(value & 7) << 14);
229        self.data_port_address = self.control_address;
230
231        self.location_bits = (self.location_bits & 1) | ((value >> 3) & 0b110) as u8;
232        self.location = parse_location_bits(self.location_bits, self.mode);
233
234        // CD5 is only writable if DMA is enabled in register #1
235        if registers.dma_enabled {
236            self.dma_active = value.bit(7);
237        }
238    }
239
240    fn new_fifo_entry(&self, word: u16, vram_size: VramSizeKb) -> VdpFifoEntry {
241        // Perform 128KB mode address conversion on FIFO push rather than pop; Overdrive 2 depends on this
242        // TODO does 128KB mode also cause invalid target FIFO entries to only take 1 slot?
243        let (address, size) = match (self.location, vram_size) {
244            (DataPortLocation::Vram | DataPortLocation::Invalid, VramSizeKb::OneTwentyEight) => {
245                (convert_128kb_vram_address(self.data_port_address), VramWriteSize::Byte)
246            }
247            _ => (self.data_port_address, VramWriteSize::Word),
248        };
249
250        VdpFifoEntry::new(self.mode, self.location, address, word, size)
251    }
252
253    fn increment_data_port_address(&mut self, registers: &Registers) {
254        self.data_port_address =
255            self.data_port_address.wrapping_add(registers.data_port_auto_increment.into());
256    }
257}
258
259fn parse_location_bits(bits: u8, mode: DataPortMode) -> DataPortLocation {
260    match (bits, mode) {
261        (0b000, _) => DataPortLocation::Vram,
262        (0b010, _) => DataPortLocation::Vsram,
263        (0b001, DataPortMode::Write) | (0b100, DataPortMode::Read) => DataPortLocation::Cram,
264        // Undocumented: Code 01100 enables 8-bit VRAM reads (verified by VDPFIFOTesting ROM)
265        (0b110, DataPortMode::Read) => DataPortLocation::Vram8Bit,
266        _ => DataPortLocation::Invalid,
267    }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
271enum PendingWrite {
272    Control(u16),
273    Data(u16),
274}
275
276impl Default for PendingWrite {
277    fn default() -> Self {
278        Self::Control(0)
279    }
280}
281
282#[derive(Debug, Clone, Encode, Decode)]
283struct InternalState {
284    // Whether the VDP is actively raising INT6
285    v_interrupt_pending: bool,
286    h_interrupt_pending: bool,
287    // V/H interrupts must be delayed by 1 CPU instruction if they are enabled while an interrupt is
288    // pending; Sesame Street Counting Cafe and Fatal Rewind depend on this
289    v_interrupt_enabled_latch: bool,
290    h_interrupt_enabled_latch: bool,
291    h_interrupt_counter: u16,
292    latched_hv_counter: Option<u16>,
293    v_border_forgotten: bool,
294    top_border: u16,
295    last_scroll_b_palettes: [u8; 2],
296    scanline: u16,
297    scanline_mclk_cycles: u64,
298    pixel: u16,
299    in_vblank: bool,
300    // Used to store writes to either VDP port while a memory-to-VRAM DMA is in progress
301    // (Can happen if a DMA is initiated using a longword write)
302    pending_writes: Vec<PendingWrite>,
303    display_enable_pending: bool,
304    data_port_read_wait: bool,
305    vram_fill_data: Option<u16>,
306    vram_copy_odd_slot: bool,
307    interlaced_frame: bool,
308    interlaced_odd: bool,
309    // Latched at start of VBlank
310    // This is not accurate to actual hardware, but nothing should change H resolution mid-frame
311    // during active display
312    frame_h_resolution: HorizontalDisplaySize,
313}
314
315impl InternalState {
316    fn new(timing_mode: TimingMode) -> Self {
317        Self {
318            v_interrupt_pending: false,
319            h_interrupt_pending: false,
320            v_interrupt_enabled_latch: false,
321            h_interrupt_enabled_latch: false,
322            h_interrupt_counter: 0,
323            latched_hv_counter: None,
324            v_border_forgotten: false,
325            top_border: VerticalDisplaySize::default().top_border(timing_mode),
326            last_scroll_b_palettes: [0; 2],
327            scanline: 0,
328            scanline_mclk_cycles: 0,
329            pixel: 0,
330            in_vblank: false,
331            pending_writes: Vec::with_capacity(10),
332            display_enable_pending: false,
333            data_port_read_wait: false,
334            vram_fill_data: None,
335            vram_copy_odd_slot: false,
336            interlaced_frame: false,
337            interlaced_odd: false,
338            frame_h_resolution: HorizontalDisplaySize::default(),
339        }
340    }
341
342    fn interlaced_odd(&self) -> bool {
343        self.interlaced_frame && self.interlaced_odd
344    }
345}
346
347#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
348struct CachedSpriteData {
349    v_position: u16,
350    h_size_cells: u8,
351    v_size_cells: u8,
352    link_data: u8,
353}
354
355impl CachedSpriteData {
356    fn update_first_word_msb(&mut self, msb: u8) {
357        self.v_position = (self.v_position & 0x00FF) | (u16::from(msb & 0x03) << 8);
358    }
359
360    fn update_first_word_lsb(&mut self, lsb: u8) {
361        self.v_position = (self.v_position & 0xFF00) | u16::from(lsb);
362    }
363
364    fn update_second_word_msb(&mut self, msb: u8) {
365        self.h_size_cells = ((msb >> 2) & 0x03) + 1;
366        self.v_size_cells = (msb & 0x03) + 1;
367    }
368
369    fn update_second_word_lsb(&mut self, lsb: u8) {
370        self.link_data = lsb & 0x7F;
371    }
372}
373
374#[derive(Debug, Clone, Encode, Decode)]
375struct SpriteData {
376    pattern_generator: u16,
377    v_position: u16,
378    h_position: u16,
379    h_size_cells: u8,
380    v_size_cells: u8,
381    palette: u8,
382    vertical_flip: bool,
383    horizontal_flip: bool,
384    priority: bool,
385    link_data: u8,
386}
387
388impl SpriteData {
389    fn create(cached_data: CachedSpriteData, uncached_bytes: &[u8]) -> Self {
390        // 3rd word
391        let priority = uncached_bytes[0].bit(7);
392        let palette = (uncached_bytes[0] >> 5) & 0x03;
393        let vertical_flip = uncached_bytes[0].bit(4);
394        let horizontal_flip = uncached_bytes[0].bit(3);
395        let pattern_generator = u16::from_be_bytes([uncached_bytes[0] & 0x07, uncached_bytes[1]]);
396
397        // 4th word
398        let h_position = u16::from_be_bytes([uncached_bytes[2] & 0x01, uncached_bytes[3]]);
399
400        Self {
401            pattern_generator,
402            v_position: cached_data.v_position,
403            h_position,
404            h_size_cells: cached_data.h_size_cells,
405            v_size_cells: cached_data.v_size_cells,
406            palette,
407            vertical_flip,
408            horizontal_flip,
409            priority,
410            link_data: cached_data.link_data,
411        }
412    }
413}
414
415#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
416struct TilePixel {
417    color: u8,
418    palette: u8,
419    priority: bool,
420}
421
422#[derive(Debug, Clone, Encode, Decode)]
423struct BgBuffers {
424    plane_a_pixels: [TilePixel; MAX_SCREEN_WIDTH],
425    plane_b_pixels: [TilePixel; MAX_SCREEN_WIDTH],
426}
427
428impl BgBuffers {
429    fn new() -> Self {
430        Self {
431            plane_a_pixels: array::from_fn(|_| TilePixel::default()),
432            plane_b_pixels: array::from_fn(|_| TilePixel::default()),
433        }
434    }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum VdpTickEffect {
439    None,
440    FrameComplete,
441}
442
443pub(crate) trait TimingModeExt: Copy {
444    fn scanlines_per_frame(self, interlaced: bool, interlaced_odd: bool) -> u16;
445
446    fn rendered_lines_per_frame(self) -> u16;
447}
448
449impl TimingModeExt for TimingMode {
450    fn scanlines_per_frame(self, interlaced: bool, interlaced_odd: bool) -> u16 {
451        match self {
452            Self::Ntsc => NTSC_SCANLINES_PER_FRAME + u16::from(interlaced_odd),
453            Self::Pal => PAL_SCANLINES_PER_FRAME - 1 + u16::from(!interlaced || interlaced_odd),
454        }
455    }
456
457    // Includes border lines
458    fn rendered_lines_per_frame(self) -> u16 {
459        match self {
460            Self::Ntsc => 224 + NTSC_TOP_BORDER + NTSC_BOTTOM_BORDER,
461            Self::Pal => 224 + PAL_V28_TOP_BORDER + PAL_V28_BOTTOM_BORDER,
462        }
463    }
464}
465
466#[derive(Debug, Clone, Copy)]
467pub struct BorderSize {
468    pub left: u32,
469    pub right: u32,
470    pub top: u32,
471    pub bottom: u32,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
475pub enum DarkenColors {
476    No,
477    Yes,
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
481pub struct VdpConfig {
482    pub enforce_sprite_limits: bool,
483    pub non_linear_color_scale: bool,
484    pub deinterlace: bool,
485    pub render_vertical_border: bool,
486    pub render_horizontal_border: bool,
487    pub plane_a_enabled: bool,
488    pub plane_b_enabled: bool,
489    pub sprites_enabled: bool,
490    pub window_enabled: bool,
491    pub color_adjustment: DarkenColors,
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, EnumAll)]
495enum VdpEvent {
496    VInterrupt,
497    ReadHScroll,
498    RenderLine,
499    FetchSpriteAttributes,
500    HBlankStart,
501    HInterrupt,
502    LatchRegisters,
503    None,
504}
505
506impl VdpEvent {
507    const NUM: usize = VdpEvent::ALL.len();
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
511struct VdpEventWithTime {
512    event: VdpEvent,
513    h: u16,
514}
515
516impl VdpEventWithTime {
517    const fn new(h: u16, event: VdpEvent) -> Self {
518        Self { event, h }
519    }
520}
521
522#[derive(Debug, Clone, Copy)]
523struct VCounter {
524    counter: u8,
525    vblank_flag: bool,
526}
527
528#[derive(Debug, Clone, Copy)]
529struct HVCounter {
530    internal_h: u16,
531    internal_v: u8,
532    hv_counter: u16,
533    vblank_flag: bool,
534}
535
536pub trait VdpBusView {
537    fn read_word_for_dma(&mut self, address: u32) -> u16;
538}
539
540type Vram = [u8; VRAM_LEN];
541type Cram = [u16; CRAM_LEN_WORDS];
542type Vsram = [u16; VSRAM_LEN_WORDS];
543
544#[derive(Debug, Clone, Encode, Decode)]
545pub struct Vdp {
546    frame_buffer: BoxedColorArray<FRAME_BUFFER_LEN>,
547    cram_dots: CramDotBuffer,
548    vram: BoxedByteArray<VRAM_LEN>,
549    cram: BoxedWordArray<CRAM_LEN_WORDS>,
550    vsram: BoxedWordArray<VSRAM_LEN_WORDS>,
551    fifo: VdpFifo,
552    dma_latency: u8,
553    // Used to store writes to data port while FIFO is full
554    pending_fifo_writes: VecDeque<VdpFifoEntry>,
555    timing_mode: TimingMode,
556    control_port: ControlPort,
557    state: InternalState,
558    sprite_state: SpriteState,
559    registers: Registers,
560    debug_register: DebugRegister,
561    latched_registers: Registers,
562    latched_h_scroll: (u16, u16),
563    latched_full_screen_v_scroll: (u16, u16),
564    cached_sprite_attributes: Box<[CachedSpriteData; MAX_SPRITES_PER_FRAME]>,
565    latched_sprite_attributes: Box<[CachedSpriteData; MAX_SPRITES_PER_FRAME]>,
566    bg_buffers: Box<BgBuffers>,
567    sprite_buffers: SpriteBuffers,
568    interlaced_sprite_buffers: SpriteBuffers,
569    config: VdpConfig,
570    color_tables: ColorTables,
571    vdp_event_times: [VdpEventWithTime; VdpEvent::NUM],
572    vdp_event_idx: u8,
573}
574
575impl Vdp {
576    #[allow(clippy::missing_panics_doc)]
577    #[must_use]
578    pub fn new(timing_mode: TimingMode, config: VdpConfig) -> Self {
579        Self {
580            frame_buffer: BoxedColorArray::new(),
581            cram_dots: CramDotBuffer::new(),
582            vram: BoxedByteArray::new(),
583            cram: BoxedWordArray::new(),
584            vsram: BoxedWordArray::new(),
585            fifo: VdpFifo::new(),
586            dma_latency: 0,
587            pending_fifo_writes: VecDeque::with_capacity(8),
588            timing_mode,
589            control_port: ControlPort::new(),
590            state: InternalState::new(timing_mode),
591            sprite_state: SpriteState::default(),
592            registers: Registers::new(),
593            debug_register: DebugRegister::new(),
594            latched_registers: Registers::new(),
595            latched_h_scroll: (0, 0),
596            latched_full_screen_v_scroll: (0, 0),
597            cached_sprite_attributes: vec![CachedSpriteData::default(); MAX_SPRITES_PER_FRAME]
598                .into_boxed_slice()
599                .try_into()
600                .unwrap(),
601            latched_sprite_attributes: vec![CachedSpriteData::default(); MAX_SPRITES_PER_FRAME]
602                .into_boxed_slice()
603                .try_into()
604                .unwrap(),
605            bg_buffers: Box::new(BgBuffers::new()),
606            sprite_buffers: SpriteBuffers::new(),
607            interlaced_sprite_buffers: SpriteBuffers::new(),
608            config,
609            color_tables: ColorTables::from_config(&config),
610            vdp_event_times: Self::vdp_event_times(HorizontalDisplaySize::default()),
611            vdp_event_idx: 0,
612        }
613    }
614
615    pub fn write_control(&mut self, value: u16) {
616        log::trace!(
617            "VDP control write on scanline {} / mclk {} / pixel {}: {value:04X} (flag = {:?}, dma_enabled = {})",
618            self.state.scanline,
619            self.state.scanline_mclk_cycles,
620            scanline_mclk_to_pixel(
621                self.state.scanline_mclk_cycles,
622                self.registers.horizontal_display_size
623            ),
624            self.control_port.write_flag,
625            self.registers.dma_enabled
626        );
627
628        if self.control_port.dma_active && self.registers.dma_mode == DmaMode::MemoryToVram {
629            // VDP is locking the bus; buffer the write until the DMA is done
630            // Some games depend on this - they'll do a longword write where the first word starts
631            // a DMA and then the second word changes the control port address
632            self.state.pending_writes.push(PendingWrite::Control(value));
633            return;
634        }
635
636        match self.control_port.write_flag {
637            ControlWriteFlag::First => {
638                // VDP register write OR first word of command write
639
640                // Always write first word to control port, even if this is a register write
641                self.control_port.write_first_command(value);
642
643                if value & 0xC000 == 0x8000 {
644                    // VDP register write
645                    self.write_vdp_register(value);
646                } else {
647                    // First word of command write
648                    self.control_port.write_flag = ControlWriteFlag::Second;
649                }
650            }
651            ControlWriteFlag::Second => {
652                // Second word of command write
653                self.control_port.write_second_command(value, &self.registers);
654                self.control_port.write_flag = ControlWriteFlag::First;
655
656                if self.control_port.dma_active {
657                    // DMA started
658                    self.state.vram_fill_data = None;
659                    self.state.vram_copy_odd_slot = false;
660
661                    // OutRunners depends on there being a delay to FIFO writes when starting
662                    // memory-to-VRAM DMA
663                    if self.registers.dma_mode == DmaMode::MemoryToVram {
664                        // Hack: 8 slots gets roughly correct alignment for direct color DMA demos,
665                        // but 8 breaks Overdrive 2's plasma twisters effect. Use a shorter delay when
666                        // DMAing to VSRAM (almost certainly working around other timing issues)
667                        self.dma_latency = match self.control_port.location {
668                            DataPortLocation::Vsram => 5,
669                            _ => 8,
670                        };
671                    }
672
673                    log::trace!(
674                        "DMA of type {:?} initiated at line {} mclk {} pixel {}",
675                        self.registers.dma_mode,
676                        self.state.scanline,
677                        self.state.scanline_mclk_cycles,
678                        self.state.pixel
679                    );
680                }
681            }
682        }
683
684        log::trace!("  Mode: {:?}", self.control_port.mode);
685        log::trace!("  Location bits: {:03b}", self.control_port.location_bits);
686        log::trace!("  Location: {:?}", self.control_port.location);
687        log::trace!("  Address: {:05X}", self.control_port.control_address);
688        log::trace!("  DMA active: {}", self.control_port.dma_active);
689    }
690
691    fn write_vdp_register(&mut self, value: u16) {
692        let prev_display_enabled = self.registers.display_enabled;
693        let prev_h_display_size = self.registers.horizontal_display_size;
694        let prev_v_display_size = self.registers.vertical_display_size;
695
696        let register_number = ((value >> 8) & 0x1F) as u8;
697        self.registers.write_internal_register(register_number, value as u8);
698
699        if !prev_display_enabled && self.registers.display_enabled && !self.fifo.is_empty() {
700            // When display switches from disabled to enabled while FIFO is not empty, wait to
701            // enable display until the FIFO is emptied.
702            // Possibly not accurate, but fixes glitches in games that re-enable display immediately
703            // after the end of a DMA (e.g. Mickey Mania, Overdrive 1 & 2)
704            self.registers.display_enabled = false;
705            self.state.display_enable_pending = true;
706        } else if register_number == 1 {
707            self.state.display_enable_pending = false;
708        }
709
710        if self.registers.hv_counter_stopped && self.state.latched_hv_counter.is_none() {
711            let HVCounter { hv_counter, .. } =
712                self.hv_counter_internal(self.state.scanline_mclk_cycles);
713            self.state.latched_hv_counter = Some(hv_counter);
714        } else if !self.registers.hv_counter_stopped {
715            self.state.latched_hv_counter = None;
716        }
717
718        self.update_latched_registers_if_necessary(register_number);
719
720        if register_number == 1 {
721            // Update enabled pixels in sprite state if register #1 was written
722            self.sprite_state.handle_display_enabled_write(
723                self.registers.horizontal_display_size,
724                self.registers.display_enabled,
725                self.state.pixel,
726            );
727
728            // Mark vertical border "forgotten" if V size was switched from V30 to V28 between lines 224-239
729            // This has a few effects:
730            // - The HINT counter continues to tick down every line throughout VBlank instead of getting reset
731            // - The VDP continues to render normally inside the vertical border
732            if prev_v_display_size == VerticalDisplaySize::ThirtyCell
733                && self.registers.vertical_display_size == VerticalDisplaySize::TwentyEightCell
734                && (VerticalDisplaySize::TwentyEightCell.active_scanlines()
735                    ..VerticalDisplaySize::ThirtyCell.active_scanlines())
736                    .contains(&self.state.scanline)
737            {
738                log::trace!(
739                    "V border forgotten; line {} pixel {}",
740                    self.state.scanline,
741                    self.state.pixel
742                );
743                self.state.v_border_forgotten = true;
744            }
745        }
746
747        if prev_h_display_size != self.registers.horizontal_display_size {
748            self.handle_h_resolution_change();
749        }
750    }
751
752    fn handle_h_resolution_change(&mut self) {
753        let h_display_size = self.registers.horizontal_display_size;
754        self.state.pixel = scanline_mclk_to_pixel(self.state.scanline_mclk_cycles, h_display_size);
755
756        let internal_h = pixel_to_internal_h(self.state.pixel, h_display_size);
757        let effective_v = if internal_h >= h_display_size.hblank_begin_h() {
758            self.state.scanline + 1
759        } else {
760            self.state.scanline
761        };
762        let active_scanlines = self.registers.vertical_display_size.active_scanlines();
763        let scanlines_per_frame = self.scanlines_in_current_frame();
764        self.state.in_vblank = (active_scanlines..scanlines_per_frame - 1).contains(&effective_v);
765
766        self.vdp_event_times = Self::vdp_event_times(h_display_size);
767        self.vdp_event_idx = 0;
768        while internal_h >= self.vdp_event_times[self.vdp_event_idx as usize].h {
769            self.vdp_event_idx += 1;
770        }
771    }
772
773    fn update_latched_registers_if_necessary(&mut self, register_number: u8) {
774        // Writing to register #2, #3, or #4 immediately updates the corresponding nametable address; these registers
775        // are not latched.
776        // Writing to register #1 immediately updates the display enabled flag.
777        // Writing to register #7 immediately updates the background color.
778        // Other register writes do not take effect until the next scanline.
779        macro_rules! relatch_registers {
780            ($self:expr, [$($field:ident),* $(,)?]) => {
781                {
782                    let mut changed = false;
783                    $(
784                        changed |= self.latched_registers.$field != self.registers.$field;
785                        self.latched_registers.$field = self.registers.$field;
786                    )*
787                    changed
788                }
789            }
790        }
791
792        let changed = match register_number {
793            1 => {
794                relatch_registers!(self, [display_enabled])
795            }
796            2 => {
797                relatch_registers!(self, [scroll_a_base_nt_addr])
798            }
799            3 => {
800                relatch_registers!(self, [window_base_nt_addr])
801            }
802            4 => {
803                relatch_registers!(self, [scroll_b_base_nt_addr])
804            }
805            7 => {
806                relatch_registers!(self, [background_palette, background_color_id])
807            }
808            _ => return,
809        };
810
811        // If this write occurred during active display, re-render the current scanline starting from the current pixel
812        if changed {
813            self.maybe_render_partial_line();
814        }
815    }
816
817    fn maybe_render_partial_line(&mut self) {
818        if self.state.scanline >= self.latched_registers.vertical_display_size.active_scanlines() {
819            return;
820        }
821
822        let active_display_range = self.registers.horizontal_display_size.active_display_h_range();
823        if active_display_range.contains(&self.state.pixel) {
824            log::trace!(
825                "Re-rendering line {} from pixel {} (frame pixel {})",
826                self.state.scanline,
827                self.state.pixel,
828                self.state.pixel.saturating_sub(active_display_range.start)
829            );
830            self.render_scanline(
831                self.state.scanline,
832                self.state.pixel.saturating_sub(active_display_range.start),
833            );
834        }
835    }
836
837    pub fn read_data(&mut self) -> u16 {
838        log::trace!(
839            "VDP data read at line {} mclk {} pixel {}",
840            self.state.scanline,
841            self.state.scanline_mclk_cycles,
842            self.state.pixel
843        );
844
845        // Reset write flag on all data port accesses
846        self.control_port.write_flag = ControlWriteFlag::First;
847
848        if self.control_port.mode != DataPortMode::Read {
849            // TODO return previous read buffer contents?
850            return 0xFFFF;
851        }
852
853        let mut value = match self.control_port.location {
854            DataPortLocation::Vram => match self.registers.vram_size {
855                VramSizeKb::SixtyFour => {
856                    let vram_addr = (self.control_port.data_port_address & 0xFFFF & !1) as usize;
857                    let msb = self.vram[vram_addr];
858                    let lsb = self.vram[vram_addr + 1];
859                    u16::from_be_bytes([msb, lsb])
860                }
861                VramSizeKb::OneTwentyEight => {
862                    let vram_addr = convert_128kb_vram_address(self.control_port.data_port_address);
863                    let byte = self.vram[vram_addr as usize];
864                    u16::from_be_bytes([byte, byte])
865                }
866            },
867            DataPortLocation::Vram8Bit => {
868                // TODO 128KB mode?
869                let vram_addr = ((self.control_port.data_port_address & 0xFFFF) ^ 1) as usize;
870                let lsb = self.vram[vram_addr];
871                u16::from_le_bytes([lsb, self.fifo.next_slot_word().msb()])
872            }
873            DataPortLocation::Cram => {
874                let cram_addr = (self.control_port.data_port_address & 0x7F) >> 1;
875                self.cram[cram_addr as usize]
876            }
877            DataPortLocation::Vsram => {
878                let vsram_addr = ((self.control_port.data_port_address & 0x7F) >> 1) as usize;
879                if vsram_addr < VSRAM_LEN_WORDS {
880                    self.vsram[vsram_addr]
881                } else {
882                    // TODO return most recently used VSRAM entry?
883                    self.vsram[0]
884                }
885            }
886            DataPortLocation::Invalid => {
887                // TODO return previous read buffer contents?
888                0xFFFF
889            }
890        };
891
892        if !self.fifo.is_empty() {
893            self.state.data_port_read_wait = true;
894
895            if let Some(read_override) = self.data_port_fifo_override() {
896                value = read_override;
897            }
898        }
899
900        self.control_port.increment_data_port_address(&self.registers);
901
902        // CRAM is 9-bit memory and VSRAM is 11-bit memory
903        // Remaining bits are filled from the last word written to the next available FIFO slot
904        match self.control_port.location {
905            DataPortLocation::Cram => (value & 0x0EEE) | (self.fifo.next_slot_word() & !0x0EEE),
906            DataPortLocation::Vsram => (value & 0x07FF) | (self.fifo.next_slot_word() & !0x07FF),
907            _ => value,
908        }
909    }
910
911    // TODO this function is not well-tested
912    fn data_port_fifo_override(&self) -> Option<u16> {
913        let mut read_override: Option<u16> = None;
914
915        let address_mask = match self.control_port.location {
916            DataPortLocation::Vram | DataPortLocation::Vram8Bit => match self.registers.vram_size {
917                VramSizeKb::SixtyFour => 0xFFFF & !1,
918                VramSizeKb::OneTwentyEight => 0x1FFFF & !1,
919            },
920            DataPortLocation::Cram | DataPortLocation::Vsram => 0x7F & !1,
921            DataPortLocation::Invalid => !0,
922        };
923
924        for entry in self.fifo.iter() {
925            if entry.mode != DataPortMode::Write
926                || entry.location != self.control_port.location
927                || (entry.address & address_mask)
928                    != (self.control_port.data_port_address & address_mask)
929            {
930                continue;
931            }
932
933            match self.control_port.location {
934                DataPortLocation::Vram => match self.registers.vram_size {
935                    VramSizeKb::SixtyFour => {
936                        read_override = Some(if !entry.address.bit(0) {
937                            entry.word
938                        } else {
939                            entry.word.swap_bytes()
940                        });
941                    }
942                    VramSizeKb::OneTwentyEight => {
943                        let byte = entry.word.lsb();
944                        read_override = Some(u16::from_le_bytes([byte, byte]));
945                    }
946                },
947                DataPortLocation::Cram | DataPortLocation::Vsram => {
948                    read_override = Some(entry.word);
949                }
950                DataPortLocation::Vram8Bit | DataPortLocation::Invalid => {}
951            }
952        }
953
954        if let Some(read_override) = read_override {
955            log::trace!(
956                "Overriding {:?} read of {:04X} to {read_override:04X}",
957                self.control_port.location,
958                self.control_port.data_port_address
959            );
960        }
961
962        read_override
963    }
964
965    pub fn write_data(&mut self, value: u16) {
966        log::trace!(
967            "VDP data write on scanline {} / mclk {} / pixel {}: {value:04X}, data port addr {:04X}",
968            self.state.scanline,
969            self.state.scanline_mclk_cycles,
970            self.state.pixel,
971            self.control_port.data_port_address
972        );
973
974        if self.control_port.dma_active && self.registers.dma_mode == DmaMode::MemoryToVram {
975            // VDP is locking the bus; buffer the write until the DMA is done
976            self.state.pending_writes.push(PendingWrite::Data(value));
977            return;
978        }
979
980        // Reset write flag on all data port accesses
981        self.control_port.write_flag = ControlWriteFlag::First;
982
983        let fifo_entry = self.control_port.new_fifo_entry(value, self.registers.vram_size);
984        self.control_port.increment_data_port_address(&self.registers);
985
986        if fifo_entry.mode == DataPortMode::Read {
987            log::debug!("Data port write with read target {fifo_entry:?}");
988        }
989
990        if self.fifo.is_full() {
991            self.pending_fifo_writes.push_back(fifo_entry);
992        } else {
993            self.push_fifo(fifo_entry);
994        }
995    }
996
997    fn push_fifo(&mut self, entry: VdpFifoEntry) {
998        // Check sprite table cache on FIFO push; this works around some timing issues in rendering
999        // Overdrive 2's textured cube effect
1000        if entry.mode == DataPortMode::Write && entry.location == DataPortLocation::Vram {
1001            // Sprite cache bytes are effectively byteswapped in 128KB mode, since LSB is written
1002            // to the target address as a single byte, but MSB can still update the paired byte
1003            // in the sprite cache
1004            let word_bytes = match self.registers.vram_size {
1005                VramSizeKb::SixtyFour => entry.word.to_be_bytes(),
1006                VramSizeKb::OneTwentyEight => entry.word.to_le_bytes(),
1007            };
1008
1009            // Full 17-bit address is checked for sprite cache updates in both 64KB and 128KB mode
1010            self.maybe_update_sprite_cache(entry.address, word_bytes[0]);
1011            self.maybe_update_sprite_cache(entry.address ^ 1, word_bytes[1]);
1012        }
1013
1014        log::trace!(
1015            "FIFO push (line {} pixel {}): {entry:04X?}",
1016            self.state.scanline,
1017            self.state.pixel
1018        );
1019
1020        self.fifo.push(entry);
1021    }
1022
1023    fn pop_fifo(&mut self) {
1024        let entry = self.fifo.front();
1025
1026        log::trace!(
1027            "FIFO pop (line {} pixel {} len {}): {entry:04X?}",
1028            self.state.scanline,
1029            self.state.pixel,
1030            self.fifo.len()
1031        );
1032
1033        let mut render_partial_line = false;
1034
1035        if entry.mode == DataPortMode::Write {
1036            match entry.location {
1037                DataPortLocation::Vram => {
1038                    let vram_addr = (entry.address & 0xFFFF) as usize;
1039                    match entry.size {
1040                        VramWriteSize::Word => {
1041                            self.vram[vram_addr] = entry.word.msb();
1042                            self.vram[vram_addr ^ 1] = entry.word.lsb();
1043                        }
1044                        VramWriteSize::Byte => {
1045                            self.vram[vram_addr] = entry.word.lsb();
1046                        }
1047                    }
1048                }
1049                DataPortLocation::Cram => {
1050                    let cram_addr = (entry.address & 0x7F) >> 1;
1051                    self.cram[cram_addr as usize] = entry.word;
1052
1053                    self.cram_dots.check_for_dot(
1054                        &self.registers,
1055                        &self.fifo,
1056                        self.state.pixel,
1057                        cram_addr,
1058                        entry.word,
1059                    );
1060
1061                    render_partial_line = true;
1062                }
1063                DataPortLocation::Vsram => {
1064                    let vsram_addr = ((entry.address & 0x7F) >> 1) as usize;
1065                    if vsram_addr < VSRAM_LEN_WORDS {
1066                        self.vsram[vsram_addr] = entry.word;
1067
1068                        render_partial_line = self.latched_registers.vertical_scroll_mode
1069                            == VerticalScrollMode::TwoCell;
1070                    }
1071                }
1072                DataPortLocation::Vram8Bit | DataPortLocation::Invalid => {}
1073            }
1074        }
1075
1076        // VRAM fill begins when an entry is popped from the FIFO after starting VRAM fill DMA
1077        // Writing to the FIFO after the DMA begins will update the data used for the fill
1078        self.state.vram_fill_data = Some(entry.word);
1079
1080        self.fifo.pop();
1081        if !self.fifo.is_full()
1082            && let Some(pending_write) = self.pending_fifo_writes.pop_front()
1083        {
1084            self.push_fifo(pending_write);
1085        }
1086
1087        self.state.data_port_read_wait &= !self.fifo.is_empty();
1088
1089        if self.state.display_enable_pending && self.fifo.is_empty() {
1090            // Display was re-enabled with a non-empty FIFO; actually enable it now
1091            self.state.display_enable_pending = false;
1092            self.registers.display_enabled = true;
1093            self.latched_registers.display_enabled = true;
1094
1095            self.sprite_state.handle_display_enabled_write(
1096                self.registers.horizontal_display_size,
1097                self.registers.display_enabled,
1098                self.state.pixel,
1099            );
1100
1101            render_partial_line = true;
1102        }
1103
1104        if render_partial_line {
1105            self.maybe_render_partial_line();
1106        }
1107    }
1108
1109    pub fn write_debug_register(&mut self, value: u16) {
1110        self.debug_register.write(value);
1111
1112        log::trace!("VDP debug register write: {:?}", self.debug_register);
1113    }
1114
1115    pub fn read_status(&mut self, m68k_opcode: u16, m68k_divider: u64) -> u16 {
1116        log::trace!("VDP status register read");
1117
1118        let read_adjustment = Self::status_read_mclk_adjustment(m68k_opcode, m68k_divider);
1119        let mut scanline_mclk = self.state.scanline_mclk_cycles + read_adjustment;
1120        let HVCounter { internal_h, internal_v: v_counter, vblank_flag, .. } =
1121            self.hv_counter_internal(scanline_mclk);
1122
1123        let hblank_flag = !self
1124            .registers
1125            .horizontal_display_size
1126            .hblank_flag_clear_h_range()
1127            .contains(&internal_h);
1128
1129        if scanline_mclk >= MCLK_CYCLES_PER_SCANLINE {
1130            scanline_mclk -= MCLK_CYCLES_PER_SCANLINE;
1131        }
1132
1133        // It must be possible for VINT to read 1 before the 68000 handles the interrupt; several
1134        // games depend on this (e.g. Ex-Mutants and Tyrants: Fight Through Time)
1135        let active_scanlines = self.registers.vertical_display_size.active_scanlines();
1136        let passed_vint = u16::from(v_counter) == active_scanlines && {
1137            let vint_mclk = self.registers.horizontal_display_size.v_interrupt_scanline_mclk();
1138            let original_scanline_mclk = self.state.scanline_mclk_cycles;
1139            scanline_mclk >= vint_mclk
1140                && (original_scanline_mclk < vint_mclk || original_scanline_mclk > scanline_mclk)
1141        };
1142        let vint_flag = self.state.v_interrupt_pending || passed_vint;
1143
1144        let status = (u16::from(self.fifo.is_empty()) << 9)
1145            | (u16::from(self.fifo.is_full()) << 8)
1146            | (u16::from(vint_flag) << 7)
1147            | (u16::from(self.sprite_state.overflow_flag()) << 6)
1148            | (u16::from(self.sprite_state.collision_flag()) << 5)
1149            | (u16::from(self.state.interlaced_odd) << 4)
1150            | (u16::from(vblank_flag || !self.registers.display_enabled) << 3)
1151            | (u16::from(hblank_flag) << 2)
1152            | (u16::from(self.control_port.dma_active) << 1)
1153            | u16::from(self.timing_mode == TimingMode::Pal);
1154
1155        // Reading status register clears the sprite overflow and collision flags
1156        self.sprite_state.clear_status_flags();
1157
1158        // Reset control write flag on all status register reads
1159        self.control_port.write_flag = ControlWriteFlag::First;
1160
1161        status
1162    }
1163
1164    fn status_read_mclk_adjustment(m68k_opcode: u16, m68k_divider: u64) -> u64 {
1165        // Timing hack: When the CPU reads the status register or the HV counter, return values
1166        // from slightly in the future to account for the actual read occurring towards the end
1167        // of the instruction.
1168        // Using the same value for everything will break either Overdrive 1 (background on the
1169        // heart screen) or Overdrive 2 (plasma twisters). It needs to vary based on what instruction
1170        // is performing the read
1171        match m68000_emu::cycles_if_move_btst_cmp(m68k_opcode) {
1172            Some(cycles) => u64::from(cycles - 4) * m68k_divider,
1173            None => 8 * m68k_divider,
1174        }
1175    }
1176
1177    #[must_use]
1178    pub fn hv_counter(&self, m68k_opcode: u16, m68k_divider: u64) -> u16 {
1179        let read_adjustment = Self::status_read_mclk_adjustment(m68k_opcode, m68k_divider);
1180        let hv = self.hv_counter_internal(self.state.scanline_mclk_cycles + read_adjustment);
1181
1182        log::trace!(
1183            "HV counter read on scanline {}; H={:02X}, V={:02X}, internal H={:03X}",
1184            self.state.scanline,
1185            hv.hv_counter.lsb(),
1186            hv.hv_counter.msb(),
1187            hv.internal_h,
1188        );
1189
1190        hv.hv_counter
1191    }
1192
1193    fn hv_counter_internal(&self, mut scanline_mclk: u64) -> HVCounter {
1194        let VCounter { counter: v_counter, vblank_flag } = self.v_counter(scanline_mclk);
1195
1196        if scanline_mclk >= MCLK_CYCLES_PER_SCANLINE {
1197            scanline_mclk -= MCLK_CYCLES_PER_SCANLINE;
1198        }
1199        let pixel = scanline_mclk_to_pixel(scanline_mclk, self.registers.horizontal_display_size);
1200        let internal_h = pixel_to_internal_h(pixel, self.registers.horizontal_display_size);
1201
1202        let hv_counter = self.state.latched_hv_counter.unwrap_or_else(|| {
1203            let h_counter = (internal_h >> 1) as u8;
1204            u16::from_be_bytes([v_counter, h_counter])
1205        });
1206
1207        HVCounter { internal_h, internal_v: v_counter, hv_counter, vblank_flag }
1208    }
1209
1210    #[inline]
1211    fn v_counter(&self, scanline_mclk: u64) -> VCounter {
1212        // Values from https://gendev.spritesmind.net/forum/viewtopic.php?t=768
1213
1214        // V counter increments for the next line when HINT is generated
1215        let in_hblank =
1216            scanline_mclk >= self.registers.horizontal_display_size.h_interrupt_scanline_mclk();
1217        let scanline = if in_hblank {
1218            let scanlines_per_frame = self.scanlines_in_current_frame();
1219            if self.state.scanline == scanlines_per_frame - 1 { 0 } else { self.state.scanline + 1 }
1220        } else {
1221            self.state.scanline
1222        };
1223
1224        let active_scanlines = match self.timing_mode {
1225            TimingMode::Ntsc => VerticalDisplaySize::TwentyEightCell.active_scanlines(),
1226            TimingMode::Pal => self.registers.vertical_display_size.active_scanlines(),
1227        };
1228
1229        let interlacing_mode = if self.state.interlaced_frame {
1230            self.registers.interlacing_mode
1231        } else {
1232            InterlacingMode::Progressive
1233        };
1234        match interlacing_mode {
1235            InterlacingMode::Progressive => {
1236                let threshold = match (self.timing_mode, self.registers.vertical_display_size) {
1237                    (TimingMode::Ntsc, _) => 0xEA,
1238                    (TimingMode::Pal, VerticalDisplaySize::TwentyEightCell) => 0x102,
1239                    (TimingMode::Pal, VerticalDisplaySize::ThirtyCell) => 0x10A,
1240                };
1241
1242                let scanlines_per_frame = match self.timing_mode {
1243                    TimingMode::Ntsc => NTSC_SCANLINES_PER_FRAME,
1244                    TimingMode::Pal => PAL_SCANLINES_PER_FRAME,
1245                };
1246
1247                let counter = if scanline <= threshold {
1248                    scanline
1249                } else {
1250                    scanline.wrapping_sub(scanlines_per_frame) & 0x1FF
1251                };
1252                let vblank_flag = counter >= active_scanlines && counter != 0x1FF;
1253                VCounter { counter: counter as u8, vblank_flag }
1254            }
1255            InterlacingMode::Interlaced | InterlacingMode::InterlacedDouble => {
1256                let threshold = match (self.timing_mode, self.registers.vertical_display_size) {
1257                    (TimingMode::Ntsc, _) => 0xEA,
1258                    (TimingMode::Pal, VerticalDisplaySize::TwentyEightCell) => 0x101,
1259                    (TimingMode::Pal, VerticalDisplaySize::ThirtyCell) => 0x109,
1260                };
1261                let scanlines_per_frame =
1262                    self.timing_mode.scanlines_per_frame(true, self.state.interlaced_odd);
1263
1264                let internal_counter = if scanline <= threshold {
1265                    scanline
1266                } else {
1267                    scanline.wrapping_sub(scanlines_per_frame) & 0x1FF
1268                };
1269                let vblank_flag = internal_counter >= active_scanlines && internal_counter != 0x1FF;
1270
1271                let external_counter = match interlacing_mode {
1272                    InterlacingMode::Interlaced => {
1273                        (internal_counter & 0xFE) | ((internal_counter >> 8) & 1)
1274                    }
1275                    InterlacingMode::InterlacedDouble => {
1276                        ((internal_counter << 1) & 0xFE) | ((internal_counter >> 7) & 1)
1277                    }
1278                    InterlacingMode::Progressive => unreachable!("nested matches"),
1279                };
1280
1281                VCounter { counter: external_counter as u8, vblank_flag }
1282            }
1283        }
1284    }
1285
1286    #[allow(clippy::missing_panics_doc)]
1287    #[must_use]
1288    pub fn tick(
1289        &mut self,
1290        master_clock_cycles: u64,
1291        memory: &mut impl VdpBusView,
1292    ) -> VdpTickEffect {
1293        // The longest 68k instruction (DIVS (xxx).l, Dn) takes 172 68k cycles / 1204 mclk cycles
1294        assert!(
1295            master_clock_cycles < 1250,
1296            "VDP tick {master_clock_cycles} mclk cycles, expected <1250"
1297        );
1298
1299        let mut tick_effect = VdpTickEffect::None;
1300        self.state.scanline_mclk_cycles += master_clock_cycles;
1301        if self.state.scanline_mclk_cycles >= MCLK_CYCLES_PER_SCANLINE {
1302            let end_of_line = self.registers.horizontal_display_size.pixels_including_hblank();
1303            self.advance_to_pixel(end_of_line, memory);
1304
1305            tick_effect = self.advance_to_next_line();
1306        }
1307
1308        let pixel = scanline_mclk_to_pixel(
1309            self.state.scanline_mclk_cycles,
1310            self.registers.horizontal_display_size,
1311        );
1312        self.advance_to_pixel(pixel, memory);
1313
1314        tick_effect
1315    }
1316
1317    fn advance_to_pixel(&mut self, end_pixel: u16, memory: &mut impl VdpBusView) {
1318        let check_access_slots = self.control_port.dma_active || !self.fifo.is_empty();
1319        match (self.registers.horizontal_display_size, check_access_slots) {
1320            (HorizontalDisplaySize::ThirtyTwoCell, false) => {
1321                self.advance_to_pixel_no_slot_check::<false>(end_pixel);
1322            }
1323            (HorizontalDisplaySize::ThirtyTwoCell, true) => {
1324                self.advance_to_pixel_check_slots::<false>(end_pixel, memory);
1325            }
1326            (HorizontalDisplaySize::FortyCell, false) => {
1327                self.advance_to_pixel_no_slot_check::<true>(end_pixel);
1328            }
1329            (HorizontalDisplaySize::FortyCell, true) => {
1330                self.advance_to_pixel_check_slots::<true>(end_pixel, memory);
1331            }
1332        }
1333    }
1334
1335    #[inline]
1336    fn advance_to_pixel_check_slots<const H40: bool>(
1337        &mut self,
1338        end_pixel: u16,
1339        memory: &mut impl VdpBusView,
1340    ) {
1341        // Slower loop - check for access slots for DMA/FIFO progress
1342        let access_slots = if H40 { H40_ACCESS_SLOTS } else { H32_ACCESS_SLOTS };
1343        let blank_refresh_slots =
1344            if H40 { H40_BLANK_REFRESH_SLOTS } else { H32_BLANK_REFRESH_SLOTS };
1345
1346        while self.state.pixel < end_pixel {
1347            let pixel = self.state.pixel;
1348            if !pixel.bit(0) {
1349                let slot_idx = (pixel >> 1) as u8;
1350                let in_blank_refresh_slot = blank_refresh_slots[slot_idx as usize];
1351
1352                // TODO correct refresh slot locations for active display
1353                if !in_blank_refresh_slot {
1354                    // Need to do DMA read before FIFO pop - reverse order breaks Overdrive 512-color
1355                    self.progress_memory_to_vram_dma(slot_idx, blank_refresh_slots, memory);
1356                }
1357
1358                let blank = !self.registers.display_enabled || self.state.in_vblank;
1359                if (blank && !blank_refresh_slots[slot_idx as usize])
1360                    || (!blank && access_slots[slot_idx as usize])
1361                {
1362                    self.handle_access_slot();
1363                }
1364
1365                // TODO correct refresh slot locations for active display
1366                if !in_blank_refresh_slot {
1367                    self.fifo.decrement_latency();
1368                }
1369            }
1370
1371            let internal_h =
1372                if H40 { pixel_to_internal_h_h40(pixel) } else { pixel_to_internal_h_h32(pixel) };
1373            while internal_h >= self.vdp_event_times[self.vdp_event_idx as usize].h {
1374                let event = self.vdp_event_times[self.vdp_event_idx as usize].event;
1375                self.vdp_event_idx += 1;
1376
1377                self.handle_vdp_event(event);
1378            }
1379
1380            self.state.pixel += 1;
1381        }
1382    }
1383
1384    #[inline]
1385    fn advance_to_pixel_no_slot_check<const H40: bool>(&mut self, end_pixel: u16) {
1386        // Faster loop - only check for passed VDP events
1387        debug_assert!(!self.control_port.dma_active && self.fifo.is_empty());
1388
1389        let end_internal_h = if H40 {
1390            pixel_to_internal_h_h40(end_pixel)
1391        } else {
1392            pixel_to_internal_h_h32(end_pixel)
1393        };
1394        while end_internal_h >= self.vdp_event_times[self.vdp_event_idx as usize].h {
1395            let internal_h = self.vdp_event_times[self.vdp_event_idx as usize].h;
1396            let pixel = if H40 {
1397                internal_h_to_pixel_h40(internal_h)
1398            } else {
1399                internal_h_to_pixel_h32(internal_h)
1400            };
1401            let event = self.vdp_event_times[self.vdp_event_idx as usize].event;
1402            self.vdp_event_idx += 1;
1403
1404            self.state.pixel = pixel;
1405            self.handle_vdp_event(event);
1406        }
1407
1408        self.state.pixel = end_pixel;
1409    }
1410
1411    fn handle_access_slot(&mut self) {
1412        // Video memory write slot
1413        // FIFO takes priority over VRAM fill/copy DMA
1414        if !self.fifo.is_empty() {
1415            if self.fifo.front().latency == 0 {
1416                self.pop_fifo();
1417            }
1418        } else if self.control_port.dma_active {
1419            match self.registers.dma_mode {
1420                DmaMode::VramFill => self.progress_vram_fill_dma(),
1421                DmaMode::VramCopy => self.progress_vram_copy_dma(),
1422                DmaMode::MemoryToVram => {}
1423            }
1424        }
1425    }
1426
1427    fn progress_memory_to_vram_dma(
1428        &mut self,
1429        slot_idx: u8,
1430        refresh_slots: &[bool; 256],
1431        memory: &mut impl VdpBusView,
1432    ) {
1433        if !self.control_port.dma_active || self.registers.dma_mode != DmaMode::MemoryToVram {
1434            return;
1435        }
1436
1437        self.dma_latency = self.dma_latency.saturating_sub(1);
1438
1439        if self.fifo.is_full() {
1440            return;
1441        }
1442
1443        // Lose an extra read slot for every VRAM refresh slot
1444        // Direct color DMA demos depend on this
1445        let should_skip_read = slot_idx != 0 && refresh_slots[(slot_idx - 1) as usize];
1446        if should_skip_read {
1447            return;
1448        }
1449
1450        let word = memory.read_word_for_dma(self.registers.dma_source_address);
1451        self.increment_dma_source_address();
1452
1453        self.push_fifo(VdpFifoEntry {
1454            latency: cmp::max(self.dma_latency, fifo::INITIAL_FIFO_LATENCY),
1455            ..self.control_port.new_fifo_entry(word, self.registers.vram_size)
1456        });
1457        self.control_port.increment_data_port_address(&self.registers);
1458
1459        self.decrement_dma_length();
1460    }
1461
1462    fn progress_vram_fill_dma(&mut self) {
1463        let Some(fill_data) = self.state.vram_fill_data else { return };
1464
1465        // VRAM fill increments source address on every write even though it does not use the address
1466        self.increment_dma_source_address();
1467
1468        match self.control_port.location {
1469            DataPortLocation::Vram | DataPortLocation::Vram8Bit => {
1470                let byte = fill_data.msb();
1471                let vram_addr = (self.control_port.data_port_address ^ 1) & 0xFFFF;
1472                self.vram[vram_addr as usize] = byte;
1473                self.maybe_update_sprite_cache(vram_addr, byte);
1474            }
1475            DataPortLocation::Cram => {
1476                // CRAM fill is bugged; uses the value from the next FIFO slot instead of fill data
1477                let word = self.fifo.next_slot_word();
1478                let cram_addr = (self.control_port.data_port_address & 0x7F) >> 1;
1479                self.cram[cram_addr as usize] = word;
1480            }
1481            DataPortLocation::Vsram => {
1482                // VSRAM fill is bugged; uses the value from the next FIFO slot instead of fill data
1483                let word = self.fifo.next_slot_word();
1484                let vsram_addr = ((self.control_port.data_port_address & 0x7F) >> 1) as usize;
1485                if vsram_addr < VSRAM_LEN_WORDS {
1486                    self.vsram[vsram_addr] = word;
1487                }
1488            }
1489            DataPortLocation::Invalid => {}
1490        }
1491
1492        self.control_port.increment_data_port_address(&self.registers);
1493        self.decrement_dma_length();
1494    }
1495
1496    fn progress_vram_copy_dma(&mut self) {
1497        self.state.vram_copy_odd_slot = !self.state.vram_copy_odd_slot;
1498        if self.state.vram_copy_odd_slot {
1499            return;
1500        }
1501
1502        let source_addr = ((self.registers.dma_source_address >> 1) ^ 1) & 0xFFFF;
1503        self.increment_dma_source_address();
1504
1505        let dest_addr = (self.control_port.data_port_address ^ 1) & 0xFFFF;
1506        self.control_port.increment_data_port_address(&self.registers);
1507
1508        let byte = self.vram[source_addr as usize];
1509        self.vram[dest_addr as usize] = byte;
1510        self.maybe_update_sprite_cache(dest_addr, byte);
1511
1512        self.decrement_dma_length();
1513    }
1514
1515    fn increment_dma_source_address(&mut self) {
1516        // DMA source address always wraps within a 0x20000-byte block
1517        self.registers.dma_source_address = (self.registers.dma_source_address & !0x1FFFF)
1518            | (self.registers.dma_source_address.wrapping_add(2) & 0x1FFFF);
1519    }
1520
1521    fn decrement_dma_length(&mut self) {
1522        // Check for 0 after decrementing; an initial DMA length of 0 should function as 65536
1523        self.registers.dma_length = self.registers.dma_length.wrapping_sub(1);
1524        if self.registers.dma_length != 0 {
1525            return;
1526        }
1527
1528        self.control_port.dma_active = false;
1529
1530        // Apply any writes that occurred mid-DMA (e.g. from a MOVE.L instruction where the first
1531        // word started a DMA)
1532        if !self.state.pending_writes.is_empty() {
1533            self.apply_pending_writes();
1534        }
1535
1536        log::trace!(
1537            "DMA of type {:?} complete at line {} mclk {}; FIFO len {}",
1538            self.registers.dma_mode,
1539            self.state.scanline,
1540            self.state.scanline_mclk_cycles,
1541            self.fifo.len()
1542        );
1543    }
1544
1545    fn vdp_event_times(h_display_size: HorizontalDisplaySize) -> [VdpEventWithTime; VdpEvent::NUM] {
1546        let mut events = [
1547            VdpEventWithTime::new(h_display_size.v_interrupt_h(), VdpEvent::VInterrupt),
1548            VdpEventWithTime::new(h_display_size.read_h_scroll_h(), VdpEvent::ReadHScroll),
1549            VdpEventWithTime::new(
1550                h_display_size.active_display_h_range().start,
1551                VdpEvent::RenderLine,
1552            ),
1553            VdpEventWithTime::new(
1554                h_display_size.fetch_sprite_attributes_h(),
1555                VdpEvent::FetchSpriteAttributes,
1556            ),
1557            VdpEventWithTime::new(h_display_size.hblank_begin_h(), VdpEvent::HBlankStart),
1558            VdpEventWithTime::new(h_display_size.h_interrupt_h(), VdpEvent::HInterrupt),
1559            VdpEventWithTime::new(h_display_size.latch_registers_h(), VdpEvent::LatchRegisters),
1560            VdpEventWithTime::new(u16::MAX, VdpEvent::None),
1561        ];
1562
1563        events.sort_by_key(|event| event.h);
1564
1565        let count_occurrences =
1566            |event: VdpEvent| events.iter().filter(|e| e.event == event).count();
1567        debug_assert!(
1568            VdpEvent::ALL.into_iter().all(|event| count_occurrences(event) == 1),
1569            "Every VdpEvent value must be present exactly once"
1570        );
1571
1572        events
1573    }
1574
1575    fn handle_vdp_event(&mut self, event: VdpEvent) {
1576        match event {
1577            VdpEvent::VInterrupt => {
1578                let active_scanlines = self.registers.vertical_display_size.active_scanlines();
1579                if self.state.scanline == active_scanlines {
1580                    log::trace!("Generating V interrupt");
1581                    self.state.v_interrupt_pending = true;
1582
1583                    // Latch H resolution at start of VBlank in case the game changes resolution
1584                    // at start of VBlank, e.g. Bugs Bunny in Double Trouble
1585                    self.state.frame_h_resolution = self.registers.horizontal_display_size;
1586                }
1587            }
1588            VdpEvent::ReadHScroll => {
1589                if !self.registers.display_enabled
1590                    || (self.state.scanline
1591                        >= self.latched_registers.vertical_display_size.active_scanlines()
1592                        && !self.state.v_border_forgotten)
1593                {
1594                    return;
1595                }
1596
1597                let raster_line = RasterLine::from_scanline(
1598                    self.state.scanline,
1599                    &self.latched_registers,
1600                    self.timing_mode,
1601                    self.state.interlaced_frame,
1602                    self.state.interlaced_odd,
1603                );
1604
1605                // Only the lowest 8 bits of raster line are used for H scroll lookups
1606                let h_scroll_scanline = raster_line.line & 0xFF;
1607
1608                self.latched_h_scroll = read_h_scroll(
1609                    &self.vram,
1610                    self.registers.h_scroll_table_base_addr,
1611                    self.registers.horizontal_scroll_mode,
1612                    h_scroll_scanline,
1613                );
1614            }
1615            VdpEvent::RenderLine => {
1616                self.sprite_state
1617                    .handle_line_end(self.registers.horizontal_display_size, self.state.pixel);
1618
1619                // Sprite processing phase 1
1620                // In actual hardware this takes place during HBlank
1621                log::trace!("Scanning sprites");
1622                self.scan_sprites_one_line_ahead();
1623
1624                // Render current line
1625                self.render_scanline(self.state.scanline, 0);
1626            }
1627            VdpEvent::FetchSpriteAttributes => {
1628                // Sprite processing phase 2
1629                // In actual hardware this takes place during active display
1630                log::trace!("Fetching sprite attributes");
1631                self.fetch_sprite_attributes();
1632            }
1633            VdpEvent::HBlankStart => {
1634                self.sprite_state.handle_hblank_start(
1635                    self.registers.horizontal_display_size,
1636                    self.registers.display_enabled,
1637                );
1638
1639                let active_scanlines = self.registers.vertical_display_size.active_scanlines();
1640                let scanlines_per_frame = self.scanlines_in_current_frame();
1641                if self.state.scanline == active_scanlines - 1
1642                    || (self.state.v_border_forgotten
1643                        && self.state.scanline
1644                            == VerticalDisplaySize::ThirtyCell.active_scanlines() - 1)
1645                {
1646                    self.state.in_vblank = true;
1647                } else if self.state.scanline == scanlines_per_frame - 2 {
1648                    self.state.in_vblank = false;
1649                }
1650            }
1651            VdpEvent::HInterrupt => {
1652                log::trace!("Latching cached sprite table");
1653                self.latched_sprite_attributes
1654                    .copy_from_slice(self.cached_sprite_attributes.as_ref());
1655
1656                self.decrement_h_interrupt_counter();
1657            }
1658            VdpEvent::LatchRegisters => {
1659                // Almost all VDP registers and the full screen V scroll values are latched within the 36 CPU cycles after
1660                // HINT is generated. Changing values after this point will not take effect until after the next scanline
1661                // is rendered.
1662                // The only VDP registers that are not latched are the nametable addresses, the display enabled bit, and
1663                // the background color.
1664                log::trace!("Latching VDP registers");
1665                self.latched_registers = self.registers.clone();
1666                self.latched_full_screen_v_scroll = (self.vsram[0], self.vsram[1]);
1667            }
1668            VdpEvent::None => {}
1669        }
1670    }
1671
1672    fn decrement_h_interrupt_counter(&mut self) {
1673        let active_scanlines = self.registers.vertical_display_size.active_scanlines();
1674        let scanlines_per_frame = self.scanlines_in_current_frame();
1675
1676        if self.state.scanline < active_scanlines
1677            || self.state.scanline == scanlines_per_frame - 1
1678            || self.state.v_border_forgotten
1679        {
1680            if self.state.h_interrupt_counter == 0 {
1681                self.state.h_interrupt_counter = self.registers.h_interrupt_interval;
1682
1683                log::trace!("Generating H interrupt (scanline {})", self.state.scanline);
1684                self.state.h_interrupt_pending = true;
1685            } else {
1686                self.state.h_interrupt_counter -= 1;
1687            }
1688        } else {
1689            // H interrupt counter is constantly refreshed during VBlank
1690            self.state.h_interrupt_counter = self.registers.h_interrupt_interval;
1691        }
1692    }
1693
1694    fn advance_to_next_line(&mut self) -> VdpTickEffect {
1695        let scanlines_per_frame = self.scanlines_in_current_frame();
1696
1697        self.cram_dots.swap_buffers_if_needed();
1698
1699        self.state.scanline_mclk_cycles -= MCLK_CYCLES_PER_SCANLINE;
1700        self.vdp_event_idx = 0;
1701        self.state.scanline += 1;
1702        self.state.pixel = 0;
1703        if self.state.scanline == scanlines_per_frame {
1704            self.state.scanline = 0;
1705            self.state.v_border_forgotten = false;
1706
1707            let next_frame_interlaced = matches!(
1708                self.registers.interlacing_mode,
1709                InterlacingMode::Interlaced | InterlacingMode::InterlacedDouble
1710            );
1711
1712            if next_frame_interlaced && !self.state.interlaced_frame {
1713                self.state.interlaced_odd = false;
1714
1715                if !self.config.deinterlace {
1716                    self.prepare_frame_buffer_for_interlaced();
1717                }
1718            }
1719
1720            self.state.interlaced_frame = next_frame_interlaced;
1721
1722            // Top border length needs to be saved at start-of-frame in case there is a mid-frame swap between V28
1723            // mode and V30 mode. Titan Overdrive 2 depends on this for the arcade scene
1724            self.state.top_border =
1725                self.registers.vertical_display_size.top_border(self.timing_mode);
1726
1727            // Re-latch H display mode at start of frame in case a game changed it mid-VBlank
1728            // Not doing this causes a glitchy frame on mode switches
1729            self.state.frame_h_resolution = self.registers.horizontal_display_size;
1730        } else if self.state.interlaced_frame {
1731            let toggle_odd_line = match self.registers.vertical_display_size {
1732                VerticalDisplaySize::TwentyEightCell => 240,
1733                VerticalDisplaySize::ThirtyCell => 256,
1734            };
1735            if self.state.scanline == toggle_odd_line {
1736                // TODO this actually happens at H=0x001 or H=0x002
1737                self.state.interlaced_odd = !self.state.interlaced_odd;
1738            }
1739        }
1740
1741        let last_scanline_of_frame =
1742            self.timing_mode.rendered_lines_per_frame() - self.state.top_border;
1743        if self.state.scanline == last_scanline_of_frame {
1744            VdpTickEffect::FrameComplete
1745        } else {
1746            VdpTickEffect::None
1747        }
1748    }
1749
1750    fn apply_pending_writes(&mut self) {
1751        let mut pending_writes = [PendingWrite::default(); 10];
1752        let pending_writes_len = self.state.pending_writes.len();
1753        pending_writes[..pending_writes_len].copy_from_slice(&self.state.pending_writes);
1754        self.state.pending_writes.clear();
1755
1756        for &pending_write in &pending_writes[..pending_writes_len] {
1757            match pending_write {
1758                PendingWrite::Control(value) => {
1759                    self.write_control(value);
1760                }
1761                PendingWrite::Data(value) => {
1762                    self.write_data(value);
1763                }
1764            }
1765        }
1766    }
1767
1768    #[inline]
1769    fn maybe_update_sprite_cache(&mut self, address: u32, value: u8) {
1770        if address.bit(2) {
1771            // Second 4 bytes of each sprite entry are not cached
1772            return;
1773        }
1774
1775        let sprite_table_addr = self.registers.masked_sprite_attribute_table_addr();
1776        let h_size = self.registers.horizontal_display_size;
1777
1778        let sprite_table_end = sprite_table_addr + 8 * h_size.sprite_table_len();
1779        if !(sprite_table_addr..sprite_table_end).contains(&address) {
1780            // Address is not in sprite table
1781            return;
1782        }
1783
1784        let idx = ((address - sprite_table_addr) / 8) as usize;
1785        match address & 0x3 {
1786            0x0 => self.cached_sprite_attributes[idx].update_first_word_msb(value),
1787            0x1 => self.cached_sprite_attributes[idx].update_first_word_lsb(value),
1788            0x2 => self.cached_sprite_attributes[idx].update_second_word_msb(value),
1789            0x3 => self.cached_sprite_attributes[idx].update_second_word_lsb(value),
1790            _ => unreachable!("value & 0x3 is always <= 0x3"),
1791        }
1792    }
1793
1794    fn prepare_frame_buffer_for_interlaced(&mut self) {
1795        // Duplicate every line to avoid a flickering frame if a game enables interlacing without
1796        // first blanking the screen
1797        let screen_width = self.screen_width();
1798        let screen_height = self.screen_height();
1799        for scanline in (0..screen_height).rev() {
1800            for pixel in 0..screen_width {
1801                let color = self.frame_buffer[(scanline * screen_width + pixel) as usize];
1802                self.frame_buffer[((2 * scanline) * screen_width + pixel) as usize] = color;
1803                self.frame_buffer[((2 * scanline + 1) * screen_width + pixel) as usize] = color;
1804            }
1805        }
1806    }
1807
1808    #[must_use]
1809    pub fn m68k_interrupt_level(&self) -> u8 {
1810        // TODO external interrupts at level 2
1811        if self.state.v_interrupt_pending && self.state.v_interrupt_enabled_latch {
1812            6
1813        } else if self.state.h_interrupt_pending && self.state.h_interrupt_enabled_latch {
1814            4
1815        } else {
1816            0
1817        }
1818    }
1819
1820    #[inline]
1821    pub fn update_interrupt_latches(&mut self) {
1822        self.state.v_interrupt_enabled_latch = self.registers.v_interrupt_enabled;
1823        self.state.h_interrupt_enabled_latch = self.registers.h_interrupt_enabled;
1824    }
1825
1826    pub fn acknowledge_m68k_interrupt(&mut self) {
1827        let interrupt_level = self.m68k_interrupt_level();
1828        log::trace!("M68K interrupt acknowledged; level {interrupt_level}");
1829        if interrupt_level == 6 {
1830            self.state.v_interrupt_pending = false;
1831        } else if interrupt_level == 4 {
1832            self.state.h_interrupt_pending = false;
1833        }
1834    }
1835
1836    #[inline]
1837    #[must_use]
1838    pub fn should_halt_cpu(&self) -> bool {
1839        (self.control_port.dma_active && self.registers.dma_mode == DmaMode::MemoryToVram)
1840            || self.state.data_port_read_wait
1841            || !self.pending_fifo_writes.is_empty()
1842    }
1843
1844    #[inline]
1845    #[must_use]
1846    pub fn long_halting_dma_in_progress(&self) -> bool {
1847        self.control_port.dma_active
1848            && self.registers.dma_mode == DmaMode::MemoryToVram
1849            && self.registers.dma_length
1850                >= self.registers.horizontal_display_size.access_slots_per_blank_line()
1851    }
1852
1853    #[inline]
1854    #[must_use]
1855    pub fn z80_interrupt_line(&self) -> InterruptLine {
1856        // Z80 INT line is low only during the first scanline of VBlank
1857        if self.state.scanline == self.registers.vertical_display_size.active_scanlines() {
1858            InterruptLine::Low
1859        } else {
1860            InterruptLine::High
1861        }
1862    }
1863
1864    fn scan_sprites_one_line_ahead(&mut self) {
1865        let scanlines_per_frame = self.scanlines_in_current_frame();
1866        let scanline_for_sprite_scan = if self.state.scanline == scanlines_per_frame - 1 {
1867            0
1868        } else {
1869            self.state.scanline + 1
1870        };
1871
1872        self.scan_sprites(scanline_for_sprite_scan);
1873    }
1874
1875    #[inline]
1876    #[must_use]
1877    pub fn frame_buffer(&self) -> &[Color; FRAME_BUFFER_LEN] {
1878        &self.frame_buffer
1879    }
1880
1881    #[inline]
1882    #[must_use]
1883    pub fn frame_buffer_mut(&mut self) -> &mut [Color; FRAME_BUFFER_LEN] {
1884        &mut self.frame_buffer
1885    }
1886
1887    #[inline]
1888    #[must_use]
1889    pub fn frame_size(&self) -> FrameSize {
1890        FrameSize { width: self.screen_width(), height: self.screen_height() }
1891    }
1892
1893    #[inline]
1894    #[must_use]
1895    pub fn composite_params(&self) -> CompositeParams {
1896        let upscale_factor = match self.state.frame_h_resolution {
1897            HorizontalDisplaySize::ThirtyTwoCell => 10,
1898            HorizontalDisplaySize::FortyCell => 8,
1899        };
1900
1901        CompositeParams { upscale_factor, samples_per_color_cycle: SamplesPerColorCycle::Fifteen }
1902    }
1903
1904    #[inline]
1905    #[must_use]
1906    pub fn scanlines_in_current_frame(&self) -> u16 {
1907        self.timing_mode.scanlines_per_frame(self.state.interlaced_frame, self.state.interlaced_odd)
1908    }
1909
1910    #[inline]
1911    #[must_use]
1912    pub fn is_interlaced_frame(&self) -> bool {
1913        self.state.interlaced_frame
1914    }
1915
1916    #[inline]
1917    #[must_use]
1918    pub fn is_interlaced_odd(&self) -> bool {
1919        self.state.interlaced_frame && self.state.interlaced_odd
1920    }
1921
1922    #[inline]
1923    #[must_use]
1924    pub fn average_scanlines_per_frame(&self) -> f64 {
1925        let interlaced_frame = self.state.interlaced_frame;
1926        match self.timing_mode {
1927            TimingMode::Ntsc => {
1928                f64::from(NTSC_SCANLINES_PER_FRAME) + 0.5 * f64::from(interlaced_frame)
1929            }
1930            TimingMode::Pal => {
1931                f64::from(PAL_SCANLINES_PER_FRAME) - 0.5 * f64::from(interlaced_frame)
1932            }
1933        }
1934    }
1935
1936    #[inline]
1937    #[must_use]
1938    pub fn screen_width(&self) -> u32 {
1939        let h_display_size = self.state.frame_h_resolution;
1940        let active_display_pixels: u32 = h_display_size.active_display_pixels().into();
1941
1942        if self.config.render_horizontal_border {
1943            u32::from(h_display_size.left_border())
1944                + active_display_pixels
1945                + u32::from(RIGHT_BORDER)
1946        } else {
1947            active_display_pixels
1948        }
1949    }
1950
1951    #[inline]
1952    #[must_use]
1953    pub fn screen_height(&self) -> u32 {
1954        let screen_height: u32 = if self.config.render_vertical_border {
1955            self.timing_mode.rendered_lines_per_frame().into()
1956        } else {
1957            self.registers.vertical_display_size.active_scanlines().into()
1958        };
1959
1960        if self.state.interlaced_frame { 2 * screen_height } else { screen_height }
1961    }
1962
1963    #[inline]
1964    #[must_use]
1965    pub fn border_size(&self) -> BorderSize {
1966        let (left, right) = if self.config.render_horizontal_border {
1967            let h_display_size = self.state.frame_h_resolution;
1968            (h_display_size.left_border(), RIGHT_BORDER)
1969        } else {
1970            (0, 0)
1971        };
1972
1973        let (top, bottom) = if self.config.render_vertical_border {
1974            let v_display_size = self.registers.vertical_display_size;
1975            (
1976                v_display_size.top_border(self.timing_mode),
1977                v_display_size.bottom_border(self.timing_mode),
1978            )
1979        } else {
1980            (0, 0)
1981        };
1982
1983        BorderSize {
1984            left: left.into(),
1985            right: right.into(),
1986            top: top.into(),
1987            bottom: bottom.into(),
1988        }
1989    }
1990
1991    #[inline]
1992    #[must_use]
1993    pub fn config(&self) -> VdpConfig {
1994        self.config
1995    }
1996
1997    #[inline]
1998    pub fn reload_config(&mut self, config: VdpConfig) {
1999        self.config = config;
2000        self.color_tables = ColorTables::from_config(&self.config);
2001    }
2002
2003    #[inline]
2004    #[must_use]
2005    pub fn scanline(&self) -> u16 {
2006        self.state.scanline
2007    }
2008
2009    #[inline]
2010    #[must_use]
2011    pub fn scanline_mclk(&self) -> u64 {
2012        self.state.scanline_mclk_cycles
2013    }
2014
2015    #[inline]
2016    #[must_use]
2017    pub fn timing_mode(&self) -> TimingMode {
2018        self.timing_mode
2019    }
2020}
2021
2022fn read_h_scroll(
2023    vram: &Vram,
2024    h_scroll_table_addr: u16,
2025    h_scroll_mode: HorizontalScrollMode,
2026    scanline: u16,
2027) -> (u16, u16) {
2028    let h_scroll_addr = match h_scroll_mode {
2029        HorizontalScrollMode::FullScreen => h_scroll_table_addr,
2030        HorizontalScrollMode::Cell => h_scroll_table_addr.wrapping_add(32 * (scanline / 8)),
2031        HorizontalScrollMode::Line => h_scroll_table_addr.wrapping_add(4 * scanline),
2032        HorizontalScrollMode::Invalid => h_scroll_table_addr.wrapping_add(4 * (scanline & 0x7)),
2033    };
2034
2035    let h_scroll_a =
2036        u16::from_be_bytes([vram[h_scroll_addr as usize], vram[(h_scroll_addr + 1) as usize]]);
2037    let h_scroll_b = u16::from_be_bytes([
2038        vram[(h_scroll_addr + 2) as usize],
2039        vram[(h_scroll_addr + 3) as usize],
2040    ]);
2041
2042    (h_scroll_a & 0x03FF, h_scroll_b & 0x03FF)
2043}
2044
2045fn convert_128kb_vram_address(address: u32) -> u32 {
2046    // Formula from https://plutiedev.com/mirror/kabuto-hardware-notes#128k-abuse
2047    (((address & 0x2) ^ 0x2) >> 1)
2048        | ((address & 0x400) >> 9)
2049        | (address & 0x3FC)
2050        | ((address & 0x1F800) >> 1)
2051}
2052
2053fn scanline_mclk_to_pixel(scanline_mclk: u64, h_display_size: HorizontalDisplaySize) -> u16 {
2054    match h_display_size {
2055        HorizontalDisplaySize::ThirtyTwoCell => scanline_mclk_to_pixel_h32(scanline_mclk),
2056        HorizontalDisplaySize::FortyCell => scanline_mclk_to_pixel_h40(scanline_mclk),
2057    }
2058}
2059
2060fn pixel_to_internal_h(pixel: u16, h_display_size: HorizontalDisplaySize) -> u16 {
2061    match h_display_size {
2062        HorizontalDisplaySize::ThirtyTwoCell => pixel_to_internal_h_h32(pixel),
2063        HorizontalDisplaySize::FortyCell => pixel_to_internal_h_h40(pixel),
2064    }
2065}
2066
2067fn scanline_mclk_to_pixel_h32(scanline_mclk: u64) -> u16 {
2068    // H32 pixel clock is always mclk/10
2069    (scanline_mclk / 10) as u16
2070}
2071
2072fn pixel_to_internal_h_h32(pixel: u16) -> u16 {
2073    if pixel <= 0x127 { pixel } else { pixel + (0x1D2 - 0x128) }
2074}
2075
2076fn internal_h_to_pixel_h32(internal_h: u16) -> u16 {
2077    if internal_h <= 0x127 { internal_h } else { internal_h - (0x1D2 - 0x128) }
2078}
2079
2080fn scanline_mclk_to_pixel_h40(scanline_mclk: u64) -> u16 {
2081    // Note H jumps 0x16C to 0x1C9 right before HSYNC
2082    const JUMP_DIFF: u64 = 0x1C9 - 0x16D;
2083
2084    // Special cases due to pixel clock varying during HSYNC in H40 mode
2085    // https://gendev.spritesmind.net/forum/viewtopic.php?t=3221
2086
2087    // Pixel clock is mclk/8 from H=0x000 through H=0x1CB
2088    if scanline_mclk < (0x1CC - JUMP_DIFF) * 8 {
2089        return (scanline_mclk / 8) as u16;
2090    }
2091
2092    // From H=0x1CC through H=0x1ED, follows this pattern, repeated twice:
2093    //   1 mclk/8, 7 mclk/10, 2 mclk/9, 7 mclk/10
2094    let hsync_start_mclk = (0x1CC - JUMP_DIFF) * 8;
2095    let hsync_end_mclk = hsync_start_mclk + 2 * (8 + 7 * 10 + 2 * 9 + 7 * 10);
2096    if (hsync_start_mclk..hsync_end_mclk).contains(&scanline_mclk) {
2097        let hsync_mclk = scanline_mclk - hsync_start_mclk;
2098        let pattern_pixel = match hsync_mclk % (8 + 7 * 10 + 2 * 9 + 7 * 10) {
2099            // 1 pixel at mclk/8
2100            0..=7 => 0,
2101            // 7 pixels at mclk/10
2102            pattern_mclk @ 8..=77 => 1 + (pattern_mclk - 8) / 10,
2103            // 2 pixels at mclk/9 (effectively)
2104            pattern_mclk @ 78..=95 => 8 + (pattern_mclk - 78) / 9,
2105            // 7 pixels at mclk/10
2106            pattern_mclk @ 96..=165 => 10 + (pattern_mclk - 96) / 10,
2107            _ => unreachable!("value % 166 is always < 166"),
2108        };
2109
2110        return if hsync_mclk < 166 {
2111            // First repetition
2112            (0x1CC - JUMP_DIFF + pattern_pixel) as u16
2113        } else {
2114            // Second repetition
2115            (0x1CC - JUMP_DIFF + 17 + pattern_pixel) as u16
2116        };
2117    }
2118
2119    // From H=0x1EE to H=0x1FF, stays at mclk/8
2120    let post_hsync_mclk = scanline_mclk - hsync_end_mclk;
2121    (0x1CC - JUMP_DIFF + 34 + post_hsync_mclk / 8) as u16
2122}
2123
2124fn pixel_to_internal_h_h40(pixel: u16) -> u16 {
2125    if pixel <= 0x16C { pixel } else { pixel + (0x1C9 - 0x16D) }
2126}
2127
2128fn internal_h_to_pixel_h40(internal_h: u16) -> u16 {
2129    if internal_h <= 0x16C { internal_h } else { internal_h - (0x1C9 - 0x16D) }
2130}