1mod palette; 2mod vce; 3mod vdc; 4 5use crate::api; 6use crate::api::PceEmulatorConfig; 7use crate::video::palette::PcePalette; 8use crate::video::vce::Vce; 9use crate::video::vdc::Vdc; 10use bincode::{Decode, Encode}; 11use jgenesis_common::frontend::{ 12 Color, CompositeParams, FiniteF64, FrameSize, NtscPerFrameParams, SamplesPerColorCycle, 13}; 14use jgenesis_common::num::U16Ext; 15use jgenesis_proc_macros::{FakeDecode, FakeEncode}; 16 17pub const FRAME_BUFFER_LEN: usize = vdc::FRAME_BUFFER_WIDTH * vdc::FRAME_BUFFER_HEIGHT;
Number of mclk cycles per scanline is fixed, unaffected by dot clock divider
20pub const MCLK_CYCLES_PER_SCANLINE: u64 = 1365;
22#[derive(Debug, Clone, Copy, PartialEq, Eq)] 23enum WordByte { 24 Low, 25 High, 26} 27 28impl WordByte { 29 fn get(self, word: u16) -> u8 { 30 match self { 31 Self::Low => word.lsb(), 32 Self::High => word.msb(), 33 } 34 } 35 36 fn set(self, word: &mut u16, byte: u8) { 37 match self { 38 Self::Low => word.set_lsb(byte), 39 Self::High => word.set_msb(byte), 40 } 41 } 42} 43 44#[derive(Debug, Clone, Encode, Decode)] 45struct VideoState { 46 scanline: u16, 47 scanline_mclk: u64, 48 cycles: u64, 49 frame_start_cycles: u64, 50 frame_x_divider: u32, 51} 52 53impl VideoState { 54 fn new() -> Self { 55 Self { scanline: 0, scanline_mclk: 0, cycles: 0, frame_start_cycles: 0, frame_x_divider: 1 } 56 } 57} 58 59#[derive(Debug, Clone, FakeEncode, FakeDecode)] 60struct Rgba8FrameBuffer(Box<[Color; FRAME_BUFFER_LEN]>); 61 62impl Default for Rgba8FrameBuffer { 63 fn default() -> Self { 64 Self(vec![Color::default(); FRAME_BUFFER_LEN].into_boxed_slice().try_into().unwrap()) 65 } 66} 67 68struct FrameRenderParams { 69 left_offset: usize, 70 top_offset: usize, 71 width: usize, 72 height: usize, 73} 74 75impl FrameRenderParams { 76 const DEFAULT: Self = Self { 77 left_offset: 0, 78 top_offset: 0, 79 width: vdc::FRAME_BUFFER_WIDTH, 80 height: vdc::FRAME_BUFFER_HEIGHT, 81 }; 82 83 const CROP_OVERSCAN: Self = Self { 84 left_offset: 4 * vdc::OVERSCAN_DOTS_DIV_4 as usize, 85 top_offset: 9, 86 width: vdc::FRAME_BUFFER_WIDTH - 2 * 4 * vdc::OVERSCAN_DOTS_DIV_4 as usize, 87 height: vdc::FRAME_BUFFER_HEIGHT - 2 * 9, 88 }; 89} 90 91#[derive(Debug, Clone, Encode, Decode)] 92pub struct VideoSubsystem { 93 vdc: Vdc, 94 vce: Vce, 95 state: VideoState, 96 frame_buffer: Rgba8FrameBuffer, 97 // Greyscale bit appears to work by suppressing the NTSC color burst, so it's effectively latched 98 // per-line even though it doesn't really work that way in hardware 99 greyscale_per_line: Box<[bool; vce::MAX_LINES_PER_FRAME]>, 100 palette: Box<PcePalette>, 101 greyscale_palette: Box<PcePalette>, 102 crop_overscan: bool, 103} 104 105impl VideoSubsystem { 106 pub fn new(config: PceEmulatorConfig) -> Self { 107 let palette = palette::for_type(config.palette); 108 let greyscale_palette = palette::make_greyscale(palette); 109 110 Self { 111 vdc: Vdc::new(config), 112 vce: Vce::new(), 113 state: VideoState::new(), 114 frame_buffer: Rgba8FrameBuffer::default(), 115 greyscale_per_line: vec![false; vce::MAX_LINES_PER_FRAME] 116 .into_boxed_slice() 117 .try_into() 118 .unwrap(), 119 palette: Box::new(*palette), 120 greyscale_palette: Box::new(greyscale_palette), 121 crop_overscan: config.crop_overscan, 122 } 123 } 124 125 pub fn step_to(&mut self, cycles: u64, irq1_pending: &mut bool) { 126 let elapsed_cycles = cycles.saturating_sub(self.state.cycles); 127 if elapsed_cycles == 0 { 128 return; 129 } 130 self.state.cycles = cycles; 131 132 let mut prev_scanline_mclk = self.state.scanline_mclk; 133 self.state.scanline_mclk += elapsed_cycles; 134 135 let dot_clock_divider = self.vce.dot_clock_divider(); 136 137 if self.state.scanline_mclk >= MCLK_CYCLES_PER_SCANLINE { 138 let elapsed_vdc_dots = 139 dot_clock_divider.divide_difference(MCLK_CYCLES_PER_SCANLINE, prev_scanline_mclk); 140 self.vdc.tick_dots(elapsed_vdc_dots, &self.vce); 141 142 prev_scanline_mclk = 0; 143 self.state.scanline_mclk -= MCLK_CYCLES_PER_SCANLINE; 144 145 let lines_per_frame = self.vce.lines_per_frame(); 146 147 self.state.scanline += 1; 148 if self.state.scanline >= lines_per_frame { 149 self.state.scanline = 0; 150 self.vdc.start_new_frame(); 151 self.state.frame_start_cycles = self.state.cycles - self.state.scanline_mclk; 152 } 153 154 self.vdc.start_new_line(self.state.scanline, &self.vce); 155 156 self.greyscale_per_line[self.state.scanline as usize] = self.vce.greyscale(); 157 } 158 159 let elapsed_vdc_dots = self 160 .vce 161 .dot_clock_divider() 162 .divide_difference(self.state.scanline_mclk, prev_scanline_mclk); 163 self.vdc.tick_dots(elapsed_vdc_dots, &self.vce); 164 165 *irq1_pending = self.vdc.irq(); 166 } 167 168 pub fn frame_complete(&self) -> bool { 169 self.vdc.frame_complete() 170 } 171 172 pub fn clear_frame_complete(&mut self) { 173 self.vdc.clear_frame_complete(); 174 } 175 176 pub fn render_rgba8_frame_buffer(&mut self) { 177 let vdc_frame_buffer = self.vdc.frame_buffer(); 178 179 let mut params = if self.crop_overscan { 180 FrameRenderParams::CROP_OVERSCAN 181 } else { 182 FrameRenderParams::DEFAULT 183 }; 184 185 let x_stride = if vdc_frame_buffer 186 .line_dividers 187 .iter() 188 .all(|÷r| divider == vdc_frame_buffer.line_dividers[0]) 189 { 190 // If dot clock divider remained unchanged for the entire frame, downsample for more 191 // reasonable behavior with shaders 192 vdc_frame_buffer.line_dividers[0] as usize 193 } else { 194 1 195 }; 196 self.state.frame_x_divider = x_stride as u32; 197 198 params.width /= x_stride; 199 200 for row in 0..params.height { 201 let active_display_row = row + params.top_offset; 202 let scanline = active_display_row + (vdc::ACTIVE_DISPLAY_LINES.start as usize); 203 let palette = if self.greyscale_per_line[scanline] { 204 &self.greyscale_palette 205 } else { 206 &self.palette 207 }; 208 209 for col in 0..params.width { 210 let active_display_col = col * x_stride + params.left_offset; 211 let vdc_color = vdc_frame_buffer.colors[active_display_row][active_display_col]; 212 let (r, g, b) = palette[(vdc_color & 0x1FF) as usize]; 213 self.frame_buffer.0[row * params.width + col] = Color::rgb(r, g, b); 214 } 215 } 216 } 217 218 pub fn frame_buffer(&self) -> &[Color] { 219 self.frame_buffer.0.as_slice() 220 } 221 222 pub fn frame_size(&self) -> FrameSize { 223 let mut frame_size = if self.crop_overscan { 224 FrameSize { 225 width: FrameRenderParams::CROP_OVERSCAN.width as u32, 226 height: FrameRenderParams::CROP_OVERSCAN.height as u32, 227 } 228 } else { 229 FrameSize { 230 width: vdc::FRAME_BUFFER_WIDTH as u32, 231 height: vdc::FRAME_BUFFER_HEIGHT as u32, 232 } 233 }; 234 235 frame_size.width /= self.state.frame_x_divider; 236 237 frame_size 238 } 239 240 pub fn ntsc_aspect_ratio(&self) -> FiniteF64 { 241 // NTSC aspect ratio should be 8:7 for 5 MHz dot clock (H256px); compute others based on that 242 debug_assert_ne!(self.state.frame_x_divider, 0); 243 let multiplier = f64::from(self.state.frame_x_divider) / 4.0; 244 FiniteF64::try_from(multiplier * 8.0 / 7.0).unwrap() 245 } 246 247 pub fn composite_params(&self) -> CompositeParams { 248 CompositeParams { 249 upscale_factor: 2 * self.state.frame_x_divider, 250 samples_per_color_cycle: SamplesPerColorCycle::Twelve, 251 } 252 } 253 254 pub fn ntsc_per_frame_params(&self) -> NtscPerFrameParams { 255 NtscPerFrameParams { 256 frame_phase_offset: 2 * self.state.frame_start_cycles, 257 per_line_phase_offset: 2 * MCLK_CYCLES_PER_SCANLINE, 258 } 259 } 260 261 pub fn target_fps(&self) -> f64 { 262 // ~60.05 Hz in 262-line mode, ~59.83 Hz in 263-line mode 263 api::MASTER_CLOCK_FREQUENCY 264 / (MCLK_CYCLES_PER_SCANLINE as f64) 265 / f64::from(self.vce.lines_per_frame()) 266 } 267 268 // $1FE000-$1FE003: VDC ports 269 pub fn read_vdc(&mut self, address: u32, bus_cycles: &mut u64, irq1_pending: &mut bool) -> u8 { 270 log::trace!( 271 "VDC register read {address:06X}, line {} mclk {}", 272 self.state.scanline, 273 self.state.scanline_mclk 274 ); 275 276 let address = address & 3; 277 278 if matches!(address, 2 | 3) { 279 while self.vdc.is_cpu_read_blocked() { 280 log::trace!( 281 "CPU read {address} stalling! Line {} cycles {}", 282 self.state.scanline, 283 self.state.scanline_mclk 284 ); 285 286 *bus_cycles += u64::from(self.vce.dot_clock_divider()); 287 self.step_to(*bus_cycles, irq1_pending); 288 } 289 } 290 291 match address { 292 0 => self.vdc.read_status(), 293 1 => 0x00, // Unused 294 2 => self.vdc.read_data(WordByte::Low), 295 3 => self.vdc.read_data(WordByte::High), 296 _ => unreachable!("address & 3 is always <= 3"), 297 } 298 } 299 300 // $1FE000-$1FE003: VDC ports 301 pub fn write_vdc( 302 &mut self, 303 address: u32, 304 value: u8, 305 bus_cycles: &mut u64, 306 irq1_pending: &mut bool, 307 ) { 308 log::trace!( 309 "VDC register write {address:06X} {value:02X}, line {} mclk {}", 310 self.state.scanline, 311 self.state.scanline_mclk 312 ); 313 314 let address = address & 3; 315 316 if matches!(address, 2 | 3) { 317 while self.vdc.is_cpu_write_blocked() { 318 log::trace!( 319 "CPU write {address} {value:02X} stalling! Line {} cycles {}", 320 self.state.scanline, 321 self.state.scanline_mclk 322 ); 323 324 *bus_cycles += u64::from(self.vce.dot_clock_divider()); 325 self.step_to(*bus_cycles, irq1_pending); 326 } 327 } 328 329 match address { 330 0 => self.vdc.write_register_select(value), 331 1 => {} // Unused 332 2 => self.vdc.write_data(value, WordByte::Low), 333 3 => self.vdc.write_data(value, WordByte::High), 334 _ => unreachable!("value & 3 is always <= 3"), 335 } 336 } 337 338 // $1FE400-$1FE407: VCE ports 339 #[allow(clippy::match_same_arms)] 340 pub fn read_vce(&mut self, address: u32) -> u8 { 341 log::trace!( 342 "VCE register read {address:06X}, line {} mclk {}", 343 self.state.scanline, 344 self.state.scanline_mclk 345 ); 346 347 match address & 7 { 348 0..=3 => 0xFF, // Write-only / unused 349 4 => self.vce.read_color_data(WordByte::Low), 350 5 => self.vce.read_color_data(WordByte::High), 351 6 | 7 => 0xFF, // Unused 352 _ => unreachable!("value & 7 is always <= 7"), 353 } 354 } 355 356 // $1FE400-$1FE407: VCE ports 357 #[allow(clippy::match_same_arms)] 358 pub fn write_vce(&mut self, address: u32, value: u8) { 359 log::trace!( 360 "VCE register write {address:06X} {value:02X}, line {} mclk {}", 361 self.state.scanline, 362 self.state.scanline_mclk 363 ); 364 365 match address & 7 { 366 0 => self.vce.write_control(value), 367 1 => {} // Unused 368 2 => self.vce.write_color_address(value, WordByte::Low), 369 3 => self.vce.write_color_address(value, WordByte::High), 370 4 => self.vce.write_color_data(value, WordByte::Low), 371 5 => self.vce.write_color_data(value, WordByte::High), 372 6 | 7 => {} // Unused 373 _ => unreachable!("value & 7 is always <= 7"), 374 } 375 } 376 377 pub fn reload_config(&mut self, config: PceEmulatorConfig) { 378 *self.palette = *palette::for_type(config.palette); 379 *self.greyscale_palette = palette::make_greyscale(&self.palette); 380 381 self.crop_overscan = config.crop_overscan; 382 383 self.vdc.reload_config(config); 384 } 385 386 pub fn dump_vram(&self, palette: u16, out: &mut [[Color; 64]]) { 387 self.vdc.dump_vram(palette, out, &self.vce, &self.palette); 388 } 389 390 pub fn dump_palettes(&self, out: &mut [Color]) { 391 self.vce.dump_palettes(out, &self.palette); 392 } 393}