Values from https://gendev.spritesmind.net/forum/viewtopic.php?p=37011#p37011
11pub const NTSC_TOP_BORDER: u16 = 11; 12pub const PAL_V28_TOP_BORDER: u16 = 38; 13pub const PAL_V30_TOP_BORDER: u16 = 30; 14 15pub const NTSC_BOTTOM_BORDER: u16 = 8; 16pub const PAL_V28_BOTTOM_BORDER: u16 = 32; 17pub const PAL_V30_BOTTOM_BORDER: u16 = 24; 18 19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 20pub enum VerticalScrollMode { 21 #[default] 22 FullScreen, 23 TwoCell, 24} 25 26impl Display for VerticalScrollMode { 27 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 28 match self { 29 Self::FullScreen => write!(f, "Full screen"), 30 Self::TwoCell => write!(f, "Per 2 cell"), 31 } 32 } 33} 34 35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 36pub enum HorizontalScrollMode { 37 #[default] 38 FullScreen, 39 Cell, 40 Line, 41 // Repeatedly uses the scroll values for the first 8 lines 42 Invalid, 43} 44 45impl Display for HorizontalScrollMode { 46 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 47 match self { 48 Self::FullScreen => write!(f, "Full screen"), 49 Self::Cell => write!(f, "Per cell"), 50 Self::Line => write!(f, "Per line"), 51 Self::Invalid => write!(f, "Prohibited"), 52 } 53 } 54} 55 56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 57pub enum HorizontalDisplaySize { 58 #[default] 59 ThirtyTwoCell, 60 FortyCell, 61} 62 63impl Display for HorizontalDisplaySize { 64 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 65 match self { 66 Self::ThirtyTwoCell => write!(f, "H32 (256px)"), 67 Self::FortyCell => write!(f, "H40 (320px)"), 68 } 69 } 70} 71 72impl HorizontalDisplaySize { 73 pub const fn active_display_pixels(self) -> u16 { 74 match self { 75 Self::ThirtyTwoCell => 256, 76 Self::FortyCell => 320, 77 } 78 } 79 80 pub const fn pixels_including_hblank(self) -> u16 { 81 match self { 82 Self::ThirtyTwoCell => 342, 83 Self::FortyCell => 420, 84 } 85 } 86 87 // Length in sprites 88 pub const fn sprite_table_len(self) -> u32 { 89 match self { 90 Self::ThirtyTwoCell => 64, 91 Self::FortyCell => 80, 92 } 93 } 94 95 pub const fn max_sprites_per_line(self) -> u16 { 96 match self { 97 Self::ThirtyTwoCell => 16, 98 Self::FortyCell => 20, 99 } 100 } 101 102 pub const fn max_sprite_tiles_per_line(self) -> u16 { 103 self.active_display_pixels() / 8 104 } 105 106 pub const fn window_width_cells(self) -> u16 { 107 match self { 108 Self::ThirtyTwoCell => 32, 109 Self::FortyCell => 64, 110 } 111 } 112 113 pub const fn sprite_attribute_table_mask(self) -> u32 { 114 // Sprite attribute table A9 is ignored in H40 mode 115 match self { 116 Self::ThirtyTwoCell => !0, 117 Self::FortyCell => !0x3FF, 118 } 119 } 120 121 pub const fn left_border(self) -> u16 { 122 match self { 123 Self::ThirtyTwoCell => H32_LEFT_BORDER, 124 Self::FortyCell => H40_LEFT_BORDER, 125 } 126 } 127} 128 129#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 130pub enum VerticalDisplaySize { 131 #[default] 132 TwentyEightCell, 133 ThirtyCell, 134} 135 136impl Display for VerticalDisplaySize { 137 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 138 match self { 139 Self::TwentyEightCell => write!(f, "V28 (224px)"), 140 Self::ThirtyCell => write!(f, "V30 (240px)"), 141 } 142 } 143} 144 145impl VerticalDisplaySize { 146 pub const fn active_scanlines(self) -> u16 { 147 match self { 148 Self::TwentyEightCell => 224, 149 Self::ThirtyCell => 240, 150 } 151 } 152 153 pub const fn top_border(self, timing_mode: TimingMode) -> u16 { 154 match (self, timing_mode) { 155 (_, TimingMode::Ntsc) => NTSC_TOP_BORDER, 156 (Self::TwentyEightCell, TimingMode::Pal) => PAL_V28_TOP_BORDER, 157 (Self::ThirtyCell, TimingMode::Pal) => PAL_V30_TOP_BORDER, 158 } 159 } 160 161 pub const fn bottom_border(self, timing_mode: TimingMode) -> u16 { 162 match (self, timing_mode) { 163 (_, TimingMode::Ntsc) => NTSC_BOTTOM_BORDER, 164 (Self::TwentyEightCell, TimingMode::Pal) => PAL_V28_BOTTOM_BORDER, 165 (Self::ThirtyCell, TimingMode::Pal) => PAL_V30_BOTTOM_BORDER, 166 } 167 } 168} 169 170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 171pub enum InterlacingMode { 172 #[default] 173 Progressive, 174 Interlaced, 175 InterlacedDouble, 176} 177 178impl Display for InterlacingMode { 179 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 180 match self { 181 Self::Progressive => write!(f, "Progressive"), 182 Self::Interlaced => write!(f, "Single-screen interlaced"), 183 Self::InterlacedDouble => write!(f, "Double-screen interlaced"), 184 } 185 } 186} 187 188impl InterlacingMode { 189 pub const fn v_scroll_mask(self) -> u16 { 190 // V scroll values are 10 bits normally, 11 bits in interlaced 2x mode 191 match self { 192 Self::Progressive | Self::Interlaced => 0x03FF, 193 Self::InterlacedDouble => 0x07FF, 194 } 195 } 196 197 pub const fn sprite_display_top(self) -> u16 { 198 match self { 199 // The sprite display area starts at $080 normally, $100 in interlaced 2x mode 200 Self::Progressive | Self::Interlaced => 0x080, 201 Self::InterlacedDouble => 0x100, 202 } 203 } 204 205 pub const fn sprite_display_mask(self) -> u16 { 206 match self { 207 Self::Progressive | Self::Interlaced => 0x1FF, 208 Self::InterlacedDouble => 0x3FF, 209 } 210 } 211 212 pub const fn cell_height(self) -> u16 { 213 1 << self.cell_height_shift() 214 } 215 216 pub const fn cell_height_shift(self) -> u16 { 217 match self { 218 // Cells are 8x8 normally, 8x16 in interlaced 2x mode 219 Self::Progressive | Self::Interlaced => 3, 220 Self::InterlacedDouble => 4, 221 } 222 } 223} 224 225#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 226pub enum ScrollSize { 227 #[default] 228 ThirtyTwo, 229 SixtyFour, 230 OneTwentyEight, 231 Invalid, 232} 233 234impl Display for ScrollSize { 235 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 236 match self { 237 Self::ThirtyTwo => write!(f, "32 tiles"), 238 Self::SixtyFour => write!(f, "64 tiles"), 239 Self::OneTwentyEight => write!(f, "128 tiles"), 240 Self::Invalid => write!(f, "Prohibited"), 241 } 242 } 243} 244 245impl ScrollSize { 246 pub fn from_bits(bits: u8) -> Self { 247 match bits & 0x03 { 248 0x00 => Self::ThirtyTwo, 249 0x01 => Self::SixtyFour, 250 0x02 => Self::Invalid, 251 0x03 => Self::OneTwentyEight, 252 _ => unreachable!("value & 0x03 is always <= 0x03"), 253 } 254 } 255 256 pub fn to_pixels(self) -> u16 { 257 match self { 258 Self::ThirtyTwo | Self::Invalid => 32 * 8, 259 Self::SixtyFour => 64 * 8, 260 Self::OneTwentyEight => 128 * 8, 261 } 262 } 263} 264 265impl From<ScrollSize> for u16 { 266 fn from(value: ScrollSize) -> Self { 267 match value { 268 ScrollSize::ThirtyTwo => 32, 269 ScrollSize::SixtyFour => 64, 270 ScrollSize::OneTwentyEight => 128, 271 ScrollSize::Invalid => 1, 272 } 273 } 274} 275 276#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 277pub enum WindowHorizontalMode { 278 #[default] 279 LeftToCenter, 280 CenterToRight, 281} 282 283impl Display for WindowHorizontalMode { 284 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 285 match self { 286 Self::LeftToCenter => write!(f, "Left to center"), 287 Self::CenterToRight => write!(f, "Center to right"), 288 } 289 } 290} 291 292impl WindowHorizontalMode { 293 pub fn h_range(self, window_x: u16, active_display_pixels: u16) -> (u16, u16) { 294 match self { 295 Self::LeftToCenter => (0, 8 * window_x), 296 Self::CenterToRight => (8 * window_x, active_display_pixels), 297 } 298 } 299} 300 301#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 302pub enum WindowVerticalMode { 303 #[default] 304 TopToCenter, 305 CenterToBottom, 306} 307 308impl Display for WindowVerticalMode { 309 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 310 match self { 311 Self::TopToCenter => write!(f, "Top to center"), 312 Self::CenterToBottom => write!(f, "Center to bottom"), 313 } 314 } 315} 316 317impl WindowVerticalMode { 318 pub fn in_window(self, scanline: u16, window_y: u16) -> bool { 319 let cell = scanline / 8; 320 match self { 321 Self::TopToCenter => cell < window_y, 322 Self::CenterToBottom => cell >= window_y, 323 } 324 } 325} 326 327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 328pub enum DmaMode { 329 #[default] 330 MemoryToVram, 331 VramFill, 332 VramCopy, 333} 334 335impl Display for DmaMode { 336 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 337 match self { 338 Self::MemoryToVram => write!(f, "Memory to VRAM"), 339 Self::VramFill => write!(f, "VRAM fill"), 340 Self::VramCopy => write!(f, "VRAM-to-VRAM copy"), 341 } 342 } 343} 344 345#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 346pub enum VramSizeKb { 347 #[default] 348 SixtyFour, 349 OneTwentyEight, 350} 351 352impl Display for VramSizeKb { 353 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 354 match self { 355 Self::SixtyFour => write!(f, "64 KB"), 356 Self::OneTwentyEight => write!(f, "128 KB"), 357 } 358 } 359} 360 361#[derive(Debug, Clone, Encode, Decode)] 362pub struct Registers { 363 // Register #0 364 pub h_interrupt_enabled: bool, 365 pub hv_counter_stopped: bool, 366 // Register #1 367 pub display_enabled: bool, 368 pub v_interrupt_enabled: bool, 369 pub dma_enabled: bool, 370 pub vertical_display_size: VerticalDisplaySize, 371 pub mode_4: bool, 372 pub vram_size: VramSizeKb, 373 // Register #2 374 pub scroll_a_base_nt_addr: u16, 375 // Register #3 376 pub window_base_nt_addr: u16, 377 // Register #4 378 pub scroll_b_base_nt_addr: u16, 379 // Register #5 380 pub sprite_attribute_table_base_addr: u32, 381 // Register #7 382 pub background_palette: u8, 383 pub background_color_id: u8, 384 // Register #10 385 pub h_interrupt_interval: u16, 386 // Register #11 387 // TODO external interrupts enabled 388 pub vertical_scroll_mode: VerticalScrollMode, 389 pub horizontal_scroll_mode: HorizontalScrollMode, 390 // Register #12 391 pub horizontal_display_size: HorizontalDisplaySize, 392 pub shadow_highlight_flag: bool, 393 pub interlacing_mode: InterlacingMode, 394 // Register #13 395 pub h_scroll_table_base_addr: u16, 396 // Register #15 397 pub data_port_auto_increment: u16, 398 // Register #16 399 pub vertical_scroll_size: ScrollSize, 400 pub horizontal_scroll_size: ScrollSize, 401 // Register #17 402 pub window_horizontal_mode: WindowHorizontalMode, 403 pub window_x_position: u16, 404 // Register #18 405 pub window_vertical_mode: WindowVerticalMode, 406 pub window_y_position: u16, 407 // Registers #19 & #20 408 pub dma_length: u16, 409 // Registers #21, #22, & #23 410 pub dma_source_address: u32, 411 pub dma_mode: DmaMode, 412} 413 414impl Registers { 415 pub fn new() -> Self { 416 Self { 417 h_interrupt_enabled: false, 418 hv_counter_stopped: false, 419 display_enabled: false, 420 v_interrupt_enabled: false, 421 dma_enabled: false, 422 vertical_display_size: VerticalDisplaySize::default(), 423 mode_4: false, 424 vram_size: VramSizeKb::default(), 425 scroll_a_base_nt_addr: 0, 426 window_base_nt_addr: 0, 427 scroll_b_base_nt_addr: 0, 428 sprite_attribute_table_base_addr: 0, 429 background_palette: 0, 430 background_color_id: 0, 431 h_interrupt_interval: 0, 432 vertical_scroll_mode: VerticalScrollMode::default(), 433 horizontal_scroll_mode: HorizontalScrollMode::default(), 434 horizontal_display_size: HorizontalDisplaySize::default(), 435 shadow_highlight_flag: false, 436 interlacing_mode: InterlacingMode::default(), 437 h_scroll_table_base_addr: 0, 438 data_port_auto_increment: 0, 439 vertical_scroll_size: ScrollSize::default(), 440 horizontal_scroll_size: ScrollSize::default(), 441 window_horizontal_mode: WindowHorizontalMode::default(), 442 window_x_position: 0, 443 window_vertical_mode: WindowVerticalMode::default(), 444 window_y_position: 0, 445 dma_length: 0, 446 dma_source_address: 0, 447 dma_mode: DmaMode::default(), 448 } 449 } 450 451 pub fn write_internal_register(&mut self, register: u8, value: u8) { 452 log::trace!("Wrote register #{register} with value {value:02X}"); 453 454 if self.mode_4 && register > 10 { 455 // Writing to register numbers >10 while in SMS mode has no effect 456 // Bass Masters Classic: Pro Edition depends on this - during boot it attempts to change 457 // the H display size while in SMS mode, and it expects that write to do nothing 458 return; 459 } 460 461 match register { 462 0 => { 463 // Register #0: Mode set register 1 464 self.h_interrupt_enabled = value.bit(4); 465 self.hv_counter_stopped = value.bit(1); 466 467 log::trace!(" H interrupt enabled: {}", self.h_interrupt_enabled); 468 log::trace!(" HV counter stopped: {}", self.hv_counter_stopped); 469 } 470 1 => { 471 // Register #1: Mode set register 2 472 self.display_enabled = value.bit(6); 473 self.v_interrupt_enabled = value.bit(5); 474 self.dma_enabled = value.bit(4); 475 self.vertical_display_size = if value.bit(3) { 476 VerticalDisplaySize::ThirtyCell 477 } else { 478 VerticalDisplaySize::TwentyEightCell 479 }; 480 481 // Undocumented: Register #1 bit 2 toggles between mode 5 (Genesis) and mode 4 (SMS) 482 // Mode 4 / SMS mode is not actually emulated, but some games depend on writes to 483 // VDP registers >10 doing nothing while in mode 4 484 self.mode_4 = !value.bit(2); 485 486 // Undocumented: Register #1 bit 7 enables "128KB" VRAM mode, which effectively enables byte-size access 487 // to VRAM 488 self.vram_size = 489 if value.bit(7) { VramSizeKb::OneTwentyEight } else { VramSizeKb::SixtyFour }; 490 491 log::trace!(" Display enabled: {}", self.display_enabled); 492 log::trace!(" V interrupt enabled: {}", self.v_interrupt_enabled); 493 log::trace!(" DMA enabled: {}", self.dma_enabled); 494 log::trace!(" Vertical display size: {:?}", self.vertical_display_size); 495 log::trace!(" Mode 4 enabled: {}", self.mode_4); 496 log::trace!(" VRAM size: {}", self.vram_size); 497 } 498 2 => { 499 // Register #2: Scroll A name table base address (bits 15-13) 500 self.scroll_a_base_nt_addr = u16::from(value & 0x38) << 10; 501 502 log::trace!( 503 " Scroll A base nametable address: {:04X}", 504 self.scroll_a_base_nt_addr 505 ); 506 } 507 3 => { 508 // Register #3: Window name table base address (bits 15-11) 509 self.window_base_nt_addr = u16::from(value & 0x3E) << 10; 510 511 log::trace!(" Window base nametable address: {:04X}", self.window_base_nt_addr); 512 } 513 4 => { 514 // Register #4: Scroll B name table base address (bits 15-13) 515 self.scroll_b_base_nt_addr = u16::from(value & 0x07) << 13; 516 517 log::trace!( 518 " Scroll B base nametable address: {:04X}", 519 self.scroll_b_base_nt_addr 520 ); 521 } 522 5 => { 523 // Register #5: Sprite attribute table base address (bits 16-9) 524 self.sprite_attribute_table_base_addr = u32::from(value) << 9; 525 526 log::trace!( 527 " Sprite attribute table base address: {:04X}", 528 self.sprite_attribute_table_base_addr 529 ); 530 } 531 7 => { 532 // Register #7: Background color 533 self.background_palette = (value >> 4) & 0x03; 534 self.background_color_id = value & 0x0F; 535 536 log::trace!(" BG palette: {}", self.background_palette); 537 log::trace!(" BG color id: {}", self.background_color_id); 538 } 539 10 => { 540 // Register #10: H interrupt interval 541 self.h_interrupt_interval = value.into(); 542 543 log::trace!(" H interrupt interval: {}", self.h_interrupt_interval); 544 } 545 11 => { 546 // Register #11: Mode set register 3 547 // TODO external interrupt enable 548 self.vertical_scroll_mode = if value.bit(2) { 549 VerticalScrollMode::TwoCell 550 } else { 551 VerticalScrollMode::FullScreen 552 }; 553 self.horizontal_scroll_mode = match value & 0x03 { 554 0x00 => HorizontalScrollMode::FullScreen, 555 0x01 => HorizontalScrollMode::Invalid, 556 0x02 => HorizontalScrollMode::Cell, 557 0x03 => HorizontalScrollMode::Line, 558 _ => unreachable!("value & 0x03 is always <= 0x03"), 559 }; 560 561 log::trace!(" Vertical scroll mode: {:?}", self.vertical_scroll_mode); 562 log::trace!(" Horizontal scroll mode: {:?}", self.horizontal_scroll_mode); 563 } 564 12 => { 565 // Register #12: Mode set register 4 566 self.horizontal_display_size = if value.bit(7) || value.bit(0) { 567 HorizontalDisplaySize::FortyCell 568 } else { 569 HorizontalDisplaySize::ThirtyTwoCell 570 }; 571 self.shadow_highlight_flag = value.bit(3); 572 self.interlacing_mode = match (value >> 1) & 0x03 { 573 // TODO how should the "prohibited" mode behave? 574 0x00 | 0x02 => InterlacingMode::Progressive, 575 0x01 => InterlacingMode::Interlaced, 576 0x03 => InterlacingMode::InterlacedDouble, 577 _ => unreachable!("value & 0x03 is always <= 0x03"), 578 }; 579 580 log::trace!(" Horizontal display size: {:?}", self.horizontal_display_size); 581 log::trace!(" Shadow/highlight flag: {}", self.shadow_highlight_flag); 582 log::trace!(" Interlacing mode: {:?}", self.interlacing_mode); 583 } 584 13 => { 585 // Register #13: Horizontal scroll table base address (bits 15-10) 586 self.h_scroll_table_base_addr = u16::from(value & 0x3F) << 10; 587 588 log::trace!(" H scroll table base address: {:04X}", self.h_scroll_table_base_addr); 589 } 590 15 => { 591 // Register #15: VRAM address auto increment 592 self.data_port_auto_increment = value.into(); 593 594 log::trace!(" Data port auto increment: {value:02X}"); 595 } 596 16 => { 597 // Register #16: Scroll size 598 self.vertical_scroll_size = ScrollSize::from_bits(value >> 4); 599 self.horizontal_scroll_size = ScrollSize::from_bits(value); 600 601 log::trace!(" Vertical scroll size: {:?}", self.vertical_scroll_size); 602 log::trace!(" Horizontal scroll size: {:?}", self.horizontal_scroll_size); 603 } 604 17 => { 605 // Register #17: Window horizontal position 606 self.window_horizontal_mode = if value.bit(7) { 607 WindowHorizontalMode::CenterToRight 608 } else { 609 WindowHorizontalMode::LeftToCenter 610 }; 611 self.window_x_position = u16::from(value & 0x1F) << 1; 612 613 log::trace!(" Window horizontal mode: {:?}", self.window_horizontal_mode); 614 log::trace!(" Window X position: {}", self.window_x_position); 615 } 616 18 => { 617 // Register #18: Window vertical position 618 self.window_vertical_mode = if value.bit(7) { 619 WindowVerticalMode::CenterToBottom 620 } else { 621 WindowVerticalMode::TopToCenter 622 }; 623 self.window_y_position = (value & 0x1F).into(); 624 625 log::trace!(" Window vertical mode: {:?}", self.window_vertical_mode); 626 log::trace!(" Window Y position: {}", self.window_y_position); 627 } 628 19 => { 629 // Register #19: DMA length counter (bits 7-0) 630 self.dma_length.set_lsb(value); 631 632 log::trace!(" DMA length: {}", self.dma_length); 633 } 634 20 => { 635 // Register #20: DMA length counter (bits 15-8) 636 self.dma_length.set_msb(value); 637 638 log::trace!(" DMA length: {}", self.dma_length); 639 } 640 21 => { 641 // Register 21: DMA source address (bits 9-1) 642 self.dma_source_address = 643 (self.dma_source_address & 0xFFFF_FE00) | (u32::from(value) << 1); 644 645 log::trace!(" DMA source address: {:06X}", self.dma_source_address); 646 } 647 22 => { 648 // Register 22: DMA source address (bits 16-9) 649 self.dma_source_address = 650 (self.dma_source_address & 0xFFFE_01FF) | (u32::from(value) << 9); 651 652 log::trace!(" DMA source address: {:06X}", self.dma_source_address); 653 } 654 23 => { 655 // Register 23: DMA source address (bits 22-17) and mode 656 self.dma_source_address = 657 (self.dma_source_address & 0x0001_FFFF) | (u32::from(value & 0x3F) << 17); 658 self.dma_mode = match value & 0xC0 { 659 0x00 => DmaMode::MemoryToVram, 660 0x40 => { 661 // If DMD1=0, DMD0 is used as bit 23 in source address 662 self.dma_source_address |= 1 << 23; 663 664 DmaMode::MemoryToVram 665 } 666 0x80 => DmaMode::VramFill, 667 0xC0 => DmaMode::VramCopy, 668 _ => unreachable!("value & 0x0C is always 0x00/0x40/0x80/0xC0"), 669 }; 670 671 log::trace!(" DMA source address: {:06X}", self.dma_source_address); 672 log::trace!(" DMA mode: {:?}", self.dma_mode); 673 } 674 _ => {} 675 } 676 } 677 678 pub fn is_line_in_v_window(&self, scanline: u16) -> bool { 679 self.window_vertical_mode.in_window(scanline, self.window_y_position) 680 } 681 682 pub fn window_h_range(&self, active_display_pixels: u16) -> (u16, u16) { 683 self.window_horizontal_mode.h_range(self.window_x_position, active_display_pixels) 684 } 685 686 pub fn masked_window_nametable_addr(&self) -> u16 { 687 // A11 is ignored in H40 mode; Cheese Cat-Astrophe depends on this 688 let mask = match self.horizontal_display_size { 689 HorizontalDisplaySize::ThirtyTwoCell => 0xF800, 690 HorizontalDisplaySize::FortyCell => 0xF000, 691 }; 692 self.window_base_nt_addr & mask 693 } 694 695 pub fn masked_sprite_attribute_table_addr(&self) -> u32 { 696 self.sprite_attribute_table_base_addr 697 & self.horizontal_display_size.sprite_attribute_table_mask() 698 } 699} 700 701#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 702pub enum Plane { 703 #[default] 704 Background, 705 Sprite, 706 ScrollA, 707 ScrollB, 708} 709 710impl Display for Plane { 711 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 712 match self { 713 Self::Background => write!(f, "Backdrop"), 714 Self::Sprite => write!(f, "Sprites"), 715 Self::ScrollA => write!(f, "Plane A"), 716 Self::ScrollB => write!(f, "Plane B"), 717 } 718 } 719} 720 721#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 722pub struct DebugRegister { 723 pub display_disabled: bool, 724 pub forced_plane: Plane, 725} 726 727impl DebugRegister { 728 pub fn new() -> Self { 729 Self { display_disabled: false, forced_plane: Plane::default() } 730 } 731 732 pub fn write(&mut self, value: u16) { 733 self.display_disabled = value.bit(6); 734 self.forced_plane = match (value >> 7) & 0x3 { 735 0x0 => Plane::Background, 736 0x1 => Plane::Sprite, 737 0x2 => Plane::ScrollA, 738 0x3 => Plane::ScrollB, 739 _ => unreachable!("value & 0x3 is always <= 0x3"), 740 }; 741 } 742}