use crate::gb::bus::{DmgBus, GbBus};
use crate::gb::cartridge::load_cartridge;
use crate::gb::console::Gb;
use crate::gb::model::DmgModel;
const BOOT_CYCLE_LIMIT: u64 = 8_000_000;
fn compute_header_checksum(rom: &[u8]) -> u8 {
rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1))
}
fn build_test_rom(logo: [u8; 48]) -> Vec<u8> {
let mut rom = build_base_test_rom();
rom[0x0104..0x0134].copy_from_slice(&logo);
rom[0x014D] = compute_header_checksum(&rom);
rom
}
fn build_base_test_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0100] = 0x18; rom[0x0101] = 0xFE; rom[0x0147] = 0x00; rom[0x0148] = 0x00; rom[0x0149] = 0x00; rom
}
fn run_until_cartridge_entry(gb: &mut Gb<DmgBus>) -> bool {
let start = gb.cycles();
loop {
if gb.cpu.regs.pc == 0x0100 {
return true;
}
if gb.cycles().saturating_sub(start) >= BOOT_CYCLE_LIMIT {
return false;
}
gb.step();
}
}
#[test]
fn test_dmg_boot_sets_correct_register_state() {
let rom = build_test_rom([0u8; 48]);
let cart = load_cartridge(&rom).expect("valid ROM");
let mut gb = Gb::new(DmgBus::new(cart, DmgModel::DmgB));
let reached = run_until_cartridge_entry(&mut gb);
assert!(
reached,
"Boot ROM never handed off to $0100 within the cycle limit"
);
assert_eq!(gb.cpu.regs.a, 0x01, "A register after boot");
assert_eq!(gb.cpu.regs.f, 0xB0, "F register after boot");
assert_eq!(gb.cpu.regs.b, 0x00, "B register after boot");
assert_eq!(gb.cpu.regs.c, 0x13, "C register after boot");
assert_eq!(gb.cpu.regs.d, 0x00, "D register after boot");
assert_eq!(gb.cpu.regs.e, 0xD8, "E register after boot");
assert_eq!(gb.cpu.regs.h, 0x01, "H register after boot");
assert_eq!(gb.cpu.regs.l, 0x4D, "L register after boot");
assert_eq!(gb.cpu.regs.sp, 0xFFFE, "SP after boot");
assert_eq!(gb.cpu.regs.pc, 0x0100, "PC after boot");
}
#[test]
fn test_dmg_boot_accepts_any_cartridge_logo() {
let custom_logo = std::array::from_fn(|index| ((index as u8) << 1) ^ 0x5A);
let rom = build_test_rom(custom_logo);
let cart = load_cartridge(&rom).expect("valid ROM");
let mut gb = Gb::new(DmgBus::new(cart, DmgModel::DmgB));
let reached = run_until_cartridge_entry(&mut gb);
assert!(
reached,
"Boot ROM must accept any cartridge logo and reach $0100"
);
}
fn boot_to_cartridge_entry(model: DmgModel) -> Gb<DmgBus> {
let rom = build_test_rom([0u8; 48]);
let cart = load_cartridge(&rom).expect("valid ROM");
let mut gb = Gb::new(DmgBus::new(cart, model));
let reached = run_until_cartridge_entry(&mut gb);
assert!(reached, "Boot ROM must reach $0100 for model {:?}", model);
gb
}
fn read_io(gb: &mut Gb<DmgBus>, addr: u16) -> u8 {
gb.cpu.bus.read(addr)
}
#[test]
fn test_dmg_production_boot_io_registers() {
let mut gb = boot_to_cartridge_entry(DmgModel::DmgB);
assert_eq!(read_io(&mut gb, 0xFF01), 0x00, "SB ($FF01)");
assert_eq!(read_io(&mut gb, 0xFF02), 0x7E, "SC ($FF02)");
assert_eq!(read_io(&mut gb, 0xFF04), 0xAB, "DIV ($FF04)");
assert_eq!(read_io(&mut gb, 0xFF05), 0x00, "TIMA ($FF05)");
assert_eq!(read_io(&mut gb, 0xFF06), 0x00, "TMA ($FF06)");
assert_eq!(read_io(&mut gb, 0xFF07), 0xF8, "TAC ($FF07)");
assert_eq!(read_io(&mut gb, 0xFF0F), 0xE1, "IF ($FF0F)");
assert_eq!(read_io(&mut gb, 0xFF10), 0x80, "NR10 ($FF10)");
assert_eq!(read_io(&mut gb, 0xFF11), 0xBF, "NR11 ($FF11)");
assert_eq!(read_io(&mut gb, 0xFF12), 0xF3, "NR12 ($FF12)");
assert_eq!(read_io(&mut gb, 0xFF13), 0xFF, "NR13 ($FF13)");
assert_eq!(read_io(&mut gb, 0xFF14), 0xBF, "NR14 ($FF14)");
assert_eq!(read_io(&mut gb, 0xFF16), 0x3F, "NR21 ($FF16)");
assert_eq!(read_io(&mut gb, 0xFF17), 0x00, "NR22 ($FF17)");
assert_eq!(read_io(&mut gb, 0xFF18), 0xFF, "NR23 ($FF18)");
assert_eq!(read_io(&mut gb, 0xFF19), 0xBF, "NR24 ($FF19)");
assert_eq!(read_io(&mut gb, 0xFF1A), 0x7F, "NR30 ($FF1A)");
assert_eq!(read_io(&mut gb, 0xFF1B), 0xFF, "NR31 ($FF1B)");
assert_eq!(read_io(&mut gb, 0xFF1C), 0x9F, "NR32 ($FF1C)");
assert_eq!(read_io(&mut gb, 0xFF1D), 0xFF, "NR33 ($FF1D)");
assert_eq!(read_io(&mut gb, 0xFF1E), 0xBF, "NR34 ($FF1E)");
assert_eq!(read_io(&mut gb, 0xFF20), 0xFF, "NR41 ($FF20)");
assert_eq!(read_io(&mut gb, 0xFF21), 0x00, "NR42 ($FF21)");
assert_eq!(read_io(&mut gb, 0xFF22), 0x00, "NR43 ($FF22)");
assert_eq!(read_io(&mut gb, 0xFF23), 0xBF, "NR44 ($FF23)");
assert_eq!(read_io(&mut gb, 0xFF24), 0x77, "NR50 ($FF24)");
assert_eq!(read_io(&mut gb, 0xFF25), 0xF3, "NR51 ($FF25)");
assert_eq!(read_io(&mut gb, 0xFF26), 0xF1, "NR52 ($FF26)");
assert_eq!(read_io(&mut gb, 0xFF40), 0x91, "LCDC ($FF40)");
assert_eq!(read_io(&mut gb, 0xFF42), 0x00, "SCY ($FF42)");
assert_eq!(read_io(&mut gb, 0xFF43), 0x00, "SCX ($FF43)");
assert_eq!(read_io(&mut gb, 0xFF45), 0x00, "LYC ($FF45)");
assert_eq!(read_io(&mut gb, 0xFF46), 0xFF, "DMA ($FF46)");
assert_eq!(read_io(&mut gb, 0xFF47), 0xFC, "BGP ($FF47)");
assert_eq!(read_io(&mut gb, 0xFF4A), 0x00, "WY ($FF4A)");
assert_eq!(read_io(&mut gb, 0xFF4B), 0x00, "WX ($FF4B)");
assert_eq!(read_io(&mut gb, 0xFFFF), 0x00, "IE ($FFFF)");
}
#[test]
fn test_dmg0_boot_io_registers() {
let mut gb = boot_to_cartridge_entry(DmgModel::Dmg0);
assert_eq!(read_io(&mut gb, 0xFF01), 0x00, "SB ($FF01)");
assert_eq!(read_io(&mut gb, 0xFF02), 0x7E, "SC ($FF02)");
assert_eq!(read_io(&mut gb, 0xFF04), 0x18, "DIV ($FF04)");
assert_eq!(read_io(&mut gb, 0xFF05), 0x00, "TIMA ($FF05)");
assert_eq!(read_io(&mut gb, 0xFF06), 0x00, "TMA ($FF06)");
assert_eq!(read_io(&mut gb, 0xFF07), 0xF8, "TAC ($FF07)");
assert_eq!(read_io(&mut gb, 0xFF0F), 0xE1, "IF ($FF0F)");
assert_eq!(read_io(&mut gb, 0xFF10), 0x80, "NR10 ($FF10)");
assert_eq!(read_io(&mut gb, 0xFF11), 0xBF, "NR11 ($FF11)");
assert_eq!(read_io(&mut gb, 0xFF12), 0xF3, "NR12 ($FF12)");
assert_eq!(read_io(&mut gb, 0xFF13), 0xFF, "NR13 ($FF13)");
assert_eq!(read_io(&mut gb, 0xFF14), 0xBF, "NR14 ($FF14)");
assert_eq!(read_io(&mut gb, 0xFF16), 0x3F, "NR21 ($FF16)");
assert_eq!(read_io(&mut gb, 0xFF17), 0x00, "NR22 ($FF17)");
assert_eq!(read_io(&mut gb, 0xFF18), 0xFF, "NR23 ($FF18)");
assert_eq!(read_io(&mut gb, 0xFF19), 0xBF, "NR24 ($FF19)");
assert_eq!(read_io(&mut gb, 0xFF1A), 0x7F, "NR30 ($FF1A)");
assert_eq!(read_io(&mut gb, 0xFF1B), 0xFF, "NR31 ($FF1B)");
assert_eq!(read_io(&mut gb, 0xFF1C), 0x9F, "NR32 ($FF1C)");
assert_eq!(read_io(&mut gb, 0xFF1D), 0xFF, "NR33 ($FF1D)");
assert_eq!(read_io(&mut gb, 0xFF1E), 0xBF, "NR34 ($FF1E)");
assert_eq!(read_io(&mut gb, 0xFF20), 0xFF, "NR41 ($FF20)");
assert_eq!(read_io(&mut gb, 0xFF21), 0x00, "NR42 ($FF21)");
assert_eq!(read_io(&mut gb, 0xFF22), 0x00, "NR43 ($FF22)");
assert_eq!(read_io(&mut gb, 0xFF23), 0xBF, "NR44 ($FF23)");
assert_eq!(read_io(&mut gb, 0xFF24), 0x77, "NR50 ($FF24)");
assert_eq!(read_io(&mut gb, 0xFF25), 0xF3, "NR51 ($FF25)");
assert_eq!(read_io(&mut gb, 0xFF26), 0xF1, "NR52 ($FF26)");
assert_eq!(read_io(&mut gb, 0xFF40), 0x91, "LCDC ($FF40)");
assert_eq!(read_io(&mut gb, 0xFF42), 0x00, "SCY ($FF42)");
assert_eq!(read_io(&mut gb, 0xFF43), 0x00, "SCX ($FF43)");
assert_eq!(read_io(&mut gb, 0xFF45), 0x00, "LYC ($FF45)");
assert_eq!(read_io(&mut gb, 0xFF46), 0xFF, "DMA ($FF46)");
assert_eq!(read_io(&mut gb, 0xFF47), 0xFC, "BGP ($FF47)");
assert_eq!(read_io(&mut gb, 0xFF4A), 0x00, "WY ($FF4A)");
assert_eq!(read_io(&mut gb, 0xFF4B), 0x00, "WX ($FF4B)");
assert_eq!(read_io(&mut gb, 0xFFFF), 0x00, "IE ($FFFF)");
}
#[test]
fn test_dmg_a_b_c_produce_identical_post_boot_state() {
let mut gb_a = boot_to_cartridge_entry(DmgModel::DmgA);
let mut gb_b = boot_to_cartridge_entry(DmgModel::DmgB);
let mut gb_c = boot_to_cartridge_entry(DmgModel::DmgC);
assert_eq!(gb_a.cpu.regs.af(), gb_b.cpu.regs.af(), "AF: DMG-A vs DMG-B");
assert_eq!(gb_b.cpu.regs.af(), gb_c.cpu.regs.af(), "AF: DMG-B vs DMG-C");
assert_eq!(gb_a.cpu.regs.bc(), gb_b.cpu.regs.bc(), "BC: DMG-A vs DMG-B");
assert_eq!(gb_b.cpu.regs.bc(), gb_c.cpu.regs.bc(), "BC: DMG-B vs DMG-C");
assert_eq!(gb_a.cpu.regs.de(), gb_b.cpu.regs.de(), "DE: DMG-A vs DMG-B");
assert_eq!(gb_b.cpu.regs.de(), gb_c.cpu.regs.de(), "DE: DMG-B vs DMG-C");
assert_eq!(gb_a.cpu.regs.hl(), gb_b.cpu.regs.hl(), "HL: DMG-A vs DMG-B");
assert_eq!(gb_b.cpu.regs.hl(), gb_c.cpu.regs.hl(), "HL: DMG-B vs DMG-C");
assert_eq!(gb_a.cpu.regs.sp, gb_b.cpu.regs.sp, "SP: DMG-A vs DMG-B");
assert_eq!(gb_b.cpu.regs.sp, gb_c.cpu.regs.sp, "SP: DMG-B vs DMG-C");
assert_eq!(gb_a.cycles(), gb_b.cycles(), "Cycles: DMG-A vs DMG-B");
assert_eq!(gb_b.cycles(), gb_c.cycles(), "Cycles: DMG-B vs DMG-C");
let io_addrs: &[u16] = &[
0xFF01, 0xFF02, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF0F, 0xFF10, 0xFF11, 0xFF12, 0xFF13,
0xFF14, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0xFF1A, 0xFF1B, 0xFF1C, 0xFF1D, 0xFF1E, 0xFF20,
0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF40, 0xFF41, 0xFF42, 0xFF43, 0xFF44,
0xFF45, 0xFF46, 0xFF47, 0xFF4A, 0xFF4B, 0xFFFF,
];
for &addr in io_addrs {
let a = read_io(&mut gb_a, addr);
let b = read_io(&mut gb_b, addr);
let c = read_io(&mut gb_c, addr);
assert_eq!(
a, b,
"IO ${:04X}: DMG-A (${:02X}) vs DMG-B (${:02X})",
addr, a, b
);
assert_eq!(
b, c,
"IO ${:04X}: DMG-B (${:02X}) vs DMG-C (${:02X})",
addr, b, c
);
}
}
#[test]
fn test_dmg0_boot_sets_correct_register_state() {
let gb = boot_to_cartridge_entry(DmgModel::Dmg0);
assert_eq!(gb.cpu.regs.a, 0x01, "A register after DMG-0 boot");
assert_eq!(gb.cpu.regs.f, 0x00, "F register after DMG-0 boot");
assert_eq!(gb.cpu.regs.b, 0xFF, "B register after DMG-0 boot");
assert_eq!(gb.cpu.regs.c, 0x13, "C register after DMG-0 boot");
assert_eq!(gb.cpu.regs.d, 0x00, "D register after DMG-0 boot");
assert_eq!(gb.cpu.regs.e, 0xC1, "E register after DMG-0 boot");
assert_eq!(gb.cpu.regs.h, 0x84, "H register after DMG-0 boot");
assert_eq!(gb.cpu.regs.l, 0x03, "L register after DMG-0 boot");
assert_eq!(gb.cpu.regs.sp, 0xFFFE, "SP after DMG-0 boot");
assert_eq!(gb.cpu.regs.pc, 0x0100, "PC after DMG-0 boot");
}
use crate::gb::bus::CgbBus;
use crate::gb::model::CgbModel;
fn build_cgb_test_rom() -> Vec<u8> {
let mut rom = build_base_test_rom();
rom[0x0143] = 0x80;
rom[0x014D] = compute_header_checksum(&rom);
rom
}
fn run_cgb_until_cartridge_entry(gb: &mut Gb<CgbBus>) -> bool {
let start = gb.cycles();
loop {
if gb.cpu.regs.pc == 0x0100 {
return true;
}
if gb.cycles().saturating_sub(start) >= BOOT_CYCLE_LIMIT {
return false;
}
gb.step();
}
}
fn make_cgb_for_boot_test(model: CgbModel) -> Gb<CgbBus> {
let rom = build_cgb_test_rom();
let cart = load_cartridge(&rom).expect("valid ROM");
Gb::new(CgbBus::new(cart, model, false))
}
fn boot_cgb_to_cartridge_entry(model: CgbModel) -> Gb<CgbBus> {
let mut gb = make_cgb_for_boot_test(model);
let reached = run_cgb_until_cartridge_entry(&mut gb);
assert!(reached, "Boot ROM must reach $0100 for model {:?}", model);
gb
}
fn read_cgb_bus(gb: &mut Gb<CgbBus>, addr: u16) -> u8 {
gb.cpu.bus.read(addr)
}
#[test]
fn test_cgb_boot_sets_correct_register_state() {
let gb = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
assert_eq!(gb.cpu.regs.a, 0x11, "A register after CGB boot");
assert_eq!(gb.cpu.regs.f, 0x80, "F register after CGB boot (Z=1)");
assert_eq!(gb.cpu.regs.b, 0x00, "B register after CGB boot");
assert_eq!(gb.cpu.regs.c, 0x00, "C register after CGB boot");
assert_eq!(gb.cpu.regs.d, 0x00, "D register after CGB boot");
assert_eq!(gb.cpu.regs.e, 0x08, "E register after CGB boot");
assert_eq!(gb.cpu.regs.h, 0x00, "H register after CGB boot");
assert_eq!(gb.cpu.regs.l, 0x7C, "L register after CGB boot");
assert_eq!(gb.cpu.regs.sp, 0xFFFE, "SP after CGB boot");
assert_eq!(gb.cpu.regs.pc, 0x0100, "PC after CGB boot");
}
#[test]
fn test_cgb_boot_sets_correct_io_registers() {
let mut gb = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
assert_eq!(read_cgb_bus(&mut gb, 0xFF24), 0x77, "NR50 ($FF24)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF25), 0xF3, "NR51 ($FF25)");
assert_eq!(
read_cgb_bus(&mut gb, 0xFF26),
0xF1,
"NR52 ($FF26) - APU on, CH1 active"
);
assert_eq!(read_cgb_bus(&mut gb, 0xFF40), 0x91, "LCDC ($FF40)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF47), 0xFC, "BGP ($FF47)");
}
#[test]
fn test_cgb_boot_rom_unmaps_after_completion() {
let mut gb = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
assert!(
!gb.cpu.bus.is_boot_rom_active(),
"Boot ROM should be unmapped after boot completion"
);
assert_eq!(
read_cgb_bus(&mut gb, 0x0000),
0x00,
"Read from $0000 should return cartridge data after boot"
);
assert_eq!(
read_cgb_bus(&mut gb, 0x0100),
0x18,
"Read from $0100 should return JR opcode"
);
assert_eq!(
read_cgb_bus(&mut gb, 0x0101),
0xFE,
"Read from $0101 should return -2 offset"
);
}
#[test]
fn test_cgb_a_through_e_produce_identical_post_boot_state() {
let mut gb_a = boot_cgb_to_cartridge_entry(CgbModel::CgbA);
let mut gb_e = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
assert_eq!(gb_a.cpu.regs.af(), gb_e.cpu.regs.af(), "AF: CGB-A vs CGB-E");
assert_eq!(gb_a.cpu.regs.bc(), gb_e.cpu.regs.bc(), "BC: CGB-A vs CGB-E");
assert_eq!(gb_a.cpu.regs.de(), gb_e.cpu.regs.de(), "DE: CGB-A vs CGB-E");
assert_eq!(gb_a.cpu.regs.hl(), gb_e.cpu.regs.hl(), "HL: CGB-A vs CGB-E");
assert_eq!(gb_a.cpu.regs.sp, gb_e.cpu.regs.sp, "SP: CGB-A vs CGB-E");
let io_addrs: &[u16] = &[0xFF24, 0xFF25, 0xFF26, 0xFF40, 0xFF47];
for &addr in io_addrs {
let a = read_cgb_bus(&mut gb_a, addr);
let e = read_cgb_bus(&mut gb_e, addr);
assert_eq!(
a, e,
"IO ${:04X}: CGB-A (${:02X}) vs CGB-E (${:02X})",
addr, a, e
);
}
}
#[test]
fn test_cgb_boot_accepts_any_cartridge() {
let mut rom = build_cgb_test_rom();
for (i, byte) in rom[0x0104..0x0134].iter_mut().enumerate() {
*byte = (((0x0104 + i) * 17) & 0xFF) as u8;
}
let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
let cart = load_cartridge(&rom).expect("valid ROM");
let mut gb = Gb::new(CgbBus::new(cart, CgbModel::CgbE, false));
let reached = run_cgb_until_cartridge_entry(&mut gb);
assert!(
reached,
"Boot ROM must accept any cartridge and reach $0100"
);
}
#[test]
fn test_cgb0_boot_sets_same_register_state_as_production() {
let gb = boot_cgb_to_cartridge_entry(CgbModel::Cgb0);
assert_eq!(gb.cpu.regs.a, 0x11, "A register after CGB-0 boot");
assert_eq!(gb.cpu.regs.f, 0x80, "F register after CGB-0 boot (Z=1)");
assert_eq!(gb.cpu.regs.b, 0x00, "B register after CGB-0 boot");
assert_eq!(gb.cpu.regs.c, 0x00, "C register after CGB-0 boot");
assert_eq!(gb.cpu.regs.d, 0x00, "D register after CGB-0 boot");
assert_eq!(gb.cpu.regs.e, 0x08, "E register after CGB-0 boot");
assert_eq!(gb.cpu.regs.h, 0x00, "H register after CGB-0 boot");
assert_eq!(gb.cpu.regs.l, 0x7C, "L register after CGB-0 boot");
assert_eq!(gb.cpu.regs.sp, 0xFFFE, "SP after CGB-0 boot");
assert_eq!(gb.cpu.regs.pc, 0x0100, "PC after CGB-0 boot");
}
#[test]
fn test_cgb0_boot_does_not_init_wave_ram() {
let mut gb = make_cgb_for_boot_test(CgbModel::Cgb0);
let wave_ram_before: Vec<u8> = (0xFF30..=0xFF3F)
.map(|addr| read_cgb_bus(&mut gb, addr))
.collect();
let reached = run_cgb_until_cartridge_entry(&mut gb);
assert!(reached, "Boot ROM must reach $0100");
for (addr, expected) in (0xFF30..=0xFF3F).zip(wave_ram_before.iter().copied()) {
let val = read_cgb_bus(&mut gb, addr);
assert_eq!(
val, expected,
"CGB-0 boot should preserve wave RAM at ${:04X} (before ${:02X}, after ${:02X})",
addr, expected, val
);
}
}
#[test]
fn test_cgb_production_boot_inits_wave_ram() {
let mut gb = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
for (i, addr) in (0xFF30..=0xFF3F).enumerate() {
let expected = if i % 2 == 0 { 0x00 } else { 0xFF };
let val = read_cgb_bus(&mut gb, addr);
assert_eq!(
val, expected,
"Production CGB wave RAM at ${:04X} should be ${:02X} (got ${:02X})",
addr, expected, val
);
}
}
#[test]
fn test_cgb0_and_production_have_same_io_state() {
let mut gb_0 = boot_cgb_to_cartridge_entry(CgbModel::Cgb0);
let mut gb_e = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
let io_addrs: &[u16] = &[0xFF24, 0xFF25, 0xFF26, 0xFF40, 0xFF47];
for &addr in io_addrs {
let val_0 = read_cgb_bus(&mut gb_0, addr);
let val_e = read_cgb_bus(&mut gb_e, addr);
assert_eq!(
val_0, val_e,
"IO ${:04X}: CGB-0 (${:02X}) vs CGB-E (${:02X})",
addr, val_0, val_e
);
}
}
fn build_cgb_test_rom_with_flag(cgb_flag: u8) -> Vec<u8> {
let mut rom = build_base_test_rom();
rom[0x0143] = cgb_flag;
rom[0x014D] = compute_header_checksum(&rom);
rom
}
fn boot_cgb_with_cgb_flag(model: CgbModel, cgb_flag: u8) -> Gb<CgbBus> {
let rom = build_cgb_test_rom_with_flag(cgb_flag);
let cart = load_cartridge(&rom).expect("valid ROM");
let mut gb = Gb::new(CgbBus::new(cart, model, false));
let reached = run_cgb_until_cartridge_entry(&mut gb);
assert!(
reached,
"Boot ROM must reach $0100 for model {:?} with CGB flag ${:02X}",
model, cgb_flag
);
gb
}
#[test]
fn test_cgb_boot_dmg_only_cartridge_sets_key0_dmg_mode() {
let mut gb = boot_cgb_with_cgb_flag(CgbModel::CgbE, 0x00);
let key0 = read_cgb_bus(&mut gb, 0xFF4C);
assert_eq!(
key0, 0xF4,
"KEY0 should be $F4 ($04 with unused bits as 1s) for DMG-only cartridge, got ${:02X}",
key0
);
}
#[test]
fn test_cgb_boot_dmg_only_cartridge_sets_opri_dmg_mode() {
let mut gb = boot_cgb_with_cgb_flag(CgbModel::CgbE, 0x00);
let opri = read_cgb_bus(&mut gb, 0xFF6C);
assert_eq!(
opri & 0x01,
0x01,
"OPRI bit 0 should be set for DMG-only cartridge, got ${:02X}",
opri
);
}
#[test]
fn test_cgb_boot_cgb_compatible_cartridge_sets_key0_cgb_mode() {
let gb = boot_cgb_with_cgb_flag(CgbModel::CgbE, 0x80);
let key0 = gb.cpu.bus.key0();
assert_eq!(
key0, 0x80,
"KEY0 raw value should be $80 for CGB-compatible cartridge, got ${:02X}",
key0
);
}
#[test]
fn test_cgb_boot_cgb_compatible_cartridge_keeps_opri_cgb_mode() {
let mut gb = boot_cgb_with_cgb_flag(CgbModel::CgbE, 0x80);
let opri = read_cgb_bus(&mut gb, 0xFF6C);
assert_eq!(
opri & 0x01,
0x00,
"OPRI bit 0 should be clear for CGB-compatible cartridge, got ${:02X}",
opri
);
}
#[test]
fn test_cgb_boot_cgb_only_cartridge_sets_key0() {
let gb = boot_cgb_with_cgb_flag(CgbModel::CgbE, 0xC0);
let key0 = gb.cpu.bus.key0();
assert_eq!(
key0, 0xC0,
"KEY0 raw value should be $C0 for CGB-only cartridge, got ${:02X}",
key0
);
}
#[test]
fn test_cgb_boot_cgb_only_cartridge_keeps_opri_cgb_mode() {
let mut gb = boot_cgb_with_cgb_flag(CgbModel::CgbE, 0xC0);
let opri = read_cgb_bus(&mut gb, 0xFF6C);
assert_eq!(
opri & 0x01,
0x00,
"OPRI bit 0 should be clear for CGB-only cartridge, got ${:02X}",
opri
);
}
#[test]
fn test_cgb_boot_locks_key0_after_boot() {
let mut gb = boot_cgb_with_cgb_flag(CgbModel::CgbE, 0x00);
assert!(
gb.cpu.bus.is_key0_locked(),
"KEY0 should be locked after boot ROM unmaps"
);
let key0_before = read_cgb_bus(&mut gb, 0xFF4C);
gb.cpu.bus.write(0xFF4C, 0x00);
let key0_after = read_cgb_bus(&mut gb, 0xFF4C);
assert_eq!(
key0_before, key0_after,
"KEY0 should not change after boot ROM unmaps (before=${:02X}, after=${:02X})",
key0_before, key0_after
);
}
#[test]
fn test_cgb0_boot_dmg_only_cartridge_sets_key0_dmg_mode() {
let mut gb = boot_cgb_with_cgb_flag(CgbModel::Cgb0, 0x00);
let key0 = read_cgb_bus(&mut gb, 0xFF4C);
assert_eq!(
key0, 0xF4,
"CGB-0: KEY0 should be $F4 for DMG-only cartridge, got ${:02X}",
key0
);
let opri = read_cgb_bus(&mut gb, 0xFF6C);
assert_eq!(
opri & 0x01,
0x01,
"CGB-0: OPRI bit 0 should be set for DMG-only cartridge, got ${:02X}",
opri
);
}
#[test]
fn test_cgb0_boot_cgb_compatible_cartridge_sets_key0_cgb_mode() {
let gb = boot_cgb_with_cgb_flag(CgbModel::Cgb0, 0x80);
let key0 = gb.cpu.bus.key0();
assert_eq!(
key0, 0x80,
"CGB-0: KEY0 raw value should be $80 for CGB-compatible cartridge, got ${:02X}",
key0
);
let mut gb = gb;
let opri = read_cgb_bus(&mut gb, 0xFF6C);
assert_eq!(
opri & 0x01,
0x00,
"CGB-0: OPRI bit 0 should be clear for CGB-compatible cartridge, got ${:02X}",
opri
);
}
fn boot_cgb_with_buttons_held(model: CgbModel, button_mask: u8) -> Gb<CgbBus> {
let rom = build_cgb_test_rom_with_flag(0x00); let cart = load_cartridge(&rom).expect("valid ROM");
let mut gb = Gb::new(CgbBus::new(cart, model, false));
for id in 0u8..8 {
let pressed = button_mask & (1 << id) != 0;
gb.cpu.bus.joypad.set_button(id, pressed);
}
let reached = run_cgb_until_cartridge_entry(&mut gb);
assert!(
reached,
"Boot ROM must reach $0100 for model {:?} with buttons ${:02X}",
model, button_mask
);
gb
}
fn read_bg_palette_color0(gb: &Gb<CgbBus>) -> u16 {
let low = gb.cpu.bus.ppu.bg_palette_ram[0] as u16;
let high = gb.cpu.bus.ppu.bg_palette_ram[1] as u16;
low | (high << 8)
}
#[test]
fn test_cgb_boot_dmg_no_buttons_uses_automatic_palette() {
let mut gb = boot_cgb_with_buttons_held(CgbModel::CgbE, 0x00);
let mut header = [0u8; 0x4C];
for (i, byte) in header.iter_mut().enumerate() {
*byte = gb.cpu.bus.read(0x0100 + i as u16);
}
let expected_palette = crate::gb::compat_palettes::get_palette_colors(&header);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Without buttons held, automatic palette should be applied. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_up_button_selects_palette_5() {
let button_mask = 0b0001_0000; let gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let expected_palette = crate::gb::compat_palettes::get_palette_colors_by_id(5);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Up button should select palette 5. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_down_button_selects_palette_8() {
let button_mask = 0b0010_0000; let gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let expected_palette = crate::gb::compat_palettes::get_palette_colors_by_id(8);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Down button should select palette 8. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_left_button_selects_palette_48() {
let button_mask = 0b0100_0000; let gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let expected_palette = crate::gb::compat_palettes::get_palette_colors_by_id(48);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Left button should select palette 48. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_right_button_selects_palette_1() {
let button_mask = 0b1000_0000; let gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let expected_palette = crate::gb::compat_palettes::get_palette_colors_by_id(1);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Right button should select palette 1. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_up_a_selects_palette_43() {
let button_mask = 0b0001_0001; let gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let expected_palette = crate::gb::compat_palettes::get_palette_colors_by_id(43);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Up+A should select palette 43. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_up_b_selects_palette_28() {
let button_mask = 0b0001_0010; let gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let expected_palette = crate::gb::compat_palettes::get_palette_colors_by_id(28);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Up+B should select palette 28. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_right_a_selects_palette_0() {
let button_mask = 0b1000_0001; let gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let expected_palette = crate::gb::compat_palettes::get_palette_colors_by_id(0);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Right+A should select palette 0. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_dmg_invalid_ab_combo_uses_automatic_palette() {
let button_mask = 0b0001_0011; let mut gb = boot_cgb_with_buttons_held(CgbModel::CgbE, button_mask);
let mut header = [0u8; 0x4C];
for (i, byte) in header.iter_mut().enumerate() {
*byte = gb.cpu.bus.read(0x0100 + i as u16);
}
let expected_palette = crate::gb::compat_palettes::get_palette_colors(&header);
let actual_color = read_bg_palette_color0(&gb);
assert_eq!(
actual_color, expected_palette.bg0[0],
"Invalid A+B combo should fall back to automatic palette. Expected ${:04X}, got ${:04X}",
expected_palette.bg0[0], actual_color
);
}
#[test]
fn test_cgb_boot_cgb_game_ignores_button_combo() {
let rom = build_cgb_test_rom_with_flag(0x80); let cart = load_cartridge(&rom).expect("valid ROM");
let mut gb = Gb::new(CgbBus::new(cart, CgbModel::CgbE, false));
gb.cpu.bus.joypad.set_button(4, true);
let reached = run_cgb_until_cartridge_entry(&mut gb);
assert!(reached, "Boot ROM must reach $0100");
assert!(
!gb.cpu.bus.ppu.dmg_compat,
"CGB game should not be in DMG compat mode, even with buttons held"
);
}
#[test]
fn test_cgb_production_boot_io_registers() {
let mut gb = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
assert_eq!(read_cgb_bus(&mut gb, 0xFF05), 0x00, "TIMA ($FF05)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF06), 0x00, "TMA ($FF06)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF07), 0xF8, "TAC ($FF07)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF0F), 0xE1, "IF ($FF0F)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF10), 0x80, "NR10 ($FF10)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF11), 0xBF, "NR11 ($FF11)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF12), 0xF3, "NR12 ($FF12)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF13), 0xFF, "NR13 ($FF13)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF14), 0xBF, "NR14 ($FF14)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF16), 0x3F, "NR21 ($FF16)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF17), 0x00, "NR22 ($FF17)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF18), 0xFF, "NR23 ($FF18)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF19), 0xBF, "NR24 ($FF19)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1A), 0x7F, "NR30 ($FF1A)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1B), 0xFF, "NR31 ($FF1B)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1C), 0x9F, "NR32 ($FF1C)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1D), 0xFF, "NR33 ($FF1D)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1E), 0xBF, "NR34 ($FF1E)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF20), 0xFF, "NR41 ($FF20)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF21), 0x00, "NR42 ($FF21)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF22), 0x00, "NR43 ($FF22)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF23), 0xBF, "NR44 ($FF23)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF24), 0x77, "NR50 ($FF24)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF25), 0xF3, "NR51 ($FF25)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF26), 0xF1, "NR52 ($FF26)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF40), 0x91, "LCDC ($FF40)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF42), 0x00, "SCY ($FF42)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF43), 0x00, "SCX ($FF43)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF45), 0x00, "LYC ($FF45)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF46), 0x00, "DMA ($FF46)"); assert_eq!(read_cgb_bus(&mut gb, 0xFF47), 0xFC, "BGP ($FF47)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4A), 0x00, "WY ($FF4A)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4B), 0x00, "WX ($FF4B)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4D), 0x7E, "KEY1 ($FF4D)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4F), 0xFE, "VBK ($FF4F)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF51), 0xFF, "HDMA1 ($FF51)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF52), 0xFF, "HDMA2 ($FF52)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF53), 0xFF, "HDMA3 ($FF53)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF54), 0xFF, "HDMA4 ($FF54)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF55), 0xFF, "HDMA5 ($FF55)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF70), 0xF8, "SVBK ($FF70)");
assert_eq!(read_cgb_bus(&mut gb, 0xFFFF), 0x00, "IE ($FFFF)");
}
#[test]
fn test_cgb0_boot_io_registers() {
let mut gb = boot_cgb_to_cartridge_entry(CgbModel::Cgb0);
assert_eq!(read_cgb_bus(&mut gb, 0xFF05), 0x00, "TIMA ($FF05)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF06), 0x00, "TMA ($FF06)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF07), 0xF8, "TAC ($FF07)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF0F), 0xE1, "IF ($FF0F)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF10), 0x80, "NR10 ($FF10)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF11), 0xBF, "NR11 ($FF11)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF12), 0xF3, "NR12 ($FF12)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF13), 0xFF, "NR13 ($FF13)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF14), 0xBF, "NR14 ($FF14)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF16), 0x3F, "NR21 ($FF16)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF17), 0x00, "NR22 ($FF17)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF18), 0xFF, "NR23 ($FF18)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF19), 0xBF, "NR24 ($FF19)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1A), 0x7F, "NR30 ($FF1A)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1B), 0xFF, "NR31 ($FF1B)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1C), 0x9F, "NR32 ($FF1C)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1D), 0xFF, "NR33 ($FF1D)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF1E), 0xBF, "NR34 ($FF1E)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF20), 0xFF, "NR41 ($FF20)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF21), 0x00, "NR42 ($FF21)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF22), 0x00, "NR43 ($FF22)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF23), 0xBF, "NR44 ($FF23)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF24), 0x77, "NR50 ($FF24)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF25), 0xF3, "NR51 ($FF25)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF26), 0xF1, "NR52 ($FF26)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF40), 0x91, "LCDC ($FF40)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF42), 0x00, "SCY ($FF42)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF43), 0x00, "SCX ($FF43)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF45), 0x00, "LYC ($FF45)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF46), 0x00, "DMA ($FF46)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF47), 0xFC, "BGP ($FF47)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4A), 0x00, "WY ($FF4A)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4B), 0x00, "WX ($FF4B)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4D), 0x7E, "KEY1 ($FF4D)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF4F), 0xFE, "VBK ($FF4F)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF51), 0xFF, "HDMA1 ($FF51)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF52), 0xFF, "HDMA2 ($FF52)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF53), 0xFF, "HDMA3 ($FF53)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF54), 0xFF, "HDMA4 ($FF54)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF55), 0xFF, "HDMA5 ($FF55)");
assert_eq!(read_cgb_bus(&mut gb, 0xFF70), 0xF8, "SVBK ($FF70)");
assert_eq!(read_cgb_bus(&mut gb, 0xFFFF), 0x00, "IE ($FFFF)");
}
#[test]
fn test_cgb_boot_div_timing() {
let mut gb_0 = boot_cgb_to_cartridge_entry(CgbModel::Cgb0);
let mut gb_e = boot_cgb_to_cartridge_entry(CgbModel::CgbE);
let div_0 = read_cgb_bus(&mut gb_0, 0xFF04);
let div_e = read_cgb_bus(&mut gb_e, 0xFF04);
assert_ne!(div_0, 0x00, "CGB-0 DIV should be non-zero after boot");
assert_ne!(div_e, 0x00, "CGB-E DIV should be non-zero after boot");
for _ in 0..1024 {
gb_0.step();
gb_e.step();
}
let div_0_after = read_cgb_bus(&mut gb_0, 0xFF04);
let div_e_after = read_cgb_bus(&mut gb_e, 0xFF04);
assert_ne!(
div_0_after, div_0,
"CGB-0 DIV should continue advancing after boot completion"
);
assert_ne!(
div_e_after, div_e,
"CGB-E DIV should continue advancing after boot completion"
);
}