1use crate::vdp; 2use crate::vdp::colors::{ColorModifier, ColorTables}; 3use crate::vdp::registers::{ 4 DebugRegister, HorizontalDisplaySize, InterlacingMode, Plane, RIGHT_BORDER, Registers, 5 ScrollSize, VerticalDisplaySize, VerticalScrollMode, 6}; 7use crate::vdp::{TilePixel, TimingModeExt, Vdp, Vram, Vsram, colors}; 8use jgenesis_common::frontend::{Color, TimingMode}; 9use jgenesis_common::num::GetBit; 10use std::{array, cmp}; 11 12#[derive(Debug, Clone, Copy, PartialEq, Eq)] 13enum BgPlane { 14 A, 15 B, 16} 17 18#[derive(Debug, Clone, Copy, PartialEq, Eq)] 19pub struct RasterLine { 20 pub line: u16, 21 pub in_v_border: bool, 22} 23 24impl RasterLine { 25 pub fn from_scanline( 26 scanline: u16, 27 registers: &Registers, 28 timing_mode: TimingMode, 29 interlaced: bool, 30 interlaced_odd: bool, 31 ) -> Self { 32 let v_display_size = registers.vertical_display_size; 33 let active_scanlines = v_display_size.active_scanlines(); 34 let scanlines_per_frame = timing_mode.scanlines_per_frame(interlaced, interlaced_odd); 35 let top_border = v_display_size.top_border(timing_mode); 36 37 if scanline < active_scanlines { 38 // Active display 39 Self { line: scanline, in_v_border: false } 40 } else if scanline >= scanlines_per_frame - top_border { 41 // Top border; bottom line is raster line 511 42 let line = 512 - (scanlines_per_frame - scanline); 43 Self { line, in_v_border: true } 44 } else { 45 // Bottom border and VBlank 46 Self { line: scanline, in_v_border: true } 47 } 48 } 49 50 pub fn to_interlaced_even(self) -> Self { 51 Self { line: (2 * self.line) & 0x1FF, in_v_border: self.in_v_border } 52 } 53 54 pub fn to_interlaced_odd(self) -> Self { 55 Self { line: (2 * self.line + 1) & 0x1FF, in_v_border: self.in_v_border } 56 } 57 58 pub fn to_frame_buffer_row( 59 self, 60 top_border: u16, 61 timing_mode: TimingMode, 62 render_vertical_border: bool, 63 ) -> Option<u32> { 64 if render_vertical_border { 65 if self.line >= 512 - top_border { 66 // Top border 67 Some((self.line - (512 - top_border)).into()) 68 } else if self.line < timing_mode.rendered_lines_per_frame() - top_border { 69 // Active display or bottom border 70 Some((self.line + top_border).into()) 71 } else { 72 // VBlank 73 None 74 } 75 } else { 76 // If not rendering the vertical border, frame buffer row == raster line 77 (!self.in_v_border).then_some(self.line.into()) 78 } 79 } 80 81 pub fn previous_line(self, v_display_size: VerticalDisplaySize) -> Self { 82 if self.line == 0 { 83 Self { line: 511, in_v_border: true } 84 } else { 85 let line = self.line - 1; 86 Self { line, in_v_border: line >= v_display_size.active_scanlines() } 87 } 88 } 89} 90 91impl Vdp { 92 pub(super) fn render_scanline(&mut self, scanline: u16, starting_pixel: u16) { 93 if starting_pixel 94 >= self.latched_registers.horizontal_display_size.active_display_pixels() - 16 95 { 96 // Don't re-render for mid-scanline writes that occur very near the end of active display; 97 // this can cause visual glitches due to some underlying issues in how timing is handled 98 // between the 68000 and VDP 99 return; 100 } 101 102 log::trace!("Rendering line {scanline} from pixel {starting_pixel}"); 103 104 let raster_line = RasterLine::from_scanline( 105 scanline, 106 &self.latched_registers, 107 self.timing_mode, 108 self.state.interlaced_frame, 109 self.state.interlaced_odd, 110 ); 111 let frame_buffer_row = raster_line.to_frame_buffer_row( 112 self.state.top_border, 113 self.timing_mode, 114 self.config.render_vertical_border, 115 ); 116 117 // TODO interlacing mode should be latched at start of VBlank 118 match self.latched_registers.interlacing_mode { 119 InterlacingMode::Progressive => { 120 self.do_render_scanline( 121 scanline, 122 raster_line, 123 starting_pixel, 124 frame_buffer_row, 125 false, 126 ); 127 } 128 InterlacingMode::Interlaced => { 129 let odd_frame = self.state.interlaced_odd(); 130 let frame_buffer_row = frame_buffer_row.map(|row| 2 * row + u32::from(odd_frame)); 131 132 self.do_render_scanline( 133 scanline, 134 raster_line, 135 starting_pixel, 136 frame_buffer_row, 137 false, 138 ); 139 140 if self.config.deinterlace 141 && let Some(frame_buffer_row) = frame_buffer_row 142 { 143 self.copy_frame_buffer_row(frame_buffer_row, frame_buffer_row ^ 1); 144 } 145 } 146 InterlacingMode::InterlacedDouble => { 147 let odd_frame = self.state.interlaced_odd(); 148 149 if self.config.deinterlace || !odd_frame { 150 self.do_render_scanline( 151 scanline, 152 raster_line.to_interlaced_even(), 153 starting_pixel, 154 frame_buffer_row.map(|row| 2 * row), 155 false, 156 ); 157 } 158 if self.config.deinterlace || odd_frame { 159 self.do_render_scanline( 160 scanline, 161 raster_line.to_interlaced_odd(), 162 starting_pixel, 163 frame_buffer_row.map(|row| 2 * row + 1), 164 true, 165 ); 166 } 167 } 168 } 169 170 if starting_pixel == 0 { 171 self.apply_cram_dots_previous_line(raster_line); 172 } 173 } 174 175 fn do_render_scanline( 176 &mut self, 177 scanline: u16, 178 raster_line: RasterLine, 179 starting_pixel: u16, 180 frame_buffer_row: Option<u32>, 181 interlaced_odd_line: bool, 182 ) { 183 if !self.registers.display_enabled { 184 let Some(frame_buffer_row) = frame_buffer_row else { return }; 185 186 let bg_color = self.backdrop_color(); 187 self.fill_frame_buffer_row(frame_buffer_row, starting_pixel, bg_color); 188 189 // Clear sprite pixel buffer in case display is enabled during active display 190 if interlaced_odd_line { 191 self.interlaced_sprite_buffers.pixels.fill(TilePixel::default()); 192 } else { 193 self.sprite_buffers.pixels.fill(TilePixel::default()); 194 } 195 196 return; 197 } 198 199 // Only perform sprite pixel and/or right border rendering if rendering from the start of the line 200 if starting_pixel == 0 { 201 // Sprite pixel rendering + tile fetching is not performed inside the non-forgotten vertical border except 202 // on the line immediately following the end of active display 203 if !raster_line.in_v_border 204 || self.state.v_border_forgotten 205 || raster_line.line 206 == self.latched_registers.vertical_display_size.active_scanlines() 207 { 208 self.render_sprite_pixels(raster_line, interlaced_odd_line); 209 } 210 211 // Check if the previous line's right border should be rendered 212 // This needs to happen after the previous line is rendered because it depends on which sprite tiles were 213 // fetched for the next/current line 214 if self.config.render_horizontal_border 215 && matches!(self.debug_register.forced_plane, Plane::ScrollA | Plane::ScrollB) 216 { 217 let prev_raster_line = 218 raster_line.previous_line(self.latched_registers.vertical_display_size); 219 if !prev_raster_line.in_v_border 220 || self.state.v_border_forgotten 221 || prev_raster_line.line == 511 222 { 223 // TODO this does not work correctly in interlaced modes - wrong frame buffer row 224 if let Some(right_border_row) = prev_raster_line.to_frame_buffer_row( 225 self.state.top_border, 226 self.timing_mode, 227 self.config.render_vertical_border, 228 ) { 229 self.render_right_border(right_border_row, self.latched_h_scroll); 230 } 231 } 232 } 233 } 234 235 let Some(frame_buffer_row) = frame_buffer_row else { return }; 236 237 if raster_line.in_v_border && !self.state.v_border_forgotten && raster_line.line != 511 { 238 self.render_vertical_border_line(scanline, frame_buffer_row, starting_pixel); 239 return; 240 } 241 242 let active_display_pixels = 243 self.latched_registers.horizontal_display_size.active_display_pixels(); 244 let (fb_start_col, fb_end_col, fb_pixel_offset) = if self.config.render_horizontal_border { 245 let left_border: u32 = 246 self.latched_registers.horizontal_display_size.left_border().into(); 247 let start_col = 248 if starting_pixel == 0 { 0 } else { u32::from(starting_pixel) + left_border }; 249 let end_col = left_border + u32::from(active_display_pixels) + u32::from(RIGHT_BORDER); 250 251 (start_col as i16, end_col as i16, left_border as i16) 252 } else { 253 (starting_pixel as i16, active_display_pixels as i16, 0) 254 }; 255 256 self.render_bgs_to_buffer(raster_line.line, fb_start_col, fb_end_col, fb_pixel_offset); 257 self.render_window_to_buffer(raster_line.line, fb_start_col, fb_end_col, fb_pixel_offset); 258 259 self.merge_layers( 260 raster_line, 261 frame_buffer_row, 262 fb_start_col as u32, 263 fb_end_col as u32, 264 fb_pixel_offset, 265 interlaced_odd_line, 266 ); 267 268 // TODO the left border should really be rendered after sprite tile fetching and reading 269 // H scroll values, but before rendering the background planes. Otherwise it uses the 270 // scroll B palettes from the current line instead of the previous line. 271 if self.config.render_horizontal_border { 272 self.render_left_border(frame_buffer_row, self.backdrop_color(), self.latched_h_scroll); 273 274 // If not using debug register to force one of the BG planes, render right border now 275 // in case the backdrop color changes between lines 276 if matches!(self.debug_register.forced_plane, Plane::Background | Plane::Sprite) { 277 self.render_right_border(frame_buffer_row, self.latched_h_scroll); 278 } 279 } 280 } 281 282 fn fill_frame_buffer_row(&mut self, row: u32, starting_pixel: u16, color: u16) { 283 let screen_width = self.screen_width(); 284 285 // TODO properly handle BG color changes inside the left border - requires adjusting when 286 // line rendering occurs, or adding some state to track which pixels not to overwrite 287 let left_border = if self.config.render_horizontal_border { 288 self.state.frame_h_resolution.left_border() 289 } else { 290 0 291 }; 292 let starting_col = 293 if starting_pixel == 0 { 0 } else { u32::from(starting_pixel + left_border) }; 294 295 for pixel in starting_col..screen_width { 296 set_in_frame_buffer( 297 &mut self.frame_buffer, 298 row, 299 pixel, 300 color, 301 ColorModifier::None, 302 screen_width, 303 &self.color_tables, 304 ); 305 } 306 } 307 308 fn copy_frame_buffer_row(&mut self, from_row: u32, to_row: u32) { 309 debug_assert_ne!(from_row, to_row); 310 311 let screen_width = self.screen_width(); 312 let from_addr = (from_row * screen_width) as usize; 313 let to_addr = (to_row * screen_width) as usize; 314 315 if to_addr > from_addr { 316 let (a, b) = self.frame_buffer.split_at_mut(to_addr); 317 b[..screen_width as usize] 318 .copy_from_slice(&a[from_addr..from_addr + screen_width as usize]); 319 } else { 320 let (a, b) = self.frame_buffer.split_at_mut(from_addr); 321 a[to_addr..to_addr + screen_width as usize] 322 .copy_from_slice(&b[..screen_width as usize]); 323 } 324 } 325 326 #[allow(clippy::identity_op)] 327 fn render_bgs_to_buffer( 328 &mut self, 329 raster_line: u16, 330 fb_start_col: i16, 331 fb_end_col: i16, 332 fb_pixel_offset: i16, 333 ) { 334 let cell_height_shift = self.latched_registers.interlacing_mode.cell_height_shift(); 335 let v_scroll_size = self.latched_registers.vertical_scroll_size; 336 let h_scroll_size = self.latched_registers.horizontal_scroll_size; 337 338 let (h_scroll_size_pixels, v_scroll_size_pixels) = match (h_scroll_size, v_scroll_size) { 339 // An invalid H scroll size always produces 32x1 scroll planes 340 (ScrollSize::Invalid, _) => (32 * 8, 1 * 8), 341 // An invalid V scroll size with valid H scroll size functions as a size of 32 342 (_, ScrollSize::Invalid) => (h_scroll_size.to_pixels(), 32 * 8), 343 _ => (h_scroll_size.to_pixels(), v_scroll_size.to_pixels()), 344 }; 345 let h_scroll_size_cells = h_scroll_size_pixels / 8; 346 347 let scroll_line_bit_mask = match self.latched_registers.interlacing_mode { 348 InterlacingMode::Progressive | InterlacingMode::Interlaced => v_scroll_size_pixels - 1, 349 InterlacingMode::InterlacedDouble => (v_scroll_size_pixels << 1) - 1, 350 }; 351 352 let (h_scroll_a, h_scroll_b) = self.latched_h_scroll; 353 354 let active_display_pixels = 355 self.latched_registers.horizontal_display_size.active_display_pixels(); 356 let active_display_cells = active_display_pixels / 8; 357 358 for plane in [BgPlane::A, BgPlane::B] { 359 let (enabled, pixel_buffer, nametable_base_addr, h_scroll) = match plane { 360 BgPlane::A => ( 361 self.config.plane_a_enabled, 362 &mut self.bg_buffers.plane_a_pixels, 363 self.latched_registers.scroll_a_base_nt_addr, 364 h_scroll_a, 365 ), 366 BgPlane::B => ( 367 self.config.plane_b_enabled, 368 &mut self.bg_buffers.plane_b_pixels, 369 self.latched_registers.scroll_b_base_nt_addr, 370 h_scroll_b, 371 ), 372 }; 373 374 if !enabled { 375 pixel_buffer.fill(TilePixel::default()); 376 continue; 377 } 378 379 // The VDP renders in columns of 16 pixels 380 // If H scroll is not a multiple of 16, consider the remainder "fine H scroll" 381 let coarse_h_scroll = h_scroll & !15; 382 let fine_h_scroll = (h_scroll & 15) as i16; 383 384 // The VDP always renders one column partially or fully to the left of active display, 385 // column -1. 386 // If fine H scrolling is used, the leftmost 1 to 15 pixels in active display come from 387 // this column 388 let start_h_column = 389 if fine_h_scroll != 0 || self.config.render_horizontal_border { -1 } else { 0 }; 390 let end_h_column = (active_display_cells / 2) as i16; 391 392 for h_column in start_h_column..end_h_column { 393 let v_scroll = read_v_scroll( 394 &self.vsram, 395 plane, 396 h_column, 397 &self.latched_registers, 398 self.latched_full_screen_v_scroll, 399 ); 400 401 let scrolled_scanline = raster_line.wrapping_add(v_scroll) & scroll_line_bit_mask; 402 let scrolled_v_cell = scrolled_scanline >> cell_height_shift; 403 404 let column_scrolled_pixel = ((16 * h_column) as u16).wrapping_sub(coarse_h_scroll); 405 let column_scrolled_h_cell = column_scrolled_pixel / 8; 406 407 // Each 16-pixel column consists of two 8-pixel cells 408 for h_cell_offset in 0..2 { 409 let cell_fb_col = 410 16 * h_column + 8 * h_cell_offset + fine_h_scroll + fb_pixel_offset; 411 if cell_fb_col + 8 <= fb_start_col || cell_fb_col >= fb_end_col { 412 continue; 413 } 414 415 let scrolled_h_cell = column_scrolled_h_cell.wrapping_add(h_cell_offset as u16) 416 & (h_scroll_size_cells - 1); 417 418 let nametable_word = read_name_table_word( 419 &self.vram, 420 nametable_base_addr, 421 h_scroll_size.into(), 422 scrolled_v_cell, 423 scrolled_h_cell, 424 ); 425 426 if plane == BgPlane::B { 427 self.state.last_scroll_b_palettes[0] = self.state.last_scroll_b_palettes[1]; 428 self.state.last_scroll_b_palettes[1] = nametable_word.palette; 429 } 430 431 let colors = read_pattern_generator_row( 432 &self.vram, 433 PatternGeneratorRowArgs { 434 vertical_flip: nametable_word.vertical_flip, 435 horizontal_flip: nametable_word.horizontal_flip, 436 pattern_generator: nametable_word.pattern_generator, 437 row: scrolled_scanline, 438 cell_height_shift, 439 }, 440 ); 441 442 for pixel_offset in 0..8 { 443 let fb_col = cell_fb_col + pixel_offset; 444 if !(fb_start_col..fb_end_col).contains(&fb_col) { 445 continue; 446 } 447 448 pixel_buffer[fb_col as usize] = TilePixel { 449 color: colors[pixel_offset as usize], 450 palette: nametable_word.palette, 451 priority: nametable_word.priority, 452 }; 453 } 454 } 455 } 456 } 457 } 458 459 fn render_window_to_buffer( 460 &mut self, 461 raster_line: u16, 462 fb_start_col: i16, 463 fb_end_col: i16, 464 fb_pixel_offset: i16, 465 ) { 466 if !self.config.window_enabled { 467 return; 468 } 469 470 let active_display_pixels = 471 self.latched_registers.horizontal_display_size.active_display_pixels(); 472 let (window_start, window_end) = if self.latched_registers.is_line_in_v_window(raster_line) 473 { 474 (0, active_display_pixels) 475 } else { 476 self.latched_registers.window_h_range(active_display_pixels) 477 }; 478 479 if window_start >= window_end { 480 // Window is empty on this line 481 return; 482 } 483 484 let window_start_cell = window_start / 8; 485 let window_end_cell = window_end / 8; 486 for h_cell in window_start_cell..window_end_cell { 487 let pixel = 8 * h_cell; 488 let cell_fb_col = (pixel as i16) + fb_pixel_offset; 489 if cell_fb_col + 8 <= fb_start_col || cell_fb_col >= fb_end_col { 490 continue; 491 } 492 493 let cell_height_shift = self.latched_registers.interlacing_mode.cell_height_shift(); 494 let v_cell = raster_line >> cell_height_shift; 495 496 let nametable_word = read_name_table_word( 497 &self.vram, 498 self.latched_registers.masked_window_nametable_addr(), 499 self.latched_registers.horizontal_display_size.window_width_cells(), 500 v_cell, 501 h_cell, 502 ); 503 504 let colors = read_pattern_generator_row( 505 &self.vram, 506 PatternGeneratorRowArgs { 507 vertical_flip: nametable_word.vertical_flip, 508 horizontal_flip: nametable_word.horizontal_flip, 509 pattern_generator: nametable_word.pattern_generator, 510 row: raster_line, 511 cell_height_shift, 512 }, 513 ); 514 515 for pixel_offset in 0..8 { 516 let fb_col = cell_fb_col + pixel_offset; 517 if !(fb_start_col..fb_end_col).contains(&fb_col) { 518 continue; 519 } 520 521 // Window replaces Plane A when enabled 522 self.bg_buffers.plane_a_pixels[fb_col as usize] = TilePixel { 523 color: colors[pixel_offset as usize], 524 palette: nametable_word.palette, 525 priority: nametable_word.priority, 526 }; 527 } 528 } 529 } 530 531 fn merge_layers( 532 &mut self, 533 raster_line: RasterLine, 534 frame_buffer_row: u32, 535 fb_start_col: u32, 536 fb_end_col: u32, 537 fb_pixel_offset: i16, 538 interlaced_odd_line: bool, 539 ) { 540 let sprite_buffers = if interlaced_odd_line { 541 &self.interlaced_sprite_buffers 542 } else { 543 &self.sprite_buffers 544 }; 545 546 let bg_cram_idx = self.backdrop_cram_idx(); 547 548 let screen_width = self.screen_width(); 549 let active_display_pixels = 550 self.latched_registers.horizontal_display_size.active_display_pixels(); 551 552 for frame_buffer_col in fb_start_col..fb_end_col { 553 let pixel = frame_buffer_col as i16 - fb_pixel_offset; 554 555 let sprite_pixel = if self.config.sprites_enabled { 556 sprite_buffers.pixels.get(pixel as usize).copied().unwrap_or(TilePixel::default()) 557 } else { 558 TilePixel::default() 559 }; 560 561 let scroll_a_pixel = self.bg_buffers.plane_a_pixels[frame_buffer_col as usize]; 562 let scroll_b_pixel = self.bg_buffers.plane_b_pixels[frame_buffer_col as usize]; 563 564 let pixel_color_args = PixelColorArgs { 565 sprite_pixel, 566 scroll_a_pixel, 567 scroll_b_pixel, 568 bg_cram_idx, 569 in_h_border: !(0..active_display_pixels as i16).contains(&pixel), 570 in_v_border: raster_line.in_v_border && !self.state.v_border_forgotten, 571 }; 572 let (pixel_cram_idx, color_modifier) = if self.latched_registers.shadow_highlight_flag { 573 determine_pixel_color::<true>(self.debug_register, pixel_color_args) 574 } else { 575 determine_pixel_color::<false>(self.debug_register, pixel_color_args) 576 }; 577 578 let pixel_color = colors::lookup(&self.cram, pixel_cram_idx); 579 580 set_in_frame_buffer( 581 &mut self.frame_buffer, 582 frame_buffer_row, 583 frame_buffer_col, 584 pixel_color, 585 color_modifier, 586 screen_width, 587 &self.color_tables, 588 ); 589 } 590 } 591 592 fn render_vertical_border_line( 593 &mut self, 594 scanline: u16, 595 frame_buffer_row: u32, 596 starting_pixel: u16, 597 ) { 598 match self.debug_register.forced_plane { 599 Plane::Background => { 600 // Fill with the background color 601 let bg_color = self.backdrop_color(); 602 self.fill_frame_buffer_row(frame_buffer_row, starting_pixel, bg_color); 603 } 604 Plane::Sprite => { 605 // Fill with color 0 606 self.fill_frame_buffer_row( 607 frame_buffer_row, 608 starting_pixel, 609 colors::lookup(&self.cram, 0), 610 ); 611 } 612 Plane::ScrollA | Plane::ScrollB => { 613 // What happens here is quite strange. In actual hardware, the VRAM chip continues cycling through the 614 // 256 bytes in the same VRAM row as the last byte accessed during rendering, which happens to be the 615 // 4th sprite tile fetched for the line immediately after active display. The VDP interprets those 616 // bytes as pixels and displays them using the last palettes that were used during rendering. 617 // 618 // A "row" in VRAM consists of 64 4-byte groups that are each separated by 1KB due to how VRAM addresses 619 // map to physical addresses in the VRAM chip. See: 620 // https://gendev.spritesmind.net/forum/viewtopic.php?p=17583#17583 621 let h_display_size = self.registers.horizontal_display_size; 622 let screen_width = self.screen_width(); 623 624 let (start_pixel, end_pixel) = if self.config.render_horizontal_border { 625 (0, screen_width as u16) 626 } else { 627 let left_border = h_display_size.left_border(); 628 let active_display_pixels = h_display_size.active_display_pixels(); 629 (left_border, left_border + active_display_pixels) 630 }; 631 632 // +2 here is needed to properly align with the horizontal borders in Overdrive 2 633 // The number of 4-byte groups is equal to half the number of pixel clocks per line, 171 in H32 mode 634 // and 210 in H40 mode 635 let group_offset = (scanline + 2 636 - self.registers.vertical_display_size.active_scanlines()) 637 .wrapping_mul(h_display_size.pixels_including_hblank() / 2); 638 639 let base_addr = self.sprite_buffers.last_tile_addresses[3]; 640 641 let mut current_addr = base_addr.wrapping_add(group_offset.wrapping_mul(1024)); 642 let mut odd_group = false; 643 let mut current_group = [0; 4]; 644 645 for pixel in 0..end_pixel { 646 // +3 here is needed to properly align with the horizontal borders in Overdrive 2 647 let tile_col = (pixel + 3) % 8; 648 if pixel == 0 || tile_col == 0 { 649 current_group.copy_from_slice( 650 &self.vram[current_addr as usize..(current_addr + 4) as usize], 651 ); 652 if odd_group { 653 current_addr = current_addr.wrapping_add(1024); 654 } else { 655 current_addr = current_addr.wrapping_add(7 * 1024); 656 } 657 odd_group = !odd_group; 658 } 659 660 if pixel < start_pixel { 661 continue; 662 } 663 664 let palette = self.state.last_scroll_b_palettes[((pixel / 8) & 1) as usize]; 665 let current_byte = current_group[(tile_col >> 1) as usize]; 666 let color_id = (current_byte >> (4 - ((tile_col & 1) << 2))) & 0x0F; 667 let color = colors::resolve(&self.cram, palette, color_id); 668 669 let frame_buffer_col = pixel - start_pixel; 670 set_in_frame_buffer( 671 &mut self.frame_buffer, 672 frame_buffer_row, 673 frame_buffer_col.into(), 674 color, 675 ColorModifier::None, 676 screen_width, 677 &self.color_tables, 678 ); 679 } 680 } 681 } 682 } 683 684 fn render_left_border( 685 &mut self, 686 frame_buffer_row: u32, 687 bg_color: u16, 688 (h_scroll_a, h_scroll_b): (u16, u16), 689 ) { 690 let screen_width = self.screen_width(); 691 let left_border: u32 = self.latched_registers.horizontal_display_size.left_border().into(); 692 693 match self.debug_register.forced_plane { 694 Plane::Background => { 695 // Fill border with background color 696 for col in 0..left_border { 697 set_in_frame_buffer( 698 &mut self.frame_buffer, 699 frame_buffer_row, 700 col, 701 bg_color, 702 ColorModifier::None, 703 screen_width, 704 &self.color_tables, 705 ); 706 } 707 } 708 Plane::Sprite => { 709 // Fill border with color 0 710 let color_0 = colors::lookup(&self.cram, 0); 711 for col in 0..left_border { 712 set_in_frame_buffer( 713 &mut self.frame_buffer, 714 frame_buffer_row, 715 col, 716 color_0, 717 ColorModifier::None, 718 screen_width, 719 &self.color_tables, 720 ); 721 } 722 } 723 Plane::ScrollA => { 724 // Actual hardware fills the non-rendered pixels with garbage that is somewhat unspecified by Overdrive 725 // 2 docs; just fill them with color 0 726 // Overdrive 2 depends on handling the Scroll A right border correctly but not the left border 727 let border_pixels = h_scroll_a & 15; 728 let border_offset = 729 16 - self.latched_registers.horizontal_display_size.left_border(); 730 let end_col = border_pixels.saturating_sub(border_offset); 731 let color_0 = colors::lookup(&self.cram, 0); 732 733 for col in 0..end_col { 734 set_in_frame_buffer( 735 &mut self.frame_buffer, 736 frame_buffer_row, 737 col.into(), 738 color_0, 739 ColorModifier::None, 740 screen_width, 741 &self.color_tables, 742 ); 743 } 744 } 745 Plane::ScrollB => { 746 // Render pixels from sprite tiles 36 and 37 using the palettes from the last 2 tiles of Scroll B in the 747 // previous rendered line 748 let border_pixels = h_scroll_b & 15; 749 let border_offset = 750 16 - self.latched_registers.horizontal_display_size.left_border(); 751 let end_col = border_pixels.saturating_sub(border_offset); 752 753 for col in 0..end_col { 754 let pixel = 15 - (end_col - 1 - col); 755 self.render_horizontal_border_sprite_pixel( 756 frame_buffer_row, 757 col.into(), 758 pixel, 759 36, 760 ); 761 } 762 } 763 } 764 } 765 766 fn render_right_border(&mut self, frame_buffer_row: u32, (h_scroll_a, h_scroll_b): (u16, u16)) { 767 let screen_width = self.screen_width() as u16; 768 let right_border_start = screen_width - RIGHT_BORDER; 769 770 match self.debug_register.forced_plane { 771 Plane::Background => { 772 // Fill border with background color 773 let bg_color = self.backdrop_color(); 774 for col in right_border_start..screen_width { 775 set_in_frame_buffer( 776 &mut self.frame_buffer, 777 frame_buffer_row, 778 col.into(), 779 bg_color, 780 ColorModifier::None, 781 screen_width.into(), 782 &self.color_tables, 783 ); 784 } 785 } 786 Plane::Sprite => { 787 // Fill border with color 0 788 let color_0 = colors::lookup(&self.cram, 0); 789 for col in right_border_start..screen_width { 790 set_in_frame_buffer( 791 &mut self.frame_buffer, 792 frame_buffer_row, 793 col.into(), 794 color_0, 795 ColorModifier::None, 796 screen_width.into(), 797 &self.color_tables, 798 ); 799 } 800 } 801 Plane::ScrollA => { 802 // Render pixels from sprite tiles 0 and 1 using the palettes from the last 2 tiles of Scroll B in the 803 // previous rendered line 804 let last_column_end = right_border_start + cmp::min(h_scroll_a & 15, RIGHT_BORDER); 805 806 for col in last_column_end..screen_width { 807 let pixel = col - last_column_end; 808 self.render_horizontal_border_sprite_pixel( 809 frame_buffer_row, 810 col.into(), 811 pixel, 812 0, 813 ); 814 } 815 } 816 Plane::ScrollB => { 817 // Render pixels from sprite tiles 4 and 5 using the palettes from the last 2 tiles of Scroll B in the 818 // previous rendered line 819 let last_column_end = right_border_start + cmp::min(h_scroll_b & 15, RIGHT_BORDER); 820 821 for col in last_column_end..screen_width { 822 let pixel = col - last_column_end; 823 self.render_horizontal_border_sprite_pixel( 824 frame_buffer_row, 825 col.into(), 826 pixel, 827 4, 828 ); 829 } 830 } 831 } 832 } 833 834 fn render_horizontal_border_sprite_pixel( 835 &mut self, 836 frame_buffer_row: u32, 837 frame_buffer_col: u32, 838 pixel: u16, 839 base_sprite: u16, 840 ) { 841 let sprite_tile = base_sprite + pixel / 8; 842 let sprite_col = pixel % 8; 843 let vram_addr = 844 self.sprite_buffers.last_tile_addresses[sprite_tile as usize] + (sprite_col >> 1); 845 let color_id = (self.vram[vram_addr as usize] >> (4 - ((sprite_col & 1) << 2))) & 0x0F; 846 let palette = self.state.last_scroll_b_palettes[(pixel / 8) as usize]; 847 let color = colors::resolve(&self.cram, palette, color_id); 848 849 let screen_width = self.screen_width(); 850 set_in_frame_buffer( 851 &mut self.frame_buffer, 852 frame_buffer_row, 853 frame_buffer_col, 854 color, 855 ColorModifier::None, 856 screen_width, 857 &self.color_tables, 858 ); 859 } 860 861 fn backdrop_cram_idx(&self) -> u8 { 862 (self.registers.background_palette << 4) | self.registers.background_color_id 863 } 864 865 fn backdrop_color(&self) -> u16 { 866 colors::lookup(&self.cram, self.backdrop_cram_idx()) 867 } 868} 869 870pub(super) fn set_in_frame_buffer( 871 frame_buffer: &mut [Color; vdp::FRAME_BUFFER_LEN], 872 row: u32, 873 col: u32, 874 color: u16, 875 modifier: ColorModifier, 876 screen_width: u32, 877 color_tables: &ColorTables, 878) { 879 let r = ((color >> 1) & 0x07) as u8; 880 let g = ((color >> 5) & 0x07) as u8; 881 let b = ((color >> 9) & 0x07) as u8; 882 let a = (color >> 15) as u8; 883 let rgb_color = colors::gen_to_rgba(r, g, b, a, modifier, color_tables); 884 885 frame_buffer[(row * screen_width + col) as usize] = rgb_color; 886} 887 888fn read_v_scroll( 889 vsram: &Vsram, 890 plane: BgPlane, 891 h_column: i16, 892 registers: &Registers, 893 latched_full_screen_v_scroll: (u16, u16), 894) -> u16 { 895 let v_scroll = match registers.vertical_scroll_mode { 896 VerticalScrollMode::FullScreen => match plane { 897 BgPlane::A => latched_full_screen_v_scroll.0, 898 BgPlane::B => latched_full_screen_v_scroll.1, 899 }, 900 VerticalScrollMode::TwoCell => { 901 read_two_cell_v_scroll(h_column, plane, vsram, registers.horizontal_display_size) 902 } 903 }; 904 905 let v_scroll_mask = registers.interlacing_mode.v_scroll_mask(); 906 v_scroll & v_scroll_mask 907} 908 909fn read_two_cell_v_scroll( 910 h_column: i16, 911 plane: BgPlane, 912 vsram: &Vsram, 913 h_display_size: HorizontalDisplaySize, 914) -> u16 { 915 let active_display_columns = (h_display_size.active_display_pixels() / 16) as i16; 916 if h_column < 0 { 917 // Column -1 behaves weirdly. 918 // In H40 mode, it uses a V scroll value of VSRAM[$4C] & VSRAM[$4E] for both backgrounds. 919 // In H32 mode, it always uses a V scroll value of 0. 920 // Source: http://gendev.spritesmind.net/forum/viewtopic.php?t=737&postdays=0&postorder=asc&start=30 921 match h_display_size { 922 HorizontalDisplaySize::ThirtyTwoCell => 0, 923 HorizontalDisplaySize::FortyCell => vsram[0x4C >> 1] & vsram[0x4E >> 1], 924 } 925 } else if h_column < active_display_columns { 926 let offset = match plane { 927 BgPlane::A => 0, 928 BgPlane::B => 1, 929 }; 930 let addr = 2 * (h_column as usize) + offset; 931 vsram[addr] 932 } else { 933 0 934 } 935} 936 937#[derive(Debug, Clone, Copy, Default)] 938struct NameTableWord { 939 priority: bool, 940 palette: u8, 941 vertical_flip: bool, 942 horizontal_flip: bool, 943 pattern_generator: u16, 944} 945 946fn read_name_table_word( 947 vram: &Vram, 948 base_addr: u16, 949 name_table_width: u16, 950 row: u16, 951 col: u16, 952) -> NameTableWord { 953 // Nametable size is limited to 8KB 954 // If dimensions are 64x128, 128x64, or 128x128 then addresses will wrap at the 8KB boundary 955 let relative_addr = (2 * (row * name_table_width + col)) & 0x1FFF; 956 let addr = base_addr.wrapping_add(relative_addr); 957 let word = u16::from_be_bytes([vram[addr as usize], vram[addr.wrapping_add(1) as usize]]); 958 959 NameTableWord { 960 priority: word.bit(15), 961 palette: ((word >> 13) & 0x03) as u8, 962 vertical_flip: word.bit(12), 963 horizontal_flip: word.bit(11), 964 pattern_generator: word & 0x07FF, 965 } 966} 967 968#[derive(Debug, Clone)] 969pub struct PatternGeneratorRowArgs { 970 pub vertical_flip: bool, 971 pub horizontal_flip: bool, 972 pub pattern_generator: u16, 973 pub row: u16, 974 pub cell_height_shift: u16, 975} 976 977#[inline] 978pub fn read_pattern_generator_row( 979 vram: &Vram, 980 PatternGeneratorRowArgs { 981 vertical_flip, 982 horizontal_flip, 983 pattern_generator, 984 row, 985 cell_height_shift, 986 }: PatternGeneratorRowArgs, 987) -> [u8; 8] { 988 let cell_height = 1 << cell_height_shift; 989 let cell_row = if vertical_flip { 990 cell_height - 1 - (row & (cell_height - 1)) 991 } else { 992 row & (cell_height - 1) 993 }; 994 995 let cell_addr = (4 * cell_height).wrapping_mul(pattern_generator); 996 let row_addr = (cell_addr + 4 * cell_row) as usize; 997 let mut colors: [u8; 8] = array::from_fn(|i| { 998 let addr = row_addr + i / 2; 999 let byte = vram[addr]; 1000 (byte >> (((i & 1) ^ 1) << 2)) & 0x0F 1001 }); 1002 1003 if horizontal_flip { 1004 colors.reverse(); 1005 } 1006 1007 colors 1008} 1009 1010#[derive(Debug, Clone, Copy)] 1011struct UnresolvedColor { 1012 palette: u8, 1013 color_id: u8, 1014 is_sprite: bool, 1015} 1016 1017struct PixelColorArgs { 1018 sprite_pixel: TilePixel, 1019 scroll_a_pixel: TilePixel, 1020 scroll_b_pixel: TilePixel, 1021 bg_cram_idx: u8, 1022 in_h_border: bool, 1023 in_v_border: bool, 1024} 1025 1026#[inline] 1027#[allow(clippy::unnested_or_patterns)] 1028fn determine_pixel_color<const SHADOW_HIGHLIGHT: bool>( 1029 debug_register: DebugRegister, 1030 PixelColorArgs { 1031 sprite_pixel, 1032 scroll_a_pixel, 1033 scroll_b_pixel, 1034 bg_cram_idx, 1035 in_h_border, 1036 in_v_border, 1037 }: PixelColorArgs, 1038) -> (u8, ColorModifier) { 1039 let sprite_cram_idx = (sprite_pixel.palette << 4) | sprite_pixel.color; 1040 let scroll_a_cram_idx = (scroll_a_pixel.palette << 4) | scroll_a_pixel.color; 1041 let scroll_b_cram_idx = (scroll_b_pixel.palette << 4) | scroll_b_pixel.color; 1042 1043 if in_h_border || in_v_border || debug_register.display_disabled { 1044 let cram_idx = match debug_register.forced_plane { 1045 Plane::Background => bg_cram_idx, 1046 Plane::Sprite => { 1047 if in_h_border { 1048 0 1049 } else { 1050 sprite_cram_idx 1051 } 1052 } 1053 Plane::ScrollA => scroll_a_cram_idx, 1054 Plane::ScrollB => scroll_b_cram_idx, 1055 }; 1056 return (cram_idx, ColorModifier::None); 1057 } 1058 1059 let mut modifier = if SHADOW_HIGHLIGHT && !scroll_a_pixel.priority && !scroll_b_pixel.priority { 1060 // If shadow/highlight bit is set and all priority flags are 0, default modifier to shadow 1061 ColorModifier::Shadow 1062 } else { 1063 ColorModifier::None 1064 }; 1065 1066 let sprite = UnresolvedColor { 1067 palette: sprite_pixel.palette, 1068 color_id: sprite_pixel.color, 1069 is_sprite: true, 1070 }; 1071 let scroll_a = UnresolvedColor { 1072 palette: scroll_a_pixel.palette, 1073 color_id: scroll_a_pixel.color, 1074 is_sprite: false, 1075 }; 1076 let scroll_b = UnresolvedColor { 1077 palette: scroll_b_pixel.palette, 1078 color_id: scroll_b_pixel.color, 1079 is_sprite: false, 1080 }; 1081 let colors = match (sprite_pixel.priority, scroll_a_pixel.priority, scroll_b_pixel.priority) { 1082 (false, false, false) | (true, false, false) | (true, true, false) | (true, true, true) => { 1083 [sprite, scroll_a, scroll_b] 1084 } 1085 (false, true, false) => [scroll_a, sprite, scroll_b], 1086 (false, false, true) => [scroll_b, sprite, scroll_a], 1087 (true, false, true) => [sprite, scroll_b, scroll_a], 1088 (false, true, true) => [scroll_a, scroll_b, sprite], 1089 }; 1090 1091 for UnresolvedColor { palette, color_id, is_sprite } in colors { 1092 if color_id == 0 { 1093 // Pixel is transparent 1094 continue; 1095 } 1096 1097 if SHADOW_HIGHLIGHT && is_sprite && palette == 3 { 1098 if color_id == 14 { 1099 // Palette 3 + color 14 = highlight; sprite is transparent, underlying pixel is highlighted 1100 modifier += ColorModifier::Highlight; 1101 continue; 1102 } else if color_id == 15 { 1103 // Palette 3 + color 15 = shadow; sprite is transparent, underlying pixel is shadowed 1104 modifier = ColorModifier::Shadow; 1105 continue; 1106 } 1107 } 1108 1109 // If debug register is used to force a plane, the 6-bit color value from that plane masks 1110 // the 6-bit color value of the frontmost pixel. 1111 // Titan Overdrive 2 uses this extensively 1112 let cram_idx_mask = match debug_register.forced_plane { 1113 Plane::Background => 0x3F, 1114 Plane::Sprite => sprite_cram_idx, 1115 Plane::ScrollA => scroll_a_cram_idx, 1116 Plane::ScrollB => scroll_b_cram_idx, 1117 }; 1118 let cram_idx = ((palette << 4) | color_id) & cram_idx_mask; 1119 1120 // Sprite color id 14 is never shadowed/highlighted, and neither is a sprite with the priority 1121 // bit set 1122 if SHADOW_HIGHLIGHT && is_sprite && (color_id == 14 || sprite_pixel.priority) { 1123 modifier = ColorModifier::None; 1124 } 1125 1126 return (cram_idx, modifier); 1127 } 1128 1129 let fallback_color = match debug_register.forced_plane { 1130 Plane::Background => bg_cram_idx, 1131 Plane::Sprite => sprite_cram_idx, 1132 Plane::ScrollA => scroll_a_cram_idx, 1133 Plane::ScrollB => scroll_b_cram_idx, 1134 }; 1135 1136 (fallback_color, modifier) 1137}