1use bincode::{Decode, Encode};
2use jgenesis_common::frontend::TimingMode;
3use jgenesis_common::num::{GetBit, U16Ext};
4use std::fmt::{Display, Formatter};

Values from https://gendev.spritesmind.net/forum/viewtopic.php?p=37011#p37011

7pub const H32_LEFT_BORDER: u16 = 14;
8pub const H40_LEFT_BORDER: u16 = 13;
9pub const RIGHT_BORDER: u16 = 14;
11pub const NTSC_TOP_BORDER: u16 = 11;
12pub const PAL_V28_TOP_BORDER: u16 = 38;
13pub const PAL_V30_TOP_BORDER: u16 = 30;
14
15pub const NTSC_BOTTOM_BORDER: u16 = 8;
16pub const PAL_V28_BOTTOM_BORDER: u16 = 32;
17pub const PAL_V30_BOTTOM_BORDER: u16 = 24;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
20pub enum VerticalScrollMode {
21    #[default]
22    FullScreen,
23    TwoCell,
24}
25
26impl Display for VerticalScrollMode {
27    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::FullScreen => write!(f, "Full screen"),
30            Self::TwoCell => write!(f, "Per 2 cell"),
31        }
32    }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
36pub enum HorizontalScrollMode {
37    #[default]
38    FullScreen,
39    Cell,
40    Line,
41    // Repeatedly uses the scroll values for the first 8 lines
42    Invalid,
43}
44
45impl Display for HorizontalScrollMode {
46    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::FullScreen => write!(f, "Full screen"),
49            Self::Cell => write!(f, "Per cell"),
50            Self::Line => write!(f, "Per line"),
51            Self::Invalid => write!(f, "Prohibited"),
52        }
53    }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
57pub enum HorizontalDisplaySize {
58    #[default]
59    ThirtyTwoCell,
60    FortyCell,
61}
62
63impl Display for HorizontalDisplaySize {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        match self {
66            Self::ThirtyTwoCell => write!(f, "H32 (256px)"),
67            Self::FortyCell => write!(f, "H40 (320px)"),
68        }
69    }
70}
71
72impl HorizontalDisplaySize {
73    pub const fn active_display_pixels(self) -> u16 {
74        match self {
75            Self::ThirtyTwoCell => 256,
76            Self::FortyCell => 320,
77        }
78    }
79
80    pub const fn pixels_including_hblank(self) -> u16 {
81        match self {
82            Self::ThirtyTwoCell => 342,
83            Self::FortyCell => 420,
84        }
85    }
86
87    // Length in sprites
88    pub const fn sprite_table_len(self) -> u32 {
89        match self {
90            Self::ThirtyTwoCell => 64,
91            Self::FortyCell => 80,
92        }
93    }
94
95    pub const fn max_sprites_per_line(self) -> u16 {
96        match self {
97            Self::ThirtyTwoCell => 16,
98            Self::FortyCell => 20,
99        }
100    }
101
102    pub const fn max_sprite_tiles_per_line(self) -> u16 {
103        self.active_display_pixels() / 8
104    }
105
106    pub const fn window_width_cells(self) -> u16 {
107        match self {
108            Self::ThirtyTwoCell => 32,
109            Self::FortyCell => 64,
110        }
111    }
112
113    pub const fn sprite_attribute_table_mask(self) -> u32 {
114        // Sprite attribute table A9 is ignored in H40 mode
115        match self {
116            Self::ThirtyTwoCell => !0,
117            Self::FortyCell => !0x3FF,
118        }
119    }
120
121    pub const fn left_border(self) -> u16 {
122        match self {
123            Self::ThirtyTwoCell => H32_LEFT_BORDER,
124            Self::FortyCell => H40_LEFT_BORDER,
125        }
126    }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
130pub enum VerticalDisplaySize {
131    #[default]
132    TwentyEightCell,
133    ThirtyCell,
134}
135
136impl Display for VerticalDisplaySize {
137    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
138        match self {
139            Self::TwentyEightCell => write!(f, "V28 (224px)"),
140            Self::ThirtyCell => write!(f, "V30 (240px)"),
141        }
142    }
143}
144
145impl VerticalDisplaySize {
146    pub const fn active_scanlines(self) -> u16 {
147        match self {
148            Self::TwentyEightCell => 224,
149            Self::ThirtyCell => 240,
150        }
151    }
152
153    pub const fn top_border(self, timing_mode: TimingMode) -> u16 {
154        match (self, timing_mode) {
155            (_, TimingMode::Ntsc) => NTSC_TOP_BORDER,
156            (Self::TwentyEightCell, TimingMode::Pal) => PAL_V28_TOP_BORDER,
157            (Self::ThirtyCell, TimingMode::Pal) => PAL_V30_TOP_BORDER,
158        }
159    }
160
161    pub const fn bottom_border(self, timing_mode: TimingMode) -> u16 {
162        match (self, timing_mode) {
163            (_, TimingMode::Ntsc) => NTSC_BOTTOM_BORDER,
164            (Self::TwentyEightCell, TimingMode::Pal) => PAL_V28_BOTTOM_BORDER,
165            (Self::ThirtyCell, TimingMode::Pal) => PAL_V30_BOTTOM_BORDER,
166        }
167    }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
171pub enum InterlacingMode {
172    #[default]
173    Progressive,
174    Interlaced,
175    InterlacedDouble,
176}
177
178impl Display for InterlacingMode {
179    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
180        match self {
181            Self::Progressive => write!(f, "Progressive"),
182            Self::Interlaced => write!(f, "Single-screen interlaced"),
183            Self::InterlacedDouble => write!(f, "Double-screen interlaced"),
184        }
185    }
186}
187
188impl InterlacingMode {
189    pub const fn v_scroll_mask(self) -> u16 {
190        // V scroll values are 10 bits normally, 11 bits in interlaced 2x mode
191        match self {
192            Self::Progressive | Self::Interlaced => 0x03FF,
193            Self::InterlacedDouble => 0x07FF,
194        }
195    }
196
197    pub const fn sprite_display_top(self) -> u16 {
198        match self {
199            // The sprite display area starts at $080 normally, $100 in interlaced 2x mode
200            Self::Progressive | Self::Interlaced => 0x080,
201            Self::InterlacedDouble => 0x100,
202        }
203    }
204
205    pub const fn sprite_display_mask(self) -> u16 {
206        match self {
207            Self::Progressive | Self::Interlaced => 0x1FF,
208            Self::InterlacedDouble => 0x3FF,
209        }
210    }
211
212    pub const fn cell_height(self) -> u16 {
213        1 << self.cell_height_shift()
214    }
215
216    pub const fn cell_height_shift(self) -> u16 {
217        match self {
218            // Cells are 8x8 normally, 8x16 in interlaced 2x mode
219            Self::Progressive | Self::Interlaced => 3,
220            Self::InterlacedDouble => 4,
221        }
222    }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
226pub enum ScrollSize {
227    #[default]
228    ThirtyTwo,
229    SixtyFour,
230    OneTwentyEight,
231    Invalid,
232}
233
234impl Display for ScrollSize {
235    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Self::ThirtyTwo => write!(f, "32 tiles"),
238            Self::SixtyFour => write!(f, "64 tiles"),
239            Self::OneTwentyEight => write!(f, "128 tiles"),
240            Self::Invalid => write!(f, "Prohibited"),
241        }
242    }
243}
244
245impl ScrollSize {
246    pub fn from_bits(bits: u8) -> Self {
247        match bits & 0x03 {
248            0x00 => Self::ThirtyTwo,
249            0x01 => Self::SixtyFour,
250            0x02 => Self::Invalid,
251            0x03 => Self::OneTwentyEight,
252            _ => unreachable!("value & 0x03 is always <= 0x03"),
253        }
254    }
255
256    pub fn to_pixels(self) -> u16 {
257        match self {
258            Self::ThirtyTwo | Self::Invalid => 32 * 8,
259            Self::SixtyFour => 64 * 8,
260            Self::OneTwentyEight => 128 * 8,
261        }
262    }
263}
264
265impl From<ScrollSize> for u16 {
266    fn from(value: ScrollSize) -> Self {
267        match value {
268            ScrollSize::ThirtyTwo => 32,
269            ScrollSize::SixtyFour => 64,
270            ScrollSize::OneTwentyEight => 128,
271            ScrollSize::Invalid => 1,
272        }
273    }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
277pub enum WindowHorizontalMode {
278    #[default]
279    LeftToCenter,
280    CenterToRight,
281}
282
283impl Display for WindowHorizontalMode {
284    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
285        match self {
286            Self::LeftToCenter => write!(f, "Left to center"),
287            Self::CenterToRight => write!(f, "Center to right"),
288        }
289    }
290}
291
292impl WindowHorizontalMode {
293    pub fn h_range(self, window_x: u16, active_display_pixels: u16) -> (u16, u16) {
294        match self {
295            Self::LeftToCenter => (0, 8 * window_x),
296            Self::CenterToRight => (8 * window_x, active_display_pixels),
297        }
298    }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
302pub enum WindowVerticalMode {
303    #[default]
304    TopToCenter,
305    CenterToBottom,
306}
307
308impl Display for WindowVerticalMode {
309    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
310        match self {
311            Self::TopToCenter => write!(f, "Top to center"),
312            Self::CenterToBottom => write!(f, "Center to bottom"),
313        }
314    }
315}
316
317impl WindowVerticalMode {
318    pub fn in_window(self, scanline: u16, window_y: u16) -> bool {
319        let cell = scanline / 8;
320        match self {
321            Self::TopToCenter => cell < window_y,
322            Self::CenterToBottom => cell >= window_y,
323        }
324    }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
328pub enum DmaMode {
329    #[default]
330    MemoryToVram,
331    VramFill,
332    VramCopy,
333}
334
335impl Display for DmaMode {
336    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
337        match self {
338            Self::MemoryToVram => write!(f, "Memory to VRAM"),
339            Self::VramFill => write!(f, "VRAM fill"),
340            Self::VramCopy => write!(f, "VRAM-to-VRAM copy"),
341        }
342    }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
346pub enum VramSizeKb {
347    #[default]
348    SixtyFour,
349    OneTwentyEight,
350}
351
352impl Display for VramSizeKb {
353    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
354        match self {
355            Self::SixtyFour => write!(f, "64 KB"),
356            Self::OneTwentyEight => write!(f, "128 KB"),
357        }
358    }
359}
360
361#[derive(Debug, Clone, Encode, Decode)]
362pub struct Registers {
363    // Register #0
364    pub h_interrupt_enabled: bool,
365    pub hv_counter_stopped: bool,
366    // Register #1
367    pub display_enabled: bool,
368    pub v_interrupt_enabled: bool,
369    pub dma_enabled: bool,
370    pub vertical_display_size: VerticalDisplaySize,
371    pub mode_4: bool,
372    pub vram_size: VramSizeKb,
373    // Register #2
374    pub scroll_a_base_nt_addr: u16,
375    // Register #3
376    pub window_base_nt_addr: u16,
377    // Register #4
378    pub scroll_b_base_nt_addr: u16,
379    // Register #5
380    pub sprite_attribute_table_base_addr: u32,
381    // Register #7
382    pub background_palette: u8,
383    pub background_color_id: u8,
384    // Register #10
385    pub h_interrupt_interval: u16,
386    // Register #11
387    // TODO external interrupts enabled
388    pub vertical_scroll_mode: VerticalScrollMode,
389    pub horizontal_scroll_mode: HorizontalScrollMode,
390    // Register #12
391    pub horizontal_display_size: HorizontalDisplaySize,
392    pub shadow_highlight_flag: bool,
393    pub interlacing_mode: InterlacingMode,
394    // Register #13
395    pub h_scroll_table_base_addr: u16,
396    // Register #15
397    pub data_port_auto_increment: u16,
398    // Register #16
399    pub vertical_scroll_size: ScrollSize,
400    pub horizontal_scroll_size: ScrollSize,
401    // Register #17
402    pub window_horizontal_mode: WindowHorizontalMode,
403    pub window_x_position: u16,
404    // Register #18
405    pub window_vertical_mode: WindowVerticalMode,
406    pub window_y_position: u16,
407    // Registers #19 & #20
408    pub dma_length: u16,
409    // Registers #21, #22, & #23
410    pub dma_source_address: u32,
411    pub dma_mode: DmaMode,
412}
413
414impl Registers {
415    pub fn new() -> Self {
416        Self {
417            h_interrupt_enabled: false,
418            hv_counter_stopped: false,
419            display_enabled: false,
420            v_interrupt_enabled: false,
421            dma_enabled: false,
422            vertical_display_size: VerticalDisplaySize::default(),
423            mode_4: false,
424            vram_size: VramSizeKb::default(),
425            scroll_a_base_nt_addr: 0,
426            window_base_nt_addr: 0,
427            scroll_b_base_nt_addr: 0,
428            sprite_attribute_table_base_addr: 0,
429            background_palette: 0,
430            background_color_id: 0,
431            h_interrupt_interval: 0,
432            vertical_scroll_mode: VerticalScrollMode::default(),
433            horizontal_scroll_mode: HorizontalScrollMode::default(),
434            horizontal_display_size: HorizontalDisplaySize::default(),
435            shadow_highlight_flag: false,
436            interlacing_mode: InterlacingMode::default(),
437            h_scroll_table_base_addr: 0,
438            data_port_auto_increment: 0,
439            vertical_scroll_size: ScrollSize::default(),
440            horizontal_scroll_size: ScrollSize::default(),
441            window_horizontal_mode: WindowHorizontalMode::default(),
442            window_x_position: 0,
443            window_vertical_mode: WindowVerticalMode::default(),
444            window_y_position: 0,
445            dma_length: 0,
446            dma_source_address: 0,
447            dma_mode: DmaMode::default(),
448        }
449    }
450
451    pub fn write_internal_register(&mut self, register: u8, value: u8) {
452        log::trace!("Wrote register #{register} with value {value:02X}");
453
454        if self.mode_4 && register > 10 {
455            // Writing to register numbers >10 while in SMS mode has no effect
456            // Bass Masters Classic: Pro Edition depends on this - during boot it attempts to change
457            // the H display size while in SMS mode, and it expects that write to do nothing
458            return;
459        }
460
461        match register {
462            0 => {
463                // Register #0: Mode set register 1
464                self.h_interrupt_enabled = value.bit(4);
465                self.hv_counter_stopped = value.bit(1);
466
467                log::trace!("  H interrupt enabled: {}", self.h_interrupt_enabled);
468                log::trace!("  HV counter stopped: {}", self.hv_counter_stopped);
469            }
470            1 => {
471                // Register #1: Mode set register 2
472                self.display_enabled = value.bit(6);
473                self.v_interrupt_enabled = value.bit(5);
474                self.dma_enabled = value.bit(4);
475                self.vertical_display_size = if value.bit(3) {
476                    VerticalDisplaySize::ThirtyCell
477                } else {
478                    VerticalDisplaySize::TwentyEightCell
479                };
480
481                // Undocumented: Register #1 bit 2 toggles between mode 5 (Genesis) and mode 4 (SMS)
482                // Mode 4 / SMS mode is not actually emulated, but some games depend on writes to
483                // VDP registers >10 doing nothing while in mode 4
484                self.mode_4 = !value.bit(2);
485
486                // Undocumented: Register #1 bit 7 enables "128KB" VRAM mode, which effectively enables byte-size access
487                // to VRAM
488                self.vram_size =
489                    if value.bit(7) { VramSizeKb::OneTwentyEight } else { VramSizeKb::SixtyFour };
490
491                log::trace!("  Display enabled: {}", self.display_enabled);
492                log::trace!("  V interrupt enabled: {}", self.v_interrupt_enabled);
493                log::trace!("  DMA enabled: {}", self.dma_enabled);
494                log::trace!("  Vertical display size: {:?}", self.vertical_display_size);
495                log::trace!("  Mode 4 enabled: {}", self.mode_4);
496                log::trace!("  VRAM size: {}", self.vram_size);
497            }
498            2 => {
499                // Register #2: Scroll A name table base address (bits 15-13)
500                self.scroll_a_base_nt_addr = u16::from(value & 0x38) << 10;
501
502                log::trace!(
503                    "  Scroll A base nametable address: {:04X}",
504                    self.scroll_a_base_nt_addr
505                );
506            }
507            3 => {
508                // Register #3: Window name table base address (bits 15-11)
509                self.window_base_nt_addr = u16::from(value & 0x3E) << 10;
510
511                log::trace!("  Window base nametable address: {:04X}", self.window_base_nt_addr);
512            }
513            4 => {
514                // Register #4: Scroll B name table base address (bits 15-13)
515                self.scroll_b_base_nt_addr = u16::from(value & 0x07) << 13;
516
517                log::trace!(
518                    "  Scroll B base nametable address: {:04X}",
519                    self.scroll_b_base_nt_addr
520                );
521            }
522            5 => {
523                // Register #5: Sprite attribute table base address (bits 16-9)
524                self.sprite_attribute_table_base_addr = u32::from(value) << 9;
525
526                log::trace!(
527                    "  Sprite attribute table base address: {:04X}",
528                    self.sprite_attribute_table_base_addr
529                );
530            }
531            7 => {
532                // Register #7: Background color
533                self.background_palette = (value >> 4) & 0x03;
534                self.background_color_id = value & 0x0F;
535
536                log::trace!("  BG palette: {}", self.background_palette);
537                log::trace!("  BG color id: {}", self.background_color_id);
538            }
539            10 => {
540                // Register #10: H interrupt interval
541                self.h_interrupt_interval = value.into();
542
543                log::trace!("  H interrupt interval: {}", self.h_interrupt_interval);
544            }
545            11 => {
546                // Register #11: Mode set register 3
547                // TODO external interrupt enable
548                self.vertical_scroll_mode = if value.bit(2) {
549                    VerticalScrollMode::TwoCell
550                } else {
551                    VerticalScrollMode::FullScreen
552                };
553                self.horizontal_scroll_mode = match value & 0x03 {
554                    0x00 => HorizontalScrollMode::FullScreen,
555                    0x01 => HorizontalScrollMode::Invalid,
556                    0x02 => HorizontalScrollMode::Cell,
557                    0x03 => HorizontalScrollMode::Line,
558                    _ => unreachable!("value & 0x03 is always <= 0x03"),
559                };
560
561                log::trace!("  Vertical scroll mode: {:?}", self.vertical_scroll_mode);
562                log::trace!("  Horizontal scroll mode: {:?}", self.horizontal_scroll_mode);
563            }
564            12 => {
565                // Register #12: Mode set register 4
566                self.horizontal_display_size = if value.bit(7) || value.bit(0) {
567                    HorizontalDisplaySize::FortyCell
568                } else {
569                    HorizontalDisplaySize::ThirtyTwoCell
570                };
571                self.shadow_highlight_flag = value.bit(3);
572                self.interlacing_mode = match (value >> 1) & 0x03 {
573                    // TODO how should the "prohibited" mode behave?
574                    0x00 | 0x02 => InterlacingMode::Progressive,
575                    0x01 => InterlacingMode::Interlaced,
576                    0x03 => InterlacingMode::InterlacedDouble,
577                    _ => unreachable!("value & 0x03 is always <= 0x03"),
578                };
579
580                log::trace!("  Horizontal display size: {:?}", self.horizontal_display_size);
581                log::trace!("  Shadow/highlight flag: {}", self.shadow_highlight_flag);
582                log::trace!("  Interlacing mode: {:?}", self.interlacing_mode);
583            }
584            13 => {
585                // Register #13: Horizontal scroll table base address (bits 15-10)
586                self.h_scroll_table_base_addr = u16::from(value & 0x3F) << 10;
587
588                log::trace!("  H scroll table base address: {:04X}", self.h_scroll_table_base_addr);
589            }
590            15 => {
591                // Register #15: VRAM address auto increment
592                self.data_port_auto_increment = value.into();
593
594                log::trace!("  Data port auto increment: {value:02X}");
595            }
596            16 => {
597                // Register #16: Scroll size
598                self.vertical_scroll_size = ScrollSize::from_bits(value >> 4);
599                self.horizontal_scroll_size = ScrollSize::from_bits(value);
600
601                log::trace!("  Vertical scroll size: {:?}", self.vertical_scroll_size);
602                log::trace!("  Horizontal scroll size: {:?}", self.horizontal_scroll_size);
603            }
604            17 => {
605                // Register #17: Window horizontal position
606                self.window_horizontal_mode = if value.bit(7) {
607                    WindowHorizontalMode::CenterToRight
608                } else {
609                    WindowHorizontalMode::LeftToCenter
610                };
611                self.window_x_position = u16::from(value & 0x1F) << 1;
612
613                log::trace!("  Window horizontal mode: {:?}", self.window_horizontal_mode);
614                log::trace!("  Window X position: {}", self.window_x_position);
615            }
616            18 => {
617                // Register #18: Window vertical position
618                self.window_vertical_mode = if value.bit(7) {
619                    WindowVerticalMode::CenterToBottom
620                } else {
621                    WindowVerticalMode::TopToCenter
622                };
623                self.window_y_position = (value & 0x1F).into();
624
625                log::trace!("  Window vertical mode: {:?}", self.window_vertical_mode);
626                log::trace!("  Window Y position: {}", self.window_y_position);
627            }
628            19 => {
629                // Register #19: DMA length counter (bits 7-0)
630                self.dma_length.set_lsb(value);
631
632                log::trace!("  DMA length: {}", self.dma_length);
633            }
634            20 => {
635                // Register #20: DMA length counter (bits 15-8)
636                self.dma_length.set_msb(value);
637
638                log::trace!("  DMA length: {}", self.dma_length);
639            }
640            21 => {
641                // Register 21: DMA source address (bits 9-1)
642                self.dma_source_address =
643                    (self.dma_source_address & 0xFFFF_FE00) | (u32::from(value) << 1);
644
645                log::trace!("  DMA source address: {:06X}", self.dma_source_address);
646            }
647            22 => {
648                // Register 22: DMA source address (bits 16-9)
649                self.dma_source_address =
650                    (self.dma_source_address & 0xFFFE_01FF) | (u32::from(value) << 9);
651
652                log::trace!("  DMA source address: {:06X}", self.dma_source_address);
653            }
654            23 => {
655                // Register 23: DMA source address (bits 22-17) and mode
656                self.dma_source_address =
657                    (self.dma_source_address & 0x0001_FFFF) | (u32::from(value & 0x3F) << 17);
658                self.dma_mode = match value & 0xC0 {
659                    0x00 => DmaMode::MemoryToVram,
660                    0x40 => {
661                        // If DMD1=0, DMD0 is used as bit 23 in source address
662                        self.dma_source_address |= 1 << 23;
663
664                        DmaMode::MemoryToVram
665                    }
666                    0x80 => DmaMode::VramFill,
667                    0xC0 => DmaMode::VramCopy,
668                    _ => unreachable!("value & 0x0C is always 0x00/0x40/0x80/0xC0"),
669                };
670
671                log::trace!("  DMA source address: {:06X}", self.dma_source_address);
672                log::trace!("  DMA mode: {:?}", self.dma_mode);
673            }
674            _ => {}
675        }
676    }
677
678    pub fn is_line_in_v_window(&self, scanline: u16) -> bool {
679        self.window_vertical_mode.in_window(scanline, self.window_y_position)
680    }
681
682    pub fn window_h_range(&self, active_display_pixels: u16) -> (u16, u16) {
683        self.window_horizontal_mode.h_range(self.window_x_position, active_display_pixels)
684    }
685
686    pub fn masked_window_nametable_addr(&self) -> u16 {
687        // A11 is ignored in H40 mode; Cheese Cat-Astrophe depends on this
688        let mask = match self.horizontal_display_size {
689            HorizontalDisplaySize::ThirtyTwoCell => 0xF800,
690            HorizontalDisplaySize::FortyCell => 0xF000,
691        };
692        self.window_base_nt_addr & mask
693    }
694
695    pub fn masked_sprite_attribute_table_addr(&self) -> u32 {
696        self.sprite_attribute_table_base_addr
697            & self.horizontal_display_size.sprite_attribute_table_mask()
698    }
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
702pub enum Plane {
703    #[default]
704    Background,
705    Sprite,
706    ScrollA,
707    ScrollB,
708}
709
710impl Display for Plane {
711    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
712        match self {
713            Self::Background => write!(f, "Backdrop"),
714            Self::Sprite => write!(f, "Sprites"),
715            Self::ScrollA => write!(f, "Plane A"),
716            Self::ScrollB => write!(f, "Plane B"),
717        }
718    }
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
722pub struct DebugRegister {
723    pub display_disabled: bool,
724    pub forced_plane: Plane,
725}
726
727impl DebugRegister {
728    pub fn new() -> Self {
729        Self { display_disabled: false, forced_plane: Plane::default() }
730    }
731
732    pub fn write(&mut self, value: u16) {
733        self.display_disabled = value.bit(6);
734        self.forced_plane = match (value >> 7) & 0x3 {
735            0x0 => Plane::Background,
736            0x1 => Plane::Sprite,
737            0x2 => Plane::ScrollA,
738            0x3 => Plane::ScrollB,
739            _ => unreachable!("value & 0x3 is always <= 0x3"),
740        };
741    }
742}