1//! Game Boy PPU (picture processing unit) 2 3mod debug; 4mod fifo; 5mod registers; 6 7use crate::HardwareMode; 8use crate::cgb::{CgbRegisters, CpuSpeed}; 9use crate::dma::DmaUnit; 10use crate::interrupts::InterruptRegisters; 11use crate::ppu::fifo::PixelFifo; 12use crate::ppu::registers::{CgbPaletteRam, Registers}; 13use crate::sm83::InterruptType; 14use bincode::{Decode, Encode}; 15use jgenesis_common::boxedarray::BoxedWordArray; 16use jgenesis_common::frontend::FrameSize; 17use jgenesis_common::num::GetBit; 18use std::ops::Range; 19 20const SCREEN_WIDTH: usize = 160; 21const SCREEN_HEIGHT: usize = 144; 22 23pub const FRAME_BUFFER_LEN: usize = SCREEN_WIDTH * SCREEN_HEIGHT; 24 25pub const FRAME_SIZE: FrameSize = 26 FrameSize { width: SCREEN_WIDTH as u32, height: SCREEN_HEIGHT as u32 }; 27 28// 144 rendered lines + 10 VBlank lines 29pub const LINES_PER_FRAME: u8 = 154; 30pub const DOTS_PER_LINE: u16 = 456; 31const OAM_SCAN_DOTS: u16 = 80; 32 33const MAX_SPRITES_PER_LINE: usize = 10; 34 35const VRAM_LEN: usize = 16 * 1024; 36const OAM_LEN: usize = 160; 37 38type Vram = [u8; VRAM_LEN]; 39type Oam = [u8; OAM_LEN]; 40 41pub type PpuFrameBuffer = [u16; FRAME_BUFFER_LEN]; 42 43#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 44pub enum PpuMode { 45 // Mode 1 46 VBlank, 47 // Mode 0 48 HBlank, 49 // Mode 2 50 ScanningOam, 51 // Glitched mode 2 that occurs after re-enabling the PPU 52 ScanningOamGlitched, 53 // Mode 3 54 Rendering, 55} 56 57impl PpuMode { 58 fn to_bits(self) -> u8 { 59 match self { 60 Self::HBlank => 0, 61 Self::VBlank => 1, 62 Self::ScanningOam | Self::ScanningOamGlitched => 2, 63 Self::Rendering => 3, 64 } 65 } 66 67 fn is_scanning_oam(self) -> bool { 68 matches!(self, Self::ScanningOam | Self::ScanningOamGlitched) 69 } 70} 71 72#[derive(Debug, Clone, Encode, Decode)] 73struct State { 74 scanline: u8, 75 dot: u16, 76 mode: PpuMode, 77 prev_stat_interrupt_line: bool, 78 stat_interrupt_pending: bool, 79 previously_enabled: bool, 80 // LY=LYC bit in STAT does not change while PPU is disabled, per: 81 // https://gbdev.gg8.se/wiki/articles/Tricky-to-emulate_games 82 frozen_ly_lyc_bit: bool, 83 skip_next_frame: bool, 84 frame_complete: bool, 85 powered_off_dots: u32, 86} 87 88impl State { 89 fn new() -> Self { 90 Self { 91 scanline: 0, 92 dot: 0, 93 mode: PpuMode::ScanningOam, 94 prev_stat_interrupt_line: false, 95 stat_interrupt_pending: false, 96 previously_enabled: true, 97 frozen_ly_lyc_bit: false, 98 skip_next_frame: true, 99 frame_complete: false, 100 powered_off_dots: 0, 101 } 102 } 103 104 fn ly(&self) -> u8 { 105 if self.scanline == LINES_PER_FRAME - 1 && self.dot >= 4 { 106 // LY=0 starts 4 dots into the final scanline. Kirby's Dream Land 2 and Wario Land 2 depend on this for 107 // minor top-of-screen effects 108 0 109 } else { 110 self.scanline 111 } 112 } 113 114 fn ly_for_compare(&self, cpu_speed: CpuSpeed) -> u8 { 115 // This handles two edge cases for LY=LYC interrupts: 116 // 117 // 1. HBlank interrupts should not block LY=LYC interrupts; Ken Griffey Jr.'s Slugfest 118 // depends on this 119 // 120 // 2. When LYC=0, the LY=LYC interrupt should not trigger before dot 9 in single speed 121 // or before dot 13 in CGB double speed. The demo Mental Respirator depends on this for the 122 // "gin & tonic trick" effect 123 match self.scanline { 124 0 => 0, 125 line @ 1..=152 => { 126 if self.dot != 0 { 127 line 128 } else { 129 line - 1 130 } 131 } 132 153 => match (self.dot, cpu_speed) { 133 (0, _) => 152, 134 (1..=8, _) | (9..=12, CpuSpeed::Double) => 153, 135 _ => 0, 136 }, 137 _ => panic!("Invalid scanline in state: {}", self.scanline), 138 } 139 } 140} 141 142#[derive(Debug, Clone, Copy, Encode, Decode)] 143struct SpriteData { 144 oam_index: u8, 145 x: u8, 146 y: u8, 147 tile_number: u8, 148 vram_bank: u8, 149 palette: u8, 150 horizontal_flip: bool, 151 vertical_flip: bool, 152 low_priority: bool, 153} 154 155#[derive(Debug, Clone, Encode, Decode)] 156pub struct Ppu { 157 hardware_mode: HardwareMode, 158 frame_buffer: BoxedWordArray<FRAME_BUFFER_LEN>, 159 vram: Box<Vram>, 160 oam: Box<Oam>, 161 registers: Registers, 162 bg_palette_ram: CgbPaletteRam, 163 sprite_palette_ram: CgbPaletteRam, 164 state: State, 165 sprite_buffer: Vec<SpriteData>, 166 fifo: PixelFifo, 167} 168 169impl Ppu { 170 pub fn new(hardware_mode: HardwareMode, rom: &[u8], boot_rom_present: bool) -> Self { 171 let mut vram = vec![0; VRAM_LEN]; 172 173 if !boot_rom_present { 174 initialize_vram(hardware_mode, rom, &mut vram); 175 } 176 177 Self { 178 hardware_mode, 179 frame_buffer: BoxedWordArray::new(), 180 vram: vram.into_boxed_slice().try_into().unwrap(), 181 oam: vec![0; OAM_LEN].into_boxed_slice().try_into().unwrap(), 182 registers: Registers::new(boot_rom_present), 183 bg_palette_ram: CgbPaletteRam::new_bg(), 184 sprite_palette_ram: CgbPaletteRam::new_obj(), 185 state: State::new(), 186 sprite_buffer: Vec::with_capacity(MAX_SPRITES_PER_LINE), 187 fifo: PixelFifo::new(hardware_mode), 188 } 189 } 190 191 pub fn tick_dot( 192 &mut self, 193 cgb_registers: CgbRegisters, 194 dma_unit: &DmaUnit, 195 interrupt_registers: &mut InterruptRegisters, 196 ) { 197 if !self.registers.ppu_enabled { 198 if self.state.previously_enabled { 199 // Disabling PPU freezes the LY=LYC bit until it's re-enabled, per: 200 // https://gbdev.gg8.se/wiki/articles/Tricky-to-emulate_games 201 self.state.frozen_ly_lyc_bit = self.state.scanline == self.registers.ly_compare; 202 203 // Disabling the PPU moves it to line 0 + mode 0 and clears the display 204 self.state.scanline = 0; 205 self.state.dot = 0; 206 self.state.mode = PpuMode::HBlank; 207 208 self.sprite_buffer.clear(); 209 self.fifo.reset_window_state(); 210 self.fifo.start_new_line(0, &self.registers, &[]); 211 212 self.state.previously_enabled = false; 213 self.state.stat_interrupt_pending = false; 214 self.state.prev_stat_interrupt_line = false; 215 216 self.state.powered_off_dots = 0; 217 } 218 219 self.state.powered_off_dots += 1; 220 if self.state.powered_off_dots == u32::from(LINES_PER_FRAME) * u32::from(DOTS_PER_LINE) 221 { 222 // Force a blank frame render if the PPU is powered off for a full frame's worth of cycles 223 self.clear_frame_buffer(); 224 self.state.frame_complete = true; 225 self.state.powered_off_dots = 0; 226 } 227 228 // Unlike TV-based systems, the PPU does not process at all when display is disabled 229 return; 230 } else if !self.state.previously_enabled { 231 self.state.previously_enabled = true; 232 233 // Restarting the PPU at dot 4 instead of 0 fixes graphical glitches in GBVideoPlayer2 234 self.state.dot = 4; 235 236 // When the PPU is re-enabled, the next frame is not displayed 237 self.state.skip_next_frame = true; 238 239 self.state.mode = PpuMode::ScanningOamGlitched; 240 } 241 242 // STAT interrupts don't seem to fire during the first 4 dots of line 0 243 if self.state.stat_interrupt_pending && (self.state.scanline != 0 || self.state.dot >= 4) { 244 log::trace!( 245 "Generating STAT interrupt at line {} dot {}", 246 self.state.scanline, 247 self.state.dot 248 ); 249 250 interrupt_registers.set_flag(InterruptType::LcdStatus); 251 self.state.stat_interrupt_pending = false; 252 } 253 254 if self.state.mode == PpuMode::Rendering { 255 let frame_buffer = (!self.state.skip_next_frame).then_some(self.frame_buffer.as_mut()); 256 self.fifo.tick( 257 &self.vram, 258 &self.registers, 259 cgb_registers, 260 &self.bg_palette_ram, 261 &self.sprite_palette_ram, 262 frame_buffer, 263 ); 264 if self.fifo.done_with_line() { 265 log::trace!( 266 "Pixel FIFO finished line {} after dot {}", 267 self.state.scanline, 268 self.state.dot 269 ); 270 self.state.mode = PpuMode::HBlank; 271 } 272 } 273 274 self.state.dot += 1; 275 if self.state.dot == DOTS_PER_LINE { 276 // Check the window Y condition again before moving to the next line. 277 // The fairylake.gb test ROM depends on this because it enables the window during mode 3 278 // with WY==LY 279 self.fifo.check_window_y(self.state.scanline, &self.registers); 280 281 self.state.dot = 0; 282 self.state.scanline += 1; 283 if self.state.scanline == LINES_PER_FRAME { 284 self.state.scanline = 0; 285 self.fifo.reset_window_state(); 286 } 287 288 if self.state.scanline < SCREEN_HEIGHT as u8 { 289 self.state.mode = PpuMode::ScanningOam; 290 291 self.sprite_buffer.clear(); 292 293 // PPU cannot read OAM while an OAM DMA is in progress 294 // TODO does anything depend on partial OAM scan when an OAM DMA finishes during mode 2? 295 if !dma_unit.oam_dma_in_progress() { 296 scan_oam( 297 self.hardware_mode, 298 cgb_registers.dmg_compatibility, 299 self.state.scanline, 300 self.registers.double_height_sprites, 301 &self.oam, 302 &mut self.sprite_buffer, 303 ); 304 } 305 } else { 306 self.state.mode = PpuMode::VBlank; 307 } 308 } else if self.state.scanline < SCREEN_HEIGHT as u8 && self.state.dot == OAM_SCAN_DOTS { 309 self.fifo.start_new_line(self.state.scanline, &self.registers, &self.sprite_buffer); 310 self.state.mode = PpuMode::Rendering; 311 } 312 313 // TODO timing 314 if self.state.scanline == SCREEN_HEIGHT as u8 && self.state.dot == 1 { 315 interrupt_registers.set_flag(InterruptType::VBlank); 316 if self.state.skip_next_frame { 317 self.state.skip_next_frame = false; 318 } else { 319 self.state.frame_complete = true; 320 } 321 322 // Obscure behavior: If the mode 2 STAT interrupt is enabled, it will trigger a STAT 323 // interrupt on line 144 around the same time that VBlank starts. 324 // 325 // GB Video Player (https://github.com/LIJI32/GBVideoPlayer) depends on this because 326 // it uses the Mode 2 STAT interrupt and expects it to trigger 145 times per frame, not 144 327 if !self.state.prev_stat_interrupt_line && self.registers.mode_2_interrupt_enabled { 328 interrupt_registers.set_flag(InterruptType::LcdStatus); 329 } 330 } 331 332 let stat_interrupt_line = self.stat_interrupt_line(cgb_registers.speed); 333 if !self.state.prev_stat_interrupt_line && stat_interrupt_line { 334 self.state.stat_interrupt_pending = true; 335 log::trace!( 336 "Setting STAT pending: LY={}, LYC={}, mode={:?}", 337 self.state.ly(), 338 self.registers.ly_compare, 339 self.state.mode 340 ); 341 } 342 self.state.prev_stat_interrupt_line = stat_interrupt_line; 343 } 344 345 fn clear_frame_buffer(&mut self) { 346 log::trace!("Clearing PPU frame buffer"); 347 348 // Disabling display makes the entire display white, which is color 0 on DMG 349 // and color 31/31/31 ($7FFF) on CGB 350 let fill_color = match self.hardware_mode { 351 HardwareMode::Dmg => 0, 352 HardwareMode::Cgb => 0b11111_11111_11111, 353 }; 354 self.frame_buffer.fill(fill_color); 355 356 // Signal that the frame should be displayed 357 self.state.frame_complete = true; 358 } 359 360 fn stat_interrupt_line(&self, cpu_speed: CpuSpeed) -> bool { 361 let lyc_interrupt_enabled = self.registers.lyc_interrupt_enabled; 362 let mode_2_interrupt_enabled = self.registers.mode_2_interrupt_enabled; 363 let mode_1_interrupt_enabled = self.registers.mode_1_interrupt_enabled; 364 let mode_0_interrupt_enabled = self.registers.mode_0_interrupt_enabled; 365 366 (lyc_interrupt_enabled && self.state.ly_for_compare(cpu_speed) == self.registers.ly_compare) 367 || (mode_2_interrupt_enabled && self.state.mode.is_scanning_oam()) 368 || (mode_1_interrupt_enabled && self.state.mode == PpuMode::VBlank) 369 || (mode_0_interrupt_enabled && self.state.mode == PpuMode::HBlank) 370 } 371 372 pub fn frame_buffer(&self) -> &PpuFrameBuffer { 373 &self.frame_buffer 374 } 375 376 pub fn frame_complete(&self) -> bool { 377 self.state.frame_complete 378 } 379 380 pub fn clear_frame_complete(&mut self) { 381 self.state.frame_complete = false; 382 } 383 384 pub fn read_vram(&self, address: u16) -> u8 { 385 if self.cpu_can_access_vram() { 386 let vram_addr = map_vram_address(address, self.registers.vram_bank); 387 self.vram[vram_addr as usize] 388 } else { 389 0xFF 390 } 391 } 392 393 pub fn write_vram(&mut self, address: u16, value: u8) { 394 if self.cpu_can_access_vram() { 395 let vram_addr = map_vram_address(address, self.registers.vram_bank); 396 self.vram[vram_addr as usize] = value; 397 } 398 } 399 400 pub fn read_oam(&self, address: u16) -> u8 { 401 if self.cpu_can_access_oam() { self.oam[(address & 0xFF) as usize] } else { 0xFF } 402 } 403 404 pub fn write_oam(&mut self, address: u16, value: u8) { 405 if self.cpu_can_access_oam() { 406 self.oam[(address & 0xFF) as usize] = value; 407 } 408 } 409 410 // OAM DMA can write to OAM at any time, even during Modes 2 and 3 411 pub fn write_oam_for_dma(&mut self, address: u16, value: u8) { 412 self.oam[(address & 0xFF) as usize] = value; 413 } 414 415 fn cpu_can_access_oam(&self) -> bool { 416 !matches!(self.state.mode, PpuMode::ScanningOam | PpuMode::Rendering) 417 } 418 419 fn cpu_can_access_vram(&self) -> bool { 420 // Allow access even during mode 3 if dot <= 84. 421 // Because of how the CPU and PPU are executed, a write at dot == 80 would have occurred 422 // on dot 78 (single-speed) or dot 79 (double-speed) on actual hardware and would not have 423 // been blocked. 424 // Allowing writes on dots 81-84 (probably 81-83 in actual hardware) is a hack to fix 425 // what seems to be a timing issue elsewhere, possibly interrupt-related. The Stunt Race FX 426 // demo depends on allowing these through 427 self.state.mode != PpuMode::Rendering || self.state.dot <= OAM_SCAN_DOTS + 4 428 } 429 430 pub fn mode(&self) -> PpuMode { 431 self.state.mode 432 } 433 434 pub fn read_register(&self, address: u16, cgb_registers: CgbRegisters) -> u8 { 435 match address & 0xFF { 436 0x40 => self.registers.read_lcdc(), 437 0x41 => self.registers.read_stat(&self.state, cgb_registers.speed), 438 0x42 => self.registers.bg_y_scroll, 439 0x43 => self.registers.bg_x_scroll, 440 // LY: Line number 441 0x44 => self.state.ly(), 442 0x45 => self.registers.ly_compare, 443 0x47 => self.registers.read_bgp(), 444 0x48 => self.registers.read_obp0(), 445 0x49 => self.registers.read_obp1(), 446 0x4A => self.registers.window_y, 447 0x4B => self.registers.window_x, 448 0x4F => self.registers.read_vbk(), 449 0x68 => self.bg_palette_ram.read_data_port_address(), 450 0x69 => self.bg_palette_ram.read_data_port(self.cpu_can_access_vram()), 451 0x6A => self.sprite_palette_ram.read_data_port_address(), 452 0x6B => self.sprite_palette_ram.read_data_port(self.cpu_can_access_vram()), 453 _ => { 454 log::warn!("PPU register read {address:04X}"); 455 0xFF 456 } 457 } 458 } 459 460 pub fn write_register( 461 &mut self, 462 address: u16, 463 value: u8, 464 speed: CpuSpeed, 465 interrupt_registers: &mut InterruptRegisters, 466 ) { 467 log::trace!( 468 "PPU register write on line {} dot {}: {address:04X} set to {value:02X}", 469 self.state.scanline, 470 self.state.dot 471 ); 472 473 match address & 0xFF { 474 0x40 => self.registers.write_lcdc(value), 475 0x41 => self.write_stat(value, interrupt_registers), 476 0x42 => self.registers.write_scy(value), 477 0x43 => self.registers.write_scx(value), 478 // LY, not writable 479 0x44 => {} 480 0x45 => self.write_lyc(value, speed), 481 0x47 => self.registers.write_bgp(value), 482 0x48 => self.registers.write_obp0(value), 483 0x49 => self.registers.write_obp1(value), 484 0x4A => self.registers.write_wy(value), 485 0x4B => self.registers.write_wx(value), 486 0x4F => self.registers.write_vbk(value), 487 0x68 => self.bg_palette_ram.write_data_port_address(value), 488 0x69 => self.bg_palette_ram.write_data_port(value, self.cpu_can_access_vram()), 489 0x6A => self.sprite_palette_ram.write_data_port_address(value), 490 0x6B => self.sprite_palette_ram.write_data_port(value, self.cpu_can_access_vram()), 491 _ => log::warn!("PPU register write {address:04X} {value:02X}"), 492 } 493 } 494 495 fn write_stat(&mut self, value: u8, interrupt_registers: &mut InterruptRegisters) { 496 if self.hardware_mode == HardwareMode::Dmg && self.registers.ppu_enabled { 497 // DMG STAT bug: If STAT is written while any of the 4 STAT conditions are true, the 498 // hardware behaves as if all 4 STAT interrupts are enabled for a single M-cycle. 499 // Road Rash (GB version) and Zerd no Densetsu depend on this 500 let dmg_stat_bug_triggered = self.state.mode != PpuMode::Rendering 501 || self.state.ly_for_compare(CpuSpeed::Normal) == self.registers.ly_compare; 502 503 if dmg_stat_bug_triggered { 504 // It seems that the DMG STAT bug does not trigger if HBlank interrupts were previously 505 // enabled and the current mode is 2 (OAM scan). This doesn't really make sense, but 506 // this fixes Initial D Gaiden and doesn't break Road Rash or Zerd no Densetsu 507 let suppress_oam_interrupts = self.registers.mode_0_interrupt_enabled 508 && self.state.mode == PpuMode::ScanningOam; 509 let bugged_stat_write = !(u8::from(suppress_oam_interrupts) << 5); 510 511 self.registers.write_stat(bugged_stat_write); 512 513 let stat_interrupt_line = self.stat_interrupt_line(CpuSpeed::Normal); 514 if !self.state.prev_stat_interrupt_line && stat_interrupt_line { 515 interrupt_registers.set_flag(InterruptType::LcdStatus); 516 } 517 self.state.prev_stat_interrupt_line = stat_interrupt_line; 518 } 519 } 520 521 self.registers.write_stat(value); 522 } 523 524 fn write_lyc(&mut self, value: u8, speed: CpuSpeed) { 525 self.registers.write_lyc(value); 526 527 // If changing LYC would cause the STAT interrupt line to go from high to low, immediately 528 // pull it low. 529 // This fixes graphical glitches in SQRKZ, where it sometimes changes LYC from 141 to 142 530 // on line=142 dot=0, and the LY=LYC STAT interrupt should trigger almost immediately. 531 // TODO timing around the LY=LYC interrupt is iffy in general - improve this 532 if value == self.state.scanline && self.state.dot == 0 { 533 let stat_interrupt_line = self.stat_interrupt_line(speed); 534 self.state.prev_stat_interrupt_line &= stat_interrupt_line; 535 } 536 } 537} 538 539fn map_vram_address(address: u16, vram_bank: u8) -> u16 { 540 (u16::from(vram_bank) << 13) | (address & 0x1FFF) 541} 542 543fn scan_oam( 544 hardware_mode: HardwareMode, 545 cgb_dmg_compatibility: bool, 546 scanline: u8, 547 double_height_sprites: bool, 548 oam: &Oam, 549 sprite_buffer: &mut Vec<SpriteData>, 550) { 551 let sprite_height = if double_height_sprites { 16 } else { 8 }; 552 553 for oam_idx in 0..OAM_LEN / 4 { 554 let oam_addr = 4 * oam_idx; 555 556 let y = oam[oam_addr]; 557 558 // Check if sprite overlaps current line 559 let sprite_top = i16::from(y) - 16; 560 let sprite_bottom = sprite_top + sprite_height; 561 if !(sprite_top..sprite_bottom).contains(&scanline.into()) { 562 continue; 563 } 564 565 let x = oam[oam_addr + 1]; 566 let tile_number = oam[oam_addr + 2]; 567 568 let attributes = oam[oam_addr + 3]; 569 let horizontal_flip = attributes.bit(5); 570 let vertical_flip = attributes.bit(6); 571 let low_priority = attributes.bit(7); 572 573 // VRAM bank is only valid in CGB mode, and palette is read from different bits 574 let (vram_bank, palette) = match (hardware_mode, cgb_dmg_compatibility) { 575 (HardwareMode::Dmg, _) | (HardwareMode::Cgb, true) => (0, attributes.bit(4).into()), 576 (HardwareMode::Cgb, false) => (attributes.bit(3).into(), attributes & 0x07), 577 }; 578 579 sprite_buffer.push(SpriteData { 580 oam_index: oam_idx as u8, 581 x, 582 y, 583 tile_number, 584 vram_bank, 585 palette, 586 horizontal_flip, 587 vertical_flip, 588 low_priority, 589 }); 590 if sprite_buffer.len() == MAX_SPRITES_PER_LINE { 591 break; 592 } 593 } 594 595 sprite_buffer.sort_by(|a, b| a.x.cmp(&b.x).then(a.oam_index.cmp(&b.oam_index))); 596} 597 598const NINTENDO_LOGO_ADDR: Range<usize> = 0x0104..0x0134; 599const LOGO_TILE_DATA_ADDR: usize = 0x0010; 600 601const TRADEMARK_TILE_DATA_ADDR: usize = 0x0190; 602const TRADEMARK_SYMBOL: [u8; 16] = [ 603 0x3C, 0x00, 0x42, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0x42, 0x00, 0x3C, 0x00, 604]; 605 606// Initialize VRAM the way that the DMG boot ROM would. The Nintendo logo is copied out of the 607// cartridge header. 608// Some games depend on this by assuming that VRAM initially contains the Nintendo logo and a 609// trademark symbol, e.g. X for its intro animation 610fn initialize_vram(hardware_mode: HardwareMode, rom: &[u8], vram: &mut [u8]) { 611 if hardware_mode != HardwareMode::Dmg { 612 // Only write the logo to VRAM on DMG 613 return; 614 } 615 616 if rom.len() < NINTENDO_LOGO_ADDR.end { 617 // Invalid ROM; don't try to initialize VRAM 618 return; 619 } 620 621 // Write logo to tile data area 622 let logo = &rom[NINTENDO_LOGO_ADDR]; 623 for (i, logo_byte) in logo.iter().copied().enumerate() { 624 for nibble_idx in 0..2 { 625 let nibble = logo_byte >> (4 * (1 - nibble_idx)); 626 627 // Duplicate pixels horizontally 628 let vram_byte = ((nibble & 8) << 4) 629 | ((nibble & 8) << 3) 630 | ((nibble & 4) << 3) 631 | ((nibble & 4) << 2) 632 | ((nibble & 2) << 2) 633 | ((nibble & 2) << 1) 634 | ((nibble & 1) << 1) 635 | (nibble & 1); 636 637 // Duplicate pixels vertically 638 let vram_addr = LOGO_TILE_DATA_ADDR + 4 * (2 * i + nibble_idx); 639 vram[vram_addr] = vram_byte; 640 vram[vram_addr + 2] = vram_byte; 641 } 642 } 643 644 // Write trademark to tile data area 645 vram[TRADEMARK_TILE_DATA_ADDR..TRADEMARK_TILE_DATA_ADDR + TRADEMARK_SYMBOL.len()] 646 .copy_from_slice(&TRADEMARK_SYMBOL); 647 648 // Populate tile map 649 // The upscaled logo is 12x2 tiles and should be centered, ranging from (X=4, Y=8) to (X=16, Y=10) 650 for tile_row in 0..2 { 651 for tile_col in 0..12 { 652 let vram_addr = 0x1800 + (8 + tile_row) * 32 + (4 + tile_col); 653 vram[vram_addr] = (1 + (12 * tile_row) + tile_col) as u8; 654 } 655 } 656 657 // Trademark symbol should be in the top row just to the right of the logo, at (X=16, Y=8) 658 vram[0x1800 + 8 * 32 + 16] = (TRADEMARK_TILE_DATA_ADDR / 16) as u8; 659}