use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
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, MemAttrs, MemOps, MemResult, Region as MmioRegion, RegionRef,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::Width;
use crate::core::wire::{Level, WireSource};
pub const CONTROL_REGION: &str = "ctrl";
pub const PAD_REGION: &str = "pads";
pub const PAUSE_PIN: &str = "nmi";
pub const RESET_PIN: &str = "reset";
const STATE_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Button {
Up,
Down,
Left,
Right,
One,
Two,
}
impl Button {
pub const ALL: [Button; 6] = [
Button::Up,
Button::Down,
Button::Left,
Button::Right,
Button::One,
Button::Two,
];
#[must_use]
pub const fn bit(self) -> u8 {
match self {
Button::Up => 0,
Button::Down => 1,
Button::Left => 2,
Button::Right => 3,
Button::One => 4,
Button::Two => 5,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Button::Up => "up",
Button::Down => "down",
Button::Left => "left",
Button::Right => "right",
Button::One => "button1",
Button::Two => "button2",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Button> {
Button::ALL.into_iter().find(|b| b.name() == name)
}
}
impl fmt::Display for Button {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Nationalisation {
#[default]
Export,
Japan,
}
impl Nationalisation {
#[must_use]
pub fn from_name(name: &str) -> Option<Nationalisation> {
match name {
"export" => Some(Nationalisation::Export),
"japan" => Some(Nationalisation::Japan),
_ => None,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Nationalisation::Export => "export",
Nationalisation::Japan => "japan",
}
}
}
#[derive(Debug, Clone, Copy)]
struct State {
pads: [u8; 2],
pause: bool,
reset: bool,
memory_control: u8,
io_control: u8,
nation: Nationalisation,
}
impl State {
fn new(nation: Nationalisation) -> State {
State {
pads: [0; 2],
pause: false,
reset: false,
memory_control: 0,
io_control: 0xff,
nation,
}
}
fn io_enabled(&self) -> bool {
self.memory_control & 0x04 == 0
}
fn th_level(&self, port: usize) -> bool {
let direction = 1u8 << (1 + port * 2);
let level = 1u8 << (5 + port * 2);
if self.io_control & direction == 0 {
self.io_control & level != 0
} else {
true
}
}
fn read_dc(&self) -> u8 {
if !self.io_enabled() {
return 0xff;
}
let a = self.pads[0] & 0x3f;
let b = self.pads[1] & 0x03;
!(a | (b << 6))
}
fn read_dd(&self) -> u8 {
if !self.io_enabled() {
return 0xff;
}
let b = (self.pads[1] >> 2) & 0x0f;
let mut value = !b & 0x0f;
value |= 0x20;
if !self.reset {
value |= 0x10;
}
if self.nation == Nationalisation::Export {
if self.th_level(0) {
value |= 0x40;
}
if self.th_level(1) {
value |= 0x80;
}
}
value
}
}
#[derive(Debug, Default)]
struct Links {
pause: Option<WireSource>,
reset: Option<WireSource>,
}
struct Shared {
state: Mutex<State>,
links: Mutex<Links>,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shared")
.field("state", &self.state)
.finish_non_exhaustive()
}
}
impl Shared {
fn drive(&self, pause: bool, reset: bool) {
let (p, r) = {
let links = self.links.lock();
(links.pause.clone(), links.reset.clone())
};
if let Some(p) = p {
p.set(Level::from_bool(pause));
}
if let Some(r) = r {
r.set(Level::from_bool(reset));
}
}
fn settle(&self) {
let (pause, reset) = {
let state = self.state.lock();
(state.pause, state.reset)
};
self.drive(pause, reset);
}
}
pub struct SmsIo {
shared: Arc<Shared>,
control_region: RegionRef,
pad_region: RegionRef,
}
impl fmt::Debug for SmsIo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SmsIo")
.field("state", &self.shared.state)
.finish_non_exhaustive()
}
}
impl Default for SmsIo {
fn default() -> Self {
SmsIo::new(Nationalisation::Export)
}
}
impl SmsIo {
#[must_use]
pub fn new(nation: Nationalisation) -> SmsIo {
let shared = Arc::new(Shared {
state: Mutex::with_rank(LockRank::DEVICE, State::new(nation)),
links: Mutex::with_rank(LockRank::WIRE, Links::default()),
});
let control_region = Arc::new(MmioRegion::io(
"sms.io.ctrl",
2,
Arc::new(ControlPorts {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
let pad_region = Arc::new(MmioRegion::io(
"sms.io.pads",
2,
Arc::new(PadPorts {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
SmsIo {
shared,
control_region,
pad_region,
}
}
pub fn from_props(props: &Props) -> Result<SmsIo> {
let mut r = props.reader();
let name = r.or_str("region", "export")?;
let nation = Nationalisation::from_name(name).ok_or_else(|| Error::Config {
at: String::from("region"),
message: alloc::format!("`{name}` is not a console region; use `export` or `japan`"),
})?;
r.finish()?;
Ok(SmsIo::new(nation))
}
#[must_use]
pub fn nationalisation(&self) -> Nationalisation {
self.shared.state.lock().nation
}
pub fn set_pressed(&self, port: usize, button: Button, pressed: bool) {
let mut state = self.shared.state.lock();
let mask = 1u8 << button.bit();
if pressed {
state.pads[port & 1] |= mask;
} else {
state.pads[port & 1] &= !mask;
}
}
pub fn set_buttons(&self, port: usize, pressed: u8) {
self.shared.state.lock().pads[port & 1] = pressed & 0x3f;
}
#[must_use]
pub fn buttons(&self, port: usize) -> u8 {
self.shared.state.lock().pads[port & 1]
}
pub fn set_pause(&self, held: bool) {
self.shared.state.lock().pause = held;
self.shared.settle();
}
pub fn pulse_pause(&self) {
self.set_pause(true);
self.set_pause(false);
}
pub fn set_reset(&self, held: bool) {
self.shared.state.lock().reset = held;
self.shared.settle();
}
#[must_use]
pub fn read_dc(&self) -> u8 {
self.shared.state.lock().read_dc()
}
#[must_use]
pub fn read_dd(&self) -> u8 {
self.shared.state.lock().read_dd()
}
#[must_use]
pub fn memory_control(&self) -> u8 {
self.shared.state.lock().memory_control
}
#[must_use]
pub fn io_control(&self) -> u8 {
self.shared.state.lock().io_control
}
pub fn write_control(&self, offset: u64, value: u8) {
let mut state = self.shared.state.lock();
if offset & 1 == 0 {
state.memory_control = value;
} else {
state.io_control = value;
}
}
pub fn attach_pause(&self, source: WireSource) {
self.shared.links.lock().pause = Some(source);
self.shared.settle();
}
pub fn attach_reset(&self, source: WireSource) {
self.shared.links.lock().reset = Some(source);
self.shared.settle();
}
}
struct ControlPorts {
shared: Arc<Shared>,
}
impl fmt::Debug for ControlPorts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ControlPorts").finish_non_exhaustive()
}
}
impl MemOps for ControlPorts {
fn read(&self, _offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
*byte = 0xff;
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 mut state = self.shared.state.lock();
if offset & 1 == 0 {
state.memory_control = *value;
} else {
state.io_control = *value;
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
struct PadPorts {
shared: Arc<Shared>,
}
impl fmt::Debug for PadPorts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PadPorts").finish_non_exhaustive()
}
}
impl MemOps for PadPorts {
fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
let state = self.shared.state.lock();
*byte = if offset & 1 == 0 {
state.read_dc()
} else {
state.read_dd()
};
Ok(())
}
fn write(&self, _offset: u64, src: &[u8], _attrs: MemAttrs) -> MemResult {
let [_] = src else {
return Err(BusError::BadAccess);
};
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "sms.io",
version: 1,
summary: "Master System I/O: two control pads at $DC/$DD, $3E/$3F, Pause as an NMI",
properties: &[PropertySpec {
name: "region",
kind: ValueKind::Str,
required: false,
summary: "console region, for the `$3F` readback: `export` or `japan`",
}],
construct: |props| Ok(Box::new(SmsIo::from_props(props)?) as Box<dyn Device>),
};
pub fn register(reg: &mut crate::core::Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for SmsIo {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
self.shared.settle();
Ok(())
}
fn unrealize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
self.shared.drive(false, false);
let mut links = self.shared.links.lock();
links.pause = None;
links.reset = None;
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
CONTROL_REGION => Some(Arc::clone(&self.control_region)),
PAD_REGION => Some(Arc::clone(&self.pad_region)),
_ => None,
}
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
match port {
PAUSE_PIN => self.attach_pause(source),
RESET_PIN => self.attach_reset(source),
_ => {
return Err(Error::Config {
at: String::from(port),
message: alloc::format!(
"the I/O chip drives `{PAUSE_PIN}` and `{RESET_PIN}`, nothing else"
),
});
}
}
Ok(())
}
fn announce(&self, port: &str) {
if port == PAUSE_PIN || port == RESET_PIN {
self.shared.settle();
}
}
fn reset(&self, _kind: ResetKind) {
{
let mut state = self.shared.state.lock();
state.memory_control = 0;
state.io_control = 0xff;
}
self.shared.settle();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = *self.shared.state.lock();
w.write_u32(STATE_VERSION)?;
w.write_u8(state.pads[0])?;
w.write_u8(state.pads[1])?;
w.write_bool(state.pause)?;
w.write_bool(state.reset)?;
w.write_u8(state.memory_control)?;
w.write_u8(state.io_control)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let version = r.read_u32()?;
if version != STATE_VERSION {
return Err(Error::State(alloc::format!(
"the I/O chip's snapshot is version {version}, this build writes {STATE_VERSION}"
)));
}
{
let mut state = self.shared.state.lock();
state.pads[0] = r.read_u8()?;
state.pads[1] = r.read_u8()?;
state.pause = r.read_bool()?;
state.reset = r.read_bool()?;
state.memory_control = r.read_u8()?;
state.io_control = r.read_u8()?;
}
self.shared.settle();
Ok(())
}
}
impl crate::machine::Instance for SmsIo {}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| Ok(Arc::new(SmsIo::from_props(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
ClassSchema::new(CLASS.name)
.prop(PropSchema::new("region", ValueKind::Str).values(&["export", "japan"]))
.port(PAUSE_PIN, PortDir::Out)
.port(RESET_PIN, PortDir::Out)
.region(CONTROL_REGION)
.region(PAD_REGION)
}