vdp.rsannotatedvdp.rssource1476 lines · 47.9 KB · raw

Sega Master System / Game Gear VDP (video display processor)

The SMS and GG VDPs are nearly identical, with only a few minor differences:

  • SMS VDP renders 256x192 frames; GG VDP also renders 256x192 but only displays the center 160x144
  • SMS VDP has 32 bytes of CRAM and uses 6-bit RGB color; GG VDP has 32 words of CRAM and uses 12-bit RGB color
7mod debug;
8mod tms9918;
10use crate::{SmsGgEmulatorConfig, SmsGgHardware};
11use bincode::{Decode, Encode};
12use jgenesis_common::boxedarray::BoxedWordArray;
13use jgenesis_common::frontend::{Color, TimingMode};
14use jgenesis_common::num::{GetBit, U16Ext};
15use jgenesis_proc_macros::EnumDisplay;
16use std::array;
17use std::fmt::{Display, Formatter};
18use z80_emu::traits::InterruptLine;
19
20const VRAM_LEN: usize = 16 * 1024;
21const COLOR_RAM_LEN: usize = 64;
22
23pub const SCREEN_WIDTH: u16 = 256;
24pub const SCREEN_HEIGHT: u16 = 240;
25pub const FRAME_BUFFER_LEN: usize = SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize;

Data address is 14 bits

28const DATA_ADDRESS_MASK: u16 = 0x3FFF;
30pub const DOTS_PER_SCANLINE: u16 = 342;
31pub const MCLK_CYCLES_PER_SCANLINE: u16 = 10 * DOTS_PER_SCANLINE;
32
33pub const NTSC_SCANLINES_PER_FRAME: u16 = 262;
34pub const PAL_SCANLINES_PER_FRAME: u16 = 313;
35
36type Vram = [u8; VRAM_LEN];
37
38trait TimingModeExt {
39    fn scanlines_per_frame(self) -> u16;
40}
41
42impl TimingModeExt for TimingMode {
43    fn scanlines_per_frame(self) -> u16 {
44        match self {
45            Self::Ntsc => NTSC_SCANLINES_PER_FRAME,
46            Self::Pal => PAL_SCANLINES_PER_FRAME,
47        }
48    }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
52pub struct ViewportSize {
53    pub width: u16,
54    pub height: u16,
55    pub top: u16,
56    pub left: u16,
57    pub top_border_height: u16,
58    pub bottom_border_height: u16,
59    pub left_border_width: u16,
60}
61
62impl ViewportSize {
63    const NTSC_SMS: Self = Self {
64        width: 256,
65        height: 224,
66        top: 0,
67        left: 0,
68        top_border_height: 16,
69        bottom_border_height: 16,
70        left_border_width: 8,
71    };
72
73    const PAL_SMS: Self = Self {
74        width: 256,
75        height: 240,
76        top: 0,
77        left: 0,
78        top_border_height: 24,
79        bottom_border_height: 24,
80        left_border_width: 8,
81    };
82
83    const GAME_GEAR: Self = Self {
84        width: 160,
85        height: 144,
86        top: 24,
87        left: 48,
88        top_border_height: 0,
89        bottom_border_height: 0,
90        left_border_width: 0,
91    };
92
93    const GAME_GEAR_EXPANDED: Self = Self {
94        width: 256,
95        height: 192,
96        top: 0,
97        left: 0,
98        top_border_height: 0,
99        bottom_border_height: 0,
100        left_border_width: 8,
101    };
102
103    pub fn height_without_border(self) -> u16 {
104        self.height - self.top_border_height - self.bottom_border_height
105    }
106
107    pub fn width_without_border(self) -> u16 {
108        self.width - self.left_border_width
109    }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, EnumDisplay)]
113pub enum VdpVersion {
114    NtscMasterSystem1,
115    PalMasterSystem1,
116    #[default]
117    NtscMasterSystem2,
118    PalMasterSystem2,
119    GameGear,
120    NtscSg1000,
121    PalSg1000,
122}
123
124impl VdpVersion {
125    #[must_use]
126    pub fn hardware(self) -> SmsGgHardware {
127        match self {
128            Self::NtscSg1000 | Self::PalSg1000 => SmsGgHardware::Sg1000,
129            Self::NtscMasterSystem1
130            | Self::NtscMasterSystem2
131            | Self::PalMasterSystem1
132            | Self::PalMasterSystem2 => SmsGgHardware::MasterSystem,
133            Self::GameGear => SmsGgHardware::GameGear,
134        }
135    }
136
137    fn is_sms1(self) -> bool {
138        matches!(self, Self::NtscMasterSystem1 | Self::PalMasterSystem1)
139    }
140
141    #[must_use]
142    pub fn timing_mode(self) -> TimingMode {
143        match self {
144            Self::NtscMasterSystem1
145            | Self::NtscMasterSystem2
146            | Self::GameGear
147            | Self::NtscSg1000 => TimingMode::Ntsc,
148            Self::PalMasterSystem1 | Self::PalMasterSystem2 | Self::PalSg1000 => TimingMode::Pal,
149        }
150    }
151
152    #[must_use]
153    const fn viewport_size(self, gg_use_sms_resolution: bool, mode: Mode) -> ViewportSize {
154        let mut viewport = match self {
155            Self::NtscMasterSystem1 | Self::NtscMasterSystem2 | Self::NtscSg1000 => {
156                ViewportSize::NTSC_SMS
157            }
158            Self::PalMasterSystem1 | Self::PalMasterSystem2 | Self::PalSg1000 => {
159                ViewportSize::PAL_SMS
160            }
161            Self::GameGear => {
162                if gg_use_sms_resolution {
163                    ViewportSize::GAME_GEAR_EXPANDED
164                } else {
165                    ViewportSize::GAME_GEAR
166                }
167            }
168        };
169
170        if matches!(mode, Mode::Four224Line) {
171            match self {
172                Self::NtscSg1000 | Self::PalSg1000 => {}
173                Self::NtscMasterSystem1
174                | Self::NtscMasterSystem2
175                | Self::PalMasterSystem1
176                | Self::PalMasterSystem2 => {
177                    viewport.top_border_height -= 16;
178                    viewport.bottom_border_height -= 16;
179                }
180                Self::GameGear => {
181                    if !gg_use_sms_resolution {
182                        viewport.top += 16;
183                    }
184                }
185            }
186        }
187
188        viewport
189    }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
193enum ControlWriteFlag {
194    First,
195    Second,
196}
197
198impl ControlWriteFlag {
199    fn toggle(self) -> Self {
200        match self {
201            Self::First => Self::Second,
202            Self::Second => Self::First,
203        }
204    }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
208enum DataWriteLocation {
209    Vram,
210    Cram,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
214enum Mode {
215    #[default]
216    Four,
217    Four224Line,
218    Text,
219    GraphicsI,
220    GraphicsII,
221    Multicolor,
222}
223
224impl Display for Mode {
225    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
226        match self {
227            Self::Four => write!(f, "4"),
228            Self::Four224Line => write!(f, "4 (224-line)"),
229            Self::Text => write!(f, "Text"),
230            Self::GraphicsI => write!(f, "Graphics I"),
231            Self::GraphicsII => write!(f, "Graphics II"),
232            Self::Multicolor => write!(f, "Multicolor"),
233        }
234    }
235}
236
237impl Mode {
238    fn from_mode_bits(hardware: SmsGgHardware, mode_bits: [bool; 4]) -> Self {
239        let mode = match mode_bits {
240            [false, _, false, true] | [false, false, true, true] | [true, true, true, true] => {
241                Self::Four
242            }
243            [true, true, false, true] => Self::Four224Line,
244            [false, false, false, false] => Self::GraphicsI,
245            [true, false, false, false] => Self::Text,
246            [false, true, false, false] => Self::GraphicsII,
247            [false, false, true, false] => Self::Multicolor,
248            _ => {
249                log::debug!("Unsupported mode, defaulting to mode 4: {mode_bits:?}");
250                Self::Four
251            }
252        };
253
254        match (hardware, mode) {
255            (SmsGgHardware::Sg1000, Mode::Four | Mode::Four224Line) => {
256                // Invalid; just render in text mode
257                Mode::Text
258            }
259            _ => mode,
260        }
261    }
262
263    const fn name_table_rows(self) -> u16 {
264        match self {
265            Self::Four224Line => 32,
266            _ => 28,
267        }
268    }
269
270    const fn active_scanlines(self) -> u16 {
271        match self {
272            Self::Four224Line => 224,
273            _ => 192,
274        }
275    }
276}
277
278#[derive(Debug, Clone, Copy, Encode, Decode)]
279struct SpriteRegisters {
280    double_sprite_height: bool,
281    double_sprite_size: bool,
282    shift_sprites_left: bool,
283    base_sprite_table_address: u16,
284    base_sprite_pattern_address: u16,
285}
286
287impl SpriteRegisters {
288    fn new() -> Self {
289        Self {
290            double_sprite_height: false,
291            double_sprite_size: false,
292            shift_sprites_left: false,
293            base_sprite_table_address: 0x3F00,
294            base_sprite_pattern_address: 0x2000,
295        }
296    }
297
298    fn sprite_height(self) -> u8 {
299        match (self.double_sprite_size, self.double_sprite_height) {
300            (true, true) => 32,
301            (true, false) | (false, true) => 16,
302            (false, false) => 8,
303        }
304    }
305
306    fn sprite_width(self) -> u8 {
307        if self.double_sprite_size { 16 } else { 8 }
308    }
309}
310
311#[derive(Debug, Clone, Encode, Decode)]
312struct Registers {
313    version: VdpVersion,
314    mode: Mode,
315    mode_bits: [bool; 4],
316    sprite: SpriteRegisters,
317    latched_sprite: SpriteRegisters,
318    control_write_flag: ControlWriteFlag,
319    latched_control_byte: u8,
320    data_write_location: DataWriteLocation,
321    data_address: u16,
322    data_read_buffer: u8,
323    cram_write_latch: u8,
324    display_enabled: bool,
325    frame_interrupt_enabled: bool,
326    frame_interrupt_pending: bool,
327    frame_interrupt_flag: bool,
328    line_interrupt_enabled: bool,
329    line_interrupt_pending: bool,
330    sprite_overflow: bool,
331    sprite_collision: bool,
332    tms9918_5th_sprite: u8,
333    vertical_scroll_lock: bool,
334    horizontal_scroll_lock: bool,
335    hide_left_column: bool,
336    base_name_table_address: u16,
337    name_table_address_mask: u16,
338    backdrop_color: u8,
339    x_scroll: u8,
340    y_scroll: u8,
341    line_counter_reload_value: u8,
342    // Registers used only in legacy TMS9918 modes
343    color_table_address: u16,
344    pattern_generator_address: u16,
345    text_mode_color_1: u8,
346}
347
348impl Registers {
349    fn new(version: VdpVersion) -> Self {
350        Self {
351            version,
352            mode: Mode::Four,
353            mode_bits: [false, false, false, true],
354            sprite: SpriteRegisters::new(),
355            latched_sprite: SpriteRegisters::new(),
356            control_write_flag: ControlWriteFlag::First,
357            latched_control_byte: 0,
358            data_write_location: DataWriteLocation::Vram,
359            data_address: 0,
360            data_read_buffer: 0,
361            cram_write_latch: 0,
362            display_enabled: false,
363            frame_interrupt_enabled: false,
364            frame_interrupt_pending: false,
365            frame_interrupt_flag: false,
366            line_interrupt_enabled: false,
367            line_interrupt_pending: false,
368            sprite_overflow: false,
369            sprite_collision: false,
370            tms9918_5th_sprite: 0,
371            vertical_scroll_lock: false,
372            horizontal_scroll_lock: false,
373            hide_left_column: false,
374            base_name_table_address: 0x3800,
375            name_table_address_mask: 0xFFFF,
376            backdrop_color: 0,
377            x_scroll: 0,
378            y_scroll: 0,
379            line_counter_reload_value: 0,
380            color_table_address: 0,
381            pattern_generator_address: 0,
382            text_mode_color_1: 0,
383        }
384    }
385
386    fn read_control(&mut self) -> u8 {
387        let status_flags = (u8::from(self.frame_interrupt_flag) << 7)
388            | (u8::from(self.sprite_overflow) << 6)
389            | (u8::from(self.sprite_collision) << 5);
390
391        let fifth_sprite =
392            if self.sprite_overflow && self.version.hardware() == SmsGgHardware::Sg1000 {
393                self.tms9918_5th_sprite
394            } else {
395                0
396            };
397
398        // Control reads clear all status/interrupt flags and reset the control write toggle
399        self.frame_interrupt_pending = false;
400        self.frame_interrupt_flag = false;
401        self.line_interrupt_pending = false;
402        self.sprite_overflow = false;
403        self.sprite_collision = false;
404        self.control_write_flag = ControlWriteFlag::First;
405
406        status_flags | fifth_sprite
407    }
408
409    fn write_control(&mut self, value: u8, vram: &Vram) {
410        let write_flag = self.control_write_flag;
411
412        log::trace!("VDP control write with flag {write_flag:?}");
413
414        match write_flag {
415            ControlWriteFlag::First => {
416                self.latched_control_byte = value;
417
418                // Set low byte of data address
419                self.data_address.set_lsb(value);
420            }
421            ControlWriteFlag::Second => {
422                self.data_address.set_msb(value & 0x3F);
423
424                log::trace!("VRAM address set to {:04X?}", self.data_address);
425
426                match value & 0xC0 {
427                    0x00 => {
428                        // VRAM read
429                        self.data_read_buffer = vram[self.data_address as usize];
430                        self.data_address = (self.data_address + 1) & DATA_ADDRESS_MASK;
431
432                        self.data_write_location = DataWriteLocation::Vram;
433
434                        log::trace!("VRAM read");
435                    }
436                    0x40 => {
437                        // VRAM write
438                        self.data_write_location = DataWriteLocation::Vram;
439
440                        log::trace!("VRAM write");
441                    }
442                    0x80 => {
443                        // Internal register write
444                        let register = value & 0x0F;
445                        self.write_internal_register(register, self.latched_control_byte);
446
447                        self.data_write_location = DataWriteLocation::Vram;
448
449                        log::debug!(
450                            "Internal register write: {register} set to {:02X}",
451                            self.latched_control_byte
452                        );
453                    }
454                    0xC0 => {
455                        // CRAM write
456                        self.data_write_location = DataWriteLocation::Cram;
457
458                        log::trace!("CRAM write");
459                    }
460                    _ => unreachable!("value & 0xC0 is always 0x00/0x40/0x80/0xC0"),
461                }
462            }
463        }
464
465        self.control_write_flag = write_flag.toggle();
466    }
467
468    fn read_data(&mut self, vram: &Vram) -> u8 {
469        let buffered_byte = self.data_read_buffer;
470
471        self.data_read_buffer = vram[self.data_address as usize];
472        self.data_address = (self.data_address + 1) & DATA_ADDRESS_MASK;
473
474        // All data accesses reset the write toggle
475        self.control_write_flag = ControlWriteFlag::First;
476
477        buffered_byte
478    }
479
480    fn write_data(&mut self, value: u8, vram: &mut Vram, cram: &mut [u8]) {
481        log::trace!("VDP data write with address {:04X}", self.data_address);
482
483        match self.data_write_location {
484            DataWriteLocation::Vram => {
485                vram[self.data_address as usize] = value;
486            }
487            DataWriteLocation::Cram => {
488                // CRAM only uses the lowest 5 or 6 address bits
489                match self.version.hardware() {
490                    SmsGgHardware::MasterSystem => {
491                        // SMS CRAM is 8-bit; writes go through directly
492                        let cram_addr = self.data_address & 0x1F;
493                        cram[cram_addr as usize] = value;
494                    }
495                    SmsGgHardware::GameGear => {
496                        // Game Gear CRAM is 16-bit; even addr writes are latched, odd addr writes
497                        // persist a 16-bit word
498                        let cram_addr = self.data_address & 0x3F;
499                        if !cram_addr.bit(0) {
500                            self.cram_write_latch = value;
501                        } else {
502                            cram[(cram_addr & !1) as usize] = self.cram_write_latch;
503                            cram[cram_addr as usize] = value;
504                        }
505                    }
506                    SmsGgHardware::Sg1000 => {
507                        // SG-1000 does not have CRAM
508                    }
509                }
510            }
511        }
512
513        self.data_address = (self.data_address + 1) & DATA_ADDRESS_MASK;
514
515        // All data accesses reset the write toggle
516        self.control_write_flag = ControlWriteFlag::First;
517
518        // Hardware quirk: writing to the data port also updates the read buffer
519        self.data_read_buffer = value;
520    }
521
522    fn write_internal_register(&mut self, register: u8, value: u8) {
523        match register {
524            0 => {
525                // Mode control #1
526                self.vertical_scroll_lock = value.bit(7);
527                self.horizontal_scroll_lock = value.bit(6);
528                self.hide_left_column = value.bit(5);
529                self.line_interrupt_enabled = value.bit(4);
530                self.sprite.shift_sprites_left = value.bit(3);
531                self.mode_bits[3] = value.bit(2);
532                self.mode_bits[1] = value.bit(1);
533                self.mode = Mode::from_mode_bits(self.version.hardware(), self.mode_bits);
534                // TODO sync/monochrome bit
535
536                log::debug!("  Vertical scroll lock: {}", self.vertical_scroll_lock);
537                log::debug!("  Horizontal scroll lock: {}", self.horizontal_scroll_lock);
538                log::debug!("  Hide left column: {}", self.hide_left_column);
539                log::debug!("  Line interrupt enabled: {}", self.line_interrupt_enabled);
540                log::debug!("  Shift sprites left: {}", self.sprite.shift_sprites_left);
541                log::debug!("  Mode: {:?}", self.mode);
542            }
543            1 => {
544                // Mode control #2
545                self.display_enabled = value.bit(6);
546                self.frame_interrupt_enabled = value.bit(5);
547                self.mode_bits[0] = value.bit(4);
548                self.mode_bits[2] = value.bit(3);
549                self.mode = Mode::from_mode_bits(self.version.hardware(), self.mode_bits);
550                self.sprite.double_sprite_height = value.bit(1);
551                self.sprite.double_sprite_size = value.bit(0);
552
553                log::debug!("  Display enabled: {}", self.display_enabled);
554                log::debug!("  Frame interrupt enabled: {}", self.frame_interrupt_enabled);
555                log::debug!("  Double sprite height: {}", self.sprite.double_sprite_height);
556                log::debug!("  Double sprite size: {}", self.sprite.double_sprite_size);
557                log::debug!("  Mode: {:?}", self.mode);
558            }
559            2 => {
560                // Base name table address (note: least significant bit is only used in legacy modes)
561                self.base_name_table_address = u16::from(value & 0x0F) << 10;
562
563                // On the SMS1, bit 0 of register #2 is ANDed with A10 when doing nametable lookups
564                self.name_table_address_mask = if self.version.is_sms1() {
565                    0xFBFF | (u16::from(value & 0x01) << 10)
566                } else {
567                    0xFFFF
568                };
569
570                log::debug!("  Base nametable address: {:04X}", self.base_name_table_address);
571                log::debug!("  Nametable address mask: {:04X}", self.name_table_address_mask);
572            }
573            3 => {
574                // Color table address (used only in TMS9918 modes)
575                self.color_table_address = u16::from(value) << 6;
576
577                log::debug!("  TMS9918 color table address: {:04X}", self.color_table_address);
578            }
579            4 => {
580                // Pattern generator start address (used only in TMS9918 modes)
581                self.pattern_generator_address = u16::from(value & 0x07) << 11;
582
583                log::debug!(
584                    "  TMS9918 pattern generator address: {:04X}",
585                    self.pattern_generator_address
586                );
587            }
588            5 => {
589                // Sprite attribute table base address (note: LSB is only used in legacy modes)
590                // TODO SMS1 hardware quirk - if bit 0 is cleared then X position and tile index are
591                // fetched from the lower half of the table instead of the upper half
592                self.sprite.base_sprite_table_address = u16::from(value & 0x7F) << 7;
593
594                log::debug!(
595                    "  Sprite attribute table address: {:04X}",
596                    self.sprite.base_sprite_table_address
597                );
598            }
599            6 => {
600                // Sprite pattern table base address (note: bits 1 and 0 are only used in legacy modes)
601                // TODO SMS1 hardware quirk - bits 1 and 0 are ANDed with bits 8 and 6 of the tile index
602                self.sprite.base_sprite_pattern_address = u16::from(value & 0x07) << 11;
603
604                log::debug!(
605                    "  Sprite pattern generator address: {:04X}",
606                    self.sprite.base_sprite_pattern_address
607                );
608            }
609            7 => {
610                // Backdrop color
611                self.backdrop_color = value & 0x0F;
612
613                // Bit 1 color in TMS9918 text mode
614                self.text_mode_color_1 = value >> 4;
615
616                log::debug!("  Backdrop color: {}", self.backdrop_color);
617                log::debug!("  TMS9918 text mode color 1: {}", self.text_mode_color_1);
618            }
619            8 => {
620                // X scrollf
621                self.x_scroll = value;
622
623                log::debug!("  X scroll: {value}");
624            }
625            9 => {
626                // Y scroll
627                // TODO updates to Y scroll should only take effect at end-of-frame?
628                self.y_scroll = value;
629
630                log::debug!("  Y scroll: {value}");
631            }
632            10 => {
633                // Line counter
634                self.line_counter_reload_value = value;
635
636                log::debug!("  Line interrupt counter reload: {value}");
637            }
638            _ => {}
639        }
640    }
641}
642
643#[derive(Debug, Clone, Copy, PartialEq, Eq)]
644enum Palette {
645    Palette0,
646    Palette1,
647}
648
649impl Palette {
650    fn base_cram_addr(self) -> u8 {
651        match self {
652            Self::Palette0 => 0x00,
653            Self::Palette1 => 0x10,
654        }
655    }
656}
657
658#[derive(Debug, Clone, Copy)]
659struct BgTileData {
660    priority: bool,
661    palette: Palette,
662    vertical_flip: bool,
663    horizontal_flip: bool,
664    tile_index: u16,
665}
666
667#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
668struct SpriteData {
669    y: u8,
670    x: u8,
671    tile_index: u16,
672}
673
674#[derive(Debug, Clone, Encode, Decode)]
675struct SpriteBuffer {
676    sprites: [SpriteData; 64],
677    len: usize,
678    overflow: bool,
679}
680
681impl SpriteBuffer {
682    fn new() -> Self {
683        Self { sprites: [SpriteData::default(); 64], len: 0, overflow: false }
684    }
685
686    fn iter(&self) -> BufferIter<'_, SpriteData> {
687        BufferIter { buffer: &self.sprites, idx: 0, len: self.len }
688    }
689
690    fn clear(&mut self) {
691        self.len = 0;
692        self.overflow = false;
693    }
694}
695
696impl<'a> IntoIterator for &'a SpriteBuffer {
697    type Item = SpriteData;
698    type IntoIter = BufferIter<'a, SpriteData>;
699
700    fn into_iter(self) -> Self::IntoIter {
701        self.iter()
702    }
703}
704
705#[derive(Debug, Clone)]
706struct BufferIter<'a, T> {
707    buffer: &'a [T],
708    idx: usize,
709    len: usize,
710}
711
712impl<T: Copy> Iterator for BufferIter<'_, T> {
713    type Item = T;
714
715    #[allow(clippy::if_then_some_else_none)]
716    fn next(&mut self) -> Option<Self::Item> {
717        if self.idx < self.len {
718            let data = self.buffer[self.idx];
719            self.idx += 1;
720            Some(data)
721        } else {
722            None
723        }
724    }
725}
726
727fn find_sprites_on_scanline(
728    scanline: u8,
729    mode: Mode,
730    registers: SpriteRegisters,
731    vram: &Vram,
732    sprite_buffer: &mut SpriteBuffer,
733    remove_sprite_limit: bool,
734) {
735    let sprite_height = registers.sprite_height();
736
737    let base_sat_addr = registers.base_sprite_table_address & 0xFF00;
738    for i in 0..64 {
739        let y = vram[(base_sat_addr | i) as usize];
740        if mode != Mode::Four224Line && y == 0xD0 {
741            return;
742        }
743
744        let x = vram[(base_sat_addr | 0x80 | (2 * i)) as usize];
745        let tile_index = vram[(base_sat_addr | 0x80 | (2 * i + 1)) as usize];
746
747        let sprite_bottom = y.wrapping_add(sprite_height);
748
749        let sprite_overlaps_line = if y < sprite_bottom {
750            (y..sprite_bottom).contains(&scanline)
751        } else {
752            scanline >= y || scanline < sprite_bottom
753        };
754        if sprite_overlaps_line {
755            if sprite_buffer.len == 8 {
756                sprite_buffer.overflow = true;
757                if !remove_sprite_limit {
758                    return;
759                }
760            }
761
762            sprite_buffer.sprites[sprite_buffer.len] =
763                SpriteData { y, x, tile_index: tile_index.into() };
764            sprite_buffer.len += 1;
765        }
766    }
767}
768
769#[derive(Debug, Clone, Encode, Decode)]
770struct SpriteLineBuffer {
771    pixels: [u8; SCREEN_WIDTH as usize],
772    collisions: [bool; SCREEN_WIDTH as usize],
773}
774
775impl SpriteLineBuffer {
776    fn new() -> Self {
777        Self { pixels: array::from_fn(|_| 0), collisions: array::from_fn(|_| false) }
778    }
779
780    fn clear(&mut self) {
781        self.pixels.fill(0);
782        self.collisions.fill(false);
783    }
784}
785
786fn render_sprite_pixels(
787    scanline: u8,
788    registers: SpriteRegisters,
789    vram: &[u8; VRAM_LEN],
790    active_sprites: &SpriteBuffer,
791    line_buffer: &mut SpriteLineBuffer,
792) {
793    let sprite_width: u16 = registers.sprite_width().into();
794    let sprite_x_downshift: i32 = registers.double_sprite_size.into();
795
796    // Mask out bits 11-12 (only used in legacy modes)
797    let base_sprite_pattern_addr = registers.base_sprite_pattern_address & 0x2000;
798
799    let sprite_x_delta = if registers.shift_sprites_left { -8 } else { 0 };
800
801    for sprite in active_sprites {
802        let sprite_left: u16 = sprite.x.into();
803        let sprite_tile_row = u16::from(scanline.wrapping_sub(sprite.y)) >> sprite_x_downshift;
804
805        let tile_index = if registers.double_sprite_height {
806            let top_tile = sprite.tile_index & 0xFE;
807            top_tile | u16::from(sprite_tile_row >= 8)
808        } else {
809            sprite.tile_index
810        };
811
812        let sprite_tile_addr = (base_sprite_pattern_addr | (tile_index * 32)) as usize;
813        let sprite_tile = &vram[sprite_tile_addr..sprite_tile_addr + 32];
814
815        for dx in 0..sprite_width {
816            let x = sprite_left + dx;
817            let pixel_idx = i32::from(x) + sprite_x_delta;
818            if !(0..SCREEN_WIDTH.into()).contains(&pixel_idx) {
819                continue;
820            }
821
822            let sprite_tile_col = dx >> sprite_x_downshift;
823            let color_id = get_color_id(sprite_tile, sprite_tile_row & 7, sprite_tile_col, false);
824            if color_id == 0 {
825                continue;
826            }
827
828            if line_buffer.pixels[pixel_idx as usize] != 0 {
829                line_buffer.collisions[pixel_idx as usize] = true;
830            } else {
831                line_buffer.pixels[pixel_idx as usize] = color_id;
832            }
833        }
834    }
835}
836
837#[derive(Debug, Clone, Encode, Decode)]
838pub struct VdpBuffer {
839    buffer: BoxedWordArray<FRAME_BUFFER_LEN>,
840    viewport: ViewportSize,
841}
842
843impl VdpBuffer {
844    fn new(version: VdpVersion, gg_use_sms_resolution: bool) -> Self {
845        Self {
846            buffer: BoxedWordArray::new(),
847            viewport: version.viewport_size(gg_use_sms_resolution, Mode::default()),
848        }
849    }
850
851    #[inline]
852    fn idx(&self, row: u16, col: u16) -> usize {
853        (self.viewport.top as usize + row as usize) * SCREEN_WIDTH as usize
854            + self.viewport.left as usize
855            + col as usize
856    }
857
858    #[inline]
859    pub fn get(&self, row: u16, col: u16) -> u16 {
860        self.buffer[self.idx(row, col)]
861    }
862
863    #[inline]
864    fn set(&mut self, row: u16, col: u16, value: u16) {
865        self.buffer[row as usize * SCREEN_WIDTH as usize + col as usize] = value;
866    }
867
868    pub fn iter(&self) -> FrameBufferRowIter<'_> {
869        FrameBufferRowIter { buffer: self, row: 0 }
870    }
871}
872
873#[derive(Debug, Clone)]
874pub struct FrameBufferRowIter<'a> {
875    buffer: &'a VdpBuffer,
876    row: u16,
877}
878
879impl<'a> Iterator for FrameBufferRowIter<'a> {
880    type Item = &'a [u16];
881
882    #[inline]
883    #[allow(clippy::if_then_some_else_none)]
884    fn next(&mut self) -> Option<Self::Item> {
885        if self.row < self.buffer.viewport.height {
886            let start_idx = self.buffer.idx(self.row, 0);
887            let row_slice =
888                &self.buffer.buffer[start_idx..start_idx + self.buffer.viewport.width as usize];
889            self.row += 1;
890            Some(row_slice)
891        } else {
892            None
893        }
894    }
895}
896
897impl<'a> IntoIterator for &'a VdpBuffer {
898    type Item = &'a [u16];
899    type IntoIter = FrameBufferRowIter<'a>;
900
901    fn into_iter(self) -> Self::IntoIter {
902        self.iter()
903    }
904}
905
906#[derive(Debug, Clone, Encode, Decode)]
907pub struct Vdp {
908    frame_buffer: VdpBuffer,
909    registers: Registers,
910    vram: Box<Vram>,
911    color_ram: [u8; COLOR_RAM_LEN],
912    scanline: u16,
913    dot: u16,
914    event_idx: u8,
915    sprite_buffer: SpriteBuffer,
916    sprite_line_buffer: SpriteLineBuffer,
917    remove_sprite_limit: bool,
918    gg_use_sms_resolution: bool,
919    line_counter: u8,
920    latched_h_counter: u8,
921}
922
923#[derive(Debug, Clone, Copy, PartialEq, Eq)]
924pub enum VdpTickEffect {
925    None,
926    FrameComplete,
927}
928
929impl Vdp {
930    pub fn new(version: VdpVersion, config: &SmsGgEmulatorConfig) -> Self {
931        Self {
932            frame_buffer: VdpBuffer::new(version, config.gg_use_sms_resolution),
933            registers: Registers::new(version),
934            vram: Box::new(array::from_fn(|_| 0)),
935            color_ram: [0; COLOR_RAM_LEN],
936            scanline: 0,
937            dot: 0,
938            event_idx: 0,
939            sprite_buffer: SpriteBuffer::new(),
940            sprite_line_buffer: SpriteLineBuffer::new(),
941            remove_sprite_limit: config.remove_sprite_limit,
942            gg_use_sms_resolution: config.gg_use_sms_resolution,
943            line_counter: 0xFF,
944            latched_h_counter: 0,
945        }
946    }
947
948    fn read_color_ram_word(&self, address: u8) -> u16 {
949        match self.registers.version.hardware() {
950            SmsGgHardware::MasterSystem => self.color_ram[address as usize].into(),
951            SmsGgHardware::GameGear => u16::from_le_bytes([
952                self.color_ram[(2 * address) as usize],
953                self.color_ram[(2 * address + 1) as usize],
954            ]),
955            SmsGgHardware::Sg1000 => {
956                // SG-1000 does not have CRAM
957                0xFFFF
958            }
959        }
960    }
961
962    fn read_name_table_word(&self, row: u16, col: u16) -> BgTileData {
963        let base_name_table_addr = match self.registers.mode {
964            // Mask out bit 11 and offset by $0700
965            Mode::Four224Line => (self.registers.base_name_table_address & 0xF000) | 0x0700,
966            // Mask out bit 10 (only used by legacy modes)
967            _ => self.registers.base_name_table_address & 0xF800,
968        };
969
970        let name_table_address_mask = if self.registers.version.is_sms1() {
971            self.registers.name_table_address_mask
972        } else {
973            0xFFFF
974        };
975        let name_table_addr =
976            (base_name_table_addr + (row << 6) + (col << 1)) & name_table_address_mask;
977        let low_byte = self.vram[name_table_addr as usize];
978        let high_byte = self.vram[(name_table_addr + 1) as usize];
979
980        let priority = high_byte.bit(4);
981        let palette = if !high_byte.bit(3) { Palette::Palette0 } else { Palette::Palette1 };
982        let vertical_flip = high_byte.bit(2);
983        let horizontal_flip = high_byte.bit(1);
984        let tile_index = (u16::from(high_byte.bit(0)) << 8) | u16::from(low_byte);
985
986        BgTileData { priority, palette, vertical_flip, horizontal_flip, tile_index }
987    }
988
989    fn render_scanline(&mut self, scanline: u16) {
990        match self.registers.mode {
991            Mode::Text => return self.render_text_scanline(scanline),
992            Mode::GraphicsI | Mode::GraphicsII => {
993                return self.render_graphics_12_scanline(scanline, self.registers.mode);
994            }
995            Mode::Multicolor => return self.render_multicolor_scanline(scanline),
996            Mode::Four | Mode::Four224Line => {}
997        }
998
999        let frame_buffer_row = self.frame_buffer_row(scanline);
1000
1001        let (coarse_x_scroll, fine_x_scroll) =
1002            if scanline < 16 && self.registers.horizontal_scroll_lock {
1003                (0, 0)
1004            } else {
1005                (u16::from(self.registers.x_scroll >> 3), u16::from(self.registers.x_scroll & 0x07))
1006            };
1007
1008        let backdrop_color = self.backdrop_color();
1009        for dot in 0..fine_x_scroll {
1010            self.frame_buffer.set(frame_buffer_row, dot, backdrop_color);
1011        }
1012
1013        for column in 0..32 {
1014            let (coarse_y_scroll, fine_y_scroll) = if column >= 24
1015                && self.registers.vertical_scroll_lock
1016            {
1017                (0, 0)
1018            } else {
1019                (u16::from(self.registers.y_scroll >> 3), u16::from(self.registers.y_scroll & 0x07))
1020            };
1021
1022            let name_table_rows = self.registers.mode.name_table_rows();
1023            let name_table_row =
1024                ((scanline + fine_y_scroll) / 8 + coarse_y_scroll) % name_table_rows;
1025            let name_table_col = (column + (32 - coarse_x_scroll)) % 32;
1026            let bg_tile_data = self.read_name_table_word(name_table_row, name_table_col);
1027
1028            let bg_tile_addr = (bg_tile_data.tile_index * 32) as usize;
1029            let bg_tile = &self.vram[bg_tile_addr..bg_tile_addr + 32];
1030
1031            let bg_base_cram_addr = bg_tile_data.palette.base_cram_addr();
1032
1033            let bg_tile_row = if bg_tile_data.vertical_flip {
1034                7 - ((scanline + fine_y_scroll) % 8)
1035            } else {
1036                (scanline + fine_y_scroll) % 8
1037            };
1038
1039            for bg_tile_col in 0..8 {
1040                let dot = 8 * column + fine_x_scroll + bg_tile_col;
1041                if dot == SCREEN_WIDTH {
1042                    break;
1043                }
1044
1045                if self.registers.hide_left_column && dot < 8 {
1046                    self.frame_buffer.set(frame_buffer_row, dot, backdrop_color);
1047                    continue;
1048                }
1049
1050                let bg_color_id =
1051                    get_color_id(bg_tile, bg_tile_row, bg_tile_col, bg_tile_data.horizontal_flip);
1052
1053                let sprite_color_id = self.sprite_line_buffer.pixels[dot as usize];
1054
1055                let pixel_color =
1056                    if sprite_color_id != 0 && (bg_color_id == 0 || !bg_tile_data.priority) {
1057                        // Sprites can only use palette 1
1058                        self.read_color_ram_word(0x10 | sprite_color_id)
1059                    } else {
1060                        self.read_color_ram_word(bg_base_cram_addr | bg_color_id)
1061                    };
1062                self.frame_buffer.set(frame_buffer_row, dot, pixel_color);
1063            }
1064        }
1065    }
1066
1067    fn clear_scanline(&mut self, scanline: u16) {
1068        let frame_buffer_row = self.frame_buffer_row(scanline);
1069        let backdrop_color = self.backdrop_color();
1070
1071        for pixel in 0..SCREEN_WIDTH {
1072            self.frame_buffer.set(frame_buffer_row, pixel, backdrop_color);
1073        }
1074    }
1075
1076    fn frame_buffer_row(&self, scanline: u16) -> u16 {
1077        scanline + self.frame_buffer.viewport.top_border_height
1078    }
1079
1080    fn backdrop_color(&self) -> u16 {
1081        match self.registers.version.hardware() {
1082            SmsGgHardware::MasterSystem | SmsGgHardware::GameGear => {
1083                // Backdrop color always reads from the second half of CRAM (sprite colors)
1084                self.read_color_ram_word(0x10 | self.registers.backdrop_color)
1085            }
1086            SmsGgHardware::Sg1000 => {
1087                let color_table = tms9918::color_table(self.registers.version.hardware());
1088                color_table[self.registers.backdrop_color as usize].into()
1089            }
1090        }
1091    }
1092
1093    fn trace_log_current_state(&self) {
1094        log::trace!("Registers: {:04X?}", self.registers);
1095
1096        log::trace!("CRAM:");
1097        for (i, value) in self.color_ram.into_iter().enumerate() {
1098            log::trace!("  {i:02X}: {value:02X}");
1099        }
1100
1101        log::trace!("Nametable ({:04X}):", self.registers.base_name_table_address);
1102        for row in 0..28 {
1103            for col in 0..32 {
1104                let name_table_word = self.read_name_table_word(row, col);
1105                log::trace!("  ({row}, {col}): {name_table_word:03X?}");
1106
1107                let name_table_addr =
1108                    (self.registers.base_name_table_address | (row << 6) | (col << 1)) as usize;
1109                let memory = &self.vram[name_table_addr..name_table_addr + 2];
1110                log::trace!("  RAM bytes: {memory:02X?}");
1111            }
1112        }
1113
1114        log::trace!("Tiles:");
1115        for i in 0..512 {
1116            let address = i * 32;
1117            let values = &self.vram[address..address + 32];
1118            log::trace!("  {i:03X}: {values:02X?}");
1119        }
1120    }
1121
1122    #[must_use]
1123    pub fn tick(&mut self) -> VdpTickEffect {
1124        if log::log_enabled!(log::Level::Trace) && self.scanline == 0 && self.dot == 0 {
1125            self.trace_log_current_state();
1126        }
1127
1128        let timing_mode = self.timing_mode();
1129        let active_scanlines = self.registers.mode.active_scanlines();
1130        let scanlines_per_frame = timing_mode.scanlines_per_frame();
1131
1132        self.dot += 1;
1133        if self.dot == DOTS_PER_SCANLINE {
1134            self.scanline += 1;
1135            self.dot = 0;
1136            self.event_idx = 0;
1137
1138            if self.scanline == scanlines_per_frame {
1139                self.scanline = 0;
1140            }
1141        }
1142
1143        self.process_events(active_scanlines, scanlines_per_frame);
1144
1145        if self.registers.display_enabled
1146            && self.scanline < active_scanlines
1147            && self
1148                .sprite_line_buffer
1149                .collisions
1150                .get(self.dot.wrapping_sub(2) as usize)
1151                .copied()
1152                .unwrap_or(false)
1153        {
1154            log::debug!("Sprite collision at line {} dot {}", self.scanline, self.dot);
1155            self.registers.sprite_collision = true;
1156        }
1157
1158        let frame_complete = self.scanline == active_scanlines + 1 && self.dot == 0;
1159        if frame_complete { VdpTickEffect::FrameComplete } else { VdpTickEffect::None }
1160    }
1161
1162    fn process_events(&mut self, active_scanlines: u16, scanlines_per_frame: u16) {
1163        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1164        enum VdpEvent {
1165            None,
1166            SpriteProcessing,
1167            RenderLine,
1168            SetSpriteOverflow,
1169            FrameInterruptFlag,
1170            FrameInterruptPending,
1171            DecrementLineCounter,
1172        }
1173
1174        // These timings are slightly early to account for how Z80 and VDP execution are interleaved.
1175        // Z80 execution is instruction-level, so when the Z80 accesses a VDP port, the VDP's current
1176        // cycle count will be pre-instruction rather than post-instruction. Any Z80-visible state
1177        // needs to be updated "early" to account for this
1178        const EVENT_DOTS: [u16; 7] = [
1179            DOTS_PER_SCANLINE - 45,
1180            DOTS_PER_SCANLINE - 35,
1181            DOTS_PER_SCANLINE - 34,
1182            DOTS_PER_SCANLINE - 33,
1183            DOTS_PER_SCANLINE - 17,
1184            DOTS_PER_SCANLINE - 16,
1185            u16::MAX,
1186        ];
1187
1188        const EVENTS: [VdpEvent; 7] = [
1189            VdpEvent::SpriteProcessing,
1190            VdpEvent::RenderLine,
1191            VdpEvent::SetSpriteOverflow,
1192            VdpEvent::FrameInterruptFlag,
1193            VdpEvent::FrameInterruptPending,
1194            VdpEvent::DecrementLineCounter,
1195            VdpEvent::None,
1196        ];
1197
1198        while self.dot >= EVENT_DOTS[self.event_idx as usize] {
1199            match EVENTS[self.event_idx as usize] {
1200                VdpEvent::SpriteProcessing => {
1201                    self.per_line_sprite_processing(active_scanlines, scanlines_per_frame);
1202                    self.registers.latched_sprite = self.registers.sprite;
1203                }
1204                VdpEvent::RenderLine => {
1205                    let next_line = if self.scanline == scanlines_per_frame - 1 {
1206                        0
1207                    } else {
1208                        self.scanline + 1
1209                    };
1210                    if next_line < active_scanlines {
1211                        if self.registers.display_enabled {
1212                            self.render_scanline(next_line);
1213                        } else {
1214                            self.clear_scanline(next_line);
1215                        }
1216                    }
1217                }
1218                VdpEvent::SetSpriteOverflow => {
1219                    self.registers.sprite_overflow |= self.sprite_buffer.overflow;
1220                }
1221                VdpEvent::FrameInterruptFlag => {
1222                    if self.scanline == active_scanlines {
1223                        self.registers.frame_interrupt_flag = true;
1224                    }
1225                }
1226                VdpEvent::FrameInterruptPending => {
1227                    if self.scanline == active_scanlines {
1228                        self.registers.frame_interrupt_pending =
1229                            self.registers.frame_interrupt_flag;
1230
1231                        self.fill_vertical_border();
1232                    }
1233                }
1234                VdpEvent::DecrementLineCounter => {
1235                    if self.scanline < active_scanlines || self.scanline == scanlines_per_frame - 1
1236                    {
1237                        let (new_counter, overflowed) = self.line_counter.overflowing_sub(1);
1238                        if overflowed {
1239                            self.line_counter = self.registers.line_counter_reload_value;
1240                            self.registers.line_interrupt_pending = true;
1241                        } else {
1242                            self.line_counter = new_counter;
1243                        }
1244                    } else {
1245                        // Line counter is constantly reloaded outside of the active display period
1246                        self.line_counter = self.registers.line_counter_reload_value;
1247                    }
1248                }
1249                VdpEvent::None => {}
1250            }
1251
1252            self.event_idx += 1;
1253        }
1254    }
1255
1256    fn per_line_sprite_processing(&mut self, active_scanlines: u16, scanlines_per_frame: u16) {
1257        if !matches!(self.registers.mode, Mode::Four | Mode::Four224Line) {
1258            return;
1259        }
1260
1261        self.sprite_buffer.clear();
1262        self.sprite_line_buffer.clear();
1263
1264        let sprite_line = if self.scanline == scanlines_per_frame - 1 {
1265            255
1266        } else if self.scanline < active_scanlines - 1 {
1267            self.scanline
1268        } else {
1269            return;
1270        };
1271        let sprite_line = sprite_line as u8;
1272
1273        find_sprites_on_scanline(
1274            sprite_line,
1275            self.registers.mode,
1276            self.registers.latched_sprite,
1277            &self.vram,
1278            &mut self.sprite_buffer,
1279            self.remove_sprite_limit,
1280        );
1281
1282        if self.registers.display_enabled {
1283            render_sprite_pixels(
1284                sprite_line,
1285                self.registers.latched_sprite,
1286                &self.vram,
1287                &self.sprite_buffer,
1288                &mut self.sprite_line_buffer,
1289            );
1290        }
1291    }
1292
1293    fn fill_vertical_border(&mut self) {
1294        let backdrop_color = self.backdrop_color();
1295
1296        let ViewportSize { top_border_height, height, bottom_border_height, .. } =
1297            self.frame_buffer.viewport;
1298
1299        let viewport_top = top_border_height;
1300        for scanline in 0..viewport_top {
1301            for pixel in 0..256 {
1302                self.frame_buffer.set(scanline, pixel, backdrop_color);
1303            }
1304        }
1305
1306        let viewport_bottom = height - bottom_border_height;
1307        for scanline in viewport_bottom..height {
1308            for pixel in 0..256 {
1309                self.frame_buffer.set(scanline, pixel, backdrop_color);
1310            }
1311        }
1312    }
1313
1314    pub fn frame_buffer(&self) -> &VdpBuffer {
1315        &self.frame_buffer
1316    }
1317
1318    pub fn viewport(&self) -> ViewportSize {
1319        self.frame_buffer.viewport
1320    }
1321
1322    pub fn read_control(&mut self) -> u8 {
1323        log::debug!("VDP control read at line {} dot {}", self.scanline, self.dot);
1324        self.registers.read_control()
1325    }
1326
1327    pub fn write_control(&mut self, value: u8) {
1328        log::debug!("VDP control write {value:02X} at line {} dot {}", self.scanline, self.dot);
1329        self.registers.write_control(value, &self.vram);
1330
1331        // Update viewport in case mode changed
1332        self.frame_buffer.viewport =
1333            self.registers.version.viewport_size(self.gg_use_sms_resolution, self.registers.mode);
1334    }
1335
1336    pub fn read_data(&mut self) -> u8 {
1337        self.registers.read_data(&self.vram)
1338    }
1339
1340    pub fn write_data(&mut self, value: u8) {
1341        self.registers.write_data(value, &mut self.vram, &mut self.color_ram);
1342    }
1343
1344    pub fn v_counter(&self) -> u8 {
1345        let scanline = if self.dot >= DOTS_PER_SCANLINE - 34 {
1346            (self.scanline + 1) % self.timing_mode().scanlines_per_frame()
1347        } else {
1348            self.scanline
1349        };
1350
1351        let v_counter = match (self.registers.version.timing_mode(), self.registers.mode) {
1352            (TimingMode::Ntsc, Mode::Four224Line) => {
1353                if scanline <= 0xEA {
1354                    scanline as u8
1355                } else {
1356                    (scanline - 6) as u8
1357                }
1358            }
1359            (TimingMode::Pal, Mode::Four224Line) => {
1360                if scanline <= 0xFF {
1361                    scanline as u8
1362                } else if scanline <= 0x102 {
1363                    (scanline - 0x100) as u8
1364                } else {
1365                    (scanline - 57) as u8
1366                }
1367            }
1368            (TimingMode::Ntsc, _) => {
1369                if scanline <= 0xDA {
1370                    scanline as u8
1371                } else {
1372                    (scanline - 6) as u8
1373                }
1374            }
1375            (TimingMode::Pal, _) => {
1376                if scanline <= 0xF2 {
1377                    scanline as u8
1378                } else {
1379                    (scanline - 57) as u8
1380                }
1381            }
1382        };
1383
1384        log::debug!(
1385            "V counter read at line {} dot {}, value {v_counter:02X}",
1386            self.scanline,
1387            self.dot
1388        );
1389
1390        v_counter
1391    }
1392
1393    pub fn h_counter(&self) -> u8 {
1394        self.latched_h_counter
1395    }
1396
1397    pub fn latch_h_counter_on_th_change(&mut self) {
1398        let mut dot = self.dot + 10;
1399        if dot >= DOTS_PER_SCANLINE {
1400            dot -= DOTS_PER_SCANLINE;
1401        }
1402
1403        self.latched_h_counter = if dot >= DOTS_PER_SCANLINE - 46 {
1404            let diff = -((DOTS_PER_SCANLINE - dot) as i16);
1405            (diff >> 1) as u8
1406        } else {
1407            (dot >> 1) as u8
1408        };
1409
1410        log::debug!(
1411            "Latched H counter at line {} dot {}, value {:02X}",
1412            self.scanline,
1413            self.dot,
1414            self.latched_h_counter
1415        );
1416    }
1417
1418    pub fn interrupt_line(&self) -> InterruptLine {
1419        if (self.registers.frame_interrupt_enabled && self.registers.frame_interrupt_pending)
1420            || (self.registers.line_interrupt_enabled && self.registers.line_interrupt_pending)
1421        {
1422            InterruptLine::Low
1423        } else {
1424            InterruptLine::High
1425        }
1426    }
1427
1428    pub fn timing_mode(&self) -> TimingMode {
1429        self.registers.version.timing_mode()
1430    }
1431
1432    pub fn update_config(&mut self, version: VdpVersion, config: &SmsGgEmulatorConfig) {
1433        self.registers.version = version;
1434        self.frame_buffer.viewport =
1435            version.viewport_size(config.gg_use_sms_resolution, self.registers.mode);
1436        self.remove_sprite_limit = config.remove_sprite_limit;
1437        self.gg_use_sms_resolution = config.gg_use_sms_resolution;
1438    }
1439}
1440
1441fn get_color_id(tile: &[u8], tile_row: u16, tile_col: u16, horizontal_flip: bool) -> u8 {
1442    let shift = if horizontal_flip { tile_col } else { 7 - tile_col };
1443    let mask = 1 << shift;
1444    ((tile[(4 * tile_row) as usize] & mask) >> shift)
1445        | (((tile[(4 * tile_row + 1) as usize] & mask) >> shift) << 1)
1446        | (((tile[(4 * tile_row + 2) as usize] & mask) >> shift) << 2)
1447        | (((tile[(4 * tile_row + 3) as usize] & mask) >> shift) << 3)
1448}
1449
1450pub fn convert_sms_color(color: u16) -> u8 {
1451    [0, 85, 170, 255][color as usize]
1452}
1453
1454#[must_use]
1455pub fn sms_color_to_rgb(color: u16) -> Color {
1456    let r = convert_sms_color(color & 0x03);
1457    let g = convert_sms_color((color >> 2) & 0x03);
1458    let b = convert_sms_color((color >> 4) & 0x03);
1459    Color::rgb(r, g, b)
1460}
1461
1462pub fn convert_gg_color(color: u16) -> u8 {
1463    [0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255][color as usize]
1464}
1465
1466#[must_use]
1467pub fn gg_color_to_rgb(color: u16) -> Color {
1468    let r = convert_gg_color(color & 0x0F);
1469    let g = convert_gg_color((color >> 4) & 0x0F);
1470    let b = convert_gg_color((color >> 8) & 0x0F);
1471    Color::rgb(r, g, b)
1472}
1473
1474pub fn convert_sg_color(color: u16) -> Color {
1475    tms9918::TMS9918_COLOR_TO_RGB8[(color & 0xF) as usize]
1476}