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 };
144 rendered lines + 10 VBlank lines
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];
Initialize VRAM the way that the DMG boot ROM would. The Nintendo logo is copied out of the cartridge header. Some games depend on this by assuming that VRAM initially contains the Nintendo logo and a 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}