-
João Magalhães authored
CGB is now going to boot using Boytacean ROM by default. Allows better image and open source only boot.
João Magalhães authoredCGB is now going to boot using Boytacean ROM by default. Allows better image and open source only boot.
gb.rs 41.21 KiB
//! Main Game Boy emulation entrypoint functions and structures.
use std::{
collections::VecDeque,
fmt::{self, Display, Formatter},
sync::{Arc, Mutex},
};
use crate::{
apu::Apu,
cheats::{
genie::{GameGenie, GameGenieCode},
shark::{GameShark, GameSharkCode},
},
cpu::Cpu,
data::{BootRom, CGB_BOOT, CGB_BOYTACEAN, DMG_BOOT, DMG_BOOTIX, MGB_BOOTIX, SGB_BOOT},
devices::{printer::PrinterDevice, stdout::StdoutDevice},
dma::Dma,
error::Error,
info::Info,
mmu::Mmu,
pad::{Pad, PadKey},
ppu::{
Ppu, PpuMode, Tile, DISPLAY_HEIGHT, DISPLAY_WIDTH, FRAME_BUFFER_RGB1555_SIZE,
FRAME_BUFFER_RGB565_SIZE, FRAME_BUFFER_SIZE, FRAME_BUFFER_XRGB8888_SIZE,
},
rom::{Cartridge, RamSize},
serial::{NullDevice, Serial, SerialDevice},
timer::Timer,
util::{read_file, SharedThread},
};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "wasm")]
use crate::{color::Pixel, gen::dependencies_map, ppu::Palette};
#[cfg(feature = "wasm")]
use std::{
convert::TryInto,
panic::{set_hook, take_hook, PanicInfo},
};
/// Enumeration that describes the multiple running
// modes of the Game Boy emulator.
// DMG = Original Game Boy
// CGB = Game Boy Color
// SGB = Super Game Boy
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GameBoyMode {
Dmg = 1,
Cgb = 2,
Sgb = 3,
}
impl GameBoyMode {
pub fn description(&self) -> &'static str {
match self {
GameBoyMode::Dmg => "Game Boy (DMG)",
GameBoyMode::Cgb => "Game Boy Color (CGB)",
GameBoyMode::Sgb => "Super Game Boy (SGB)",
}
}
pub fn from_u8(value: u8) -> Self {
match value {
1 => GameBoyMode::Dmg,
2 => GameBoyMode::Cgb,
3 => GameBoyMode::Sgb,
_ => panic!("Invalid mode value: {}", value),
}
}
pub fn from_string(value: &str) -> Self {
match value {
"dmg" | "DMG" => GameBoyMode::Dmg,
"cgb" | "CGB" => GameBoyMode::Cgb,
"sgb" | "SGB" => GameBoyMode::Sgb,
_ => panic!("Invalid mode value: {}", value),
}
}
pub fn to_string(&self, uppercase: Option<bool>) -> String {
let uppercase = uppercase.unwrap_or(false);
match self {
GameBoyMode::Dmg => (if uppercase { "DMG" } else { "dmg" }).to_string(),
GameBoyMode::Cgb => (if uppercase { "CGB" } else { "cgb" }).to_string(),
GameBoyMode::Sgb => (if uppercase { "SGB" } else { "sgb" }).to_string(),
}
}
pub fn is_dmg(&self) -> bool {
*self == GameBoyMode::Dmg
}
pub fn is_cgb(&self) -> bool {
*self == GameBoyMode::Cgb
}
pub fn is_sgb(&self) -> bool {
*self == GameBoyMode::Sgb
}
}
impl Display for GameBoyMode {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description())
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GameBoySpeed {
Normal = 0,
Double = 1,
}
impl GameBoySpeed {
pub fn description(&self) -> &'static str {
match self {
GameBoySpeed::Normal => "Normal Speed",
GameBoySpeed::Double => "Double Speed",
}
}
pub fn switch(&self) -> Self {
match self {
GameBoySpeed::Normal => GameBoySpeed::Double,
GameBoySpeed::Double => GameBoySpeed::Normal,
}
}
pub fn multiplier(&self) -> u8 {
match self {
GameBoySpeed::Normal => 1,
GameBoySpeed::Double => 2,
}
}
pub fn from_u8(value: u8) -> Self {
match value {
0 => GameBoySpeed::Normal,
1 => GameBoySpeed::Double,
_ => panic!("Invalid speed value: {}", value),
}
}
}
impl Display for GameBoySpeed {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description())
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct GameBoyConfig {
/// The current running mode of the emulator, this
/// may affect many aspects of the emulation, like
/// CPU frequency, PPU frequency, Boot rome size, etc.
mode: GameBoyMode,
/// If the PPU is enabled, it will be clocked.
ppu_enabled: bool,
/// If the APU is enabled, it will be clocked.
apu_enabled: bool,
/// if the DMA is enabled, it will be clocked.
dma_enabled: bool,
/// If the timer is enabled, it will be clocked.
timer_enabled: bool,
/// If the serial is enabled, it will be clocked.
serial_enabled: bool,
/// The current frequency at which the Game Boy
/// emulator is being handled. This is a "hint" that
/// may help components to adjust their internal
/// logic to match the current frequency. For example
/// the APU will adjust its internal clock to match
/// this hint.
clock_freq: u32,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl GameBoyConfig {
pub fn is_dmg(&self) -> bool {
self.mode == GameBoyMode::Dmg
}
pub fn is_cgb(&self) -> bool {
self.mode == GameBoyMode::Cgb
}
pub fn is_sgb(&self) -> bool {
self.mode == GameBoyMode::Sgb
}
pub fn mode(&self) -> GameBoyMode {
self.mode
}
pub fn set_mode(&mut self, value: GameBoyMode) {
self.mode = value;
}
pub fn ppu_enabled(&self) -> bool {
self.ppu_enabled
}
pub fn set_ppu_enabled(&mut self, value: bool) {
self.ppu_enabled = value;
}
pub fn apu_enabled(&self) -> bool {
self.apu_enabled
}
pub fn set_apu_enabled(&mut self, value: bool) {
self.apu_enabled = value;
}
pub fn dma_enabled(&self) -> bool {
self.dma_enabled
}
pub fn set_dma_enabled(&mut self, value: bool) {
self.dma_enabled = value;
}
pub fn timer_enabled(&self) -> bool {
self.timer_enabled
}
pub fn set_timer_enabled(&mut self, value: bool) {
self.timer_enabled = value;
}
pub fn serial_enabled(&self) -> bool {
self.serial_enabled
}
pub fn set_serial_enabled(&mut self, value: bool) {
self.serial_enabled = value;
}
pub fn clock_freq(&self) -> u32 {
self.clock_freq
}
pub fn set_clock_freq(&mut self, value: u32) {
self.clock_freq = value;
}
}
impl Default for GameBoyConfig {
fn default() -> Self {
Self {
mode: GameBoyMode::Dmg,
ppu_enabled: true,
apu_enabled: true,
dma_enabled: true,
timer_enabled: true,
serial_enabled: true,
clock_freq: GameBoy::CPU_FREQ,
}
}
}
/// Aggregation structure allowing the bundling of
/// all the components of a GameBoy into a single
/// element for easy access.
pub struct Components {
pub ppu: Ppu,
pub apu: Apu,
pub dma: Dma,
pub pad: Pad,
pub timer: Timer,
pub serial: Serial,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct Registers {
pub pc: u16,
pub sp: u16,
pub a: u8,
pub b: u8,
pub c: u8,
pub d: u8,
pub e: u8,
pub h: u8,
pub l: u8,
pub scy: u8,
pub scx: u8,
pub wy: u8,
pub wx: u8,
pub ly: u8,
pub lyc: u8,
}
pub trait AudioProvider {
fn audio_output(&self) -> u8;
fn audio_buffer(&self) -> &VecDeque<u8>;
fn clear_audio_buffer(&mut self);
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct ClockFrame {
pub cycles: u64,
pub frames: u16,
frame_buffer: Option<Vec<u8>>,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl ClockFrame {
pub fn frame_buffer_eager(&mut self) -> Option<Vec<u8>> {
self.frame_buffer.take()
}
}
/// Top level structure that abstracts the usage of the
/// Game Boy system under the Boytacean emulator.
///
/// Should serve as the main entry-point API.
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct GameBoy {
/// The current running mode of the emulator, this
/// may affect many aspects of the emulation, like
/// CPU frequency, PPU frequency, Boot rome size, etc.
///
/// This is a clone of the configuration value
/// kept for performance reasons.
mode: GameBoyMode,
/// If the PPU is enabled, it will be clocked.
///
/// This is a clone of the configuration value
/// kept for performance reasons.
ppu_enabled: bool,
/// If the APU is enabled, it will be clocked.
///
/// This is a clone of the configuration value
/// kept for performance reasons.
apu_enabled: bool,
/// If the DMA is enabled, it will be clocked.
///
/// This is a clone of the configuration value
/// kept for performance reasons.
dma_enabled: bool,
/// If the timer is enabled, it will be clocked.
///
/// This is a clone of the configuration value
/// kept for performance reasons.
timer_enabled: bool,
/// If the serial is enabled, it will be clocked.
///
/// This is a clone of the configuration value
/// kept for performance reasons.
serial_enabled: bool,
/// The current frequency at which the Game Boy
/// emulator is being handled. This is a "hint" that
/// may help components to adjust their internal
/// logic to match the current frequency. For example
/// the APU will adjust its internal clock to match
/// this hint.
///
/// This is a clone of the configuration value
/// kept for performance reasons.
clock_freq: u32,
/// The boot ROM that will (or was) used to boot the
/// current Game Boy system.
///
/// This should be explicitly set by the developed when
/// set the boot ROM in the system's memory.
///
/// The loading process used to load the boot ROM is not
/// taken in consideration for this value.
boot_rom: BootRom,
/// Reference to the Game Boy CPU component to be
/// used as the main element of the system, when
/// clocked, the amount of ticks from it will be
/// used as reference or the rest of the components.
cpu: Cpu,
/// The reference counted and mutable reference to
/// Game Boy configuration structure that can be
/// used by the GB components to access global
/// configuration values on the current emulator.
///
/// If performance is required (may value access)
/// the values should be cloned and stored locally.
gbc: SharedThread<GameBoyConfig>,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl GameBoy {
#[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
pub fn new(mode: Option<GameBoyMode>) -> Self {
let mode = mode.unwrap_or(GameBoyMode::Dmg);
let gbc = Arc::new(Mutex::new(GameBoyConfig {
mode,
ppu_enabled: true,
apu_enabled: true,
dma_enabled: true,
timer_enabled: true,
serial_enabled: true,
clock_freq: GameBoy::CPU_FREQ,
}));
let components = Components {
ppu: Ppu::new(mode, gbc.clone()),
apu: Apu::default(),
dma: Dma::default(),
pad: Pad::default(),
timer: Timer::default(),
serial: Serial::default(),
};
let mmu = Mmu::new(components, mode, gbc.clone());
let cpu = Cpu::new(mmu, gbc.clone());
Self {
mode,
boot_rom: BootRom::None,
ppu_enabled: true,
apu_enabled: true,
dma_enabled: true,
timer_enabled: true,
serial_enabled: true,
clock_freq: GameBoy::CPU_FREQ,
cpu,
gbc,
}
}
pub fn verify_rom(data: &[u8]) -> bool {
Cartridge::from_data(data).is_ok()
}
pub fn reset(&mut self) {
self.ppu().reset();
self.apu().reset();
self.timer().reset();
self.serial().reset();
self.mmu().reset();
self.cpu.reset();
self.reset_cheats();
}
pub fn reload(&mut self) {
let rom = self.rom().clone();
self.reset();
self.load(true).unwrap();
self.load_cartridge(rom).unwrap();
}
/// Advance the clock of the system by one tick, this will
/// usually imply executing one CPU instruction and advancing
/// all the other components of the system by the required
/// amount of cycles.
///
/// This method takes into account the current speed of the
/// system (single or double) and will execute the required
/// amount of cycles in the other components of the system
/// accordingly.
///
/// The amount of cycles executed by the CPU is returned.
pub fn clock(&mut self) -> u16 {
let cycles = self.cpu_clock() as u16;
let cycles_n = cycles / self.multiplier() as u16;
self.clock_devices(cycles, cycles_n);
cycles
}
/// Risky function that will clock the CPU multiple times
/// allowing an undefined number of cycles to be executed
/// in the other Game Boy components.
///
/// This can cause unwanted behaviour in components like
/// the PPU where only one mode switch operation is expected
/// per each clock call.
///
/// At the end of this execution major synchronization issues
/// may arise, so use with caution.
pub fn clock_many(&mut self, count: usize) -> u16 {
let mut cycles = 0u16;
for _ in 0..count {
cycles += self.cpu_clock() as u16;
}
let cycles_n = cycles / self.multiplier() as u16;
self.clock_devices(cycles, cycles_n);
cycles
}
/// Function equivalent to `clock()` but that allows pre-emptive
/// breaking of the clock cycle loop if the PC (Program Counter)
/// reaches the provided address.
pub fn clock_step(&mut self, addr: u16) -> u16 {
let cycles = self.cpu_clock() as u16;
if self.cpu_i().pc() == addr {
return cycles;
}
let cycles_n = cycles / self.multiplier() as u16;
self.clock_devices(cycles, cycles_n);
cycles
}
/// Equivalent to `clock()` but allows the execution of multiple
/// clock operations in a single call.
pub fn clocks(&mut self, count: usize) -> u64 {
let mut cycles = 0_u64;
for _ in 0..count {
cycles += self.clock() as u64;
}
cycles
}
/// Clocks the emulator until the limit of cycles that has been
/// provided and returns the amount of cycles that have been
/// clocked.
pub fn clocks_cycles(&mut self, limit: usize) -> u64 {
let mut cycles = 0_u64;
while cycles < limit as u64 {
cycles += self.clock() as u64;
}
cycles
}
/// Clocks the emulator until the limit of cycles that has been
/// provided and returns the amount of cycles that have been
/// clocked together with the frame buffer of the PPU.
///
/// Allows a caller to clock the emulator and at the same time
/// retrieve the frame buffer of the PPU at the proper timing
/// (on V-Blank).
///
/// This method allows for complex foreign call optimizations
/// by preventing the need to call the emulator clock multiple
/// times to obtain the right frame buffer retrieval timing.
pub fn clocks_frame_buffer(&mut self, limit: usize) -> ClockFrame {
let mut cycles = 0_u64;
let mut frames = 0_u16;
let mut frame_buffer: Option<Vec<u8>> = None;
let mut last_frame = self.ppu_frame();
while cycles < limit as u64 {
cycles += self.clock() as u64;
if self.ppu_frame() != last_frame {
frame_buffer = Some(self.frame_buffer().to_vec());
last_frame = self.ppu_frame();
frames += 1;
}
}
ClockFrame {
cycles,
frames,
frame_buffer,
}
}
pub fn next_frame(&mut self) -> u32 {
let mut cycles = 0u32;
let current_frame = self.ppu_frame();
while self.ppu_frame() == current_frame {
cycles += self.clock() as u32;
}
cycles
}
pub fn step_to(&mut self, addr: u16) -> u32 {
let mut cycles = 0u32;
while self.cpu_i().pc() != addr {
cycles += self.clock_step(addr) as u32;
}
cycles
}
#[inline(always)]
fn clock_devices(&mut self, cycles: u16, cycles_n: u16) {
if self.ppu_enabled {
self.ppu_clock(cycles_n);
}
if self.apu_enabled {
self.apu_clock(cycles_n);
}
if self.dma_enabled {
self.dma_clock(cycles);
}
if self.timer_enabled {
self.timer_clock(cycles);
}
if self.serial_enabled {
self.serial_clock(cycles);
}
}
pub fn key_press(&mut self, key: PadKey) {
self.pad().key_press(key);
}
pub fn key_lift(&mut self, key: PadKey) {
self.pad().key_lift(key);
}
pub fn cpu_clock(&mut self) -> u8 {
self.cpu.clock()
}
pub fn ppu_clock(&mut self, cycles: u16) {
self.ppu().clock(cycles)
}
pub fn apu_clock(&mut self, cycles: u16) {
self.apu().clock(cycles)
}
pub fn dma_clock(&mut self, cycles: u16) {
self.mmu().clock_dma(cycles);
}
pub fn timer_clock(&mut self, cycles: u16) {
self.timer().clock(cycles)
}
pub fn serial_clock(&mut self, cycles: u16) {
self.serial().clock(cycles)
}
pub fn ppu_ly(&mut self) -> u8 {
self.ppu().ly()
}
pub fn ppu_mode(&mut self) -> PpuMode {
self.ppu().mode()
}
pub fn ppu_frame(&mut self) -> u16 {
self.ppu().frame_index()
}
/// Direct boot method that immediately jumps the machine
/// to the post boot state, this will effectively skip the
/// boot sequence and jump to the cartridge execution.
pub fn boot(&mut self) {
self.load_boot_state();
}
/// Unsafe load strategy that will panic the current system
/// in case there are boot ROM loading issues.
pub fn load_unsafe(&mut self, boot: bool) {
self.load(boot).unwrap();
}
/// Loads the machine directly to after the boot execution state,
/// setting the state of the system accordingly and updating the
/// Program Counter (PC) to the post boot address (0x0100).
///
/// Should allow the machine to jump to the cartridge (ROM) execution
/// directly, skipping the boot sequence.
///
/// Currently supports only DMG machines.
pub fn load_boot_state(&mut self) {
self.cpu.boot();
}
pub fn vram_eager(&mut self) -> Vec<u8> {
self.ppu().vram().to_vec()
}
pub fn hram_eager(&mut self) -> Vec<u8> {
self.ppu().vram().to_vec()
}
pub fn frame_buffer_eager(&mut self) -> Vec<u8> {
self.frame_buffer().to_vec()
}
pub fn frame_buffer_raw_eager(&mut self) -> Vec<u8> {
self.frame_buffer_raw().to_vec()
}
pub fn audio_buffer_eager(&mut self, clear: bool) -> Vec<u8> {
let buffer = Vec::from(self.audio_buffer().clone());
if clear {
self.clear_audio_buffer();
}
buffer
}
pub fn audio_output(&self) -> u8 {
self.apu_i().output()
}
pub fn audio_all_output(&self) -> Vec<u8> {
vec![
self.audio_output(),
self.audio_ch1_output(),
self.audio_ch2_output(),
self.audio_ch3_output(),
self.audio_ch4_output(),
]
}
pub fn audio_ch1_output(&self) -> u8 {
self.apu_i().ch1_output()
}
pub fn audio_ch2_output(&self) -> u8 {
self.apu_i().ch2_output()
}
pub fn audio_ch3_output(&self) -> u8 {
self.apu_i().ch3_output()
}
pub fn audio_ch4_output(&self) -> u8 {
self.apu_i().ch4_output()
}
pub fn audio_ch1_enabled(&self) -> bool {
self.apu_i().ch2_out_enabled()
}
pub fn set_audio_ch1_enabled(&mut self, enabled: bool) {
self.apu().set_ch1_out_enabled(enabled)
}
pub fn audio_ch2_enabled(&self) -> bool {
self.apu_i().ch2_out_enabled()
}
pub fn set_audio_ch2_enabled(&mut self, enabled: bool) {
self.apu().set_ch2_out_enabled(enabled)
}
pub fn audio_ch3_enabled(&self) -> bool {
self.apu_i().ch3_out_enabled()
}
pub fn set_audio_ch3_enabled(&mut self, enabled: bool) {
self.apu().set_ch3_out_enabled(enabled)
}
pub fn audio_ch4_enabled(&self) -> bool {
self.apu_i().ch4_out_enabled()
}
pub fn set_audio_ch4_enabled(&mut self, enabled: bool) {
self.apu().set_ch4_out_enabled(enabled)
}
pub fn audio_sampling_rate(&self) -> u16 {
self.apu_i().sampling_rate()
}
pub fn audio_channels(&self) -> u8 {
self.apu_i().channels()
}
pub fn cartridge_eager(&mut self) -> Cartridge {
self.mmu().rom().clone()
}
pub fn ram_data_eager(&mut self) -> Vec<u8> {
self.mmu().rom().ram_data_eager()
}
pub fn set_ram_data(&mut self, ram_data: Vec<u8>) {
self.mmu().rom().set_ram_data(&ram_data)
}
pub fn registers(&mut self) -> Registers {
let ppu_registers = self.ppu().registers();
Registers {
pc: self.cpu.pc,
sp: self.cpu.sp,
a: self.cpu.a,
b: self.cpu.b,
c: self.cpu.c,
d: self.cpu.d,
e: self.cpu.e,
h: self.cpu.h,
l: self.cpu.l,
scy: ppu_registers.scy,
scx: ppu_registers.scx,
wy: ppu_registers.wy,
wx: ppu_registers.wx,
ly: ppu_registers.ly,
lyc: ppu_registers.lyc,
}
}
/// Obtains the tile structure for the tile at the
/// given index, no conversion in the pixel buffer
/// is done so that the color reference is the GB one.
pub fn get_tile(&mut self, index: usize) -> Tile {
self.ppu().tiles()[index]
}
/// Obtains the pixel buffer for the tile at the
/// provided index, converting the color buffer
/// using the currently loaded (background) palette.
pub fn get_tile_buffer(&mut self, index: usize) -> Vec<u8> {
let tile = self.get_tile(index);
tile.palette_buffer(self.ppu().palette_bg())
}
pub fn is_dmg(&self) -> bool {
self.mode == GameBoyMode::Dmg
}
pub fn is_cgb(&self) -> bool {
self.mode == GameBoyMode::Cgb
}
pub fn is_sgb(&self) -> bool {
self.mode == GameBoyMode::Sgb
}
pub fn speed(&self) -> GameBoySpeed {
self.mmu_i().speed()
}
pub fn multiplier(&self) -> u8 {
self.mmu_i().speed().multiplier()
}
pub fn mode(&self) -> GameBoyMode {
self.mode
}
pub fn set_mode(&mut self, value: GameBoyMode) {
self.mode = value;
(*self.gbc).lock().unwrap().set_mode(value);
self.mmu().set_mode(value);
self.ppu().set_gb_mode(value);
}
pub fn ppu_enabled(&self) -> bool {
self.ppu_enabled
}
pub fn set_ppu_enabled(&mut self, value: bool) {
self.ppu_enabled = value;
(*self.gbc).lock().unwrap().set_ppu_enabled(value);
}
pub fn apu_enabled(&self) -> bool {
self.apu_enabled
}
pub fn set_apu_enabled(&mut self, value: bool) {
self.apu_enabled = value;
(*self.gbc).lock().unwrap().set_apu_enabled(value);
}
pub fn dma_enabled(&self) -> bool {
self.dma_enabled
}
pub fn set_dma_enabled(&mut self, value: bool) {
self.dma_enabled = value;
(*self.gbc).lock().unwrap().set_dma_enabled(value);
}
pub fn timer_enabled(&self) -> bool {
self.timer_enabled
}
pub fn set_timer_enabled(&mut self, value: bool) {
self.timer_enabled = value;
(*self.gbc).lock().unwrap().set_timer_enabled(value);
}
pub fn serial_enabled(&self) -> bool {
self.serial_enabled
}
pub fn set_serial_enabled(&mut self, value: bool) {
self.serial_enabled = value;
(*self.gbc).lock().unwrap().set_serial_enabled(value);
}
pub fn set_all_enabled(&mut self, value: bool) {
self.set_ppu_enabled(value);
self.set_apu_enabled(value);
self.set_dma_enabled(value);
self.set_timer_enabled(value);
self.set_serial_enabled(value);
}
pub fn clock_freq(&self) -> u32 {
self.clock_freq
}
pub fn set_clock_freq(&mut self, value: u32) {
self.clock_freq = value;
(*self.gbc).lock().unwrap().set_clock_freq(value);
self.apu().set_clock_freq(value);
}
pub fn clock_freq_s(&self) -> String {
format!("{:.02} Mhz", self.clock_freq() as f32 / 1000.0 / 1000.0)
}
pub fn boot_rom(&self) -> BootRom {
self.boot_rom
}
pub fn set_boot_rom(&mut self, value: BootRom) {
self.boot_rom = value;
}
pub fn boot_rom_s(&self) -> String {
String::from(self.boot_rom().description())
}
pub fn attach_null_serial(&mut self) {
self.attach_serial(Box::<NullDevice>::default());
}
pub fn attach_stdout_serial(&mut self) {
self.attach_serial(Box::<StdoutDevice>::default());
}
pub fn attach_printer_serial(&mut self) {
self.attach_serial(Box::<PrinterDevice>::default());
}
pub fn display_width(&self) -> usize {
DISPLAY_WIDTH
}
pub fn display_height(&self) -> usize {
DISPLAY_HEIGHT
}
pub fn ram_size(&self) -> RamSize {
match self.mode {
GameBoyMode::Dmg => RamSize::Size8K,
GameBoyMode::Cgb => RamSize::Size32K,
GameBoyMode::Sgb => RamSize::Size8K,
}
}
pub fn vram_size(&self) -> RamSize {
match self.mode {
GameBoyMode::Dmg => RamSize::Size8K,
GameBoyMode::Cgb => RamSize::Size16K,
GameBoyMode::Sgb => RamSize::Size8K,
}
}
pub fn description(&self, column_length: usize) -> String {
let version_l = format!("{:width$}", "Version", width = column_length);
let mode_l = format!("{:width$}", "Mode", width = column_length);
let boot_rom_l = format!("{:width$}", "Boot ROM", width = column_length);
let clock_l = format!("{:width$}", "Clock", width = column_length);
let ram_size_l = format!("{:width$}", "RAM Size", width = column_length);
let vram_size_l = format!("{:width$}", "VRAM Size", width = column_length);
let serial_l = format!("{:width$}", "Serial", width = column_length);
format!(
"{} {}\n{} {}\n{} {}\n{} {}\n{} {}\n{} {}\n{} {}",
version_l,
Info::version(),
mode_l,
self.mode(),
boot_rom_l,
self.boot_rom(),
clock_l,
self.clock_freq_s(),
ram_size_l,
self.ram_size(),
vram_size_l,
self.vram_size(),
serial_l,
self.serial_i().device().description(),
)
}
}
/// Gameboy implementations that are meant with performance
/// in mind and that do not support WASM interface of copy.
impl GameBoy {
/// The logic frequency of the Game Boy
/// CPU in hz.
pub const CPU_FREQ: u32 = 4194304;
/// The visual frequency (refresh rate)
/// of the Game Boy, close to 60 hz.
pub const VISUAL_FREQ: f32 = 59.7275;
/// The cycles taken to run a complete frame
/// loop in the Game Boy's PPU (in CPU cycles).
pub const LCD_CYCLES: u32 = 70224;
pub fn cpu(&mut self) -> &mut Cpu {
&mut self.cpu
}
pub fn cpu_i(&self) -> &Cpu {
&self.cpu
}
pub fn mmu(&mut self) -> &mut Mmu {
self.cpu.mmu()
}
pub fn mmu_i(&self) -> &Mmu {
self.cpu.mmu_i()
}
pub fn ppu(&mut self) -> &mut Ppu {
self.cpu.ppu()
}
pub fn ppu_i(&self) -> &Ppu {
self.cpu.ppu_i()
}
pub fn apu(&mut self) -> &mut Apu {
self.cpu.apu()
}
pub fn apu_i(&self) -> &Apu {
self.cpu.apu_i()
}
pub fn dma(&mut self) -> &mut Dma {
self.cpu.dma()
}
pub fn dma_i(&self) -> &Dma {
self.cpu.dma_i()
}
pub fn pad(&mut self) -> &mut Pad {
self.cpu.pad()
}
pub fn pad_i(&self) -> &Pad {
self.cpu.pad_i()
}
pub fn timer(&mut self) -> &mut Timer {
self.cpu.timer()
}
pub fn timer_i(&self) -> &Timer {
self.cpu.timer_i()
}
pub fn serial(&mut self) -> &mut Serial {
self.cpu.serial()
}
pub fn serial_i(&self) -> &Serial {
self.cpu.serial_i()
}
pub fn rom(&mut self) -> &mut Cartridge {
self.mmu().rom()
}
pub fn rom_i(&self) -> &Cartridge {
self.mmu_i().rom_i()
}
pub fn frame_buffer(&mut self) -> &[u8; FRAME_BUFFER_SIZE] {
self.ppu().frame_buffer()
}
pub fn frame_buffer_xrgb8888(&mut self) -> [u8; FRAME_BUFFER_XRGB8888_SIZE] {
self.ppu().frame_buffer_xrgb8888()
}
pub fn frame_buffer_xrgb8888_u32(&mut self) -> [u32; FRAME_BUFFER_SIZE] {
self.ppu().frame_buffer_xrgb8888_u32()
}
pub fn frame_buffer_rgb1555(&mut self) -> [u8; FRAME_BUFFER_RGB1555_SIZE] {
self.ppu().frame_buffer_rgb1555()
}
pub fn frame_buffer_rgb1555_u16(&mut self) -> [u16; FRAME_BUFFER_SIZE] {
self.ppu().frame_buffer_rgb1555_u16()
}
pub fn frame_buffer_rgb565(&mut self) -> [u8; FRAME_BUFFER_RGB565_SIZE] {
self.ppu().frame_buffer_rgb565()
}
pub fn frame_buffer_rgb565_u16(&mut self) -> [u16; FRAME_BUFFER_SIZE] {
self.ppu().frame_buffer_rgb565_u16()
}
pub fn frame_buffer_raw(&mut self) -> [u8; FRAME_BUFFER_SIZE] {
self.ppu().frame_buffer_raw()
}
pub fn audio_buffer(&mut self) -> &VecDeque<u8> {
self.apu().audio_buffer()
}
pub fn cartridge(&mut self) -> &mut Cartridge {
self.mmu().rom()
}
pub fn cartridge_i(&self) -> &Cartridge {
self.mmu_i().rom_i()
}
pub fn load(&mut self, boot: bool) -> Result<(), Error> {
let boot_rom = self.boot_rom().reusable(self.mode());
match self.mode() {
GameBoyMode::Dmg => self.load_dmg(boot, boot_rom)?,
GameBoyMode::Cgb => self.load_cgb(boot, boot_rom)?,
GameBoyMode::Sgb => unimplemented!("SGB is not supported"),
}
Ok(())
}
pub fn load_dmg(&mut self, boot: bool, boot_rom: Option<BootRom>) -> Result<(), Error> {
self.mmu().allocate_dmg();
if boot {
self.load_boot_dmg(boot_rom)?;
}
Ok(())
}
pub fn load_cgb(&mut self, boot: bool, boot_rom: Option<BootRom>) -> Result<(), Error> {
self.mmu().allocate_cgb();
if boot {
self.load_boot_cgb(boot_rom)?;
}
Ok(())
}
pub fn load_boot(&mut self, data: &[u8]) {
self.cpu.mmu().write_boot(0x0000, data);
}
pub fn load_boot_path(&mut self, path: &str) -> Result<(), Error> {
let data = read_file(path)?;
self.load_boot(&data);
self.boot_rom = BootRom::Other;
Ok(())
}
pub fn load_boot_static(&mut self, boot_rom: BootRom) {
match boot_rom {
BootRom::Dmg => self.load_boot(&DMG_BOOT),
BootRom::Sgb => self.load_boot(&SGB_BOOT),
BootRom::DmgBootix => self.load_boot(&DMG_BOOTIX),
BootRom::MgbBootix => self.load_boot(&MGB_BOOTIX),
BootRom::Cgb => self.load_boot(&CGB_BOOT),
BootRom::CgbBoytacean => self.load_boot(&CGB_BOYTACEAN),
BootRom::Other | BootRom::None => (),
}
self.boot_rom = boot_rom;
}
pub fn load_boot_file(&mut self, boot_rom: BootRom) -> Result<(), Error> {
match boot_rom {
BootRom::Dmg => self.load_boot_path("./res/boot/dmg_boot.bin")?,
BootRom::Sgb => self.load_boot_path("./res/boot/sgb_boot.bin")?,
BootRom::DmgBootix => self.load_boot_path("./res/boot/dmg_bootix.bin")?,
BootRom::MgbBootix => self.load_boot_path("./res/boot/mgb_bootix.bin")?,
BootRom::Cgb => self.load_boot_path("./res/boot/cgb_boot.bin")?,
BootRom::CgbBoytacean => self.load_boot_path("./res/boot/cgb_boytacean.bin")?,
BootRom::Other | BootRom::None => (),
}
self.boot_rom = boot_rom;
Ok(())
}
pub fn load_boot_default(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
self.load_boot_dmg(boot_rom)
}
pub fn load_boot_smart(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
match self.mode() {
GameBoyMode::Dmg => self.load_boot_dmg(boot_rom)?,
GameBoyMode::Cgb => self.load_boot_cgb(boot_rom)?,
GameBoyMode::Sgb => unimplemented!("SGB is not supported"),
}
Ok(())
}
pub fn load_boot_dmg(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
let boot_rom = boot_rom.unwrap_or(BootRom::DmgBootix);
if !boot_rom.is_dmg_compat() {
return Err(Error::IncompatibleBootRom);
}
self.load_boot_static(boot_rom);
Ok(())
}
pub fn load_boot_cgb(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
let boot_rom = boot_rom.unwrap_or(BootRom::CgbBoytacean);
if !boot_rom.is_cgb_compat() {
return Err(Error::IncompatibleBootRom);
}
self.load_boot_static(boot_rom);
Ok(())
}
pub fn load_boot_default_f(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
self.load_boot_dmg_f(boot_rom)?;
Ok(())
}
pub fn load_boot_smart_f(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
match self.mode() {
GameBoyMode::Dmg => self.load_boot_dmg_f(boot_rom)?,
GameBoyMode::Cgb => self.load_boot_cgb_f(boot_rom)?,
GameBoyMode::Sgb => unimplemented!("SGB is not supported"),
}
Ok(())
}
pub fn load_boot_dmg_f(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
let boot_rom = boot_rom.unwrap_or(BootRom::DmgBootix);
if !boot_rom.is_dmg_compat() {
return Err(Error::IncompatibleBootRom);
}
self.load_boot_file(boot_rom)?;
Ok(())
}
pub fn load_boot_cgb_f(&mut self, boot_rom: Option<BootRom>) -> Result<(), Error> {
let boot_rom = boot_rom.unwrap_or(BootRom::Cgb);
if !boot_rom.is_cgb_compat() {
return Err(Error::IncompatibleBootRom);
}
self.load_boot_file(boot_rom)?;
Ok(())
}
pub fn load_cartridge(&mut self, rom: Cartridge) -> Result<&mut Cartridge, Error> {
self.mmu().set_rom(rom);
Ok(self.mmu().rom())
}
pub fn load_rom(
&mut self,
data: &[u8],
ram_data: Option<&[u8]>,
) -> Result<&mut Cartridge, Error> {
let mut rom = Cartridge::from_data(data)?;
if let Some(ram_data) = ram_data {
rom.set_ram_data(ram_data)
}
self.load_cartridge(rom)
}
pub fn load_rom_file(
&mut self,
path: &str,
ram_path: Option<&str>,
) -> Result<&mut Cartridge, Error> {
let data = read_file(path)?;
match ram_path {
Some(ram_path) => {
let ram_data = read_file(ram_path)?;
self.load_rom(&data, Some(&ram_data))
}
None => self.load_rom(&data, None),
}
}
pub fn attach_serial(&mut self, device: Box<dyn SerialDevice>) {
self.serial().set_device(device);
}
pub fn read_memory(&mut self, addr: u16) -> u8 {
self.mmu().read(addr)
}
pub fn write_memory(&mut self, addr: u16, value: u8) {
self.mmu().write(addr, value);
}
pub fn set_speed_callback(&mut self, callback: fn(speed: GameBoySpeed)) {
self.mmu().set_speed_callback(callback);
}
pub fn reset_cheats(&mut self) {
self.reset_game_genie();
self.reset_game_shark();
}
pub fn add_cheat_code(&mut self, code: &str) -> Result<bool, Error> {
if GameGenie::is_code(code) {
return self.add_game_genie_code(code).map(|_| true);
}
if GameShark::is_code(code) {
return self.add_game_shark_code(code).map(|_| true);
}
Err(Error::CustomError(String::from("Not a valid cheat code")))
}
pub fn add_game_genie_code(&mut self, code: &str) -> Result<&GameGenieCode, Error> {
let rom = self.mmu().rom();
if rom.game_genie().is_none() {
let game_genie = GameGenie::default();
rom.attach_genie(game_genie);
}
let game_genie = rom.game_genie_mut().as_mut().unwrap();
game_genie.add_code(code)
}
pub fn add_game_shark_code(&mut self, code: &str) -> Result<&GameSharkCode, Error> {
let rom = self.rom();
if rom.game_shark().is_none() {
let game_shark = GameShark::default();
rom.attach_shark(game_shark);
}
let game_shark = rom.game_shark_mut().as_mut().unwrap();
game_shark.add_code(code)
}
pub fn reset_game_genie(&mut self) {
let rom = self.rom();
if rom.game_genie().is_some() {
rom.game_genie_mut().as_mut().unwrap().reset();
}
}
pub fn reset_game_shark(&mut self) {
let rom = self.mmu().rom();
if rom.game_shark().is_some() {
rom.game_shark_mut().as_mut().unwrap().reset();
}
}
}
#[cfg(feature = "wasm")]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl GameBoy {
pub fn set_panic_hook_wa() {
let prev = take_hook();
set_hook(Box::new(move |info| {
hook_impl(info);
prev(info);
}));
}
pub fn load_rom_wa(&mut self, data: &[u8]) -> Result<Cartridge, String> {
let rom = self.load_rom(data, None).map_err(|e| e.to_string())?;
rom.set_rumble_cb(|active| {
rumble_callback(active);
});
Ok(rom.clone())
}
pub fn load_callbacks_wa(&mut self) {
self.set_speed_callback(|speed| {
speed_callback(speed);
});
}
pub fn load_null_wa(&mut self) {
let null = Box::<NullDevice>::default();
self.attach_serial(null);
}
pub fn load_logger_wa(&mut self) {
let mut logger = Box::<StdoutDevice>::default();
logger.set_callback(|data| {
logger_callback(data.to_vec());
});
self.attach_serial(logger);
}
pub fn load_printer_wa(&mut self) {
let mut printer = Box::<PrinterDevice>::default();
printer.set_callback(|image_buffer| {
printer_callback(image_buffer.to_vec());
});
self.attach_serial(printer);
}
/// Updates the emulation mode using the cartridge info
/// for the provided data to obtain the CGB flag value
/// and set the mode accordingly.
///
/// This can be an expensive operation as it will require
/// cartridge data parsing to obtain the CGB flag.
/// It will also have to clone the data buffer.
pub fn infer_mode_wa(&mut self, data: &[u8]) -> Result<(), String> {
let mode = Cartridge::from_data(data)
.map_err(|e| e.to_string())?
.gb_mode();
self.set_mode(mode);
Ok(())
}
pub fn set_palette_colors_wa(&mut self, value: Vec<JsValue>) {
let palette: Palette = value
.into_iter()
.map(|v| Self::js_to_pixel(&v))
.collect::<Vec<Pixel>>()
.try_into()
.unwrap();
self.ppu().set_palette_colors(&palette);
}
pub fn wasm_engine_wa(&self) -> Option<String> {
let dependencies = dependencies_map();
if !dependencies.contains_key("wasm-bindgen") {
return None;
}
Some(String::from(format!(
"wasm-bindgen/{}",
*dependencies.get("wasm-bindgen").unwrap()
)))
}
fn js_to_pixel(value: &JsValue) -> Pixel {
value
.as_string()
.unwrap()
.chars()
.collect::<Vec<char>>()
.chunks(2)
.map(|s| s.iter().collect::<String>())
.map(|s| u8::from_str_radix(&s, 16).unwrap())
.collect::<Vec<u8>>()
.try_into()
.unwrap()
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn panic(message: &str);
#[wasm_bindgen(js_namespace = window, js_name = speedCallback)]
fn speed_callback(speed: GameBoySpeed);
#[wasm_bindgen(js_namespace = window, js_name = loggerCallback)]
fn logger_callback(data: Vec<u8>);
#[wasm_bindgen(js_namespace = window, js_name = printerCallback)]
fn printer_callback(image_buffer: Vec<u8>);
#[wasm_bindgen(js_namespace = window, js_name = rumbleCallback)]
fn rumble_callback(active: bool);
}
#[cfg(feature = "wasm")]
pub fn hook_impl(info: &PanicInfo) {
let message = info.to_string();
panic(message.as_str());
}
impl AudioProvider for GameBoy {
fn audio_output(&self) -> u8 {
self.apu_i().output()
}
fn audio_buffer(&self) -> &VecDeque<u8> {
self.apu_i().audio_buffer()
}
fn clear_audio_buffer(&mut self) {
self.apu().clear_audio_buffer()
}
}
impl Default for GameBoy {
fn default() -> Self {
Self::new(None)
}
}
impl Display for GameBoy {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description(9))
}
}