Game Boy internal memory
Initial contents of $FF80-$FFC2; the rest of HRAM is presumably randomized These values are normally written by the CGB boot ROM
20#[rustfmt::skip] 21const CGB_INITIAL_HRAM: &[u8; 67] = &[ 22 0x00, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 23 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 24 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 25 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 26 0x7F, 0x00, 0x00, 27];
29#[derive(Debug, Clone, Encode, Decode)] 30pub struct Memory { 31 main_ram: Box<MainRam>, 32 main_ram_bank: u8, 33 hram: Box<Hram>, 34 boot_rom: Box<[u8]>, 35 boot_rom_present: bool, 36 boot_rom_mapped: bool, 37 hardware_mode: HardwareMode, 38} 39 40impl Memory { 41 pub fn new( 42 mut boot_rom: Option<Vec<u8>>, 43 hardware_mode: HardwareMode, 44 ) -> Result<Self, GameBoyLoadError> { 45 let boot_rom_len = match hardware_mode { 46 HardwareMode::Dmg => DMG_BOOT_ROM_LEN, 47 HardwareMode::Cgb => CGB_BOOT_ROM_LEN, 48 }; 49 50 // If boot ROM is present, validate that it's the expected length 51 // Allow CGB boot ROMs to be either 2048 bytes or 2048+256 bytes (with a filler $01 page) 52 if let Some(boot_rom) = &mut boot_rom 53 && boot_rom.len() != boot_rom_len 54 { 55 if hardware_mode == HardwareMode::Cgb && boot_rom.len() == boot_rom_len - 256 { 56 *boot_rom = pad_cgb_boot_rom(boot_rom); 57 } else { 58 return Err(GameBoyLoadError::InvalidBootRomSize { 59 actual: boot_rom.len(), 60 expected: boot_rom_len, 61 }); 62 } 63 } 64 65 let boot_rom_present = boot_rom.is_some(); 66 let boot_rom = boot_rom.unwrap_or_else(|| vec![0; boot_rom_len]).into_boxed_slice(); 67 68 let mut main_ram: Vec<u8> = iter::repeat_with(rand::random).take(MAIN_RAM_LEN).collect(); 69 let mut hram: Vec<u8> = iter::repeat_with(rand::random).take(HRAM_LEN).collect(); 70 71 if !boot_rom_present { 72 // Randomize RAM contents at power-on except for bank 2, which the CGB boot ROM normally 73 // zerofills. Worms Armageddon depends on bank 2 being zeroed out. 74 // Hardware mode check is not necessary because banks 2-7 are not accessible on DMG 75 main_ram[0x2000..0x3000].fill(0); 76 77 hram.fill(0); 78 if hardware_mode == HardwareMode::Cgb { 79 hram[..CGB_INITIAL_HRAM.len()].copy_from_slice(CGB_INITIAL_HRAM); 80 } 81 } 82 83 Ok(Self { 84 main_ram: main_ram.into_boxed_slice().try_into().unwrap(), 85 main_ram_bank: 0, 86 hram: hram.into_boxed_slice().try_into().unwrap(), 87 boot_rom, 88 boot_rom_present, 89 boot_rom_mapped: boot_rom_present, 90 hardware_mode, 91 }) 92 } 93 94 pub fn read_main_ram(&self, address: u16) -> u8 { 95 let ram_addr = map_ram_address(address, self.main_ram_bank); 96 self.main_ram[ram_addr as usize] 97 } 98 99 pub fn write_main_ram(&mut self, address: u16, value: u8) { 100 let ram_addr = map_ram_address(address, self.main_ram_bank); 101 self.main_ram[ram_addr as usize] = value; 102 } 103 104 pub fn read_hram(&self, address: u16) -> u8 { 105 self.hram[(address & 0x7F) as usize] 106 } 107 108 pub fn write_hram(&mut self, address: u16, value: u8) { 109 self.hram[(address & 0x7F) as usize] = value; 110 } 111 112 pub fn try_read_boot_rom(&self, address: u16) -> Option<u8> { 113 if !self.boot_rom_mapped { 114 return None; 115 } 116 117 // DMG boot ROM is 256 bytes, mapped to $0000-$00FF 118 // CGB boot ROM is 2048 bytes, first 256 mapped to $0000-$00FF and remaining 1792 mapped 119 // to $0200-$08FF 120 match (address, self.hardware_mode) { 121 (0x0000..=0x00FF, _) | (0x0200..=0x08FF, HardwareMode::Cgb) => { 122 Some(self.boot_rom[address as usize]) 123 } 124 _ => None, 125 } 126 } 127 128 pub fn read_svbk(&self) -> u8 { 129 0xF8 | self.main_ram_bank 130 } 131 132 pub fn write_svbk(&mut self, value: u8) { 133 self.main_ram_bank = value & 0x07; 134 135 log::trace!("SVBK write: WRAM bank set to {}", self.main_ram_bank); 136 } 137 138 // $FF50: BANK (used only by boot ROM to unmap itself) 139 pub fn write_bank(&mut self, value: u8) { 140 self.boot_rom_mapped &= !value.bit(0); 141 142 log::debug!("BANK write: {value:02X} (boot ROM mapped: {})", self.boot_rom_mapped); 143 } 144 145 pub fn boot_rom_mapped(&self) -> bool { 146 self.boot_rom_mapped 147 } 148} 149 150fn map_ram_address(address: u16, ram_bank: u8) -> u16 { 151 match address & 0x1FFF { 152 address @ 0x0000..=0x0FFF => address, 153 0x1000..=0x1FFF => { 154 // RAM bank of 0 functions as 1 155 let ram_bank = if ram_bank == 0 { 1 } else { u16::from(ram_bank) }; 156 (ram_bank << 12) | (address & 0x0FFF) 157 } 158 _ => unreachable!("address & 0x1FFF is always <= 0x1FFF"), 159 } 160}
Pad a 2048-byte CGB boot ROM to 2304 bytes by adding a filler $01 page
163fn pad_cgb_boot_rom(boot_rom: &[u8]) -> Vec<u8> { 164 let mut new_boot_rom = vec![0; CGB_BOOT_ROM_LEN]; 165 new_boot_rom[..0x0100].copy_from_slice(&boot_rom[..0x0100]); 166 new_boot_rom.extend(iter::repeat_n(0, 256)); 167 new_boot_rom[0x0200..].copy_from_slice(&boot_rom[0x0100..0x0800]); 168 169 new_boot_rom 170}