mod audio; mod cartridge; mod desktop; use std::{error::Error, ffi::OsString, io, path::PathBuf, process::ExitCode}; use gba_core::{ cpu::Cpu, io::{IF, TIMER_BASE}, memory::{Memory, ROM_START}, }; use gba_demos::{ bitmap_demo::BitmapMode, demo_bios, demo_program, exception_demo_program, timer_demo::{timer_demo, TIMER_DEMO_STEPS}, DEMO_STEPS, EXCEPTION_DEMO_STEPS, }; #[derive(Debug, PartialEq, Eq)] enum RunMode { Window, Rom(cartridge::Options), TestSuite(PathBuf), SmokeTest, CpuDemo, TimerDemo, GraphicsDemo, GraphicsSmokeTest, EffectsDemo, EffectsSmokeTest, MosaicDemo, MosaicSmokeTest, RasterDemo, RasterSmokeTest, TileDemo, TileSmokeTest, AffineDemo, AffineSmokeTest, AffineRasterDemo, AffineRasterSmokeTest, BitmapDemo(BitmapMode), BitmapSmokeTest(BitmapMode), Help, } fn parse_args(args: &[OsString]) -> Result { if args.first().is_some_and(|arg| arg == "--test-suite") { return cartridge::suite::parse_args(args).map(RunMode::TestSuite); } if args.iter().any(|arg| { [ "--rom", "--steps", "--window", "--frames", "--rtc", "--audio", "--speed", "--save-type", "--save-file", "--state-file", ] .iter() .any(|flag| arg == flag) }) { return cartridge::parse_args(args).map(RunMode::Rom); } match args { [] => Ok(RunMode::Window), [arg] if arg == "--smoke-test" => Ok(RunMode::SmokeTest), [arg] if arg == "--cpu-demo" => Ok(RunMode::CpuDemo), [arg] if arg == "--timer-demo" => Ok(RunMode::TimerDemo), [arg] if arg == "--graphics-demo" => Ok(RunMode::GraphicsDemo), [arg] if arg == "--graphics-smoke-test" => Ok(RunMode::GraphicsSmokeTest), [arg] if arg == "--raster-demo" => Ok(RunMode::RasterDemo), [arg] if arg == "--raster-smoke-test" => Ok(RunMode::RasterSmokeTest), [arg] if arg == "--mosaic-demo" => Ok(RunMode::MosaicDemo), [arg] if arg == "--mosaic-smoke-test" => Ok(RunMode::MosaicSmokeTest), [arg] if arg == "--effects-demo" => Ok(RunMode::EffectsDemo), [arg] if arg == "--effects-smoke-test" => Ok(RunMode::EffectsSmokeTest), [arg] if arg == "--tile-demo" => Ok(RunMode::TileDemo), [arg] if arg == "--tile-smoke-test" => Ok(RunMode::TileSmokeTest), [arg] if arg == "--affine-raster-demo" => Ok(RunMode::AffineRasterDemo), [arg] if arg == "--affine-raster-smoke-test" => Ok(RunMode::AffineRasterSmokeTest), [arg] if arg == "--affine-demo" => Ok(RunMode::AffineDemo), [arg] if arg == "--affine-smoke-test" => Ok(RunMode::AffineSmokeTest), [arg] if arg == "--bitmap4-demo" => Ok(RunMode::BitmapDemo(BitmapMode::Mode4)), [arg] if arg == "--bitmap5-demo" => Ok(RunMode::BitmapDemo(BitmapMode::Mode5)), [arg] if arg == "--bitmap4-smoke-test" => Ok(RunMode::BitmapSmokeTest(BitmapMode::Mode4)), [arg] if arg == "--bitmap5-smoke-test" => Ok(RunMode::BitmapSmokeTest(BitmapMode::Mode5)), [arg] if arg == "--help" || arg == "-h" => Ok(RunMode::Help), _ => Err(io::Error::new( io::ErrorKind::InvalidInput, "usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER] [--state-file PATH]); or gameboy-rust --test-suite PATH.json", )), } } fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("{error}"); ExitCode::FAILURE } } } fn run() -> Result<(), Box> { match parse_args(&std::env::args_os().skip(1).collect::>())? { RunMode::Window => desktop::run(None), RunMode::Rom(options) => cartridge::execute(options, &mut io::stdout().lock()), RunMode::TestSuite(path) => cartridge::suite::execute(&path, &mut io::stdout().lock()), RunMode::SmokeTest => desktop::run(Some(60)), RunMode::CpuDemo => run_cpu_demo(), RunMode::TimerDemo => run_timer_demo(), RunMode::GraphicsDemo => desktop::run_graphics(false), RunMode::GraphicsSmokeTest => desktop::run_graphics(true), RunMode::RasterDemo => desktop::run_raster(false), RunMode::RasterSmokeTest => desktop::run_raster(true), RunMode::MosaicDemo => desktop::run_mosaic(false), RunMode::MosaicSmokeTest => desktop::run_mosaic(true), RunMode::EffectsDemo => desktop::run_effects(false), RunMode::EffectsSmokeTest => desktop::run_effects(true), RunMode::TileDemo => desktop::run_tiles(false), RunMode::TileSmokeTest => desktop::run_tiles(true), RunMode::AffineRasterDemo => desktop::run_affine_raster(false), RunMode::AffineRasterSmokeTest => desktop::run_affine_raster(true), RunMode::AffineDemo => desktop::run_affine(false), RunMode::AffineSmokeTest => desktop::run_affine(true), RunMode::BitmapDemo(mode) => desktop::run_bitmap(mode, false), RunMode::BitmapSmokeTest(mode) => desktop::run_bitmap(mode, true), RunMode::Help => { println!( "GBA Rust — emulator foundation, not game-compatible yet\n\ No arguments Open the native 240x160 display test at 4x scale\n\ --rom PATH --steps COUNT Run a raw ROM in the terminal (1..=100000000 steps)\n\ --rom PATH --window Open a raw ROM with keyboard input\n\ --frames COUNT Exit ROM window after 1..=100000 captured frames\n\ --speed MULTIPLIER ROM-window speed: integer 1..=16 (default: 1); mutes audio above 1x\n\ --audio Enable macOS output at 1x (default: muted)\n\ --rtc Select cartridge RTC (UTC startup, host elapsed time)\n\ --save-type flash64|flash128 Select nominal Macronix Flash emulation\n\ --save-file PATH Load raw Flash image; persist on clean exit or window diagnostic\n\ --state-file PATH ROM window save state; F5 saves, F9 loads (default: ROM path + .state)\n\ --test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\ --cpu-demo Run the terminal-only ARM/Thumb instruction demo\n\ --timer-demo Run the timer IRQ demo with nominal cycle costs\n\ --graphics-demo Open the CPU-driven Mode 3 demo\n\ --graphics-smoke-test Verify 60 CPU-driven frames with scripted input\n\ --raster-demo Open the HBlank DMA color-band demo\n\ --raster-smoke-test Verify 60 scanline-captured raster frames\n\ --mosaic-demo Open the CPU-driven mosaic demo\n\ --mosaic-smoke-test Verify 128 background and sprite mosaic frames\n\ --effects-demo Open the CPU-driven window and color-effects demo\n\ --effects-smoke-test Verify 60 window and color-effects frames\n\ --tile-demo Open the CPU-driven Mode 0 scrolling demo\n\ --tile-smoke-test Verify 60 tile frames with scripted input\n\ --affine-raster-demo Open the HBlank DMA affine distortion demo\n\ --affine-raster-smoke-test Verify 60 affine raster frames\n\ --affine-demo Open the CPU-driven Mode 2 background demo\n\ --affine-smoke-test Verify 60 transformed background frames\n\ --bitmap4-demo Open the indexed-color page-flipping demo\n\ --bitmap5-demo Open the 160x128 RGB555 page-flipping demo\n\ --bitmap4-smoke-test Verify 60 Mode 4 bitmap frames\n\ --bitmap5-smoke-test Verify 60 Mode 5 bitmap frames\n\ --smoke-test Open the host display test for 60 frames\n\ --help, -h Show this help\n\n\ Display test: Arrows move; Space pauses; Escape exits.\n\ CPU graphics: Arrows move; Z/X change color; Enter resets; Escape exits.\n\ Raster: Left/Right move bands; Enter resets; Escape exits.\n\ Mosaic: Arrows scroll; Z bypasses BG mosaic; X bypasses OBJ mosaic; Q/W transform; Enter resets; Escape exits.\n\ Window effects: Arrows move; Z blends; X darkens; Enter resets; Escape exits.\n\ Tile graphics: Arrows scroll; Q rotates; W zooms; Z flips without Q/W; X changes priority; Enter resets; Escape exits.\n\ Affine raster: Arrows pan; Q rotates; W zooms; Z bypasses distortion; Enter resets; Escape exits.\n\ Affine backgrounds: Arrows pan; Q rotates; W zooms; Z disables wrapping; Enter resets; Escape exits.\n\ Bitmap pages: Arrows pan; Q rotates; W zooms; Z forces page1; Enter resets; Escape exits.\n\ All demos use original test content. ROM mode uses our limited BIOS replacement.\n\ Terminal ROM mode has no window, input, or audio. ROM windows offer --audio on macOS. Saves require --save-type and --save-file.\n\ ROM window: Arrows=D-pad; Z/X=A/B; Q/W=L/R; Enter=Start; Backspace=Select; F5=save state; F9=load state; Escape exits.\n\ Commercial games are not supported." ); Ok(()) } } } fn run_cpu_demo() -> Result<(), Box> { let mut memory = Memory::new(demo_program())?; let mut cpu = Cpu::new(ROM_START); println!("GBA core demo — not yet able to run games"); // Fixed instruction count: the demo ends in an intentional infinite branch. for _ in 0..DEMO_STEPS { let pc = cpu.pc(); let instruction_set = cpu.instruction_set(); cpu.step(&mut memory)?; let flags = cpu.flags(); println!( "{pc:#010x} {instruction_set:?}: r0={} r1={} r2={} r4={} r5={} r6={} r7={} sp={:#010x} NZCV={}{}{}{} next_pc={:#010x}", cpu.registers()[0], cpu.registers()[1], cpu.registers()[2], cpu.registers()[4], cpu.registers()[5], cpu.registers()[6], cpu.registers()[7], cpu.registers()[13], u8::from(flags.negative), u8::from(flags.zero), u8::from(flags.carry), u8::from(flags.overflow), cpu.pc(), ); } println!("Exception demo — original test handler, not Nintendo BIOS services"); let mut memory = Memory::with_bios(exception_demo_program(), demo_bios())?; let mut cpu = Cpu::new(ROM_START); for _ in 0..EXCEPTION_DEMO_STEPS { let pc = cpu.pc(); let mode = cpu.mode(); let state = cpu.instruction_set(); cpu.step(&mut memory)?; println!( "{pc:#010x} {mode:?}/{state:?} -> {:?}/{:?} CPSR={:#010x} handler_calls={} next_pc={:#010x}", cpu.mode(), cpu.instruction_set(), cpu.cpsr(), cpu.registers()[10], cpu.pc(), ); } Ok(()) } fn run_timer_demo() -> Result<(), Box> { let mut machine = timer_demo()?; println!( "Timer IRQ demo — nominal instruction/bus costs; no prefetch or sub-instruction timing" ); for _ in 0..TIMER_DEMO_STEPS { let event = machine.step()?; println!( "cycle={} (+{}) {event:?} mode={:?} pc={:#010x} timer0={:#06x} IF={:#06x} handler_calls={}", machine.cycles(), machine.last_timing().total(), machine.cpu().mode(), machine.cpu().pc(), machine.memory().read16(TIMER_BASE)?, machine.memory().read16(IF)?, machine.cpu().registers()[10], ); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn command_line_selects_window_demo_smoke_test_or_help() { assert_eq!(parse_args(&[]).unwrap(), RunMode::Window); for (argument, expected) in [ ("--cpu-demo", RunMode::CpuDemo), ("--timer-demo", RunMode::TimerDemo), ("--bitmap4-demo", RunMode::BitmapDemo(BitmapMode::Mode4)), ("--bitmap5-demo", RunMode::BitmapDemo(BitmapMode::Mode5)), ( "--bitmap4-smoke-test", RunMode::BitmapSmokeTest(BitmapMode::Mode4), ), ( "--bitmap5-smoke-test", RunMode::BitmapSmokeTest(BitmapMode::Mode5), ), ("--affine-raster-demo", RunMode::AffineRasterDemo), ("--affine-raster-smoke-test", RunMode::AffineRasterSmokeTest), ("--affine-demo", RunMode::AffineDemo), ("--affine-smoke-test", RunMode::AffineSmokeTest), ("--raster-demo", RunMode::RasterDemo), ("--raster-smoke-test", RunMode::RasterSmokeTest), ("--mosaic-demo", RunMode::MosaicDemo), ("--mosaic-smoke-test", RunMode::MosaicSmokeTest), ("--effects-demo", RunMode::EffectsDemo), ("--effects-smoke-test", RunMode::EffectsSmokeTest), ("--tile-demo", RunMode::TileDemo), ("--tile-smoke-test", RunMode::TileSmokeTest), ("--graphics-demo", RunMode::GraphicsDemo), ("--graphics-smoke-test", RunMode::GraphicsSmokeTest), ("--smoke-test", RunMode::SmokeTest), ("--help", RunMode::Help), ("-h", RunMode::Help), ] { assert_eq!(parse_args(&[argument.into()]).unwrap(), expected); } } #[test] fn command_line_rejects_bare_paths_and_conflicting_options() { assert!(parse_args(&["emerald.gba".into()]).is_err()); assert!(parse_args(&["--unknown".into()]).is_err()); assert!(parse_args(&["--cpu-demo".into(), "--smoke-test".into()]).is_err()); } }