vdc.rsannotatedvdc.rssource1360 lines · 47.8 KB · raw

HuC6270 VDC (video display controller)

3mod debug;
4mod registers;
6use crate::api::PceEmulatorConfig;
7use crate::video::MCLK_CYCLES_PER_SCANLINE;
8use crate::video::vce::{DotClockDivider, Vce};
9use crate::video::vdc::registers::{SpriteAccessWidth, VramAccessWidth};
10use bincode::{Decode, Encode};
11use jgenesis_common::boxedarray::{Boxed2DWordArray, BoxedWordArray};
12use jgenesis_common::define_bit_enum;
13use jgenesis_common::num::GetBit;
14use registers::VdcRegisters;
15use std::ops::Range;
16use std::{array, cmp, hint, mem};
17
18pub const VRAM_LEN_WORDS: usize = 64 * 1024 / 2;
19pub const SPRITE_TABLE_LEN: usize = 64;
20
21pub const DOTS_PER_LINE_DIV_4: u64 = MCLK_CYCLES_PER_SCANLINE / 4;
22pub const DOTS_PER_LINE_DIV_3: u64 = MCLK_CYCLES_PER_SCANLINE / 3;
23pub const DOTS_PER_LINE_DIV_2: u64 = MCLK_CYCLES_PER_SCANLINE / 2;

Guesses, probably not accurate

26pub const OVERSCAN_DOTS_DIV_4: u16 = 13;
27pub const OVERSCAN_DOTS_DIV_3: u16 = 13 * 4 / 3; // ~17
28pub const OVERSCAN_DOTS_DIV_2: u16 = 13 * 4 / 2; // 26

Numbers derived from Mednafen's frame X offsets Dot clock divider 3 and 2 seem to have more left padding than just 11*ratio

32pub const LEFT_BORDER_DIV_4: u16 = 11;
33pub const LEFT_BORDER_DIV_3: u16 = 21;
34pub const LEFT_BORDER_DIV_2: u16 = 70;
36pub const STANDARD_WIDTH_DIV_4: u16 = 256;
37pub const STANDARD_WIDTH_DIV_3: u16 = 256 * 4 / 3; // ~341
38pub const STANDARD_WIDTH_DIV_2: u16 = 256 * 4 / 2; // 512
39
40pub const MAX_WIDTH_DIV_4: u16 = STANDARD_WIDTH_DIV_4 + 2 * OVERSCAN_DOTS_DIV_4;
41pub const MAX_WIDTH_DIV_3: u16 = STANDARD_WIDTH_DIV_3 + 2 * OVERSCAN_DOTS_DIV_3;
42pub const MAX_WIDTH_DIV_2: u16 = STANDARD_WIDTH_DIV_2 + 2 * OVERSCAN_DOTS_DIV_2;
43
44pub const LINE_BUFFER_LEN: usize = MAX_WIDTH_DIV_2 as usize;
45
46pub const ACTIVE_DISPLAY_DOTS_DIV_4: Range<u16> =
47    LEFT_BORDER_DIV_4..LEFT_BORDER_DIV_4 + STANDARD_WIDTH_DIV_4 + 2 * OVERSCAN_DOTS_DIV_4;
48pub const ACTIVE_DISPLAY_DOTS_DIV_3: Range<u16> =
49    LEFT_BORDER_DIV_3..LEFT_BORDER_DIV_3 + STANDARD_WIDTH_DIV_3 + 2 * OVERSCAN_DOTS_DIV_3;
50pub const ACTIVE_DISPLAY_DOTS_DIV_2: Range<u16> =
51    LEFT_BORDER_DIV_2..LEFT_BORDER_DIV_2 + STANDARD_WIDTH_DIV_2 + 2 * OVERSCAN_DOTS_DIV_2;

Raster compare counter always resets to 64 (0x40) at the beginning of HBlank before the first line of active display

54pub const RASTER_COMPARE_DISPLAY_START: u16 = 64;

Raster compare IRQ seems to trigger a bit before the end of active display

57pub const RASTER_COMPARE_INCREMENT_OFFSET: u16 = 8;

14 lines of top blanking before active display, 4 lines of bottom blanking + 3 lines of VSYNC after

60pub const ACTIVE_DISPLAY_LINES: Range<u16> = 14..256;

Large enough to fit video output at H1365px, after removing overscan

63pub const FRAME_BUFFER_WIDTH: usize = (2 * MAX_WIDTH_DIV_2) as usize;

There are always 242 lines of active display, regardless of vertical display settings Some of these lines are usually overscan, where the VDC constantly outputs sprite color 0

66pub const FRAME_BUFFER_HEIGHT: usize = 242;
68pub const DMA_DOTS_PER_WORD: u8 = 4;
69
70pub const MAX_SPRITES_PER_LINE: usize = 16;
71
72impl DotClockDivider {
73    pub fn dots_per_line(self) -> u64 {
74        match self {
75            Self::Four => DOTS_PER_LINE_DIV_4,
76            Self::Three => DOTS_PER_LINE_DIV_3,
77            Self::Two => DOTS_PER_LINE_DIV_2,
78        }
79    }
80
81    pub fn overscan_dots(self) -> u16 {
82        match self {
83            Self::Four => OVERSCAN_DOTS_DIV_4,
84            Self::Three => OVERSCAN_DOTS_DIV_3,
85            Self::Two => OVERSCAN_DOTS_DIV_2,
86        }
87    }
88
89    pub fn standard_width_dots(self) -> u16 {
90        match self {
91            Self::Four => STANDARD_WIDTH_DIV_4,
92            Self::Three => STANDARD_WIDTH_DIV_3,
93            Self::Two => STANDARD_WIDTH_DIV_2,
94        }
95    }
96
97    pub fn max_width_dots(self) -> u16 {
98        match self {
99            Self::Four => MAX_WIDTH_DIV_4,
100            Self::Three => MAX_WIDTH_DIV_3,
101            Self::Two => MAX_WIDTH_DIV_2,
102        }
103    }
104
105    pub fn active_display_dots(self) -> Range<u16> {
106        match self {
107            Self::Four => ACTIVE_DISPLAY_DOTS_DIV_4,
108            Self::Three => ACTIVE_DISPLAY_DOTS_DIV_3,
109            Self::Two => ACTIVE_DISPLAY_DOTS_DIV_2,
110        }
111    }
112}
113
114#[derive(Debug, Clone, Encode, Decode)]
115pub struct VdcFrameBuffer {
116    // Contains GRB333 VCE colors
117    pub colors: Boxed2DWordArray<FRAME_BUFFER_HEIGHT, FRAME_BUFFER_WIDTH>,
118    pub line_dividers: Box<[DotClockDivider; FRAME_BUFFER_HEIGHT]>,
119}
120
121impl VdcFrameBuffer {
122    fn new() -> Self {
123        Self {
124            colors: Boxed2DWordArray::new(),
125            line_dividers: Box::new(array::from_fn(|_| DotClockDivider::default())),
126        }
127    }
128}
129
130define_bit_enum!(CgMode, [ZeroOne, TwoThree]);

Single = 16px, Double = 32px

133define_bit_enum!(SpriteWidth, [Single, Double]);
135impl SpriteWidth {
136    // 16px tiles
137    pub fn to_sprite_tiles(self) -> u16 {
138        match self {
139            Self::Single => 1,
140            Self::Double => 2,
141        }
142    }
143
144    pub fn tile_number_mask(self) -> u16 {
145        match self {
146            Self::Single => !0,
147            Self::Double => !1,
148        }
149    }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
153pub enum SpriteHeight {
154    #[default]
155    Single, // 16px
156    Double, // 32px
157    Quad,   // 64px
158}
159
160impl SpriteHeight {
161    fn from_bits(bits: u16) -> Self {
162        match bits & 3 {
163            0 => Self::Single,
164            1 => Self::Double,
165            2 | 3 => Self::Quad,
166            _ => unreachable!("value & 3 is always <= 3"),
167        }
168    }
169
170    pub fn to_pixels(self) -> u16 {
171        match self {
172            Self::Single => 16,
173            Self::Double => 32,
174            Self::Quad => 64,
175        }
176    }
177
178    pub fn tile_number_mask(self) -> u16 {
179        match self {
180            Self::Single => !0,
181            Self::Double => !0b010,
182            Self::Quad => !0b110,
183        }
184    }
185}
186
187#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
188pub struct SpriteTableEntry {
189    pub x: u16,
190    pub y: u16,
191    pub tile_number: u16,
192    pub h_flip: bool,
193    pub v_flip: bool,
194    pub width: SpriteWidth,
195    pub height: SpriteHeight,
196    pub palette: u16,
197    pub priority: bool,
198    pub cg_mode: CgMode,
199}
200
201impl SpriteTableEntry {
202    pub fn write_word(&mut self, i: u16, word: u16) {
203        match i & 3 {
204            0 => self.write_first_word(word),
205            1 => self.write_second_word(word),
206            2 => self.write_third_word(word),
207            3 => self.write_fourth_word(word),
208            _ => unreachable!("value & 3 is always <= 3"),
209        }
210    }
211
212    pub fn write_first_word(&mut self, word: u16) {
213        self.y = word & 0x3FF;
214    }
215
216    pub fn write_second_word(&mut self, word: u16) {
217        self.x = word & 0x3FF;
218    }
219
220    pub fn write_third_word(&mut self, word: u16) {
221        self.cg_mode = CgMode::from_bit(word.bit(0));
222        self.tile_number = (word >> 1) & 0x3FF;
223    }
224
225    pub fn write_fourth_word(&mut self, word: u16) {
226        self.palette = word & 0xF;
227        self.priority = word.bit(7);
228        self.width = SpriteWidth::from_bit(word.bit(8));
229        self.h_flip = word.bit(11);
230        self.height = SpriteHeight::from_bits(word >> 12);
231        self.v_flip = word.bit(15);
232    }
233}
234
235#[derive(Debug, Clone, Encode, Decode)]
236pub struct EvaluatedSpriteEntry {
237    pub sprite_idx: u8,
238    pub x: u16,
239    pub tile_number: u16,
240    pub tile_row: u16,
241    pub h_flip: bool,
242    pub priority: bool,
243    pub palette: u16,
244    pub cg_mode: CgMode,
245}
246
247#[derive(Debug, Clone, Copy)]
248pub struct BgTileRow {
249    pub cg0: u16,
250    pub cg1: u16,
251    pub palette: u16,
252}
253
254#[derive(Debug, Clone, Copy, Encode, Decode)]
255pub struct SpritePixel {
256    pub sprite_idx: u8,
257    pub priority: bool,
258    pub palette: u16,
259    pub color_idx: u16,
260}
261
262impl SpritePixel {
263    pub const TRANSPARENT: Self = Self { sprite_idx: 0, priority: false, palette: 0, color_idx: 0 };
264
265    pub fn transparent(self) -> bool {
266        self.color_idx == 0
267    }
268}
269
270define_bit_enum!(DmaStep, [Increment, Decrement]);
271
272impl DmaStep {
273    fn apply(self, address: &mut u16) {
274        match self {
275            Self::Increment => {
276                *address = address.wrapping_add(1);
277            }
278            Self::Decrement => {
279                *address = address.wrapping_sub(1);
280            }
281        }
282    }
283}
284
285#[derive(Debug, Clone, Copy, Encode, Decode)]
286pub struct LatchedVerticalState {
287    pub v_sync_width: u16,
288    pub v_display_start: u16,
289    pub v_display_width: u16,
290    pub v_display_end: u16,
291    // The VDC enters "burst mode" when both BG and sprites are disabled at start of frame
292    // Burst mode enables DMA and unlimited VRAM access throughout the entire frame, not only during VBlank
293    pub burst_mode: bool,
294}
295
296impl LatchedVerticalState {
297    fn latch(registers: &VdcRegisters) -> Self {
298        Self {
299            v_sync_width: registers.v_sync_width,
300            v_display_start: registers.v_display_start,
301            v_display_width: registers.v_display_width,
302            v_display_end: registers.v_display_end,
303            burst_mode: !registers.bg_enabled && !registers.sprites_enabled,
304        }
305    }
306}
307
308#[derive(Debug, Clone, Copy, Encode, Decode)]
309pub struct LatchedHorizontalState {
310    pub h_sync_width: u16,
311    pub h_display_start: u16,
312    pub h_display_width: u16,
313    pub h_display_end: u16,
314    pub bg_x_scroll: u16,
315    pub vram_access_width: VramAccessWidth,
316    pub sprite_access_width: SpriteAccessWidth,
317}
318
319impl LatchedHorizontalState {
320    fn latch(registers: &VdcRegisters) -> Self {
321        Self {
322            h_sync_width: registers.h_sync_width,
323            h_display_start: registers.h_display_start,
324            h_display_width: registers.h_display_width,
325            h_display_end: registers.h_display_end,
326            bg_x_scroll: registers.bg_x_scroll,
327            vram_access_width: registers.vram_access_width,
328            sprite_access_width: registers.sprite_access_width,
329        }
330    }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
334pub enum VerticalMode {
335    TopBorder,
336    ActiveDisplay,
337    BottomBorder,
338    VSync,
339}
340
341impl VerticalMode {
342    fn length(self, latch: LatchedVerticalState) -> u16 {
343        match self {
344            Self::TopBorder => latch.v_display_start,
345            Self::ActiveDisplay => latch.v_display_width,
346            Self::BottomBorder => latch.v_display_end,
347            Self::VSync => latch.v_sync_width,
348        }
349    }
350
351    fn next(self) -> Self {
352        match self {
353            Self::TopBorder => Self::ActiveDisplay,
354            Self::ActiveDisplay => Self::BottomBorder,
355            Self::BottomBorder => Self::VSync,
356            Self::VSync => Self::TopBorder,
357        }
358    }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
362pub enum HorizontalMode {
363    LeftBorder,
364    ActiveDisplay,
365    RightBorder,
366    HSync,
367}
368
369impl HorizontalMode {
370    fn length(self, latch: LatchedHorizontalState) -> u16 {
371        match self {
372            Self::LeftBorder => latch.h_display_start,
373            Self::ActiveDisplay => latch.h_display_width,
374            Self::RightBorder => latch.h_display_end,
375            Self::HSync => latch.h_sync_width,
376        }
377    }
378
379    fn next(self) -> Self {
380        match self {
381            Self::LeftBorder => Self::ActiveDisplay,
382            Self::ActiveDisplay => Self::RightBorder,
383            Self::RightBorder => Self::HSync,
384            Self::HSync => Self::LeftBorder,
385        }
386    }
387}
388
389#[derive(Debug, Clone, Encode, Decode)]
390pub struct DmaState {
391    pub vram_triggered: bool,
392    pub vram_active: bool,
393    pub sat_triggered: bool,
394    pub sat_active: bool,
395    pub sat_address: u16,
396    pub dots_till_next_word: u8,
397}
398
399impl DmaState {
400    fn new() -> Self {
401        Self {
402            vram_triggered: false,
403            vram_active: false,
404            sat_triggered: false,
405            sat_active: false,
406            sat_address: 0,
407            dots_till_next_word: DMA_DOTS_PER_WORD,
408        }
409    }
410
411    fn start_vram(&mut self) {
412        self.vram_active = true;
413
414        // Don't interrupt an in-progress VRAM-to-SAT DMA read
415        if !self.sat_active {
416            self.dots_till_next_word = DMA_DOTS_PER_WORD;
417        }
418    }
419
420    fn start_sat(&mut self) {
421        self.sat_active = true;
422        self.sat_address = 0;
423        self.dots_till_next_word = DMA_DOTS_PER_WORD;
424    }
425
426    fn halt(&mut self) {
427        self.vram_active = false;
428        self.sat_active = false;
429    }
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum VdcIrq {
434    VBlank,
435    RasterCompare,
436    SpriteOverflow,
437    SpriteCollision,
438    VramDma,
439    SatDma,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
443pub enum PendingCpuAccess {
444    Read { address: u16 },
445    Write { address: u16, value: u16 },
446}
447
448#[derive(Debug, Clone, Encode, Decode)]
449pub struct VdcState {
450    pub scanline: u16,
451    pub scanline_dot: u16,
452    pub h_latch: LatchedHorizontalState,
453    pub v_latch: LatchedVerticalState,
454    pub h_mode: HorizontalMode,
455    pub v_mode: VerticalMode,
456    pub h_counter: u16,
457    pub h_mode_start_dot: u16,
458    pub v_counter: u16,
459    pub v_mode_start_line: u16,
460    pub bg_y_counter: u16,
461    pub bg_y_scroll_written: bool,
462    pub dma: DmaState,
463    // Dot clock divider is not _really_ latched per line, but pretending that it is
464    // simplifies a lot of things
465    pub line_divider: DotClockDivider,
466    pub vblank_irq_pending: bool,
467    pub raster_compare_irq_pending: bool,
468    pub sprite_overflow_irq_pending: bool,
469    pub sprite_collision_irq_pending: bool,
470    pub vram_dma_irq_pending: bool,
471    pub sat_dma_irq_pending: bool,
472    pub any_irq_pending: bool,
473    pub vblank_irq_this_frame: bool,
474    // If true, generate sprite overflow IRQ the next time active display begins
475    pub sprite_overflow_irq_at_display: bool,
476    // If Some, generate sprite collision IRQ at this dot
477    pub sprite_collision_irq_dot: Option<u16>,
478    pub raster_compare_counter: u16,
479    pub frame_complete: bool,
480    pub pending_cpu_access: Option<PendingCpuAccess>,
481    pub sprite_fetch_dots_this_line: u64,
482}
483
484impl VdcState {
485    fn new(registers: &VdcRegisters) -> Self {
486        Self {
487            scanline: 0,
488            scanline_dot: 0,
489            h_latch: LatchedHorizontalState::latch(registers),
490            v_latch: LatchedVerticalState::latch(registers),
491            h_mode: HorizontalMode::LeftBorder,
492            v_mode: VerticalMode::TopBorder,
493            h_counter: 0,
494            h_mode_start_dot: 0,
495            v_counter: 0,
496            v_mode_start_line: 0,
497            bg_y_counter: 0,
498            bg_y_scroll_written: false,
499            dma: DmaState::new(),
500            line_divider: DotClockDivider::default(),
501            vblank_irq_pending: false,
502            raster_compare_irq_pending: false,
503            sprite_overflow_irq_pending: false,
504            sprite_collision_irq_pending: false,
505            vram_dma_irq_pending: false,
506            sat_dma_irq_pending: false,
507            any_irq_pending: false,
508            vblank_irq_this_frame: false,
509            sprite_overflow_irq_at_display: false,
510            sprite_collision_irq_dot: None,
511            raster_compare_counter: RASTER_COMPARE_DISPLAY_START,
512            frame_complete: false,
513            pending_cpu_access: None,
514            sprite_fetch_dots_this_line: 0,
515        }
516    }
517
518    fn can_start_vram_dma(&self) -> bool {
519        if self.v_latch.burst_mode {
520            // Can always run during burst mode
521            return true;
522        }
523
524        if self.v_mode == VerticalMode::ActiveDisplay {
525            // Can never run during active display
526            return false;
527        }
528
529        if self.v_mode == VerticalMode::TopBorder
530            && self.v_counter == self.v_latch.v_display_start - 1
531            && matches!(self.h_mode, HorizontalMode::RightBorder | HorizontalMode::HSync)
532        {
533            // Can't run during HBlank on the last line before active display
534            return false;
535        }
536
537        true
538    }
539}
540
541#[derive(Debug, Clone, Encode, Decode)]
542pub struct Vdc {
543    vram: BoxedWordArray<VRAM_LEN_WORDS>,
544    sprite_table: Box<[SpriteTableEntry; SPRITE_TABLE_LEN]>,
545    registers: VdcRegisters,
546    state: VdcState,
547    sprite_evaluation_buffer: Vec<EvaluatedSpriteEntry>,
548    sprite_line_buffer: Box<[SpritePixel; LINE_BUFFER_LEN]>,
549    enforce_sprite_limits: bool,
550    frame_buffer: VdcFrameBuffer,
551    // Contains 9-bit color indices (0-255 for BG colors, 256-511 for sprite colors)
552    line_buffer: Box<[u16; LINE_BUFFER_LEN]>,
553    selected_register: u8,
554}
555
556impl Vdc {
557    pub fn new(config: PceEmulatorConfig) -> Self {
558        let registers = VdcRegisters::new();
559        let state = VdcState::new(&registers);
560
561        Self {
562            vram: BoxedWordArray::new_random(),
563            sprite_table: vec![SpriteTableEntry::default(); SPRITE_TABLE_LEN]
564                .into_boxed_slice()
565                .try_into()
566                .unwrap(),
567            registers,
568            state,
569            sprite_evaluation_buffer: Vec::with_capacity(MAX_SPRITES_PER_LINE),
570            sprite_line_buffer: vec![SpritePixel::TRANSPARENT; LINE_BUFFER_LEN]
571                .into_boxed_slice()
572                .try_into()
573                .unwrap(),
574            enforce_sprite_limits: !config.remove_sprite_limits,
575            frame_buffer: VdcFrameBuffer::new(),
576            line_buffer: Box::new(array::from_fn(|_| 0)),
577            selected_register: 0x1F,
578        }
579    }
580
581    pub fn tick_dots(&mut self, dots: u64, vce: &Vce) {
582        let active_display_dots = self.state.line_divider.active_display_dots();
583
584        let line_divider = self.state.line_divider as u16;
585
586        let active_line = ACTIVE_DISPLAY_LINES.contains(&self.state.scanline);
587        let frame_buffer_row = self.state.scanline.wrapping_sub(ACTIVE_DISPLAY_LINES.start);
588        debug_assert!(!active_line || (frame_buffer_row as usize) < self.frame_buffer.colors.len());
589
590        let mut h_mode_length = self.state.h_mode.length(self.state.h_latch);
591
592        let burst_mode = self.state.v_latch.burst_mode;
593
594        // TODO this is very inefficient
595        for _ in 0..dots {
596            // DMA always takes priority over CPU VRAM access
597            // SAT DMA probably takes priority over VRAM copy DMA?
598            if self.state.dma.sat_active {
599                self.progress_sat_dma();
600            } else if self.state.dma.vram_active {
601                self.progress_vram_dma();
602            } else if self.state.pending_cpu_access.is_some() {
603                self.progress_cpu_access();
604            }
605
606            // Render color to frame buffer if inside VCE active display
607            if active_line && active_display_dots.contains(&self.state.scanline_dot) {
608                let color = if !burst_mode
609                    && self.state.v_mode == VerticalMode::ActiveDisplay
610                    && self.state.h_mode == HorizontalMode::ActiveDisplay
611                {
612                    let line_buffer_idx = self.state.scanline_dot - self.state.h_mode_start_dot;
613                    vce.read_color(self.line_buffer[line_buffer_idx as usize])
614                } else {
615                    // Always render overscan color in burst mode and outside of VDC active display
616                    vce.overscan_color()
617                };
618
619                let frame_buffer_col =
620                    line_divider * (self.state.scanline_dot - active_display_dots.start);
621                for i in 0..line_divider {
622                    self.frame_buffer.colors[frame_buffer_row as usize]
623                        [(frame_buffer_col + i) as usize] = color;
624                }
625            }
626
627            // Increment raster compare counter shortly before the end of horizontal display
628            // (Timing is probably not accurate)
629            if self.state.h_mode == HorizontalMode::ActiveDisplay
630                && self.state.h_counter
631                    == self
632                        .state
633                        .h_latch
634                        .h_display_width
635                        .wrapping_sub(RASTER_COMPARE_INCREMENT_OFFSET)
636            {
637                self.increment_raster_compare_counter();
638
639                if self.state.sprite_overflow_irq_at_display {
640                    self.set_irq(VdcIrq::SpriteOverflow);
641                    self.state.sprite_overflow_irq_at_display = false;
642                }
643            }
644
645            self.state.scanline_dot += 1;
646            if self.state.sprite_collision_irq_dot.is_some_and(|dot| dot == self.state.scanline_dot)
647            {
648                self.set_irq(VdcIrq::SpriteCollision);
649                self.state.sprite_collision_irq_dot = None;
650            }
651
652            self.state.h_counter += 1;
653            if self.state.h_counter >= h_mode_length {
654                self.state.h_counter = 0;
655                self.state.h_mode = self.state.h_mode.next();
656                self.state.h_mode_start_dot = self.state.scanline_dot;
657
658                h_mode_length = self.state.h_mode.length(self.state.h_latch);
659
660                let is_sprite_line = self.state.v_mode == VerticalMode::ActiveDisplay
661                    || (self.state.v_mode == VerticalMode::TopBorder
662                        && self.state.v_counter == self.state.v_latch.v_display_start - 1);
663
664                match self.state.h_mode {
665                    HorizontalMode::ActiveDisplay => {
666                        if self.state.v_mode == VerticalMode::ActiveDisplay {
667                            self.render_line();
668                        }
669
670                        if is_sprite_line && !burst_mode {
671                            self.run_sprite_evaluation();
672                        }
673                    }
674                    HorizontalMode::RightBorder => {
675                        if self.state.v_mode == VerticalMode::TopBorder
676                            && self.state.v_counter == self.state.v_latch.v_display_start - 1
677                        {
678                            // DMAs cannot run once sprite tile fetching for the first line of active display begins
679                            if !burst_mode {
680                                self.state.dma.halt();
681                            }
682                        }
683
684                        if is_sprite_line && !burst_mode {
685                            self.fetch_sprite_tiles();
686                        }
687                    }
688                    _ => {}
689                }
690            }
691        }
692    }
693
694    fn increment_raster_compare_counter(&mut self) {
695        if self.state.v_mode == VerticalMode::TopBorder
696            && self.state.v_counter == self.state.v_latch.v_display_start - 1
697        {
698            self.state.raster_compare_counter = RASTER_COMPARE_DISPLAY_START;
699        } else {
700            self.state.raster_compare_counter += 1;
701        }
702
703        if self.state.raster_compare_counter == self.registers.raster_compare {
704            self.set_irq(VdcIrq::RasterCompare);
705        }
706    }
707
708    pub fn start_new_line(&mut self, scanline: u16, vce: &Vce) {
709        if self.state.h_mode == HorizontalMode::ActiveDisplay {
710            if self.state.h_counter
711                < self.state.h_latch.h_display_width.saturating_sub(RASTER_COMPARE_INCREMENT_OFFSET)
712            {
713                // If active display began but the raster compare increment didn't happen, do it at the
714                // line change
715                //
716                // D&D: Order of the Griffon depends on this else there will be a glitchy line under
717                // the character portraits; it depends on the increment happening twice in one line
718                // when it changes the dot clock divider from 4 to 3
719                self.increment_raster_compare_counter();
720            }
721
722            if ACTIVE_DISPLAY_LINES.contains(&self.state.scanline) {
723                let active_display_dots = self.state.line_divider.active_display_dots();
724                if active_display_dots.contains(&self.state.scanline_dot) {
725                    // Fill remainder of frame buffer row with overscan color
726                    // Prevents some visual glitches in D&D: Order of the Griffon due to mid-frame
727                    // dot clock divider changes
728                    let frame_buffer_row =
729                        (self.state.scanline - ACTIVE_DISPLAY_LINES.start) as usize;
730                    let frame_buffer_col = (self.state.scanline_dot - active_display_dots.start)
731                        as usize
732                        * self.state.line_divider as usize;
733                    self.frame_buffer.colors[frame_buffer_row][frame_buffer_col..]
734                        .fill(vce.overscan_color());
735                }
736            }
737        }
738
739        let dot_clock_divider = vce.dot_clock_divider();
740        let lines_per_frame = vce.lines_per_frame();
741
742        // TODO latch timing is probably not accurate for everything in here
743        self.state.h_latch = LatchedHorizontalState::latch(&self.registers);
744        self.state.h_mode = HorizontalMode::LeftBorder;
745        self.state.h_counter = 0;
746        self.state.h_mode_start_dot = 0;
747
748        self.state.line_divider = dot_clock_divider;
749
750        self.state.scanline = scanline;
751        self.state.scanline_dot = 0;
752
753        if self.state.bg_y_scroll_written {
754            self.state.bg_y_counter = self.registers.bg_y_scroll;
755            self.state.bg_y_scroll_written = false;
756        }
757        self.state.bg_y_counter = self.state.bg_y_counter.wrapping_add(1);
758
759        if ACTIVE_DISPLAY_LINES.contains(&scanline) {
760            let frame_buffer_row = scanline - ACTIVE_DISPLAY_LINES.start;
761            self.frame_buffer.line_dividers[frame_buffer_row as usize] = dot_clock_divider;
762        }
763
764        self.state.frame_complete |= scanline == ACTIVE_DISPLAY_LINES.end;
765
766        if scanline != 0 {
767            self.state.v_counter += 1;
768            if self.state.v_counter >= self.state.v_mode.length(self.state.v_latch) {
769                self.state.v_counter = 0;
770                self.state.v_mode = self.state.v_mode.next();
771                self.state.v_mode_start_line = self.state.scanline;
772
773                match self.state.v_mode {
774                    VerticalMode::ActiveDisplay => {
775                        self.state.bg_y_counter = self.registers.bg_y_scroll;
776
777                        if !self.state.v_latch.burst_mode {
778                            // DMAs cannot run during active display when not in burst mode
779                            self.state.dma.halt();
780                        }
781                    }
782                    VerticalMode::BottomBorder => {
783                        if self.state.dma.sat_triggered || self.registers.sat_dma_repeat {
784                            self.state.dma.start_sat();
785                            self.state.dma.sat_triggered = false;
786
787                            log::trace!("Starting VRAM-to-SAT DMA on line {scanline}");
788                        }
789
790                        if self.state.dma.vram_triggered {
791                            self.state.dma.start_vram();
792
793                            log::trace!("Starting VRAM-to-VRAM DMA on line {scanline}");
794                        }
795
796                        self.set_irq(VdcIrq::VBlank);
797
798                        self.state.vblank_irq_this_frame = true;
799
800                        self.state.sprite_collision_irq_dot = None;
801                    }
802                    _ => {}
803                }
804            }
805        }
806
807        if !self.state.vblank_irq_this_frame && scanline == lines_per_frame - 2 {
808            // VDC supposedly always generates a VBlank IRQ when the VCE asserts VSYNC if it didn't
809            // already generate one earlier in the frame
810            self.set_irq(VdcIrq::VBlank);
811        }
812    }
813
814    pub fn start_new_frame(&mut self) {
815        self.state.v_latch = LatchedVerticalState::latch(&self.registers);
816        self.state.v_mode = VerticalMode::TopBorder;
817        self.state.v_counter = 0;
818        self.state.v_mode_start_line = 0;
819        self.state.vblank_irq_this_frame = false;
820    }
821
822    pub fn frame_complete(&self) -> bool {
823        self.state.frame_complete
824    }
825
826    pub fn clear_frame_complete(&mut self) {
827        self.state.frame_complete = false;
828    }
829
830    pub fn frame_buffer(&self) -> &VdcFrameBuffer {
831        &self.frame_buffer
832    }
833
834    pub fn irq(&self) -> bool {
835        self.state.any_irq_pending
836    }
837
838    pub fn is_cpu_read_blocked(&self) -> bool {
839        // VRR (VRAM read register)
840        // Block if any CPU access is in progress
841        self.state.pending_cpu_access.is_some()
842    }
843
844    #[allow(clippy::match_same_arms)]
845    pub fn is_cpu_write_blocked(&self) -> bool {
846        match self.selected_register {
847            // MAWR (Memory address write register)
848            // Block if a CPU write is in progress
849            0x00 => matches!(self.state.pending_cpu_access, Some(PendingCpuAccess::Write { .. })),
850            // MARR (Memory address read register)
851            // Block if any CPU access is in progress (since MSB write can initiate a read)
852            0x01 => self.state.pending_cpu_access.is_some(),
853            // VWR (VRAM write register)
854            // Block if any CPU access is in progress
855            0x02 => self.state.pending_cpu_access.is_some(),
856            _ => false,
857        }
858    }
859
860    pub fn reload_config(&mut self, config: PceEmulatorConfig) {
861        self.enforce_sprite_limits = !config.remove_sprite_limits;
862    }
863
864    fn render_line(&mut self) {
865        const BACKDROP_COLOR: u16 = 0x000;
866
867        self.line_buffer.fill(BACKDROP_COLOR);
868
869        if self.state.v_latch.burst_mode
870            || (!self.registers.bg_enabled && !self.registers.sprites_enabled)
871        {
872            return;
873        }
874
875        let line_width_dots =
876            cmp::min(self.state.line_divider.max_width_dots(), self.state.h_latch.h_display_width);
877
878        let screen_width_tiles = self.registers.virtual_screen_width.to_tiles();
879        let screen_height_tiles = self.registers.virtual_screen_height.to_tiles();
880
881        let bg_x_scroll = self.state.h_latch.bg_x_scroll;
882        let bg_y_counter = self.state.bg_y_counter;
883
884        let mut bg_tile_x = (bg_x_scroll / 8) & (screen_width_tiles - 1);
885        let bg_tile_y = (bg_y_counter / 8) & (screen_height_tiles - 1);
886
887        let tile_row = (bg_y_counter & 7) as usize;
888
889        let start_x = -i32::from(bg_x_scroll & 7);
890        let end_x = i32::from(line_width_dots);
891        for x in (start_x..end_x).step_by(8) {
892            // BAT (BG attribute table) always starts at $0000 in VRAM and has 1 word per tile
893            let bat_addr = bg_tile_y * screen_width_tiles + bg_tile_x;
894
895            let BgTileRow { cg0, cg1, palette: bg_palette } = if self.registers.bg_enabled {
896                self.read_bg_tile_row(bat_addr, tile_row)
897            } else {
898                BgTileRow { cg0: 0, cg1: 0, palette: 0 }
899            };
900
901            for tile_col in 0..8 {
902                let pixel = x + tile_col;
903                if !(0..end_x).contains(&pixel) {
904                    continue;
905                }
906
907                let bitplane_shift = 7 - tile_col;
908                let bg_color_idx = ((cg0 >> bitplane_shift) & 1)
909                    | (((cg0 >> (8 + bitplane_shift)) & 1) << 1)
910                    | (((cg1 >> bitplane_shift) & 1) << 2)
911                    | (((cg1 >> (8 + bitplane_shift)) & 1) << 3);
912
913                let sprite_pixel = if self.registers.sprites_enabled {
914                    self.sprite_line_buffer[pixel as usize]
915                } else {
916                    SpritePixel::TRANSPARENT
917                };
918
919                let rendered_color = if !sprite_pixel.transparent()
920                    && (sprite_pixel.priority || bg_color_idx == 0)
921                {
922                    0x100 | (sprite_pixel.palette << 4) | sprite_pixel.color_idx
923                } else if bg_color_idx != 0 {
924                    (bg_palette << 4) | bg_color_idx
925                } else {
926                    BACKDROP_COLOR
927                };
928
929                self.line_buffer[pixel as usize] = rendered_color;
930            }
931
932            bg_tile_x = (bg_tile_x + 1) & (screen_width_tiles - 1);
933        }
934    }
935
936    fn read_bg_tile_row(&self, bat_addr: u16, tile_row: usize) -> BgTileRow {
937        let bg_attributes = self.vram[bat_addr as usize];
938        let tile_number = bg_attributes & 0xFFF;
939        let palette = bg_attributes >> 12;
940
941        let tile_addr = (16 * tile_number) as usize;
942
943        let (mut cg0, mut cg1) = if tile_addr < VRAM_LEN_WORDS {
944            (self.vram[tile_addr + tile_row], self.vram[tile_addr + tile_row + 8])
945        } else {
946            // Tiles 2048-4095 are supposedly filled with "garbage"
947            (0xFFFF, 0xFFFF)
948        };
949
950        if self.state.h_latch.vram_access_width == VramAccessWidth::Four {
951            hint::cold_path();
952
953            // VDC only fetches half of the bitplanes in width 4
954            match self.registers.bg_cg_mode {
955                CgMode::ZeroOne => {
956                    cg1 = 0;
957                }
958                CgMode::TwoThree => {
959                    cg0 = mem::take(&mut cg1);
960                }
961            }
962        }
963
964        BgTileRow { cg0, cg1, palette }
965    }
966
967    fn progress_sat_dma(&mut self) {
968        self.state.dma.dots_till_next_word -= 1;
969        if self.state.dma.dots_till_next_word != 0 {
970            return;
971        }
972
973        self.state.dma.dots_till_next_word = DMA_DOTS_PER_WORD;
974
975        let sat_address = self.state.dma.sat_address;
976        let word = self.read_vram(self.registers.sat_dma_source_address.wrapping_add(sat_address));
977
978        let sprite_idx = sat_address / 4;
979        self.sprite_table[sprite_idx as usize].write_word(sat_address % 4, word);
980
981        self.state.dma.sat_address = sat_address.wrapping_add(1);
982        if self.state.dma.sat_address == (4 * SPRITE_TABLE_LEN) as u16 {
983            self.state.dma.sat_active = false;
984            self.set_irq(VdcIrq::SatDma);
985
986            log::trace!("Finished SAT DMA on line {}", self.state.scanline);
987        }
988    }
989
990    fn progress_vram_dma(&mut self) {
991        self.state.dma.dots_till_next_word -= 1;
992        if self.state.dma.dots_till_next_word != 0 {
993            return;
994        }
995
996        self.state.dma.dots_till_next_word = DMA_DOTS_PER_WORD;
997
998        let word = self.read_vram(self.registers.vram_dma_source_address);
999        self.registers.vram_dma_source_step.apply(&mut self.registers.vram_dma_source_address);
1000
1001        self.write_vram(self.registers.vram_dma_destination_address, word);
1002        self.registers
1003            .vram_dma_destination_step
1004            .apply(&mut self.registers.vram_dma_destination_address);
1005
1006        let overflowed;
1007        (self.registers.vram_dma_length, overflowed) =
1008            self.registers.vram_dma_length.overflowing_sub(1);
1009
1010        if overflowed {
1011            self.state.dma.vram_triggered = false;
1012            self.state.dma.vram_active = false;
1013            self.set_irq(VdcIrq::VramDma);
1014
1015            log::trace!("Finished VRAM DMA on line {}", self.state.scanline);
1016        }
1017    }
1018
1019    fn progress_cpu_access(&mut self) {
1020        if !self.can_perform_cpu_access() {
1021            return;
1022        }
1023
1024        match self.state.pending_cpu_access.take() {
1025            Some(PendingCpuAccess::Read { address }) => {
1026                self.registers.vram_read_buffer = self.read_vram(address);
1027            }
1028            Some(PendingCpuAccess::Write { address, value }) => {
1029                self.write_vram(address, value);
1030            }
1031            None => {}
1032        }
1033    }
1034
1035    fn can_perform_cpu_access(&self) -> bool {
1036        if self.state.dma.vram_active || self.state.dma.sat_active {
1037            // CPU cannot access VRAM during DMA
1038            return false;
1039        }
1040
1041        if self.state.v_latch.burst_mode {
1042            // CPU can always access during burst mode (when DMA is not running)
1043            return true;
1044        }
1045
1046        if self.state.v_mode == VerticalMode::ActiveDisplay
1047            && self.state.h_mode == HorizontalMode::ActiveDisplay
1048        {
1049            // BG tile fetching + sprite evaluation + rendering
1050            // CPU access slots depend on VRAM access width:
1051            // Width 1:  CPU BAT CPU ??? CPU CG0 CPU CG1 (8 slots, 4 accesses)
1052            // Width 2:    BAT     CPU     CG0     CG1   (4 slots, 1 access)
1053            // Width 4:        BAT           CG0/CG1     (2 slots, 0 accesses)
1054
1055            // TODO what happens if BG is disabled?
1056            if !self.registers.bg_enabled {
1057                return true;
1058            }
1059
1060            return match self.state.h_latch.vram_access_width {
1061                VramAccessWidth::One => self.state.h_counter & 1 == 0,
1062                VramAccessWidth::Two => self.state.h_counter & 7 == 2,
1063                VramAccessWidth::Four => false,
1064            };
1065        }
1066
1067        let is_fetching_sprite_tiles = match self.state.v_mode {
1068            VerticalMode::ActiveDisplay => self.state.h_mode != HorizontalMode::ActiveDisplay,
1069            VerticalMode::TopBorder => {
1070                matches!(self.state.h_mode, HorizontalMode::RightBorder | HorizontalMode::HSync)
1071                    && self.state.v_counter == self.state.v_latch.v_display_start - 1
1072            }
1073            VerticalMode::BottomBorder | VerticalMode::VSync => false,
1074        };
1075
1076        if self.registers.sprites_enabled && is_fetching_sprite_tiles {
1077            // CPU cannot access VRAM during sprite tile fetching, regardless of sprite access width
1078            // Check if tile fetching is done for this line
1079            // TODO this doesn't handle horizontal timings or dot clock divider changing between lines
1080            let sprite_dot: u64 = match self.state.h_mode {
1081                HorizontalMode::RightBorder => self.state.h_counter.into(),
1082                HorizontalMode::HSync => {
1083                    (self.state.h_latch.h_display_end + self.state.h_counter).into()
1084                }
1085                HorizontalMode::LeftBorder => {
1086                    let hds: u64 = self.state.h_latch.h_display_start.into();
1087                    let hdw: u64 = self.state.h_latch.h_display_width.into();
1088                    let sprite_dots_per_line = (MCLK_CYCLES_PER_SCANLINE
1089                        / u64::from(self.state.line_divider))
1090                    .saturating_sub(hdw);
1091                    sprite_dots_per_line.saturating_sub(hds) + u64::from(self.state.h_counter)
1092                }
1093                HorizontalMode::ActiveDisplay => unreachable!(
1094                    "is_fetching_sprite_tiles is never true when h_mode is ActiveDisplay"
1095                ),
1096            };
1097
1098            return sprite_dot >= self.state.sprite_fetch_dots_this_line;
1099        }
1100
1101        true
1102    }
1103
1104    fn run_sprite_evaluation(&mut self) {
1105        // In sprite coordinates, Y=64 is the first line of active display
1106        const SCREEN_TOP: u16 = 64;
1107
1108        // The official HuC6270 manual suggests that sprite evaluation runs for 1 tile longer than
1109        // active display width, and that eval takes 4 dots per sprite.
1110        // It also says that VRAM access width can affect sprite eval, but I don't think this makes
1111        // sense? Sprite eval doesn't need to access VRAM
1112        let sprites_evaluated_this_line = if self.enforce_sprite_limits {
1113            let sprite_eval_cycles = self.state.h_latch.h_display_width + 8;
1114
1115            cmp::min((sprite_eval_cycles / 4) as usize, self.sprite_table.len())
1116        } else {
1117            self.sprite_table.len()
1118        };
1119
1120        self.sprite_evaluation_buffer.clear();
1121
1122        let sprite_line = match self.state.v_mode {
1123            VerticalMode::ActiveDisplay => SCREEN_TOP + self.state.v_counter + 1,
1124            _ => SCREEN_TOP, // Last line of top border; evaluate for first line of active display
1125        };
1126
1127        for (sprite_idx, sprite) in
1128            self.sprite_table[..sprites_evaluated_this_line].iter().enumerate()
1129        {
1130            let sprite_height_pixels = sprite.height.to_pixels();
1131            let sprite_y_range = sprite.y..sprite.y + sprite_height_pixels;
1132            if !sprite_y_range.contains(&sprite_line) {
1133                continue;
1134            }
1135
1136            let mut sprite_row = sprite_line - sprite.y;
1137            if sprite.v_flip {
1138                sprite_row = sprite_height_pixels - 1 - sprite_row;
1139            }
1140
1141            let mut base_tile_number = sprite.tile_number
1142                & sprite.width.tile_number_mask()
1143                & sprite.height.tile_number_mask();
1144            base_tile_number += 2 * (sprite_row / 16);
1145            sprite_row %= 16;
1146
1147            let width_tiles = sprite.width.to_sprite_tiles();
1148            for i in 0..width_tiles {
1149                let x_tile = match sprite.width {
1150                    SpriteWidth::Single => 0,
1151                    SpriteWidth::Double => i ^ u16::from(sprite.h_flip),
1152                };
1153
1154                let x = sprite.x + 16 * i;
1155                let tile_number = base_tile_number + x_tile;
1156
1157                if self.sprite_evaluation_buffer.len() == MAX_SPRITES_PER_LINE {
1158                    self.state.sprite_overflow_irq_at_display = true;
1159                    if self.enforce_sprite_limits {
1160                        return;
1161                    }
1162                }
1163
1164                self.sprite_evaluation_buffer.push(EvaluatedSpriteEntry {
1165                    sprite_idx: sprite_idx as u8,
1166                    x,
1167                    tile_number,
1168                    tile_row: sprite_row,
1169                    h_flip: sprite.h_flip,
1170                    priority: sprite.priority,
1171                    palette: sprite.palette,
1172                    cg_mode: sprite.cg_mode,
1173                });
1174            }
1175        }
1176    }
1177
1178    fn fetch_sprite_tiles(&mut self) {
1179        // In sprite coordinates, X=32 is the leftmost column of active display
1180        const SCREEN_LEFT: u16 = 32;
1181
1182        self.sprite_line_buffer.fill(SpritePixel::TRANSPARENT);
1183        self.state.sprite_fetch_dots_this_line = 0;
1184
1185        if !self.registers.sprites_enabled {
1186            return;
1187        }
1188
1189        // TODO this is not quite right if the game changes HDS or the dot clock divider during the right border or HSync
1190        let sprite_fetch_cycles = {
1191            let dots_per_line = MCLK_CYCLES_PER_SCANLINE / u64::from(self.state.line_divider);
1192
1193            // Based on https://pcengine.proboards.com/thread/84/why-pce-games-horizontal-rare?page=2
1194            // Games that use sprite access width other than 1 (e.g. R-Type) have too many sprites
1195            // per line without the extra 16
1196            let hdw = self.state.h_latch.h_display_width;
1197            dots_per_line.saturating_sub((hdw + 16).into())
1198        };
1199
1200        let cycles_per_sprite = match self.state.h_latch.sprite_access_width {
1201            SpriteAccessWidth::One | SpriteAccessWidth::TwoHalfBpp => 4,
1202            SpriteAccessWidth::TwoFullBpp | SpriteAccessWidth::Four => 8,
1203        };
1204
1205        let num_sprite_tiles = cmp::min(
1206            (sprite_fetch_cycles / cycles_per_sprite) as usize,
1207            cmp::min(MAX_SPRITES_PER_LINE, self.sprite_evaluation_buffer.len()),
1208        );
1209
1210        self.state.sprite_fetch_dots_this_line = (num_sprite_tiles as u64) * cycles_per_sprite;
1211
1212        let half_bpp = self.state.h_latch.sprite_access_width.is_half_bpp();
1213
1214        for (i, sprite) in self.sprite_evaluation_buffer.iter().enumerate() {
1215            let within_sprite_limits = i < num_sprite_tiles;
1216            if self.enforce_sprite_limits && !within_sprite_limits {
1217                break;
1218            }
1219
1220            let tile_addr = (64 * sprite.tile_number) as usize;
1221            let tile_data = if tile_addr < VRAM_LEN_WORDS {
1222                &self.vram[tile_addr..tile_addr + 64]
1223            } else {
1224                // Tiles 512-1023 supposedly contain "garbage"
1225                &[0xFFFF; 64]
1226            };
1227
1228            let tile_row = sprite.tile_row as usize;
1229            let mut cg0 = tile_data[tile_row];
1230            let mut cg1 = tile_data[tile_row + 16];
1231            let mut cg2 = tile_data[tile_row + 32];
1232            let mut cg3 = tile_data[tile_row + 48];
1233
1234            if half_bpp {
1235                hint::cold_path();
1236
1237                match sprite.cg_mode {
1238                    CgMode::ZeroOne => {
1239                        cg2 = 0;
1240                        cg3 = 0;
1241                    }
1242                    CgMode::TwoThree => {
1243                        cg0 = mem::take(&mut cg2);
1244                        cg1 = mem::take(&mut cg3);
1245                    }
1246                }
1247            }
1248
1249            let mut color_indices: [u16; 16] = array::from_fn(|i| {
1250                ((cg0 >> (15 - i)) & 1)
1251                    | (((cg1 >> (15 - i)) & 1) << 1)
1252                    | (((cg2 >> (15 - i)) & 1) << 2)
1253                    | (((cg3 >> (15 - i)) & 1) << 3)
1254            });
1255            if sprite.h_flip {
1256                color_indices.reverse();
1257            }
1258
1259            for (i, color_idx) in color_indices.into_iter().enumerate() {
1260                if color_idx == 0 {
1261                    // Transparent
1262                    continue;
1263                }
1264
1265                let x = sprite.x + i as u16;
1266                if x.wrapping_sub(SCREEN_LEFT) >= LINE_BUFFER_LEN as u16 {
1267                    // Horizontally out of bounds
1268                    continue;
1269                }
1270
1271                let line_buffer_idx = (x - SCREEN_LEFT) as usize;
1272                if !self.sprite_line_buffer[line_buffer_idx].transparent() {
1273                    // Already an opaque sprite pixel in this position
1274
1275                    // Check for sprite 0 collision, only if sprite is within hardware sprite limits
1276                    if within_sprite_limits
1277                        && self.sprite_line_buffer[line_buffer_idx].sprite_idx == 0
1278                    {
1279                        // TODO timing probably not accurate
1280                        let collision_dot =
1281                            self.state.h_latch.h_display_start + line_buffer_idx as u16;
1282                        self.state.sprite_collision_irq_dot = Some(cmp::min(
1283                            collision_dot,
1284                            self.state.sprite_collision_irq_dot.unwrap_or(u16::MAX),
1285                        ));
1286                    }
1287                    continue;
1288                }
1289
1290                self.sprite_line_buffer[line_buffer_idx] = SpritePixel {
1291                    sprite_idx: sprite.sprite_idx,
1292                    priority: sprite.priority,
1293                    palette: sprite.palette,
1294                    color_idx,
1295                };
1296            }
1297        }
1298    }
1299
1300    fn read_vram(&self, address: u16) -> u16 {
1301        // Actual hardware usually returns "corrupted" data for out-of-bounds VRAM addresses
1302        self.vram.get(address as usize).copied().unwrap_or(0xFFFF)
1303    }
1304
1305    fn write_vram(&mut self, address: u16, value: u16) {
1306        let address = address as usize;
1307        if address < VRAM_LEN_WORDS {
1308            self.vram[address] = value;
1309            log::trace!("  VRAM WRITE: {address:04X} = {value:04X}");
1310        }
1311    }
1312
1313    fn increment_vram_read_address(&mut self) {
1314        self.registers.vram_read_address =
1315            self.registers.vram_read_address.wrapping_add(self.registers.vram_address_increment);
1316    }
1317
1318    fn increment_vram_write_address(&mut self) {
1319        self.registers.vram_write_address =
1320            self.registers.vram_write_address.wrapping_add(self.registers.vram_address_increment);
1321    }
1322
1323    fn set_irq(&mut self, irq: VdcIrq) {
1324        log::trace!(
1325            "Triggering IRQ {irq:?} (if enabled), line {} dot {}",
1326            self.state.scanline,
1327            self.state.scanline_dot
1328        );
1329
1330        match irq {
1331            VdcIrq::VBlank => {
1332                self.state.vblank_irq_pending |= self.registers.vblank_irq_enabled;
1333            }
1334            VdcIrq::RasterCompare => {
1335                self.state.raster_compare_irq_pending |= self.registers.raster_compare_irq_enabled;
1336            }
1337            VdcIrq::SpriteOverflow => {
1338                self.state.sprite_overflow_irq_pending |=
1339                    self.registers.sprite_overflow_irq_enabled;
1340            }
1341            VdcIrq::SpriteCollision => {
1342                self.state.sprite_collision_irq_pending |=
1343                    self.registers.sprite_collision_irq_enabled;
1344            }
1345            VdcIrq::VramDma => {
1346                self.state.vram_dma_irq_pending |= self.registers.vram_dma_irq_enabled;
1347            }
1348            VdcIrq::SatDma => {
1349                self.state.sat_dma_irq_pending |= self.registers.sat_dma_irq_enabled;
1350            }
1351        }
1352
1353        self.state.any_irq_pending = self.state.vblank_irq_pending
1354            || self.state.raster_compare_irq_pending
1355            || self.state.sprite_collision_irq_pending
1356            || self.state.sprite_overflow_irq_pending
1357            || self.state.vram_dma_irq_pending
1358            || self.state.sat_dma_irq_pending;
1359    }
1360}