use crate::gb::bus::{CgbBus, DmgBus};
use crate::gb::cartridge::load_cartridge;
use crate::gb::console::Gb;
use crate::gb::console::save_state::{GB_SAVESTATE_VERSION, GbSaveState};
use crate::gb::model::GbHardware;
use crate::platform::app_context::{IntoSharedAppContext, SharedAppContext};
use crate::platform::emulator::{Emulator, SystemType};
use std::path::PathBuf;
enum GbConsole {
Dmg(Box<Gb<DmgBus>>),
Cgb(Box<Gb<CgbBus>>),
}
impl GbConsole {
fn step(&mut self) -> u8 {
match self {
Self::Dmg(gb) => gb.step(),
Self::Cgb(gb) => gb.step(),
}
}
fn is_frame_ready(&self) -> bool {
match self {
Self::Dmg(gb) => gb.is_frame_ready(),
Self::Cgb(gb) => gb.is_frame_ready(),
}
}
fn clear_frame_ready(&mut self) {
match self {
Self::Dmg(gb) => gb.clear_frame_ready(),
Self::Cgb(gb) => gb.clear_frame_ready(),
}
}
fn screen_snapshot(&self) -> Vec<u8> {
match self {
Self::Dmg(gb) => gb.screen_snapshot(),
Self::Cgb(gb) => gb.screen_snapshot(),
}
}
fn screen_crc32(&self) -> u32 {
match self {
Self::Dmg(gb) => gb.screen_crc32(),
Self::Cgb(gb) => gb.screen_crc32(),
}
}
fn reset(&mut self, soft_reset: bool) {
match self {
Self::Dmg(gb) => gb.reset(),
Self::Cgb(gb) => gb.reset(soft_reset),
}
}
fn set_joypad_button(&mut self, id: u8, pressed: bool) {
match self {
Self::Dmg(gb) => gb.cpu.bus.set_joypad_button(id, pressed),
Self::Cgb(gb) => gb.cpu.bus.set_joypad_button(id, pressed),
}
}
fn get_joypad_button_states(&self) -> u8 {
match self {
Self::Dmg(gb) => gb.cpu.bus.joypad.get_states(),
Self::Cgb(gb) => gb.cpu.bus.joypad.get_states(),
}
}
fn sample_ready(&self) -> bool {
match self {
Self::Dmg(gb) => gb.cpu.bus.sample_ready(),
Self::Cgb(gb) => gb.cpu.bus.sample_ready(),
}
}
fn take_sample(&mut self) -> Option<f32> {
match self {
Self::Dmg(gb) => gb.cpu.bus.take_sample(),
Self::Cgb(gb) => gb.cpu.bus.take_sample(),
}
}
fn set_audio_sample_rate(&mut self, rate: f32) {
match self {
Self::Dmg(gb) => gb.cpu.bus.set_audio_sample_rate(rate),
Self::Cgb(gb) => gb.cpu.bus.set_audio_sample_rate(rate),
}
}
fn has_battery(&self) -> bool {
match self {
Self::Dmg(gb) => gb.cpu.bus.has_battery(),
Self::Cgb(gb) => gb.cpu.bus.has_battery(),
}
}
fn cart_ram_snapshot(&self) -> Vec<u8> {
match self {
Self::Dmg(gb) => gb.cpu.bus.cart_ram_snapshot(),
Self::Cgb(gb) => gb.cpu.bus.cart_ram_snapshot(),
}
}
fn restore_cart_ram(&mut self, data: &[u8]) {
match self {
Self::Dmg(gb) => gb.cpu.bus.restore_cart_ram(data),
Self::Cgb(gb) => gb.cpu.bus.restore_cart_ram(data),
}
}
fn save_state_bytes(&self) -> Result<Vec<u8>, String> {
let state = match self {
Self::Dmg(gb) => GbSaveState {
version: GB_SAVESTATE_VERSION,
cpu: gb.cpu.capture_state(),
bus: gb.cpu.bus.capture_bus_state(),
cart_ram: gb.cpu.bus.cart_ram_snapshot(),
mbc_state: gb.cpu.bus.mbc_state_snapshot(),
},
Self::Cgb(gb) => GbSaveState {
version: GB_SAVESTATE_VERSION,
cpu: gb.cpu.capture_state(),
bus: gb.cpu.bus.capture_bus_state(),
cart_ram: gb.cpu.bus.cart_ram_snapshot(),
mbc_state: gb.cpu.bus.mbc_state_snapshot(),
},
};
state
.to_bytes()
.map_err(|e| format!("save state serialization failed: {e}"))
}
fn load_state_bytes(&mut self, data: &[u8]) -> Result<(), String> {
let state = GbSaveState::from_bytes(data)
.map_err(|e| format!("save state deserialization failed: {e}"))?;
match self {
Self::Dmg(gb) => {
gb.cpu.restore_state(&state.cpu);
gb.cpu.bus.restore_bus_state(&state.bus)?;
gb.reconcile_stop_display_after_state_load();
gb.cpu.bus.restore_cart_ram(&state.cart_ram);
gb.cpu.bus.restore_mbc_state(&state.mbc_state);
gb.cpu.bus.joypad.clear_buttons();
}
Self::Cgb(gb) => {
gb.cpu.restore_state(&state.cpu);
gb.cpu.bus.restore_bus_state(&state.bus)?;
gb.reconcile_stop_display_after_state_load();
gb.cpu.bus.restore_cart_ram(&state.cart_ram);
gb.cpu.bus.restore_mbc_state(&state.mbc_state);
gb.cpu.bus.joypad.clear_buttons();
}
}
Ok(())
}
}
pub struct GameBoy {
gb: Option<GbConsole>,
app_context: SharedAppContext,
rom_path: Option<PathBuf>,
}
impl GameBoy {
pub const SCREEN_WIDTH: u32 = 160;
pub const SCREEN_HEIGHT: u32 = 144;
pub fn new(app_context: impl IntoSharedAppContext) -> Self {
Self {
gb: None,
app_context: app_context.into_shared(),
rom_path: None,
}
}
pub fn load_rom(&mut self, bytes: &[u8], name: &str) -> Result<(), String> {
let cart = load_cartridge(bytes).map_err(|e| format!("{e:?}"))?;
let is_cgb_rom = cart.is_cgb();
let hardware = self.app_context.borrow().config().gb.hardware;
let use_cgb_bus = match hardware {
None => {
is_cgb_rom
}
Some(GbHardware::Dmg) => {
if is_cgb_rom && cart.read(0x0143) == 0xC0 {
return Err(
"This CGB-only game requires --gb-hardware cgb or --gb-hardware gba"
.to_string(),
);
}
false
}
Some(GbHardware::Cgb) | Some(GbHardware::Gba) => {
true
}
};
self.gb = Some(if use_cgb_bus {
let config = self.app_context.borrow().config().gb.clone();
let skip_boot_rom = !config.boot_animation;
let mut gb = Gb::new(CgbBus::new(cart, config.cgb_variant, skip_boot_rom));
if skip_boot_rom {
gb.cpu.reset_registers_cgb();
}
GbConsole::Cgb(Box::new(gb))
} else {
let dmg_variant = self.app_context.borrow().config().gb.dmg_variant;
GbConsole::Dmg(Box::new(Gb::new(DmgBus::new(cart, dmg_variant))))
});
self.rom_path = Some(PathBuf::from(name));
self.load_save_ram_from_disk();
Ok(())
}
pub fn run_tick(&mut self) -> u8 {
match &mut self.gb {
Some(gb) => gb.step(),
None => 0,
}
}
pub fn is_frame_ready(&self) -> bool {
self.gb.as_ref().is_some_and(|gb| gb.is_frame_ready())
}
pub fn clear_frame_ready(&mut self) {
if let Some(gb) = &mut self.gb {
gb.clear_frame_ready();
}
}
pub fn screen_snapshot(&self) -> Vec<u8> {
self.gb.as_ref().map_or_else(
|| vec![0u8; (Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT * 3) as usize],
|gb| gb.screen_snapshot(),
)
}
pub fn screen_crc32(&self) -> u32 {
self.gb.as_ref().map_or(0, |gb| gb.screen_crc32())
}
pub fn cropped_screen_snapshot(&self) -> Vec<u8> {
self.screen_snapshot()
}
pub fn set_button(&mut self, id: u8, pressed: bool) {
if let Some(gb) = &mut self.gb {
gb.set_joypad_button(id, pressed);
}
}
pub fn set_joypad_button_states(&mut self, state: u8) {
for id in 0u8..8 {
let pressed = state & (1 << id) != 0;
self.set_button(id, pressed);
}
}
pub fn get_joypad_button_states(&self) -> u8 {
self.gb
.as_ref()
.map_or(0, |gb| gb.get_joypad_button_states())
}
pub fn save_state_bytes(&self) -> Result<Vec<u8>, String> {
match &self.gb {
Some(gb) => gb.save_state_bytes(),
None => Err("No ROM loaded".into()),
}
}
pub fn load_state_bytes(&mut self, data: &[u8]) -> Result<(), String> {
match &mut self.gb {
Some(gb) => gb.load_state_bytes(data),
None => Err("No ROM loaded".into()),
}
}
pub fn reset(&mut self, soft_reset: bool) {
if let Some(gb) = &mut self.gb {
gb.reset(soft_reset);
}
}
pub fn sample_ready(&self) -> bool {
self.gb.as_ref().is_some_and(|gb| gb.sample_ready())
}
pub fn get_sample(&mut self) -> Option<f32> {
self.gb.as_mut().and_then(|gb| gb.take_sample())
}
pub fn set_audio_sample_rate(&mut self, rate: f32) {
if let Some(gb) = &mut self.gb {
gb.set_audio_sample_rate(rate);
}
}
pub fn app_context(&self) -> &SharedAppContext {
&self.app_context
}
pub fn state_path(&self) -> Option<PathBuf> {
self.rom_path.as_ref().map(|p| p.with_extension("state"))
}
pub fn has_battery(&self) -> bool {
self.gb.as_ref().is_some_and(|gb| gb.has_battery())
}
pub fn cart_ram_snapshot(&self) -> Vec<u8> {
self.gb
.as_ref()
.map_or_else(Vec::new, |gb| gb.cart_ram_snapshot())
}
fn sav_path(&self) -> Option<PathBuf> {
self.rom_path.as_ref().map(|p| p.with_extension("sav"))
}
pub fn save_ram_to_disk(&self) -> Result<(), String> {
if !self.has_battery() {
return Ok(());
}
let Some(sav_path) = self.sav_path() else {
return Ok(());
};
let data = self.cart_ram_snapshot();
if data.is_empty() {
return Ok(());
}
let mut temp_path = sav_path.clone();
temp_path.set_extension(format!("sav.tmp.{}", std::process::id()));
if let Some(parent) = sav_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("failed to create dir {}: {e}", parent.display()))?;
}
std::fs::write(&temp_path, &data)
.map_err(|e| format!("failed to write {}: {e}", temp_path.display()))?;
if sav_path.exists() {
let _ = std::fs::remove_file(&sav_path);
}
std::fs::rename(&temp_path, &sav_path)
.map_err(|e| format!("failed to rename to {}: {e}", sav_path.display()))
}
fn load_save_ram_from_disk(&mut self) {
if !self.has_battery() {
return;
}
let Some(sav_path) = self.sav_path() else {
return;
};
if !sav_path.exists() {
return;
}
match std::fs::read(&sav_path) {
Ok(data) => {
if let Some(gb) = &mut self.gb {
gb.restore_cart_ram(&data);
}
}
Err(e) => {
crate::platform::debugging::log_info(format!(
"Warning: failed to read save file {}: {e}",
sav_path.display()
));
}
}
}
#[cfg(feature = "native")]
pub fn create_debugger_snapshot(
&self,
view_state: &mut crate::gb::debugging::GbDebuggerViewState,
) -> crate::gb::debugging::GbDebuggerSnapshot {
self.gb.as_ref().map_or_else(
|| {
crate::gb::debugging::GbDebuggerSnapshot::default()
},
|gb_console| match gb_console {
GbConsole::Dmg(gb) => view_state.snapshot(gb.as_ref()),
GbConsole::Cgb(gb) => view_state.snapshot(gb.as_ref()),
},
)
}
#[cfg(feature = "native")]
pub fn create_ppu_viewer_snapshot(
&self,
) -> crate::gb::debugging::ppu_viewer::GbPpuViewerSnapshot {
use crate::gb::debugging::ppu_viewer::GbPpuViewerSnapshot;
self.gb.as_ref().map_or_else(
|| {
GbPpuViewerSnapshot {
vram: [0; 0x2000],
vram_bank1: [0; 0x2000],
oam: [0; 0xA0],
bg_palette_ram: [0; 64],
obj_palette_ram: [0; 64],
lcdc: 0,
scx: 0,
scy: 0,
bgp: 0,
obp0: 0,
obp1: 0,
cgb_mode: false,
}
},
|gb_console| match gb_console {
GbConsole::Dmg(gb) => GbPpuViewerSnapshot::from_gb(gb.as_ref()),
GbConsole::Cgb(gb) => GbPpuViewerSnapshot::from_gb(gb.as_ref()),
},
)
}
#[cfg(feature = "native")]
pub fn is_cgb_mode(&self) -> bool {
self.gb
.as_ref()
.is_some_and(|gb_console| matches!(gb_console, GbConsole::Cgb(_)))
}
#[cfg(feature = "native")]
pub fn run_frame_with_debugger(
&mut self,
controller: &mut crate::gb::debugging::control::GbDebuggerController,
audio_cell: &std::cell::RefCell<Option<crate::frontends::native::NativeAudio>>,
) {
use crate::platform::audio::EmulatorAudio;
if let Some(gb_console) = self.gb.as_mut() {
match gb_console {
GbConsole::Dmg(gb) => {
controller.run_frame(gb.as_mut(), &mut |gb| {
if let Some(ref mut audio) = *audio_cell.borrow_mut() {
while gb.cpu.bus.sample_ready() {
if let Some(sample) = gb.cpu.bus.take_sample() {
audio.queue_sample(sample);
}
}
}
});
}
GbConsole::Cgb(gb) => {
controller.run_frame(gb.as_mut(), &mut |gb| {
if let Some(ref mut audio) = *audio_cell.borrow_mut() {
while gb.cpu.bus.sample_ready() {
if let Some(sample) = gb.cpu.bus.take_sample() {
audio.queue_sample(sample);
}
}
}
});
}
}
}
}
#[cfg(feature = "native")]
pub fn toggle_debugger_with_controller(
&mut self,
controller: &mut crate::gb::debugging::control::GbDebuggerController,
) {
if let Some(gb_console) = self.gb.as_mut() {
match gb_console {
GbConsole::Dmg(gb) => controller.toggle_debugger(gb.as_mut()),
GbConsole::Cgb(gb) => controller.toggle_debugger(gb.as_mut()),
}
}
}
#[cfg(feature = "native")]
pub fn step_over_with_controller(
&mut self,
controller: &mut crate::gb::debugging::control::GbDebuggerController,
) {
if let Some(gb_console) = self.gb.as_mut() {
match gb_console {
GbConsole::Dmg(gb) => controller.step_over(gb.as_mut()),
GbConsole::Cgb(gb) => controller.step_over(gb.as_mut()),
}
}
}
#[cfg(feature = "native")]
pub fn step_into_with_controller(
&mut self,
controller: &mut crate::gb::debugging::control::GbDebuggerController,
) {
if let Some(gb_console) = self.gb.as_mut() {
match gb_console {
GbConsole::Dmg(gb) => controller.step_into(gb.as_mut()),
GbConsole::Cgb(gb) => controller.step_into(gb.as_mut()),
}
}
}
#[cfg(feature = "native")]
pub fn apply_ui_action_with_controller(
&mut self,
controller: &mut crate::gb::debugging::control::GbDebuggerController,
action: crate::gb::debugging::ui::GbDebuggerUiAction,
) {
if let Some(gb_console) = self.gb.as_mut() {
match gb_console {
GbConsole::Dmg(gb) => controller.apply_ui_action(gb.as_mut(), action),
GbConsole::Cgb(gb) => controller.apply_ui_action(gb.as_mut(), action),
}
}
}
}
impl Emulator for GameBoy {
fn system_type(&self) -> SystemType {
SystemType::GameBoy
}
fn allowed_shaders(&self) -> &'static [&'static str] {
&["none", "dmg"]
}
fn load_rom(&mut self, bytes: &[u8], name: &str) -> Result<(), String> {
GameBoy::load_rom(self, bytes, name)
}
fn run_tick(&mut self) -> u8 {
GameBoy::run_tick(self)
}
fn is_ready_to_render(&self) -> bool {
self.is_frame_ready()
}
fn clear_ready_to_render(&mut self) {
self.clear_frame_ready()
}
fn screen_width(&self) -> u32 {
GameBoy::SCREEN_WIDTH
}
fn screen_height(&self) -> u32 {
GameBoy::SCREEN_HEIGHT
}
fn screen_snapshot(&self) -> Vec<u8> {
GameBoy::screen_snapshot(self)
}
fn cropped_screen_snapshot(&self, _h_overscan: u32, _v_overscan: u32) -> Vec<u8> {
GameBoy::cropped_screen_snapshot(self)
}
fn screen_crc32(&self) -> u32 {
GameBoy::screen_crc32(self)
}
fn sample_ready(&self) -> bool {
GameBoy::sample_ready(self)
}
fn get_sample(&mut self) -> Option<f32> {
GameBoy::get_sample(self)
}
fn set_audio_sample_rate(&mut self, rate: f32) {
GameBoy::set_audio_sample_rate(self, rate)
}
fn set_button(&mut self, port: u8, button_id: u8, pressed: bool) {
if port == 0 || port == 1 {
GameBoy::set_button(self, button_id, pressed);
}
}
fn set_joypad_button_states(&mut self, port: u8, state: u8) {
if port == 0 || port == 1 {
GameBoy::set_joypad_button_states(self, state);
}
}
fn get_joypad_button_states(&self, port: u8) -> u8 {
if port == 0 || port == 1 {
GameBoy::get_joypad_button_states(self)
} else {
0
}
}
fn save_state_bytes(&self) -> Result<Vec<u8>, String> {
GameBoy::save_state_bytes(self)
}
fn load_state_bytes(&mut self, data: &[u8]) -> Result<(), String> {
GameBoy::load_state_bytes(self, data)
}
fn reset(&mut self, soft_reset: bool) {
GameBoy::reset(self, soft_reset)
}
fn save_ram(&self) -> Result<(), String> {
self.save_ram_to_disk()
}
fn app_context(&self) -> &SharedAppContext {
GameBoy::app_context(self)
}
fn target_frame_duration(&self) -> std::time::Duration {
std::time::Duration::from_secs_f64(70_224.0 / 4_194_304.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gb::model::GbHardware;
use crate::platform::app_context::AppContext;
use crate::platform::config::Config;
fn make_gameboy() -> GameBoy {
let config = Config::default();
let app_context = AppContext::new_with_config(config).into_shared();
GameBoy::new(app_context)
}
fn minimal_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0147] = 0x00; rom[0x0148] = 0x00; rom[0x0149] = 0x00; let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
rom
}
fn minimal_cgb_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0143] = 0xC0; rom[0x0147] = 0x00; rom[0x0148] = 0x00; rom[0x0149] = 0x00; let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
rom
}
fn minimal_dual_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0143] = 0x80; rom[0x0147] = 0x00; rom[0x0148] = 0x00; rom[0x0149] = 0x00; let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
rom
}
fn make_gameboy_with_hardware(hardware: GbHardware) -> GameBoy {
let mut config = Config::default();
config.gb.hardware = Some(hardware);
let app_context = AppContext::new_with_config(config).into_shared();
GameBoy::new(app_context)
}
fn mbc5_battery_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0147] = 0x1B; rom[0x0148] = 0x00; rom[0x0149] = 0x02; let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
rom
}
#[test]
fn test_no_rom_run_tick_returns_zero() {
let mut gb = make_gameboy();
assert_eq!(gb.run_tick(), 0);
}
#[test]
fn test_no_rom_is_frame_ready_returns_false() {
let gb = make_gameboy();
assert!(!gb.is_frame_ready());
}
#[test]
fn test_no_rom_screen_snapshot_returns_correct_size() {
let gb = make_gameboy();
let snap = gb.screen_snapshot();
assert_eq!(snap.len(), 160 * 144 * 3);
}
#[test]
fn test_no_rom_get_joypad_button_states_returns_zero() {
let gb = make_gameboy();
assert_eq!(gb.get_joypad_button_states(), 0);
}
#[test]
fn test_no_rom_set_button_does_not_panic() {
let mut gb = make_gameboy();
gb.set_button(0, true); }
#[test]
fn test_no_rom_save_state_returns_err() {
let gb = make_gameboy();
assert!(gb.save_state_bytes().is_err());
}
#[test]
fn test_no_rom_load_state_returns_err() {
let mut gb = make_gameboy();
assert!(gb.load_state_bytes(&[0u8; 4]).is_err());
}
#[test]
fn test_load_valid_rom_succeeds() {
let mut gb = make_gameboy();
assert!(gb.load_rom(&minimal_rom(), "test.gb").is_ok());
}
#[test]
fn test_load_cgb_rom_succeeds() {
let mut gb = make_gameboy();
assert!(gb.load_rom(&minimal_cgb_rom(), "test.gbc").is_ok());
}
#[test]
fn test_cgb_rom_run_tick_returns_nonzero_cycles() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
let cycles = gb.run_tick();
assert!(cycles > 0, "expected non-zero cycles, got {cycles}");
}
#[test]
fn test_cgb_rom_joypad_roundtrip() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
let mask: u8 = 0b0000_0001; gb.set_joypad_button_states(mask);
assert_eq!(gb.get_joypad_button_states(), mask);
}
#[test]
fn test_cgb_rom_audio_sample_rate_does_not_panic() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
gb.set_audio_sample_rate(48_000.0);
}
#[test]
fn test_load_invalid_rom_returns_err() {
let mut gb = make_gameboy();
assert!(gb.load_rom(&[0u8; 16], "bad.gb").is_err());
}
#[test]
fn test_run_tick_after_load_returns_nonzero_cycles() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let cycles = gb.run_tick();
assert!(cycles > 0, "expected non-zero cycles, got {cycles}");
}
#[test]
fn test_set_get_joypad_button_states_roundtrip() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let mask: u8 = 0b0000_1001; gb.set_joypad_button_states(mask);
assert_eq!(gb.get_joypad_button_states(), mask);
}
#[test]
fn test_screen_constants() {
assert_eq!(GameBoy::SCREEN_WIDTH, 160);
assert_eq!(GameBoy::SCREEN_HEIGHT, 144);
}
#[test]
fn test_reset_soft_after_load_does_not_panic() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
gb.reset(true); }
#[test]
fn test_reset_hard_after_load_does_not_panic() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
gb.reset(false); }
#[test]
fn test_app_context_returns_reference() {
let gb = make_gameboy();
let _ = gb.app_context(); }
#[test]
fn test_sample_not_ready_before_rom_load() {
let gb = make_gameboy();
assert!(!gb.sample_ready());
}
#[test]
fn test_get_sample_returns_none_before_rom_load() {
let mut gb = make_gameboy();
assert!(gb.get_sample().is_none());
}
#[test]
fn test_sample_ready_after_ticks_with_rom() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..30 {
gb.run_tick();
}
assert!(
gb.sample_ready(),
"sample must be ready after running 30 ticks"
);
}
#[test]
fn test_get_sample_consumes_one_queued_sample() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..30 {
gb.run_tick();
}
assert!(gb.sample_ready());
assert!(gb.get_sample().is_some());
}
#[test]
fn test_set_audio_sample_rate_does_not_panic() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
gb.set_audio_sample_rate(48_000.0);
}
#[test]
fn test_dmg_save_state_returns_ok_after_rom_load() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..10 {
gb.run_tick();
}
assert!(gb.save_state_bytes().is_ok());
}
#[test]
fn test_cgb_save_state_returns_ok_after_rom_load() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
for _ in 0..10 {
gb.run_tick();
}
assert!(gb.save_state_bytes().is_ok());
}
#[test]
fn test_dmg_save_load_roundtrip() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..10 {
gb.run_tick();
}
let snap1 = gb.screen_crc32();
let state = gb.save_state_bytes().unwrap();
for _ in 0..50 {
gb.run_tick();
}
gb.load_state_bytes(&state).unwrap();
assert_eq!(gb.screen_crc32(), snap1);
}
#[test]
fn test_cgb_save_load_roundtrip() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
for _ in 0..10 {
gb.run_tick();
}
let snap1 = gb.screen_crc32();
let state = gb.save_state_bytes().unwrap();
for _ in 0..50 {
gb.run_tick();
}
gb.load_state_bytes(&state).unwrap();
assert_eq!(gb.screen_crc32(), snap1);
}
#[test]
fn test_load_state_clears_joypad_button_states() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
gb.set_button(4, true); assert_ne!(
gb.get_joypad_button_states() & (1 << 4),
0,
"Up should be pressed before save"
);
let state = gb.save_state_bytes().unwrap();
gb.load_state_bytes(&state).unwrap();
assert_eq!(
gb.get_joypad_button_states(),
0,
"All joypad buttons must be cleared after loading a save state"
);
}
#[test]
fn test_load_invalid_state_returns_err() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let result = gb.load_state_bytes(b"invalid json");
assert!(result.is_err());
}
#[test]
fn test_load_incompatible_version_returns_err() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..5 {
gb.run_tick();
}
let mut state = gb.save_state_bytes().unwrap();
let json_str = String::from_utf8(state).unwrap();
let corrupted = json_str.replacen(
&format!("\"version\":{}", GB_SAVESTATE_VERSION),
"\"version\":9999",
1,
);
state = corrupted.into_bytes();
let result = gb.load_state_bytes(&state);
assert!(result.is_err());
assert!(result.unwrap_err().contains("incompatible"));
}
#[test]
fn test_state_path_with_rom_loaded() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "roms/test.gb").unwrap();
let path = gb.state_path().expect("state_path should be Some");
assert_eq!(path.to_str().unwrap(), "roms/test.state");
}
#[test]
fn test_state_path_without_rom_loaded() {
let gb = make_gameboy();
assert!(gb.state_path().is_none());
}
#[test]
fn test_load_dmg_state_into_cgb_returns_err() {
let mut dmg_gb = make_gameboy();
dmg_gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..10 {
dmg_gb.run_tick();
}
let dmg_state = dmg_gb.save_state_bytes().unwrap();
let mut cgb_gb = make_gameboy();
cgb_gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
let result = cgb_gb.load_state_bytes(&dmg_state);
assert!(result.is_err());
assert!(result.unwrap_err().contains("bus type mismatch"));
}
#[test]
fn test_load_cgb_state_into_dmg_returns_err() {
let mut cgb_gb = make_gameboy();
cgb_gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
for _ in 0..10 {
cgb_gb.run_tick();
}
let cgb_state = cgb_gb.save_state_bytes().unwrap();
let mut dmg_gb = make_gameboy();
dmg_gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let result = dmg_gb.load_state_bytes(&cgb_state);
assert!(result.is_err());
assert!(result.unwrap_err().contains("bus type mismatch"));
}
#[test]
fn test_emulator_trait_port1_reads_buttons_set_via_port0() {
use crate::platform::emulator::Emulator;
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let emu: &mut dyn Emulator = &mut gb;
let state: u8 = 0b0000_1001; emu.set_joypad_button_states(0, state);
assert_eq!(
emu.get_joypad_button_states(1),
state,
"port 1 must reflect buttons set via port 0 (keyboard)"
);
}
#[test]
fn test_emulator_trait_port1_set_joypad_states_applied_to_gb() {
use crate::platform::emulator::Emulator;
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let emu: &mut dyn Emulator = &mut gb;
let state: u8 = 0b0100_0010; emu.set_joypad_button_states(1, state);
assert_eq!(
emu.get_joypad_button_states(0),
state,
"port 0 must reflect buttons set via port 1 (autorun playback)"
);
assert_eq!(
emu.get_joypad_button_states(1),
state,
"port 1 must reflect buttons set via port 1 (autorun playback)"
);
}
#[test]
fn test_emulator_trait_port2_returns_zero_for_gb() {
use crate::platform::emulator::Emulator;
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let emu: &mut dyn Emulator = &mut gb;
emu.set_joypad_button_states(2, 0xFF);
assert_eq!(
emu.get_joypad_button_states(2),
0,
"port 2 must return 0 for GB (no second controller)"
);
}
#[test]
fn test_emulator_trait_port1_set_button_applied_to_gb() {
use crate::platform::emulator::Emulator;
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let emu: &mut dyn Emulator = &mut gb;
emu.set_button(1, 0, true);
assert_ne!(
emu.get_joypad_button_states(1),
0,
"setting a button via port 1 must update the GB joypad"
);
}
#[test]
fn test_gb_allowed_shaders_includes_expected_presets() {
use crate::platform::emulator::Emulator;
let gb = make_gameboy();
let shaders = gb.allowed_shaders();
assert!(
shaders.contains(&"none"),
"GB must allow the 'none' (stock) shader"
);
assert!(shaders.contains(&"dmg"), "GB must allow the 'dmg' shader");
assert!(
!shaders.contains(&"crt"),
"GB must NOT allow the 'crt' shader"
);
assert!(
!shaders.contains(&"ntsc"),
"GB must NOT allow the 'ntsc' shader"
);
assert!(
!shaders.contains(&"pal"),
"GB must NOT allow the 'pal' shader"
);
assert!(
!shaders.contains(&"smooth"),
"GB must NOT allow the 'smooth' shader"
);
}
#[test]
fn test_dmg_rom_with_no_hardware_option_uses_dmg_bus() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Dmg(_))));
}
#[test]
fn test_dual_rom_with_no_hardware_option_uses_cgb_bus() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_dual_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[test]
fn test_cgb_only_rom_with_no_hardware_option_uses_cgb_bus() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[test]
fn test_dmg_rom_with_dmg_hardware_uses_dmg_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Dmg);
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Dmg(_))));
}
#[test]
fn test_dual_rom_with_dmg_hardware_uses_dmg_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Dmg);
gb.load_rom(&minimal_dual_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Dmg(_))));
}
#[test]
fn test_cgb_only_rom_with_dmg_hardware_returns_error() {
let mut gb = make_gameboy_with_hardware(GbHardware::Dmg);
let result = gb.load_rom(&minimal_cgb_rom(), "test.gbc");
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(err_msg.contains("CGB-only"));
assert!(err_msg.contains("--gb-hardware cgb"));
}
#[test]
fn test_dmg_rom_with_cgb_hardware_uses_cgb_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Cgb);
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[test]
fn test_dual_rom_with_cgb_hardware_uses_cgb_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Cgb);
gb.load_rom(&minimal_dual_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[test]
fn test_cgb_only_rom_with_cgb_hardware_uses_cgb_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Cgb);
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[test]
fn test_dmg_rom_with_gba_hardware_uses_cgb_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Gba);
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[test]
fn test_dual_rom_with_gba_hardware_uses_cgb_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Gba);
gb.load_rom(&minimal_dual_rom(), "test.gb").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[test]
fn test_cgb_only_rom_with_gba_hardware_uses_cgb_bus() {
let mut gb = make_gameboy_with_hardware(GbHardware::Gba);
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
assert!(matches!(gb.gb, Some(GbConsole::Cgb(_))));
}
#[cfg(feature = "native")]
#[test]
fn test_create_debugger_snapshot_with_no_rom_returns_default() {
let gb = make_gameboy();
let mut view_state = crate::gb::debugging::GbDebuggerViewState::default();
let snapshot = gb.create_debugger_snapshot(&mut view_state);
assert_eq!(snapshot.cpu_regs.pc, 0);
assert_eq!(snapshot.cpu_regs.sp, 0);
assert_eq!(snapshot.wram_hexdump_base, 0xC000);
assert_eq!(snapshot.vram_hexdump_base, 0x8000);
assert_eq!(snapshot.wram_hexdump_bytes.len(), 256);
assert_eq!(snapshot.vram_hexdump_bytes.len(), 256);
}
#[cfg(feature = "native")]
#[test]
fn test_create_debugger_snapshot_with_rom_captures_state() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..10 {
gb.run_tick();
}
let mut view_state = crate::gb::debugging::GbDebuggerViewState::default();
let snapshot = gb.create_debugger_snapshot(&mut view_state);
assert!(snapshot.cpu_regs.pc > 0 || snapshot.cpu_regs.cycles > 0);
}
#[cfg(feature = "native")]
#[test]
fn test_create_ppu_viewer_snapshot_with_no_rom_returns_defaults() {
let gb = make_gameboy();
let snapshot = gb.create_ppu_viewer_snapshot();
assert_eq!(snapshot.vram, [0; 0x2000]);
assert_eq!(snapshot.vram_bank1, [0; 0x2000]);
assert_eq!(snapshot.oam, [0; 0xA0]);
assert_eq!(snapshot.lcdc, 0);
assert!(!snapshot.cgb_mode);
}
#[cfg(feature = "native")]
#[test]
fn test_create_ppu_viewer_snapshot_with_rom_captures_ppu_state() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
for _ in 0..100 {
gb.run_tick();
}
let snapshot = gb.create_ppu_viewer_snapshot();
assert_eq!(snapshot.vram.len(), 0x2000);
assert_eq!(snapshot.oam.len(), 0xA0);
assert!(!snapshot.cgb_mode);
}
#[cfg(feature = "native")]
#[test]
fn test_is_cgb_mode_with_no_rom_returns_false() {
let gb = make_gameboy();
assert!(!gb.is_cgb_mode());
}
#[cfg(feature = "native")]
#[test]
fn test_is_cgb_mode_with_dmg_rom_returns_false() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
assert!(!gb.is_cgb_mode());
}
#[cfg(feature = "native")]
#[test]
fn test_is_cgb_mode_with_cgb_rom_returns_true() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_cgb_rom(), "test.gbc").unwrap();
assert!(gb.is_cgb_mode());
}
#[cfg(feature = "native")]
#[test]
fn test_toggle_debugger_with_controller_opens_when_closed() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], false);
assert!(!controller.is_debugger_open());
gb.toggle_debugger_with_controller(&mut controller);
assert!(controller.is_debugger_open());
assert!(controller.is_paused());
}
#[cfg(feature = "native")]
#[test]
fn test_toggle_debugger_with_controller_closes_when_open() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], true);
assert!(controller.is_debugger_open());
gb.toggle_debugger_with_controller(&mut controller);
assert!(!controller.is_debugger_open());
assert!(!controller.is_paused());
}
#[cfg(feature = "native")]
#[test]
fn test_toggle_debugger_with_no_rom_does_not_panic() {
let mut gb = make_gameboy();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], false);
gb.toggle_debugger_with_controller(&mut controller);
}
#[cfg(feature = "native")]
#[test]
fn test_step_into_with_controller_advances_instruction() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], true);
let mut view_state = crate::gb::debugging::GbDebuggerViewState::default();
let snapshot_before = gb.create_debugger_snapshot(&mut view_state);
let pc_before = snapshot_before.cpu_regs.pc;
gb.step_into_with_controller(&mut controller);
let snapshot_after = gb.create_debugger_snapshot(&mut view_state);
let pc_after = snapshot_after.cpu_regs.pc;
assert!(
pc_after != pc_before
|| snapshot_after.cpu_regs.cycles > snapshot_before.cpu_regs.cycles
);
}
#[cfg(feature = "native")]
#[test]
fn test_step_over_with_controller_executes_instruction() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], true);
let mut view_state = crate::gb::debugging::GbDebuggerViewState::default();
let snapshot_before = gb.create_debugger_snapshot(&mut view_state);
let cycles_before = snapshot_before.cpu_regs.cycles;
gb.step_over_with_controller(&mut controller);
let snapshot_after = gb.create_debugger_snapshot(&mut view_state);
let cycles_after = snapshot_after.cpu_regs.cycles;
assert!(cycles_after > cycles_before);
}
#[cfg(feature = "native")]
#[test]
fn test_apply_ui_action_step_into_advances_execution() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], true);
let action = crate::gb::debugging::ui::GbDebuggerUiAction {
step_into: true,
..Default::default()
};
let mut view_state = crate::gb::debugging::GbDebuggerViewState::default();
let cycles_before = gb.create_debugger_snapshot(&mut view_state).cpu_regs.cycles;
gb.apply_ui_action_with_controller(&mut controller, action);
let cycles_after = gb.create_debugger_snapshot(&mut view_state).cpu_regs.cycles;
assert!(cycles_after > cycles_before);
}
#[cfg(feature = "native")]
#[test]
fn test_apply_ui_action_continue_unpauses_execution() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], true);
assert!(controller.is_paused());
let action = crate::gb::debugging::ui::GbDebuggerUiAction {
continue_run: true,
..Default::default()
};
gb.apply_ui_action_with_controller(&mut controller, action);
assert!(!controller.is_paused());
}
#[cfg(feature = "native")]
#[test]
fn test_wrapper_methods_with_no_rom_do_not_panic() {
let mut gb = make_gameboy();
let mut controller = crate::gb::debugging::control::GbDebuggerController::new(&[], false);
gb.toggle_debugger_with_controller(&mut controller);
gb.step_into_with_controller(&mut controller);
gb.step_over_with_controller(&mut controller);
let action = crate::gb::debugging::ui::GbDebuggerUiAction::default();
gb.apply_ui_action_with_controller(&mut controller, action);
}
#[test]
fn test_has_battery_returns_true_for_mbc5_battery_cart() {
let mut gb = make_gameboy();
gb.load_rom(&mbc5_battery_rom(), "test.gb").unwrap();
assert!(
gb.has_battery(),
"MBC5+RAM+BATTERY cart should report has_battery=true"
);
}
#[test]
fn test_has_battery_returns_false_for_rom_only_cart() {
let mut gb = make_gameboy();
gb.load_rom(&minimal_rom(), "test.gb").unwrap();
assert!(
!gb.has_battery(),
"ROM-only cart should report has_battery=false"
);
}
#[test]
fn test_save_ram_writes_sav_file_for_battery_cart() {
let dir = tempfile::tempdir().unwrap();
let rom_path = dir.path().join("test_battery.gb");
let sav_path = dir.path().join("test_battery.sav");
std::fs::write(&rom_path, mbc5_battery_rom()).unwrap();
let mut gb = make_gameboy();
gb.load_rom(
&std::fs::read(&rom_path).unwrap(),
rom_path.to_str().unwrap(),
)
.unwrap();
let result = gb.save_ram();
assert!(result.is_ok(), "save_ram should succeed");
assert!(sav_path.exists(), ".sav file should be created");
}
#[test]
fn test_load_rom_restores_sav_file_for_battery_cart() {
let dir = tempfile::tempdir().unwrap();
let rom_path = dir.path().join("test_battery.gb");
let sav_path = dir.path().join("test_battery.sav");
std::fs::write(&rom_path, mbc5_battery_rom()).unwrap();
let mut save_data = vec![0u8; 8 * 1024]; save_data[0] = 0xAA;
save_data[1] = 0xBB;
save_data[42] = 0xCC;
std::fs::write(&sav_path, &save_data).unwrap();
let mut gb = make_gameboy();
gb.load_rom(
&std::fs::read(&rom_path).unwrap(),
rom_path.to_str().unwrap(),
)
.unwrap();
let ram_snapshot = gb.cart_ram_snapshot();
assert_eq!(ram_snapshot[0], 0xAA, "save data byte 0 should be restored");
assert_eq!(ram_snapshot[1], 0xBB, "save data byte 1 should be restored");
assert_eq!(
ram_snapshot[42], 0xCC,
"save data byte 42 should be restored"
);
}
}