6use crate::GenesisVdp; 7use crate::api::GenesisVdpInfo; 8use crate::registers::SystemRegisters; 9use crate::vdp::registers::{FrameBufferMode, Registers, SelectedFrameBuffer, VerticalResolution}; 10use bincode::{Decode, Encode}; 11use genesis_components::vdp::BorderSize; 12use genesis_config::{S32XColorTint, S32XVideoOut, S32XVoidColor, Sega32XEmulatorConfig}; 13use jgenesis_common::boxedarray::{Boxed2DWordArray, BoxedColorArray, BoxedWordArray}; 14use jgenesis_common::frontend::{ 15 Color, CompositeParams, FiniteF64, FrameSize, RenderFrameOptions, Renderer, 16 SamplesPerColorCycle, TimingMode, 17}; 18use jgenesis_common::num::{GetBit, U16Ext}; 19use std::cmp; 20use std::collections::VecDeque; 21use std::ops::Range; 22 23const MCLK_CYCLES_PER_SCANLINE: u64 = genesis_components::vdp::MCLK_CYCLES_PER_SCANLINE;
25 pixels after Genesis H40 HBlank start
Manual says VBlank begins 27 dots after HBlank begins on the last line, but 27 breaks some tests that depend on timing between H and V interrupts
31const VBLANK_START_MCLK_CYCLES: u64 = HBLANK_START_MCLK_CYCLES + 22 * 8;
VDP latches registers 76 dots after HBlank begins
37const RENDER_LINE_MCLK_CYCLES: u64 = HBLANK_END_MCLK_CYCLES;
DRAM refresh takes ~40 SH-2 cycles
32X VDP only supports 320x224 and 320x240 frame sizes
The H32 frame buffer should be large enough to store frames as H1280px resolution (4 * 320)
52const EXPANDED_FRAME_BUFFER_LEN: usize = genesis_components::vdp::FRAME_BUFFER_LEN * 4;
Offset between the left edge of the Genesis H32 frame and the 32X frame, in H1280px pixels
Due to HSYNC/blanking/border timings being slightly different between H32 and H40 mode, and due to the 32X VDP always assuming H40 mode, the Genesis and 32X frames are slightly offset when the Genesis VDP is in H32 mode.
59const H32_H_OFFSET: u32 = 13;
61type FrameBufferRam = [u16; FRAME_BUFFER_LEN_WORDS]; 62type Cram = [u16; CRAM_LEN_WORDS]; 63 64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 65enum WhichFrameBuffer { 66 #[default] 67 Genesis, 68 ExpandedH32, 69 ExpandedH40, 70} 71 72#[derive(Debug, Clone, Encode, Decode)] 73struct State { 74 next_render_buffer: WhichFrameBuffer, 75 fully_blank_frame: bool, 76 scanline: u16, 77 scanline_mclk: u64, 78 scanlines_in_current_frame: u16, 79 h_interrupt_this_line: bool, 80 h_interrupt_counter: u16, 81 h_interrupt_interval_written: bool, 82 vblank_flag: bool, 83 display_frame_buffer: SelectedFrameBuffer, 84 auto_fill_mclk_remaining: u64, 85 fb_write_timing_fifo: VecDeque<u64>, 86 last_fb_write_cycles: u64, 87 cycles_till_next_render: u64, 88} 89 90impl State { 91 fn new() -> Self { 92 Self { 93 next_render_buffer: WhichFrameBuffer::Genesis, 94 fully_blank_frame: true, 95 scanline: 0, 96 scanline_mclk: 0, 97 scanlines_in_current_frame: u16::MAX, 98 h_interrupt_this_line: true, 99 h_interrupt_counter: 0, 100 h_interrupt_interval_written: false, 101 vblank_flag: false, 102 display_frame_buffer: SelectedFrameBuffer::default(), 103 auto_fill_mclk_remaining: 0, 104 fb_write_timing_fifo: VecDeque::with_capacity(4), 105 last_fb_write_cycles: 0, 106 cycles_till_next_render: u64::MAX, 107 } 108 } 109} 110 111#[derive(Debug, Clone, Encode, Decode)] 112struct VdpConfig { 113 video_out: S32XVideoOut, 114 color_tint: S32XColorTint, 115 show_high_priority: bool, 116 show_low_priority: bool, 117 void_color: S32XVoidColor, 118} 119 120impl VdpConfig { 121 fn from_emu_config(config: &Sega32XEmulatorConfig) -> Self { 122 Self { 123 video_out: config.video_out, 124 color_tint: config.color_tint, 125 show_high_priority: config.show_high_priority, 126 show_low_priority: config.show_low_priority, 127 void_color: config.void_color, 128 } 129 } 130} 131 132#[derive(Debug, Clone, Encode, Decode)] 133pub struct Vdp { 134 frame_buffer_0: BoxedWordArray<FRAME_BUFFER_LEN_WORDS>, 135 frame_buffer_1: BoxedWordArray<FRAME_BUFFER_LEN_WORDS>, 136 rendered_frame: Boxed2DWordArray<{ V30_FRAME_HEIGHT as usize }, { FRAME_WIDTH as usize }>, 137 // 1280x224 or 1280x240 (not including borders) 138 // Needed for when a game enables H32 mode on the Genesis side (NFL Quarterback Club does this) 139 expanded_frame_buffer: BoxedColorArray<EXPANDED_FRAME_BUFFER_LEN>, 140 cram: BoxedWordArray<CRAM_LEN_WORDS>, 141 registers: Registers, 142 // Per documentation, the VDP latches registers for rendering once per line beginning shortly 143 // after the start of HBlank 144 latched: Registers, 145 state: State, 146 timing_mode: TimingMode, 147 config: VdpConfig, 148} 149 150macro_rules! front_frame_buffer { 151 ($self:expr) => { 152 match $self.state.display_frame_buffer { 153 SelectedFrameBuffer::Zero => &$self.frame_buffer_0, 154 SelectedFrameBuffer::One => &$self.frame_buffer_1, 155 } 156 }; 157} 158 159macro_rules! back_frame_buffer { 160 ($self:expr) => { 161 match $self.state.display_frame_buffer { 162 SelectedFrameBuffer::Zero => &$self.frame_buffer_1, 163 SelectedFrameBuffer::One => &$self.frame_buffer_0, 164 } 165 }; 166} 167 168macro_rules! back_frame_buffer_mut { 169 ($self:expr) => { 170 match $self.state.display_frame_buffer { 171 SelectedFrameBuffer::Zero => &mut $self.frame_buffer_1, 172 SelectedFrameBuffer::One => &mut $self.frame_buffer_0, 173 } 174 }; 175} 176 177impl Vdp { 178 pub fn new(timing_mode: TimingMode, config: &Sega32XEmulatorConfig) -> Self { 179 Self { 180 frame_buffer_0: BoxedWordArray::new(), 181 frame_buffer_1: BoxedWordArray::new(), 182 rendered_frame: Boxed2DWordArray::new(), 183 expanded_frame_buffer: BoxedColorArray::new(), 184 cram: BoxedWordArray::new(), 185 registers: Registers::default(), 186 latched: Registers::default(), 187 state: State::new(), 188 timing_mode, 189 config: VdpConfig::from_emu_config(config), 190 } 191 } 192 193 pub fn tick( 194 &mut self, 195 mclk_cycles: u64, 196 registers: &mut SystemRegisters, 197 vdp_info: GenesisVdpInfo, 198 ) { 199 self.state.auto_fill_mclk_remaining = 200 self.state.auto_fill_mclk_remaining.saturating_sub(mclk_cycles); 201 202 let prev_scanline_mclk = self.state.scanline_mclk; 203 self.state.scanline_mclk += mclk_cycles; 204 205 if prev_scanline_mclk < LATCH_REGISTERS_MCLK_CYCLES 206 && self.state.scanline_mclk >= LATCH_REGISTERS_MCLK_CYCLES 207 { 208 self.latch_registers(); 209 } 210 211 if (self.state.h_interrupt_this_line || self.registers.h_interrupt_in_vblank) 212 && prev_scanline_mclk < HBLANK_START_MCLK_CYCLES 213 && self.state.scanline_mclk >= HBLANK_START_MCLK_CYCLES 214 { 215 self.handle_hblank_start(registers); 216 } 217 218 if prev_scanline_mclk < VBLANK_START_MCLK_CYCLES 219 && self.state.scanline_mclk >= VBLANK_START_MCLK_CYCLES 220 { 221 if self.state.scanline == self.latched.active_scanlines_per_frame - 1 { 222 self.handle_vblank_start(registers, vdp_info); 223 } else if self.state.scanline == self.state.scanlines_in_current_frame - 1 { 224 self.state.vblank_flag = false; 225 registers.notify_vblank_end(); 226 } 227 } 228 229 if self.state.scanline_mclk >= MCLK_CYCLES_PER_SCANLINE { 230 self.state.scanline_mclk -= MCLK_CYCLES_PER_SCANLINE; 231 self.state.scanline += 1; 232 233 if self.state.scanline_mclk >= LATCH_REGISTERS_MCLK_CYCLES { 234 self.latch_registers(); 235 } 236 237 if self.state.scanline == VerticalResolution::V30.active_scanlines_per_frame() 238 && !self.state.vblank_flag 239 { 240 // Workaround for a timing edge case related to V28/V30 mode switch 241 // TODO is it even allowed to switch between V28 and V30 outside of VBlank? 242 self.state.vblank_flag = true; 243 self.state.scanlines_in_current_frame = vdp_info.scanlines_in_current_frame; 244 } 245 246 if self.state.scanline >= self.state.scanlines_in_current_frame { 247 self.state.scanline = 0; 248 } else if self.state.scanline == self.state.scanlines_in_current_frame - 1 { 249 // First HINT before rendering line 0 250 self.state.h_interrupt_this_line = true; 251 } 252 253 if self.state.scanline_mclk >= RENDER_LINE_MCLK_CYCLES { 254 self.handle_hblank_end(); 255 self.state.cycles_till_next_render = 256 MCLK_CYCLES_PER_SCANLINE + RENDER_LINE_MCLK_CYCLES - self.state.scanline_mclk; 257 } 258 } else if prev_scanline_mclk < RENDER_LINE_MCLK_CYCLES 259 && self.state.scanline_mclk >= RENDER_LINE_MCLK_CYCLES 260 { 261 self.handle_hblank_end(); 262 self.state.cycles_till_next_render = 263 MCLK_CYCLES_PER_SCANLINE + RENDER_LINE_MCLK_CYCLES - self.state.scanline_mclk; 264 } 265 } 266 267 fn latch_registers(&mut self) { 268 self.latched = self.registers.clone(); 269 270 // In case VDP was switched to blank mode with a pending frame buffer swap 271 if self.latched.frame_buffer_mode == FrameBufferMode::Blank 272 && self.state.display_frame_buffer != self.registers.display_frame_buffer 273 { 274 self.state.display_frame_buffer = self.registers.display_frame_buffer; 275 log::debug!("Front frame buffer set to {:?}", self.state.display_frame_buffer); 276 } 277 } 278 279 fn handle_hblank_start(&mut self, registers: &mut SystemRegisters) { 280 if self.state.h_interrupt_counter == 0 { 281 self.state.h_interrupt_counter = self.registers.h_interrupt_interval; 282 registers.notify_h_interrupt(); 283 } else { 284 self.state.h_interrupt_counter -= 1; 285 } 286 } 287 288 fn handle_hblank_end(&mut self) { 289 let active_scanlines_per_frame = self.latched.active_scanlines_per_frame; 290 291 if self.state.h_interrupt_interval_written { 292 // HINT interval changes are only latched after HBlanks that could have triggered HINTs 293 if self.registers.h_interrupt_in_vblank 294 || self.state.scanline <= active_scanlines_per_frame 295 { 296 self.state.h_interrupt_counter = self.registers.h_interrupt_interval; 297 self.state.h_interrupt_interval_written = false; 298 } 299 } 300 301 if self.state.scanline < active_scanlines_per_frame { 302 self.render_line(); 303 } 304 } 305 306 fn handle_vblank_start(&mut self, registers: &mut SystemRegisters, vdp_info: GenesisVdpInfo) { 307 if self.state.vblank_flag { 308 return; 309 } 310 311 registers.notify_vblank_start(); 312 313 // Beginning of VBlank; frame buffer switches take effect 314 if self.state.display_frame_buffer != self.registers.display_frame_buffer { 315 log::debug!( 316 "VBlank: Changing front frame buffer to {:?}", 317 self.registers.display_frame_buffer 318 ); 319 } 320 self.state.display_frame_buffer = self.registers.display_frame_buffer; 321 322 // Grab scanlines in frame at start of VBlank to avoid a dependency on which order 323 // the VDPs execute in, since interlacing state is latched at the start of line 0 324 self.state.scanlines_in_current_frame = vdp_info.scanlines_in_current_frame; 325 326 // No HINTs during VBlank (unless the HEN bit is set) 327 self.state.h_interrupt_this_line = false; 328 329 self.state.vblank_flag = true; 330 } 331 332 pub fn mclk_cycles_until_next_event(&self, h_interrupt_enabled: bool) -> u64 { 333 // Sync at every line render and register latch 334 let mut cycles_till_next = self.state.cycles_till_next_render; 335 336 let cycles_till_register_latch = if self.state.scanline_mclk < LATCH_REGISTERS_MCLK_CYCLES { 337 LATCH_REGISTERS_MCLK_CYCLES - self.state.scanline_mclk 338 } else { 339 MCLK_CYCLES_PER_SCANLINE - self.state.scanline_mclk + LATCH_REGISTERS_MCLK_CYCLES 340 }; 341 cycles_till_next = cmp::min(cycles_till_next, cycles_till_register_latch); 342 343 // Sync at HINT if HINT will trigger on this line 344 if h_interrupt_enabled 345 && self.state.h_interrupt_counter == 0 346 && self.state.scanline_mclk < HBLANK_START_MCLK_CYCLES 347 { 348 cycles_till_next = 349 cmp::min(cycles_till_next, HBLANK_START_MCLK_CYCLES - self.state.scanline_mclk); 350 } 351 352 // Sync at VBlank start 353 if self.state.scanline == self.latched.active_scanlines_per_frame - 1 354 && self.state.scanline_mclk < VBLANK_START_MCLK_CYCLES 355 { 356 cycles_till_next = 357 cmp::min(cycles_till_next, VBLANK_START_MCLK_CYCLES - self.state.scanline_mclk); 358 } 359 360 // Sync at auto fill end if an auto fill is in progress 361 if self.state.auto_fill_mclk_remaining != 0 { 362 cycles_till_next = cmp::min(cycles_till_next, self.state.auto_fill_mclk_remaining); 363 } 364 365 cycles_till_next 366 } 367 368 fn render_line(&mut self) { 369 self.state.fully_blank_frame |= self.state.scanline == 0; 370 self.state.fully_blank_frame &= self.latched.frame_buffer_mode == FrameBufferMode::Blank; 371 372 match self.latched.frame_buffer_mode { 373 FrameBufferMode::Blank => { 374 self.rendered_frame[self.state.scanline as usize].fill(0); 375 } 376 FrameBufferMode::PackedPixel => self.render_packed_pixel(), 377 FrameBufferMode::DirectColor => self.render_direct_color(), 378 FrameBufferMode::RunLength => self.render_run_length(), 379 } 380 } 381 382 fn priority_mask_fn(&self) -> impl Fn(u16) -> u16 + 'static { 383 let vdp_priority = u16::from(self.latched.priority) << 15; 384 let show_high_priority = self.config.show_high_priority; 385 let show_low_priority = self.config.show_low_priority; 386 let void_color = match self.config.void_color { 387 S32XVoidColor::PaletteRam { idx } => self.cram[idx as usize], 388 S32XVoidColor::Direct { r, g, b, a } => { 389 u16::from(r & 0x1F) 390 | (u16::from(g & 0x1F) << 5) 391 | (u16::from(b & 0x1F) << 10) 392 | (u16::from(a) << 15) 393 } 394 }; 395 396 move |mut pixel| { 397 let pixel_priority = pixel.bit(15); 398 if (pixel_priority && !show_high_priority) || (!pixel_priority && !show_low_priority) { 399 pixel = void_color; 400 } 401 pixel ^ vdp_priority 402 } 403 } 404 405 fn render_packed_pixel(&mut self) { 406 let line = self.state.scanline as usize; 407 let frame_buffer = front_frame_buffer!(self); 408 let line_addr = frame_buffer[line]; 409 410 log::trace!( 411 "Rendering line {line} from buffer {:?} in packed pixel mode, addr={line_addr:04X}", 412 self.state.display_frame_buffer 413 ); 414 415 let mask_fn = self.priority_mask_fn(); 416 417 if self.screen_left_shift(line_addr) { 418 for pixel in 0..FRAME_WIDTH as u16 { 419 let frame_buffer_addr = line_addr.wrapping_add((pixel + 1) >> 1); 420 let color = (frame_buffer[frame_buffer_addr as usize] >> (8 * (pixel & 1))) & 0xFF; 421 422 self.rendered_frame[line][pixel as usize] = mask_fn(self.cram[color as usize]); 423 } 424 } else { 425 for pixel in (0..FRAME_WIDTH as u16).step_by(2) { 426 let frame_buffer_addr = line_addr.wrapping_add(pixel >> 1); 427 let [msb, lsb] = frame_buffer[frame_buffer_addr as usize].to_be_bytes(); 428 429 self.rendered_frame[line][pixel as usize] = mask_fn(self.cram[msb as usize]); 430 self.rendered_frame[line][(pixel + 1) as usize] = mask_fn(self.cram[lsb as usize]); 431 } 432 } 433 } 434 435 fn render_direct_color(&mut self) { 436 let line = self.state.scanline as usize; 437 let frame_buffer = front_frame_buffer!(self); 438 let mut line_addr = frame_buffer[line]; 439 440 log::trace!( 441 "Rendering line {line} from frame buffer {:?} in direct color mode, addr={line_addr:04X}", 442 self.state.display_frame_buffer 443 ); 444 445 let mask_fn = self.priority_mask_fn(); 446 447 if self.screen_left_shift(line_addr) { 448 line_addr = line_addr.wrapping_add(1); 449 } 450 451 for pixel in 0..FRAME_WIDTH as u16 { 452 let color = frame_buffer[line_addr as usize]; 453 self.rendered_frame[line][pixel as usize] = mask_fn(color); 454 455 line_addr = line_addr.wrapping_add(1); 456 } 457 } 458 459 fn render_run_length(&mut self) { 460 let line = self.state.scanline as usize; 461 let frame_buffer = front_frame_buffer!(self); 462 let mut line_addr = frame_buffer[line]; 463 464 log::trace!( 465 "Rendering line {line} from buffer {:?} in run length mode, addr={line_addr:04X}", 466 self.state.display_frame_buffer 467 ); 468 469 let mask_fn = self.priority_mask_fn(); 470 471 let mut skip_first = self.screen_left_shift(line_addr); 472 473 let mut pixel = 0; 474 while pixel < FRAME_WIDTH { 475 let [run_length_byte, color_idx] = frame_buffer[line_addr as usize].to_be_bytes(); 476 line_addr = line_addr.wrapping_add(1); 477 478 let color = self.cram[color_idx as usize]; 479 let mut run_length = u16::from(run_length_byte) + 1; 480 481 while pixel < FRAME_WIDTH && run_length != 0 { 482 run_length -= 1; 483 if skip_first { 484 skip_first = false; 485 continue; 486 } 487 488 self.rendered_frame[line][pixel as usize] = mask_fn(color); 489 pixel += 1; 490 } 491 } 492 } 493 494 fn screen_left_shift(&self, line_address: u16) -> bool { 495 // Screen shift does not apply when the low byte of the line address is 0xFF 496 // Mentioned in 32X hardware manual supplement and verified on hardware 497 self.latched.screen_left_shift && line_address.lsb() != 0xFF 498 } 499 500 pub fn read_register(&self, address: u32) -> u16 { 501 match address & 0xF { 502 0x0 => self.registers.read_display_mode(self.timing_mode), 503 0x2 => self.registers.read_screen_shift(), 504 0x4 => self.registers.read_auto_fill_length(), 505 0x6 => self.registers.read_auto_fill_start_address(), 506 0xA => self.read_frame_buffer_control(), 507 _ => { 508 log::warn!("Invalid VDP register read {address:08X}"); 509 0 510 } 511 } 512 } 513 514 pub fn write_register_byte(&mut self, address: u32, value: u8) { 515 let mut word = self.read_register(address & !1); 516 if !address.bit(0) { 517 word.set_msb(value); 518 } else { 519 word.set_lsb(value); 520 } 521 self.write_register(address & !1, word); 522 } 523 524 pub fn write_register(&mut self, address: u32, value: u16) { 525 log::trace!( 526 "VDP register write on line {}: {address:08X} {value:04X}", 527 self.state.scanline 528 ); 529 530 match address & 0xF { 531 0x0 => self.registers.write_display_mode(value), 532 0x2 => self.registers.write_screen_shift(value), 533 0x4 => self.registers.write_auto_fill_length(value), 534 0x6 => self.registers.write_auto_fill_start_address(value), 535 0x8 => { 536 self.registers.write_auto_fill_data(value); 537 538 // Writing auto fill data initiates auto fill 539 self.do_auto_fill(); 540 } 541 0xA => { 542 self.registers.write_frame_buffer_control(value); 543 if self.state.vblank_flag 544 || self.latched.frame_buffer_mode == FrameBufferMode::Blank 545 { 546 self.state.display_frame_buffer = self.registers.display_frame_buffer; 547 log::debug!("Front frame buffer set to {:?}", self.state.display_frame_buffer); 548 } 549 } 550 0xC | 0xE => { 551 log::warn!("Invalid VDP register write {address:08X} {value:04X}"); 552 } 553 _ => panic!("VDP register write to an unaligned address: {address:08X} {value:04X}"), 554 } 555 } 556 557 pub fn read_frame_buffer(&self, address: u32) -> u16 { 558 let frame_buffer = back_frame_buffer!(self); 559 frame_buffer[((address & 0x1FFFF) >> 1) as usize] 560 } 561 562 pub fn write_frame_buffer_byte(&mut self, address: u32, value: u8) { 563 log::trace!( 564 "Frame buffer byte write {:05X} {value:02X} (word addr {:04X})", 565 address & 0x1FFFF, 566 (address >> 1) & 0xFFFF 567 ); 568 569 if value == 0 { 570 return; 571 } 572 573 let frame_buffer = back_frame_buffer_mut!(self); 574 let frame_buffer_addr = ((address & 0x1FFFF) >> 1) as usize; 575 576 if !address.bit(0) { 577 frame_buffer[frame_buffer_addr].set_msb(value); 578 } else { 579 frame_buffer[frame_buffer_addr].set_lsb(value); 580 } 581 } 582 583 pub fn write_frame_buffer_word(&mut self, address: u32, value: u16) { 584 log::trace!( 585 "Frame buffer write {:05X} {value:04X} (word addr {:04X})", 586 address & 0x1FFFF, 587 (address >> 1) & 0xFFFF 588 ); 589 590 let frame_buffer = back_frame_buffer_mut!(self); 591 frame_buffer[((address & 0x1FFFF) >> 1) as usize] = value; 592 } 593 594 pub fn frame_buffer_overwrite_word(&mut self, address: u32, value: u16) { 595 log::trace!( 596 "Overwrite image write {:05X} {value:04X} (word addr {:04X})", 597 address & 0x1FFFF, 598 (address >> 1) & 0xFFFF 599 ); 600 601 if value == 0 { 602 return; 603 } 604 605 let frame_buffer = back_frame_buffer_mut!(self); 606 let frame_buffer_addr = ((address & 0x1FFFF) >> 1) as usize; 607 608 let [msb, lsb] = value.to_be_bytes(); 609 610 if msb != 0 { 611 frame_buffer[frame_buffer_addr].set_msb(msb); 612 } 613 614 if lsb != 0 { 615 frame_buffer[frame_buffer_addr].set_lsb(lsb); 616 } 617 } 618 619 #[must_use] 620 pub fn frame_buffer_write_latency(&mut self, cycles: u64) -> u64 { 621 if cycles <= self.state.last_fb_write_cycles { 622 // Can happen if both SH-2s are writing to the frame buffer simultaneously 623 // Just ignore and return minimum latency 624 return 1; 625 } 626 627 // Progress times in FIFO 628 let cycle_diff = cycles - self.state.last_fb_write_cycles; 629 for _ in 0..cycle_diff { 630 let Some(front) = self.state.fb_write_timing_fifo.front_mut() else { break }; 631 632 *front -= 1; 633 if *front == 0 { 634 self.state.fb_write_timing_fifo.pop_front(); 635 } 636 } 637 638 // VDP can only accept 1 write every 4 cycles 639 let initial_write_time = if cycle_diff < 3 { 3 - (cycle_diff - 1) } else { 1 }; 640 641 // If the 4-entry FIFO is full, must wait for an empty slot 642 let fifo_wait_time = if self.state.fb_write_timing_fifo.len() == 4 { 643 1 + self.state.fb_write_timing_fifo.pop_front().unwrap() 644 } else { 645 0 646 }; 647 648 let wait_cycles = cmp::max(initial_write_time, fifo_wait_time); 649 debug_assert!((1..=5).contains(&wait_cycles)); 650 651 self.state.fb_write_timing_fifo.push_back(5); 652 self.state.last_fb_write_cycles = cycles + wait_cycles - 1; 653 654 wait_cycles 655 } 656 657 pub fn read_cram(&self, address: u32) -> u16 { 658 // TODO block access to CRAM when in use? 659 self.cram[((address & 0x1FF) >> 1) as usize] 660 } 661 662 pub fn write_cram(&mut self, address: u32, value: u16) { 663 // TODO block access to CRAM while in use? 664 self.cram[((address & 0x1FF) >> 1) as usize] = value; 665 } 666 667 // Interrupt mask bit 7: HEN (H interrupts enabled during VBlank) 668 pub fn hen_bit(&self) -> bool { 669 self.registers.h_interrupt_in_vblank 670 } 671 672 // Interrupt mask bit 7: HEN (H interrupts enabled during VBlank) 673 pub fn write_hen_bit(&mut self, hen: bool) { 674 self.registers.h_interrupt_in_vblank = hen; 675 } 676 677 // SH-2: $4004 678 pub fn h_interrupt_interval(&self) -> u16 { 679 self.registers.h_interrupt_interval 680 } 681 682 // SH-2: $4004 683 pub fn write_h_interrupt_interval(&mut self, value: u16) { 684 self.registers.h_interrupt_interval = value & 0xFF; 685 self.state.h_interrupt_interval_written = true; 686 log::trace!("H interrupt interval write: {value:04X}"); 687 } 688 689 // 68000: $A1518A 690 // SH-2: $410A 691 fn read_frame_buffer_control(&self) -> u16 { 692 let in_vblank = self.state.vblank_flag; 693 let in_hblank = self.in_hblank(); 694 695 let cram_accessible = in_vblank || in_hblank; 696 697 // Metal Head depends on the FEN bit reading 1 during DRAM refresh (beginning of HBlank every line) 698 // or else it will freeze after the Sega splash screen 699 let frame_buffer_busy = self.state.auto_fill_mclk_remaining != 0 700 || DRAM_REFRESH_MCLK_CYCLES.contains(&self.state.scanline_mclk); 701 702 (u16::from(in_vblank) << 15) 703 | (u16::from(in_hblank) << 14) 704 | (u16::from(cram_accessible) << 13) 705 | (u16::from(frame_buffer_busy) << 1) 706 | (self.state.display_frame_buffer as u16) 707 } 708 709 fn do_auto_fill(&mut self) { 710 let frame_buffer = back_frame_buffer_mut!(self); 711 712 let data = self.registers.auto_fill_data; 713 for _ in 0..self.registers.auto_fill_length { 714 frame_buffer[self.registers.auto_fill_start_address as usize] = data; 715 self.registers.increment_auto_fill_address(); 716 } 717 718 // Note: Auto fill finishing too quickly will cause major glitches in Mortal Kombat II. 719 // 720 // At VINT, the master SH-2 immediately starts drawing the next frame without syncing with 721 // the 68000. It first zeroes out the frame buffer using auto fills and then starts drawing 722 // sprites and the HUD. 723 // 724 // If the auto fills finish before the 68000 interrupts the master SH-2 to send updated I/O 725 // data and then the SH-2 sees that user inputs have changed, it will get very confused and 726 // draw a single glitched frame that was partly drawn using the previous inputs and partly 727 // drawn using the new inputs. Depending on emulated SH-2 speed, this can also cause 728 // gameplay glitches as the game may briefly stop responding to user inputs. 729 // 730 // Official documentation appears to say that auto fill takes (7 + 3 * length) Sclk cycles 731 let auto_fill_sclk = 7 + 3 * u64::from(self.registers.auto_fill_length); 732 self.state.auto_fill_mclk_remaining = auto_fill_sclk * 7 / 3; 733 } 734 735 fn in_hblank(&self) -> bool { 736 !(HBLANK_END_MCLK_CYCLES..HBLANK_START_MCLK_CYCLES).contains(&self.state.scanline_mclk) 737 } 738 739 pub fn scanline(&self) -> u16 { 740 self.state.scanline 741 } 742 743 pub fn scanline_mclk(&self) -> u64 { 744 self.state.scanline_mclk 745 } 746 747 pub fn composite_frame(&mut self, genesis_vdp: &mut GenesisVdp) { 748 // Default to rendering from the Genesis VDP frame buffer, switch later if necessary 749 self.state.next_render_buffer = WhichFrameBuffer::Genesis; 750 751 if (self.config.video_out != S32XVideoOut::S32XOnly && self.state.fully_blank_frame) 752 || self.config.video_out == S32XVideoOut::GenesisOnly 753 { 754 // Leave Genesis frame as-is 755 return; 756 } 757 758 let genesis_frame_size = genesis_vdp.frame_size(); 759 let border_size = genesis_vdp.border_size(); 760 761 let h32 = genesis_frame_size.width - border_size.left - border_size.right == 256; 762 if h32 { 763 // If the Genesis VDP is in H32 mode, render the frame in 1280x224 so that it's possible 764 // to composite the 320x224 32X frame with the 256x224 Genesis frame without needing to 765 // blend or filter any pixels. 766 // NFL Quarterback Club depends on this - it uses H32 mode in menus 767 self.composite_frame_expanded::<true>(genesis_frame_size, border_size, genesis_vdp); 768 return; 769 } 770 771 // Otherwise, composite in H40 into the Genesis VDP frame buffer 772 self.composite_frame_inner::<false, false>(genesis_frame_size, border_size, genesis_vdp); 773 } 774 775 fn composite_frame_expanded<const H32: bool>( 776 &mut self, 777 genesis_frame_size: FrameSize, 778 border_size: BorderSize, 779 genesis_vdp: &mut GenesisVdp, 780 ) { 781 let genesis_frame_width = 782 genesis_frame_size.width.wrapping_sub(border_size.left).wrapping_sub(border_size.right); 783 if H32 { 784 debug_assert_eq!(genesis_frame_width, 256); 785 } else { 786 debug_assert_eq!(genesis_frame_width, 320); 787 } 788 789 self.state.next_render_buffer = 790 if H32 { WhichFrameBuffer::ExpandedH32 } else { WhichFrameBuffer::ExpandedH40 }; 791 792 if self.config.video_out != S32XVideoOut::S32XOnly { 793 self.copy_genesis_frame_buffer_to_expanded::<H32>( 794 genesis_frame_size, 795 border_size, 796 genesis_vdp.frame_buffer_mut(), 797 ); 798 } else { 799 self.expanded_frame_buffer.fill(Color::rgba(0, 0, 0, 0)); 800 } 801 802 self.composite_frame_inner::<H32, true>(genesis_frame_size, border_size, genesis_vdp); 803 } 804 805 fn composite_frame_inner<const H32: bool, const EXPAND_H: bool>( 806 &mut self, 807 frame_size: FrameSize, 808 border_size: BorderSize, 809 genesis_vdp: &mut GenesisVdp, 810 ) { 811 fn should_use_32x_pixel(s32x_only: bool, s32x_pixel: u16, gen_color: Color) -> bool { 812 s32x_only || s32x_pixel.bit(15) || gen_color.a == 0 813 } 814 815 assert!( 816 !H32 || EXPAND_H, 817 "Does not make sense to not expand if Genesis VDP is in H32 mode" 818 ); 819 820 let interlaced_frame: u32 = genesis_vdp.is_interlaced_frame().into(); 821 let interlaced_odd: u32 = genesis_vdp.is_interlaced_odd().into(); 822 823 let frame_buffer = if EXPAND_H { 824 self.expanded_frame_buffer.as_mut_slice() 825 } else { 826 genesis_vdp.frame_buffer_mut() 827 }; 828 829 let active_lines_per_frame: u32 = self.latched.active_scanlines_per_frame.into(); 830 let s32x_only = self.config.video_out == S32XVideoOut::S32XOnly; 831 832 let frame_width = if EXPAND_H { 833 determine_expanded_buffer_width::<H32>(frame_size, border_size) 834 } else { 835 frame_size.width 836 }; 837 let left_offset = match (EXPAND_H, H32) { 838 (false, _) => border_size.left, 839 (true, false) => genesis_expanded_pixel_width::<false>() * border_size.left, 840 (true, true) => { 841 genesis_expanded_pixel_width::<true>() * border_size.left + H32_H_OFFSET 842 } 843 }; 844 845 let top_offset = border_size.top << interlaced_frame; 846 847 let color_tables = ColorTables::from_tint(self.config.color_tint); 848 849 for line in 0..active_lines_per_frame { 850 let effective_line = (line << interlaced_frame) + interlaced_odd; 851 let fb_row_addr = ((effective_line + top_offset) * frame_width + left_offset) as usize; 852 853 if EXPAND_H { 854 for pixel in 0..FRAME_WIDTH { 855 let s32x_pixel = self.rendered_frame[line as usize][pixel as usize]; 856 let fb_addr = fb_row_addr + 4 * pixel as usize; 857 858 for i in 0..4 { 859 if should_use_32x_pixel(s32x_only, s32x_pixel, frame_buffer[fb_addr + i]) { 860 frame_buffer[fb_addr + i] = u16_to_rgb(s32x_pixel, color_tables); 861 } 862 } 863 } 864 } else { 865 for pixel in 0..FRAME_WIDTH { 866 let s32x_pixel = self.rendered_frame[line as usize][pixel as usize]; 867 let fb_addr = fb_row_addr + pixel as usize; 868 869 if should_use_32x_pixel(s32x_only, s32x_pixel, frame_buffer[fb_addr]) { 870 frame_buffer[fb_addr] = u16_to_rgb(s32x_pixel, color_tables); 871 } 872 } 873 } 874 } 875 876 // TODO how do interlaced modes actually work with 32X? Needs testing 877 if interlaced_frame != 0 { 878 for line in 0..active_lines_per_frame { 879 let from_line = 2 * line + interlaced_odd; 880 let to_line = from_line ^ 1; 881 882 let from_line_fb_addr = ((from_line + top_offset) * frame_width) as usize; 883 let to_line_fb_addr = ((to_line + top_offset) * frame_width) as usize; 884 885 if to_line_fb_addr > from_line_fb_addr { 886 let (a, b) = frame_buffer.split_at_mut(to_line_fb_addr); 887 b[..frame_width as usize].copy_from_slice( 888 &a[from_line_fb_addr..from_line_fb_addr + frame_width as usize], 889 ); 890 } else { 891 let (a, b) = frame_buffer.split_at_mut(from_line_fb_addr); 892 a[to_line_fb_addr..to_line_fb_addr + frame_width as usize] 893 .copy_from_slice(&b[..frame_width as usize]); 894 } 895 } 896 } 897 } 898 899 fn copy_genesis_frame_buffer_to_expanded<const H32: bool>( 900 &mut self, 901 frame_size: FrameSize, 902 border_size: BorderSize, 903 frame_buffer: &mut [Color; genesis_components::vdp::FRAME_BUFFER_LEN], 904 ) { 905 let expanded_frame_width = determine_expanded_buffer_width::<H32>(frame_size, border_size); 906 let gen_pixel_width = genesis_expanded_pixel_width::<H32>(); 907 908 // Expand the frame buffer from 256x224 to 1280x224 (plus borders) 909 for line in 0..frame_size.height { 910 let h32_fb_line_addr = line * expanded_frame_width; 911 for pixel in 0..frame_size.width { 912 let genesis_fb_addr = (line * frame_size.width + pixel) as usize; 913 let h32_fb_addr = (h32_fb_line_addr + gen_pixel_width * pixel) as usize; 914 self.expanded_frame_buffer[h32_fb_addr..h32_fb_addr + gen_pixel_width as usize] 915 .fill(frame_buffer[genesis_fb_addr]); 916 } 917 918 if gen_pixel_width * frame_size.width != expanded_frame_width { 919 // Clear right border pixels so 32X pixels will always display over them 920 self.expanded_frame_buffer[(h32_fb_line_addr + gen_pixel_width * frame_size.width) 921 as usize 922 ..(h32_fb_line_addr + expanded_frame_width) as usize] 923 .fill(Color::rgba(0, 0, 0, 0)); 924 } 925 } 926 } 927 928 pub fn render_frame<R: Renderer>( 929 &self, 930 genesis_vdp: &GenesisVdp, 931 aspect_ratio: Option<FiniteF64>, 932 renderer: &mut R, 933 ) -> Result<(), R::Err> { 934 if self.state.next_render_buffer == WhichFrameBuffer::Genesis { 935 let target_fps = 936 genesis_components::target_framerate(genesis_vdp, genesis_vdp.timing_mode()); 937 return renderer.render_frame( 938 genesis_vdp.frame_buffer(), 939 genesis_vdp.frame_size(), 940 target_fps, 941 RenderFrameOptions { 942 pixel_aspect_ratio: aspect_ratio, 943 composite_params: Some(genesis_vdp.composite_params()), 944 ..RenderFrameOptions::default() 945 }, 946 ); 947 } 948 949 let h32 = self.state.next_render_buffer == WhichFrameBuffer::ExpandedH32; 950 if h32 { 951 self.render_expanded_frame::<true, _>(genesis_vdp, aspect_ratio, renderer) 952 } else { 953 self.render_expanded_frame::<false, _>(genesis_vdp, aspect_ratio, renderer) 954 } 955 } 956 957 fn render_expanded_frame<const H32: bool, R: Renderer>( 958 &self, 959 genesis_vdp: &GenesisVdp, 960 mut aspect_ratio: Option<FiniteF64>, 961 renderer: &mut R, 962 ) -> Result<(), R::Err> { 963 let mut frame_size = genesis_vdp.frame_size(); 964 965 frame_size.width = 966 determine_expanded_buffer_width::<H32>(frame_size, genesis_vdp.border_size()); 967 968 let gen_pixel_width = genesis_expanded_pixel_width::<H32>(); 969 aspect_ratio = aspect_ratio 970 .map(|par| par * FiniteF64::try_from(1.0 / f64::from(gen_pixel_width)).unwrap()); 971 972 let target_fps = 973 genesis_components::target_framerate(genesis_vdp, genesis_vdp.timing_mode()); 974 renderer.render_frame( 975 self.expanded_frame_buffer.as_ref(), 976 frame_size, 977 target_fps, 978 RenderFrameOptions { 979 pixel_aspect_ratio: aspect_ratio, 980 composite_params: Some(CompositeParams { 981 upscale_factor: 2, 982 samples_per_color_cycle: SamplesPerColorCycle::Fifteen, 983 }), 984 ..RenderFrameOptions::default() 985 }, 986 ) 987 } 988 989 pub fn reload_config(&mut self, config: &Sega32XEmulatorConfig) { 990 self.config = VdpConfig::from_emu_config(config); 991 } 992} 993 994const RGB_5_TO_8: &[u8; 32] = &[ 995 0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, 996 181, 189, 197, 206, 214, 222, 230, 239, 247, 255, 997];
Scaled to 0-251 instead of 0-255
Scaled to 0-247 instead of 0-255
1011#[derive(Debug, Clone, Copy)] 1012struct ColorTables { 1013 red: &'static [u8; 32], 1014 green: &'static [u8; 32], 1015 blue: &'static [u8; 32], 1016} 1017 1018impl ColorTables { 1019 fn from_tint(color_tint: S32XColorTint) -> Self { 1020 match color_tint { 1021 S32XColorTint::None => Self { red: RGB_5_TO_8, green: RGB_5_TO_8, blue: RGB_5_TO_8 }, 1022 // yellow tint = blue deficiency 1023 S32XColorTint::SlightYellow => { 1024 Self { red: RGB_5_TO_8, green: RGB_5_TO_8, blue: RGB_5_TO_8_SLIGHT_DARK } 1025 } 1026 S32XColorTint::Yellow => { 1027 Self { red: RGB_5_TO_8, green: RGB_5_TO_8, blue: RGB_5_TO_8_DARK } 1028 } 1029 // purple tint = green deficiency 1030 S32XColorTint::SlightPurple => { 1031 Self { red: RGB_5_TO_8, green: RGB_5_TO_8_SLIGHT_DARK, blue: RGB_5_TO_8 } 1032 } 1033 S32XColorTint::Purple => { 1034 Self { red: RGB_5_TO_8, green: RGB_5_TO_8_DARK, blue: RGB_5_TO_8 } 1035 } 1036 } 1037 } 1038} 1039 1040fn u16_to_rgb(s32x_pixel: u16, color_tables: ColorTables) -> Color { 1041 let r = s32x_pixel & 0x1F; 1042 let g = (s32x_pixel >> 5) & 0x1F; 1043 let b = (s32x_pixel >> 10) & 0x1F; 1044 1045 Color::rgb( 1046 color_tables.red[r as usize], 1047 color_tables.green[g as usize], 1048 color_tables.blue[b as usize], 1049 ) 1050} 1051 1052const fn genesis_expanded_pixel_width<const H32: bool>() -> u32 { 1053 if H32 { 5 } else { 4 } 1054} 1055 1056fn determine_expanded_buffer_width<const H32: bool>( 1057 frame_size: FrameSize, 1058 border_size: BorderSize, 1059) -> u32 { 1060 let gen_pixel_width = genesis_expanded_pixel_width::<H32>(); 1061 1062 if H32 && border_size.right < H32_H_OFFSET.div_ceil(gen_pixel_width) { 1063 gen_pixel_width * frame_size.width + H32_H_OFFSET 1064 } else { 1065 gen_pixel_width * frame_size.width 1066 } 1067}