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};
Use NonZeroU16 to cut the buffer sizes in half
12type LineDotBuffer = [Option<NonZeroU16>; MAX_SCREEN_WIDTH];
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}