1use crate::vdp::colors::ColorModifier;
2use crate::vdp::fifo::VdpFifo;
3use crate::vdp::registers::{RIGHT_BORDER, Registers};
4use crate::vdp::render::RasterLine;
5use crate::vdp::{MAX_SCREEN_WIDTH, Vdp, colors};
6use bincode::{Decode, Encode};
7use std::num::NonZeroU16;
8use std::ops::Range;
9use std::{array, mem};
10
11// Use NonZeroU16 to cut the buffer sizes in half
12type LineDotBuffer = [Option<NonZeroU16>; MAX_SCREEN_WIDTH];
13
14#[derive(Debug, Clone, Encode, Decode)]
15struct LineBuffer {
16    dots: Box<LineDotBuffer>,
17    any: bool,
18}
19
20impl LineBuffer {
21    fn new() -> Self {
22        Self { dots: Box::new(array::from_fn(|_| None)), any: false }
23    }
24
25    fn clear(&mut self) {
26        self.dots.fill(None);
27        self.any = false;
28    }
29}
30
31#[derive(Debug, Clone, Encode, Decode)]
32pub struct CramDotBuffer {
33    prev_line: LineBuffer,
34    current_line: LineBuffer,
35}
36
37impl CramDotBuffer {
38    pub fn new() -> Self {
39        Self { prev_line: LineBuffer::new(), current_line: LineBuffer::new() }
40    }
41
42    pub fn check_for_dot(
43        &mut self,
44        registers: &Registers,
45        fifo: &VdpFifo,
46        pixel: u16,
47        cram_addr: u32,
48        color: u16,
49    ) {
50        let h_display_size = registers.horizontal_display_size;
51        let active_display_range = h_display_size.active_display_h_range();
52        let left_border = h_display_size.left_border();
53
54        // +5 is necessary for correct alignment of Direct Color DMA demos
55        // Also to avoid CRAM dots being visible within active display on some screens in Overdrive 1
56        let left = active_display_range.start - left_border + 5;
57        let right = active_display_range.end + RIGHT_BORDER + 5;
58
59        self.check_for_dot_inner(left..right, pixel, color);
60
61        // Hack: Use "CRAM dots" to make Direct Color DMA demos work
62        // If the backdrop color is changed while display is disabled, fill in some following pixels
63        // with the new backdrop color
64        if !registers.display_enabled
65            && (cram_addr >> 4) as u8 == registers.background_palette
66            && (cram_addr & 0xF) as u8 == registers.background_color_id
67        {
68            let n = if fifo.len() > 1 {
69                // Filling in 3 pixels is necessary to avoid vertical lines around refresh slots
70                3
71            } else {
72                right.saturating_sub(pixel)
73            };
74            for i in 1..=n {
75                self.check_for_dot_inner(left..right, pixel + i, color);
76            }
77        }
78    }
79
80    fn check_for_dot_inner(&mut self, range: Range<u16>, pixel: u16, color: u16) {
81        if !range.contains(&pixel) {
82            return;
83        }
84
85        let idx = (pixel - range.start) as usize;
86        self.current_line.dots[idx] = Some(NonZeroU16::new(color | 0x8000).unwrap());
87        self.current_line.any = true;
88    }
89
90    pub fn swap_buffers_if_needed(&mut self) {
91        if !self.current_line.any && !self.prev_line.any {
92            return;
93        }
94
95        mem::swap(&mut self.current_line.dots, &mut self.prev_line.dots);
96        self.prev_line.any = self.current_line.any;
97        self.current_line.clear();
98    }
99}
100
101impl Vdp {
102    pub(super) fn apply_cram_dots_previous_line(&mut self, line: RasterLine) {
103        if !self.cram_dots.prev_line.any {
104            return;
105        }
106
107        let timing_mode = self.timing_mode;
108        let h_display_size = self.registers.horizontal_display_size;
109        let v_display_size = self.registers.vertical_display_size;
110
111        let prev_line = line.previous_line(v_display_size);
112        let Some(mut fb_row) = prev_line.to_frame_buffer_row(
113            v_display_size.top_border(timing_mode),
114            timing_mode,
115            self.config.render_vertical_border,
116        ) else {
117            return;
118        };
119
120        let interlaced = self.state.interlaced_frame;
121        if interlaced {
122            fb_row *= 2;
123            fb_row += u32::from(self.state.interlaced_odd);
124        }
125
126        let screen_width = self.screen_width();
127        let left_border_offset = if self.config.render_horizontal_border {
128            0
129        } else {
130            -i32::from(h_display_size.left_border())
131        };
132
133        self.apply_cram_dots_inner(fb_row, screen_width, left_border_offset);
134        if interlaced && self.config.deinterlace {
135            self.apply_cram_dots_inner(fb_row ^ 1, screen_width, left_border_offset);
136        }
137    }
138
139    fn apply_cram_dots_inner(&mut self, fb_row: u32, screen_width: u32, left_border_offset: i32) {
140        for (x, color) in self.cram_dots.prev_line.dots.iter().copied().enumerate() {
141            let Some(color) = color.map(NonZeroU16::get) else { continue };
142
143            let fb_col = x as i32 + left_border_offset;
144            if !(0..screen_width as i32).contains(&fb_col) {
145                continue;
146            }
147            let fb_col = fb_col as u32;
148
149            let fb_idx = (fb_row * screen_width + fb_col) as usize;
150
151            let r = ((color >> 1) & 7) as u8;
152            let g = ((color >> 5) & 7) as u8;
153            let b = ((color >> 9) & 7) as u8;
154
155            // Reuse "alpha" from the existing pixel; some 32X games depend on this (e.g. Toughman Contest)
156            let a = self.frame_buffer[fb_idx].a;
157
158            let rgba_color =
159                colors::gen_to_rgba(r, g, b, a, ColorModifier::None, &self.color_tables);
160
161            self.frame_buffer[fb_idx] = rgba_color;
162        }
163    }
164}