use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{
AccessConstraints, MemAttrs, MemOps, MemResult, RamStore, Region as MmioRegion, RegionRef,
RomStore,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicBool, AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::Width;
pub const RTC_HZ: u64 = 32_768;
pub const ROM_WINDOW: u64 = 0x8000;
pub const ROM_BANK: u64 = 0x4000;
pub const RAM_BANK: u64 = 0x2000;
pub const ROM_REGION: &str = "rom";
pub const RAM_REGION: &str = "ram";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Mapper {
None,
Mbc1,
Mbc2,
Mbc3,
Mbc5,
}
impl Mapper {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Mapper::None => "ROM only",
Mapper::Mbc1 => "MBC1",
Mapper::Mbc2 => "MBC2",
Mapper::Mbc3 => "MBC3",
Mapper::Mbc5 => "MBC5",
}
}
}
impl fmt::Display for Mapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CartKind {
pub mapper: Mapper,
pub ram: bool,
pub battery: bool,
pub rtc: bool,
}
impl CartKind {
const fn new(mapper: Mapper, ram: bool, battery: bool, rtc: bool) -> CartKind {
CartKind {
mapper,
ram,
battery,
rtc,
}
}
#[must_use]
pub const fn from_byte(byte: u8) -> Option<CartKind> {
use Mapper::{Mbc1, Mbc2, Mbc3, Mbc5, None as Plain};
Some(match byte {
0x00 => CartKind::new(Plain, false, false, false),
0x01 => CartKind::new(Mbc1, false, false, false),
0x02 => CartKind::new(Mbc1, true, false, false),
0x03 => CartKind::new(Mbc1, true, true, false),
0x05 => CartKind::new(Mbc2, true, false, false),
0x06 => CartKind::new(Mbc2, true, true, false),
0x08 => CartKind::new(Plain, true, false, false),
0x09 => CartKind::new(Plain, true, true, false),
0x0f => CartKind::new(Mbc3, false, true, true),
0x10 => CartKind::new(Mbc3, true, true, true),
0x11 => CartKind::new(Mbc3, false, false, false),
0x12 => CartKind::new(Mbc3, true, false, false),
0x13 => CartKind::new(Mbc3, true, true, false),
0x19 => CartKind::new(Mbc5, false, false, false),
0x1a => CartKind::new(Mbc5, true, false, false),
0x1b => CartKind::new(Mbc5, true, true, false),
0x1c => CartKind::new(Mbc5, false, false, false),
0x1d => CartKind::new(Mbc5, true, false, false),
0x1e => CartKind::new(Mbc5, true, true, false),
_ => return None,
})
}
}
#[derive(Clone)]
pub struct Cartridge {
bytes: Arc<[u8]>,
title: String,
kind: CartKind,
rom_banks: u32,
ram_len: u64,
header_checksum_ok: bool,
}
impl fmt::Debug for Cartridge {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Cartridge")
.field("title", &self.title)
.field("kind", &self.kind)
.field("rom_banks", &self.rom_banks)
.field("ram_len", &self.ram_len)
.field("header_checksum_ok", &self.header_checksum_ok)
.finish()
}
}
const HEADER_END: usize = 0x0150;
impl Cartridge {
pub fn parse(bytes: impl Into<Arc<[u8]>>) -> Result<Cartridge> {
let bytes: Arc<[u8]> = bytes.into();
if bytes.len() < HEADER_END {
return Err(config(alloc::format!(
"a cartridge image is at least {HEADER_END} bytes; this one is {}",
bytes.len()
)));
}
let kind = CartKind::from_byte(bytes[0x0147]).ok_or_else(|| {
config(alloc::format!(
"cartridge type ${:02x} at $0147 is not an assigned value",
bytes[0x0147]
))
})?;
let size_byte = bytes[0x0148];
if size_byte > 8 {
return Err(config(alloc::format!(
"ROM size byte ${size_byte:02x} at $0148 is not an assigned value"
)));
}
let rom_banks = 2u32 << size_byte;
let declared = u64::from(rom_banks) * ROM_BANK;
if declared != bytes.len() as u64 {
return Err(config(alloc::format!(
"the header declares {declared} bytes of ROM ({rom_banks} banks) but the image \
is {} bytes",
bytes.len()
)));
}
let ram_len = match bytes[0x0149] {
0x00 | 0x01 => 0,
0x02 => RAM_BANK,
0x03 => 4 * RAM_BANK,
0x04 => 16 * RAM_BANK,
0x05 => 8 * RAM_BANK,
other => {
return Err(config(alloc::format!(
"RAM size byte ${other:02x} at $0149 is not an assigned value"
)));
}
};
let ram_len = if kind.mapper == Mapper::Mbc2 {
512
} else {
ram_len
};
let title = bytes[0x0134..0x0144]
.iter()
.take_while(|b| **b != 0)
.filter(|b| b.is_ascii_graphic() || **b == b' ')
.map(|b| *b as char)
.collect::<String>()
.trim_end()
.into();
let mut sum = 0u8;
for byte in &bytes[0x0134..0x014d] {
sum = sum.wrapping_sub(*byte).wrapping_sub(1);
}
let header_checksum_ok = sum == bytes[0x014d];
Ok(Cartridge {
bytes,
title,
kind,
rom_banks,
ram_len,
header_checksum_ok,
})
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub fn kind(&self) -> CartKind {
self.kind
}
#[must_use]
pub fn rom_banks(&self) -> u32 {
self.rom_banks
}
#[must_use]
pub fn ram_len(&self) -> u64 {
self.ram_len
}
#[must_use]
pub fn header_checksum_ok(&self) -> bool {
self.header_checksum_ok
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
}
fn config(message: impl Into<String>) -> Error {
Error::Config {
at: String::from("gb.cart"),
message: message.into(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rtc {
pub seconds: u8,
pub minutes: u8,
pub hours: u8,
pub day_low: u8,
pub day_high: u8,
}
impl Rtc {
#[must_use]
pub const fn halted(&self) -> bool {
self.day_high & 0x40 != 0
}
fn tick_second(&mut self) {
self.seconds += 1;
if self.seconds < 60 {
return;
}
self.seconds = 0;
self.minutes += 1;
if self.minutes < 60 {
return;
}
self.minutes = 0;
self.hours += 1;
if self.hours < 24 {
return;
}
self.hours = 0;
let day = (u16::from(self.day_high & 1) << 8) | u16::from(self.day_low);
let day = day.wrapping_add(1);
self.day_low = day as u8;
self.day_high = (self.day_high & 0xfe) | ((day >> 8) as u8 & 1);
if day > 0x1ff {
self.day_high |= 0x80;
self.day_low = 0;
self.day_high &= 0xfe;
}
}
fn read(&self, index: u8) -> u8 {
match index {
0x08 => self.seconds,
0x09 => self.minutes,
0x0a => self.hours,
0x0b => self.day_low,
_ => self.day_high | 0x3e,
}
}
fn write(&mut self, index: u8, value: u8) {
match index {
0x08 => self.seconds = value & 0x3f,
0x09 => self.minutes = value & 0x3f,
0x0a => self.hours = value & 0x1f,
0x0b => self.day_low = value,
_ => self.day_high = value & 0xc1,
}
}
}
struct Shared {
cart: Cartridge,
rom: Arc<RomStore>,
ram: Option<Arc<RamStore>>,
banks: Mutex<Banks>,
rom0_base: AtomicU64,
rom1_base: AtomicU64,
ram_base: AtomicU64,
ram_enabled: AtomicBool,
lazy: Mutex<Option<LazyHandle>>,
tick: AtomicU64,
}
#[derive(Debug, Clone, Copy, Default)]
struct Banks {
bank_low: u16,
bank_high: u8,
advanced: bool,
rtc_select: u8,
latch_state: u8,
rtc: Rtc,
rtc_latched: Rtc,
rtc_residual: u64,
}
impl Shared {
fn republish(&self, banks: &Banks) {
let rom_banks = u64::from(self.cart.rom_banks);
let mask = rom_banks - 1; let (rom0, rom1) = match self.cart.kind.mapper {
Mapper::None => (0, 1),
Mapper::Mbc1 => {
let low = if banks.bank_low & 0x1f == 0 {
1
} else {
banks.bank_low & 0x1f
};
let high = u64::from(banks.bank_high & 3) << 5;
let lower = if banks.advanced { high } else { 0 };
(lower & mask, (high | u64::from(low)) & mask)
}
Mapper::Mbc2 => {
let low = if banks.bank_low & 0x0f == 0 {
1
} else {
banks.bank_low & 0x0f
};
(0, u64::from(low) & mask)
}
Mapper::Mbc3 => {
let low = if banks.bank_low & 0x7f == 0 {
1
} else {
banks.bank_low & 0x7f
};
(0, u64::from(low) & mask)
}
Mapper::Mbc5 => {
let bank = (u64::from(banks.bank_high & 1) << 8) | u64::from(banks.bank_low & 0xff);
(0, bank & mask)
}
};
self.rom0_base.store(rom0 * ROM_BANK, Ordering::Relaxed);
self.rom1_base.store(rom1 * ROM_BANK, Ordering::Relaxed);
let ram_banks = match &self.ram {
Some(ram) => (ram.len() / RAM_BANK).max(1),
None => 1,
};
let ram_bank = match self.cart.kind.mapper {
Mapper::Mbc1 if !banks.advanced => 0,
Mapper::Mbc1 | Mapper::Mbc3 => u64::from(banks.bank_high & 3),
Mapper::Mbc5 => u64::from(banks.bank_high & 0x0f),
_ => 0,
};
self.ram_base
.store((ram_bank % ram_banks) * RAM_BANK, Ordering::Relaxed);
}
fn sync(&self, attrs: MemAttrs) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
let _ = handle.sync(kind);
}
fn advance_to(&self, tick: u64) {
let now = self.tick.load(Ordering::Relaxed);
if tick <= now {
return;
}
let elapsed = tick - now;
{
let mut banks = self.banks.lock();
if self.cart.kind.rtc && !banks.rtc.halted() {
let total = banks.rtc_residual + elapsed;
let seconds = total / RTC_HZ;
banks.rtc_residual = total % RTC_HZ;
for _ in 0..seconds.min(u64::from(u32::MAX)) {
banks.rtc.tick_second();
}
} else {
banks.rtc_residual = 0;
}
}
self.tick.store(tick, Ordering::Relaxed);
}
fn next_event_tick(&self) -> Option<u64> {
if !self.cart.kind.rtc {
return None;
}
let residual = self.banks.lock().rtc_residual;
Some(self.tick.load(Ordering::Relaxed) + (RTC_HZ - residual).max(1))
}
}
pub struct GbCart {
shared: Arc<Shared>,
rom_region: RegionRef,
ram_region: RegionRef,
}
impl fmt::Debug for GbCart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GbCart")
.field("cart", &self.shared.cart)
.finish_non_exhaustive()
}
}
impl GbCart {
#[must_use]
pub fn new(cart: Cartridge) -> GbCart {
let rom = Arc::new(RomStore::new(cart.bytes.to_vec()));
let ram = (cart.ram_len > 0).then(|| Arc::new(RamStore::new(cart.ram_len)));
let shared = Arc::new(Shared {
cart,
rom,
ram,
banks: Mutex::with_rank(LockRank::DEVICE, Banks::default()),
rom0_base: AtomicU64::new(0),
rom1_base: AtomicU64::new(ROM_BANK),
ram_base: AtomicU64::new(0),
ram_enabled: AtomicBool::new(false),
lazy: Mutex::new(None),
tick: AtomicU64::new(0),
});
shared.republish(&Banks::default());
let rom_region = Arc::new(MmioRegion::io(
"gb.cart.rom",
ROM_WINDOW,
Arc::new(RomWindow {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
let ram_region = Arc::new(MmioRegion::io(
"gb.cart.ram",
RAM_BANK,
Arc::new(RamWindow {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
GbCart {
shared,
rom_region,
ram_region,
}
}
pub fn from_props(props: &Props) -> Result<GbCart> {
let mut r = props.reader();
let media = r.require_media("rom")?;
r.finish()?;
Ok(GbCart::new(Cartridge::parse(media.to_bytes())?))
}
#[must_use]
pub fn cartridge(&self) -> &Cartridge {
&self.shared.cart
}
#[must_use]
pub fn rom_bank(&self) -> u64 {
self.shared.rom1_base.load(Ordering::Relaxed) / ROM_BANK
}
#[must_use]
pub fn rom_bank_low(&self) -> u64 {
self.shared.rom0_base.load(Ordering::Relaxed) / ROM_BANK
}
#[must_use]
pub fn ram_bank(&self) -> u64 {
self.shared.ram_base.load(Ordering::Relaxed) / RAM_BANK
}
#[must_use]
pub fn ram_enabled(&self) -> bool {
self.shared.ram_enabled.load(Ordering::Relaxed)
}
#[must_use]
pub fn rtc(&self) -> Option<Rtc> {
self.shared
.cart
.kind
.rtc
.then(|| self.shared.banks.lock().rtc)
}
#[must_use]
pub fn peek_ram(&self, offset: u64) -> Option<u8> {
self.shared.ram.as_ref()?.read_u8(offset).ok()
}
pub fn poke_ram(&self, offset: u64, value: u8) {
if let Some(ram) = &self.shared.ram {
let _ = ram.write_u8(offset, value);
}
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
}
struct RomWindow {
shared: Arc<Shared>,
}
impl fmt::Debug for RomWindow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RomWindow").finish_non_exhaustive()
}
}
impl MemOps for RomWindow {
fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
let addr = if offset < ROM_BANK {
self.shared.rom0_base.load(Ordering::Relaxed) + offset
} else {
self.shared.rom1_base.load(Ordering::Relaxed) + (offset - ROM_BANK)
};
*byte = self
.shared
.rom
.as_bytes()
.get(addr as usize)
.copied()
.unwrap_or(0xff);
Ok(())
}
fn write(&self, offset: u64, src: &[u8], _attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
let value = *value;
let mut banks = self.shared.banks.lock();
match self.shared.cart.kind.mapper {
Mapper::None => {}
Mapper::Mbc1 => match offset >> 13 {
0 => self
.shared
.ram_enabled
.store(value & 0x0f == 0x0a, Ordering::Relaxed),
1 => banks.bank_low = u16::from(value & 0x1f),
2 => banks.bank_high = value & 0x03,
_ => banks.advanced = value & 1 != 0,
},
Mapper::Mbc2 => {
if offset < 0x4000 {
if offset & 0x0100 == 0 {
self.shared
.ram_enabled
.store(value & 0x0f == 0x0a, Ordering::Relaxed);
} else {
banks.bank_low = u16::from(value & 0x0f);
}
}
}
Mapper::Mbc3 => match offset >> 13 {
0 => self
.shared
.ram_enabled
.store(value & 0x0f == 0x0a, Ordering::Relaxed),
1 => banks.bank_low = u16::from(value & 0x7f),
2 => {
if (0x08..=0x0c).contains(&value) {
banks.rtc_select = value;
} else {
banks.rtc_select = 0;
banks.bank_high = value & 0x03;
}
}
_ => {
if banks.latch_state == 0 && value == 1 {
banks.rtc_latched = banks.rtc;
}
banks.latch_state = value;
}
},
Mapper::Mbc5 => match offset >> 12 {
0 | 1 => self
.shared
.ram_enabled
.store(value & 0x0f == 0x0a, Ordering::Relaxed),
2 => banks.bank_low = (banks.bank_low & 0x100) | u16::from(value),
3 => banks.bank_low = (banks.bank_low & 0xff) | (u16::from(value & 1) << 8),
4 | 5 => banks.bank_high = value & 0x0f,
_ => {}
},
}
self.shared.republish(&banks);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
struct RamWindow {
shared: Arc<Shared>,
}
impl fmt::Debug for RamWindow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RamWindow").finish_non_exhaustive()
}
}
impl MemOps for RamWindow {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
if !self.shared.ram_enabled.load(Ordering::Relaxed) {
*byte = 0xff;
return Ok(());
}
let select = self.shared.banks.lock().rtc_select;
if select != 0 {
self.shared.sync(attrs);
*byte = self.shared.banks.lock().rtc_latched.read(select);
return Ok(());
}
let Some(ram) = &self.shared.ram else {
*byte = 0xff;
return Ok(());
};
let base = self.shared.ram_base.load(Ordering::Relaxed);
let addr = (base + offset) % ram.len().max(1);
*byte = ram.read_u8(addr).unwrap_or(0xff);
if self.shared.cart.kind.mapper == Mapper::Mbc2 {
*byte |= 0xf0;
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if !self.shared.ram_enabled.load(Ordering::Relaxed) {
return Ok(());
}
let select = self.shared.banks.lock().rtc_select;
if select != 0 {
self.shared.sync(attrs);
let mut banks = self.shared.banks.lock();
banks.rtc.write(select, *value);
banks.rtc_latched.write(select, *value);
banks.rtc_residual = 0;
return Ok(());
}
let Some(ram) = &self.shared.ram else {
return Ok(());
};
let base = self.shared.ram_base.load(Ordering::Relaxed);
let addr = (base + offset) % ram.len().max(1);
let _ = ram.write_u8(addr, *value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "gb.cart",
version: 1,
summary: "Game Boy cartridge: header, ROM-only, MBC1, MBC2, MBC3 (with RTC) and MBC5",
properties: &[PropertySpec {
name: "rom",
kind: ValueKind::Media,
required: true,
summary: "the cartridge image, as the name of a media slot (`rom = \"cart\"`)",
}],
construct: |props| Ok(Box::new(GbCart::from_props(props)?) as Box<dyn Device>),
};
pub fn register(reg: &mut crate::core::Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for GbCart {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
ROM_REGION => Some(Arc::clone(&self.rom_region)),
RAM_REGION => Some(Arc::clone(&self.ram_region)),
_ => None,
}
}
fn reset(&self, kind: ResetKind) {
let mut banks = self.shared.banks.lock();
let rtc = banks.rtc;
*banks = Banks::default();
if kind != ResetKind::Cold && self.shared.cart.kind.battery {
banks.rtc = rtc;
banks.rtc_latched = rtc;
}
self.shared.ram_enabled.store(false, Ordering::Relaxed);
self.shared.republish(&banks);
drop(banks);
if kind == ResetKind::Cold
&& let Some(ram) = &self.shared.ram
{
let _ = ram.fill(0, ram.len(), 0);
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let banks = *self.shared.banks.lock();
w.write_u16(banks.bank_low)?;
w.write_u8(banks.bank_high)?;
w.write_bool(banks.advanced)?;
w.write_u8(banks.rtc_select)?;
w.write_u8(banks.latch_state)?;
w.write_bool(self.shared.ram_enabled.load(Ordering::Relaxed))?;
w.write_u64(self.shared.tick.load(Ordering::Relaxed))?;
w.write_u64(banks.rtc_residual)?;
for rtc in [banks.rtc, banks.rtc_latched] {
w.write_u8(rtc.seconds)?;
w.write_u8(rtc.minutes)?;
w.write_u8(rtc.hours)?;
w.write_u8(rtc.day_low)?;
w.write_u8(rtc.day_high)?;
}
match &self.shared.ram {
Some(ram) => {
let mut bytes = vec![0u8; ram.len() as usize];
ram.read_at(0, &mut bytes)
.map_err(|e| Error::State(alloc::format!("cartridge RAM: {e}")))?;
w.write_bytes(&bytes)?;
}
None => w.write_bytes(&[])?,
}
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let mut banks = Banks {
bank_low: r.read_u16()?,
bank_high: r.read_u8()?,
advanced: r.read_bool()?,
rtc_select: r.read_u8()?,
latch_state: r.read_u8()?,
..Banks::default()
};
let ram_enabled = r.read_bool()?;
let tick = r.read_u64()?;
banks.rtc_residual = r.read_u64()?;
for slot in [false, true] {
let rtc = Rtc {
seconds: r.read_u8()?,
minutes: r.read_u8()?,
hours: r.read_u8()?,
day_low: r.read_u8()?,
day_high: r.read_u8()?,
};
if slot {
banks.rtc_latched = rtc;
} else {
banks.rtc = rtc;
}
}
let bytes = r.read_bytes()?;
if let Some(ram) = &self.shared.ram {
if bytes.len() as u64 != ram.len() {
return Err(Error::State(alloc::format!(
"snapshot holds {} bytes of cartridge RAM, this cartridge has {}",
bytes.len(),
ram.len()
)));
}
ram.write_at(0, bytes)
.map_err(|e| Error::State(alloc::format!("cartridge RAM: {e}")))?;
}
self.shared
.ram_enabled
.store(ram_enabled, Ordering::Relaxed);
self.shared.tick.store(tick, Ordering::Relaxed);
self.shared.republish(&banks);
*self.shared.banks.lock() = banks;
Ok(())
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.tick.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
self.shared.advance_to(tick);
}
fn next_event_tick(&self) -> Option<u64> {
self.shared.next_event_tick()
}
fn attach_lazy(&self, handle: LazyHandle) {
GbCart::attach_lazy(self, handle);
}
}
impl crate::machine::Instance for GbCart {}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| Ok(Arc::new(GbCart::from_props(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PropSchema};
ClassSchema::new(CLASS.name)
.prop(PropSchema::new("rom", ValueKind::Media).required())
.region(ROM_REGION)
.region(RAM_REGION)
}
#[must_use]
pub fn synthetic_image(banks: u32, kind_byte: u8, ram_byte: u8, program: &[u8]) -> Vec<u8> {
assert!(
(2..=512).contains(&banks) && banks.is_power_of_two(),
"a cartridge has a power-of-two number of banks, at least two"
);
let mut rom = vec![0u8; banks as usize * ROM_BANK as usize];
rom[0x0100] = 0x00;
rom[0x0101] = 0xc3;
rom[0x0102] = 0x50;
rom[0x0103] = 0x01;
rom[0x0150..0x0150 + program.len()].copy_from_slice(program);
rom[0x0147] = kind_byte;
rom[0x0148] = (banks.trailing_zeros() - 1) as u8;
rom[0x0149] = ram_byte;
let mut sum = 0u8;
for byte in &rom[0x0134..0x014d] {
sum = sum.wrapping_sub(*byte).wrapping_sub(1);
}
rom[0x014d] = sum;
rom
}