use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
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::space::{
AccessConstraints, AddressSpace, Mapping, MemAttrs, MemOps, MemResult, RamStore,
Region as MmioRegion, RegionRef, RomStore, RomWrite,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::Width;
pub const BANK_SIZE: u64 = 0x4000;
pub const FIXED_LEN: u64 = 0x0400;
pub const WINDOW_LEN: u64 = 0xc000;
pub const REGISTER_BASE: u64 = 0xfffc;
pub const DEFAULT_RAM: u64 = 0x8000;
pub const ROM_REGION: &str = "rom";
pub const REGISTER_REGION: &str = "regs";
const STATE_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Registers {
control: u8,
bank: [u8; 3],
}
impl Default for Registers {
fn default() -> Registers {
Registers {
control: 0,
bank: [0, 1, 2],
}
}
}
impl Registers {
fn ram_enabled(&self) -> bool {
self.control & 0x08 != 0
}
fn ram_bank(&self) -> u64 {
u64::from(self.control & 0x10 != 0)
}
}
struct Shared {
regs: Mutex<Registers>,
space: Mutex<Option<Arc<AddressSpace>>>,
rom: Arc<RomStore>,
ram: Arc<RamStore>,
banks: u64,
slot0: RegionRef,
slot1: RegionRef,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shared")
.field("regs", &self.regs)
.field("banks", &self.banks)
.finish_non_exhaustive()
}
}
impl Shared {
fn apply(&self, regs: Registers) {
let space = self.space.lock().clone();
let Some(space) = space else {
return;
};
let bank0 = u64::from(regs.bank[0]) % self.banks;
let bank1 = u64::from(regs.bank[1]) % self.banks;
let _ = space.rebase(&self.slot0, bank0 * BANK_SIZE + FIXED_LEN);
let _ = space.rebase(&self.slot1, bank1 * BANK_SIZE);
}
}
pub struct SegaMapper {
shared: Arc<Shared>,
rom_region: RegionRef,
regs_region: RegionRef,
}
impl fmt::Debug for SegaMapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SegaMapper")
.field("shared", &self.shared)
.finish_non_exhaustive()
}
}
impl SegaMapper {
pub fn new(image: &[u8], ram_len: u64) -> Result<SegaMapper> {
if image.is_empty() {
return Err(Error::Config {
at: String::from("rom"),
message: String::from("a Master System cartridge image cannot be empty"),
});
}
let padded = (image.len() as u64).next_multiple_of(BANK_SIZE);
let mut bytes = Vec::with_capacity(padded as usize);
bytes.extend_from_slice(image);
bytes.resize(padded as usize, 0xff);
let banks = padded / BANK_SIZE;
let rom = Arc::new(RomStore::new(bytes));
let ram = Arc::new(RamStore::new(ram_len.max(BANK_SIZE)));
let leaf: RegionRef = Arc::new(MmioRegion::rom(
"sms.cart.image",
Arc::clone(&rom),
RomWrite::Ignore,
));
let fixed: RegionRef = Arc::new(MmioRegion::alias(
"sms.cart.fixed",
Arc::clone(&leaf),
0,
FIXED_LEN,
)?);
let slot0: RegionRef = Arc::new(MmioRegion::alias(
"sms.cart.slot0",
Arc::clone(&leaf),
FIXED_LEN,
BANK_SIZE - FIXED_LEN,
)?);
let slot1: RegionRef = Arc::new(MmioRegion::alias(
"sms.cart.slot1",
Arc::clone(&leaf),
(1 % banks) * BANK_SIZE,
BANK_SIZE,
)?);
let shared = Arc::new(Shared {
regs: Mutex::with_rank(LockRank::DEVICE, Registers::default()),
space: Mutex::new(None),
rom,
ram,
banks,
slot0: Arc::clone(&slot0),
slot1: Arc::clone(&slot1),
});
let slot2: RegionRef = Arc::new(MmioRegion::io(
"sms.cart.slot2",
BANK_SIZE,
Arc::new(Slot2 {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
let rom_region: RegionRef = Arc::new(MmioRegion::container(
"sms.cart.rom",
WINDOW_LEN,
alloc::vec![
Mapping::new(fixed, 0),
Mapping::new(slot0, FIXED_LEN),
Mapping::new(slot1, BANK_SIZE),
Mapping::new(slot2, BANK_SIZE * 2),
],
));
let regs_region: RegionRef = Arc::new(MmioRegion::io(
"sms.cart.regs",
4,
Arc::new(MapperRegs {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
Ok(SegaMapper {
shared,
rom_region,
regs_region,
})
}
pub fn from_props(props: &Props) -> Result<SegaMapper> {
let mut r = props.reader();
let media = r.require_media("rom")?;
let ram = r.or_size("ram", DEFAULT_RAM)?;
r.finish()?;
SegaMapper::new(media.bytes(), ram)
}
#[must_use]
pub fn banks(&self) -> u64 {
self.shared.banks
}
#[must_use]
pub fn bank(&self, slot: usize) -> u8 {
self.shared.regs.lock().bank[slot % 3]
}
#[must_use]
pub fn control(&self) -> u8 {
self.shared.regs.lock().control
}
#[must_use]
pub fn ram_enabled(&self) -> bool {
self.shared.regs.lock().ram_enabled()
}
#[must_use]
pub fn ram(&self) -> &Arc<RamStore> {
&self.shared.ram
}
pub fn attach_space(&self, space: Arc<AddressSpace>) {
*self.shared.space.lock() = Some(space);
let regs = *self.shared.regs.lock();
self.shared.apply(regs);
}
pub fn write_register(&self, index: usize, value: u8) {
let regs = {
let mut regs = self.shared.regs.lock();
match index % 4 {
0 => regs.control = value,
n => regs.bank[n - 1] = value,
}
*regs
};
self.shared.apply(regs);
}
}
struct Slot2 {
shared: Arc<Shared>,
}
impl fmt::Debug for Slot2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Slot2").finish_non_exhaustive()
}
}
impl MemOps for Slot2 {
fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let regs = *self.shared.regs.lock();
if regs.ram_enabled() {
let base = regs.ram_bank() * BANK_SIZE + offset;
let len = self.shared.ram.len();
if len == 0 {
return Err(BusError::BadAccess);
}
for (i, byte) in dst.iter_mut().enumerate() {
*byte = self.shared.ram.read_u8((base + i as u64) % len)?;
}
return Ok(());
}
let bytes = self.shared.rom.as_bytes();
let base = (u64::from(regs.bank[2]) % self.shared.banks) * BANK_SIZE + offset;
for (i, byte) in dst.iter_mut().enumerate() {
*byte = bytes[((base + i as u64) as usize) % bytes.len()];
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], _attrs: MemAttrs) -> MemResult {
let regs = *self.shared.regs.lock();
let bank = regs.ram_bank();
if !regs.ram_enabled() {
return Ok(());
}
let len = self.shared.ram.len();
if len == 0 {
return Err(BusError::BadAccess);
}
let base = bank * BANK_SIZE + offset;
for (i, byte) in src.iter().enumerate() {
self.shared.ram.write_u8((base + i as u64) % len, *byte)?;
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::ANY
}
}
struct MapperRegs {
shared: Arc<Shared>,
}
impl fmt::Debug for MapperRegs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MapperRegs").finish_non_exhaustive()
}
}
impl MemOps for MapperRegs {
fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
let regs = self.shared.regs.lock();
*byte = match offset & 3 {
0 => regs.control,
n => regs.bank[(n - 1) as usize],
};
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Ok(());
}
let regs = {
let mut regs = self.shared.regs.lock();
match offset & 3 {
0 => regs.control = *value,
n => regs.bank[(n - 1) as usize] = *value,
}
*regs
};
self.shared.apply(regs);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "sms.mapper",
version: 1,
summary: "Sega's standard SMS mapper: three 16 KiB slots, a fixed first KiB, cartridge RAM",
properties: &[
PropertySpec {
name: "rom",
kind: ValueKind::Media,
required: true,
summary: "the cartridge image, as the name of a media slot (`rom = \"cart\"`)",
},
PropertySpec {
name: "ram",
kind: ValueKind::Size,
required: false,
summary: "cartridge RAM, up to two 16 KiB banks (default 32K)",
},
],
construct: |props| Ok(Box::new(SegaMapper::from_props(props)?) as Box<dyn Device>),
};
pub fn register(reg: &mut crate::core::Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for SegaMapper {
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)),
REGISTER_REGION => Some(Arc::clone(&self.regs_region)),
_ => None,
}
}
fn reset(&self, _kind: ResetKind) {
let regs = {
let mut regs = self.shared.regs.lock();
*regs = Registers::default();
*regs
};
self.shared.apply(regs);
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let regs = *self.shared.regs.lock();
w.write_u32(STATE_VERSION)?;
w.write_u8(regs.control)?;
for bank in regs.bank {
w.write_u8(bank)?;
}
let len = self.shared.ram.len();
let mut bytes = alloc::vec![0u8; len as usize];
self.shared.ram.read_at(0, &mut bytes).map_err(Error::Bus)?;
w.write_bytes(&bytes)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let version = r.read_u32()?;
if version != STATE_VERSION {
return Err(Error::State(alloc::format!(
"the mapper's snapshot is version {version}, this build writes {STATE_VERSION}"
)));
}
let control = r.read_u8()?;
let bank = [r.read_u8()?, r.read_u8()?, r.read_u8()?];
let bytes = r.read_bytes()?;
if bytes.len() as u64 != self.shared.ram.len() {
return Err(Error::State(String::from(
"the mapper's snapshot has a different amount of cartridge RAM",
)));
}
self.shared.ram.write_at(0, bytes).map_err(Error::Bus)?;
let regs = {
let mut regs = self.shared.regs.lock();
*regs = Registers { control, bank };
*regs
};
self.shared.apply(regs);
Ok(())
}
}
impl crate::machine::Instance for SegaMapper {
fn bind(&self, ctx: &crate::machine::BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| Error::Config {
at: String::from(ctx.path()),
message: String::from(
"the mapper needs the address space its windows are mapped into, because a \
bank switch slides them: add `space = mem` to the object",
),
})?;
self.attach_space(Arc::clone(space));
Ok(())
}
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| {
Ok(Arc::new(SegaMapper::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())
.prop(PropSchema::new("ram", ValueKind::Size))
.region(ROM_REGION)
.region(REGISTER_REGION)
}