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, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{
AccessConstraints, MemAttrs, MemOps, MemResult, Region as MmioRegion, RegionRef,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicU8, AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::machine::realize::Instance;
const CLASS_NAME: &str = "nes.ports";
const STATE_VERSION: u32 = 1;
pub const PORT1: &str = "port1";
pub const PORT2: &str = "port2";
pub const DEFAULT_PAD_PORT: &str = "nes-pads";
const OPEN_BUS_BITS: u8 = 0xe0;
pub mod buttons {
pub const A: u8 = 0x80;
pub const B: u8 = 0x40;
pub const SELECT: u8 = 0x20;
pub const START: u8 = 0x10;
pub const UP: u8 = 0x08;
pub const DOWN: u8 = 0x04;
pub const LEFT: u8 = 0x02;
pub const RIGHT: u8 = 0x01;
pub const NONE: u8 = 0x00;
}
#[derive(Debug, Default)]
pub struct Pad {
held: [AtomicU8; 2],
}
impl Pad {
#[must_use]
pub fn new() -> Pad {
Pad::default()
}
pub fn set(&self, port: usize, held: u8) {
if let Some(cell) = self.held.get(port) {
cell.store(held, Ordering::Relaxed);
}
}
#[must_use]
pub fn get(&self, port: usize) -> u8 {
self.held
.get(port)
.map_or(buttons::NONE, |c| c.load(Ordering::Relaxed))
}
}
pub mod pads {
use super::Pad;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::core::sync::{Global, LockRank};
static TABLE: Global<BTreeMap<String, Arc<Pad>>> =
Global::with_rank(LockRank::LEAF, BTreeMap::new());
#[must_use]
pub fn open(name: &str) -> Arc<Pad> {
let mut table = TABLE.lock();
if let Some(pad) = table.get(name) {
return Arc::clone(pad);
}
let pad = Arc::new(Pad::new());
table.insert(name.to_string(), Arc::clone(&pad));
pad
}
#[must_use]
pub fn get(name: &str) -> Option<Arc<Pad>> {
TABLE.lock().get(name).map(Arc::clone)
}
pub fn close(name: &str) -> bool {
TABLE.lock().remove(name).is_some()
}
#[must_use]
pub fn names() -> Vec<String> {
TABLE.lock().keys().cloned().collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct Regs {
strobe: bool,
shift: [u8; 2],
raised_at: u64,
}
struct Shared {
pad: Arc<Pad>,
regs: Mutex<Regs>,
phase: u64,
cycle: AtomicU64,
lazy: Mutex<Option<LazyHandle>>,
}
#[inline]
const fn is_get(cycle: u64, phase: u64) -> bool {
(cycle.wrapping_sub(1).wrapping_add(phase)) & 1 == 0
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shared")
.field("regs", &self.regs)
.field("cycle", &self.cycle.load(Ordering::Relaxed))
.finish()
}
}
impl Shared {
fn latch(&self, regs: &mut Regs) {
regs.shift = [self.pad.get(0), self.pad.get(1)];
}
fn read_port(&self, port: usize, bus: u8, advance: bool) -> u8 {
let mut regs = self.regs.lock();
if regs.strobe {
self.latch(&mut regs);
}
let bit = regs.shift[port] >> 7;
if advance && !regs.strobe {
regs.shift[port] = (regs.shift[port] << 1) | 1;
}
(bus & OPEN_BUS_BITS) | bit
}
fn write_strobe(&self, value: u8, cycle: u64) {
let mut regs = self.regs.lock();
let was = regs.strobe;
regs.strobe = value & 1 != 0;
if regs.strobe {
if !was {
regs.raised_at = cycle;
}
return;
}
if was {
let raised = regs.raised_at;
let saw_put = cycle > raised + 1 || !is_get(cycle, self.phase);
if saw_put {
self.latch(&mut regs);
}
}
}
fn sync(&self, attrs: MemAttrs) -> u64 {
let handle = self.lazy.lock().clone();
if let Some(handle) = handle {
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
if let Ok(tick) = handle.sync(kind) {
self.cycle.store(tick, Ordering::Relaxed);
}
}
self.cycle.load(Ordering::Relaxed)
}
}
#[derive(Debug)]
pub struct NesPorts {
shared: Arc<Shared>,
port1: RegionRef,
port2: RegionRef,
port_name: String,
}
impl NesPorts {
pub fn new(props: &Props) -> Result<NesPorts> {
let mut r = props.reader();
let name: String = r.or("pads", String::from(DEFAULT_PAD_PORT))?;
let phase = r.or_range::<u64>("put-phase", 0, 0..=1)?;
r.finish()?;
Ok(NesPorts::with_pad_phase(pads::open(&name), name, phase))
}
#[must_use]
pub fn with_pad(pad: Arc<Pad>, port_name: String) -> NesPorts {
NesPorts::with_pad_phase(pad, port_name, 0)
}
#[must_use]
pub fn with_pad_phase(pad: Arc<Pad>, port_name: String, phase: u64) -> NesPorts {
let shared = Arc::new(Shared {
pad,
regs: Mutex::with_rank(LockRank::DEVICE, Regs::default()),
phase: phase & 1,
cycle: AtomicU64::new(0),
lazy: Mutex::new(None),
});
let port = |index: usize, name: &'static str| {
Arc::new(MmioRegion::io(
name,
1,
Arc::new(PortWindow {
shared: Arc::clone(&shared),
index,
}) as Arc<dyn MemOps>,
)) as RegionRef
};
NesPorts {
port1: port(0, "nes.ports.4016"),
port2: port(1, "nes.ports.4017"),
shared,
port_name,
}
}
#[must_use]
pub fn pad(&self) -> &Arc<Pad> {
&self.shared.pad
}
#[must_use]
pub fn pad_name(&self) -> &str {
&self.port_name
}
}
#[derive(Debug)]
struct PortWindow {
shared: Arc<Shared>,
index: usize,
}
impl MemOps for PortWindow {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let ([byte], 0) = (dst, offset) else {
return Err(BusError::BadAccess);
};
*byte = self.shared.read_port(self.index, attrs.bus, !attrs.debug);
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let ([value], 0) = (src, offset) else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Ok(());
}
if self.index == 0 {
let cycle = self.shared.sync(attrs);
self.shared.write_strobe(*value, cycle);
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
impl Device for NesPorts {
fn class(&self) -> &'static DeviceClass {
&PORTS_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.cycle.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
self.shared.cycle.store(tick, Ordering::Relaxed);
}
fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
fn reset(&self, _kind: ResetKind) {
*self.shared.regs.lock() = Regs::default();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let regs = *self.shared.regs.lock();
w.write_bool(regs.strobe)?;
w.write_u8(regs.shift[0])?;
w.write_u8(regs.shift[1])?;
w.write_u64(regs.raised_at)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let strobe = r.read_bool()?;
let first = r.read_u8()?;
let second = r.read_u8()?;
let raised_at = r.read_u64().unwrap_or(0);
*self.shared.regs.lock() = Regs {
strobe,
shift: [first, second],
raised_at,
};
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
PORT1 => Some(Arc::clone(&self.port1)),
PORT2 => Some(Arc::clone(&self.port2)),
_ => None,
}
}
}
impl Instance for NesPorts {}
static PORTS_PROPERTIES: &[PropertySpec] = &[
PropertySpec {
name: "pads",
kind: ValueKind::Str,
required: false,
summary: "the host pad port to read buttons from, by name (default \"nes-pads\")",
},
PropertySpec {
name: "put-phase",
kind: ValueKind::Uint,
required: false,
summary: "which CPU cycles are puts (0 or 1); must match the APU's",
},
];
pub static PORTS_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "NES controller ports ($4016/$4017): the OUT0 latch and two 8-bit shift registers",
properties: PORTS_PROPERTIES,
construct: |props| Ok(Box::new(NesPorts::new(props)?) as Box<dyn Device>),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&PORTS_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(NesPorts::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("pads", ValueKind::Str))
.prop(PropSchema::new("put-phase", ValueKind::Uint).range(0, 1))
.region(PORT1)
.region(PORT2)
}
#[cfg(test)]
mod tests {
use super::*;
fn strobe(p: &NesPorts) {
p.shared.write_strobe(1, 10);
p.shared.write_strobe(0, 14);
}
use crate::core::space::AddressSpace;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
fn ports(name: &str) -> NesPorts {
pads::close(name);
NesPorts::with_pad(pads::open(name), String::from(name))
}
fn sequence(p: &NesPorts) -> u8 {
strobe(p);
let mut out = 0u8;
for _ in 0..8 {
out = (out << 1) | (p.shared.read_port(0, 0x40, true) & 1);
}
out
}
#[test]
fn the_shift_register_reports_the_buttons_a_first() {
let p = ports("test-order");
p.pad().set(0, buttons::START | buttons::RIGHT);
assert_eq!(sequence(&p), buttons::START | buttons::RIGHT);
p.pad().set(0, buttons::A);
assert_eq!(sequence(&p), buttons::A);
p.pad().set(0, buttons::NONE);
assert_eq!(sequence(&p), 0);
}
#[test]
fn an_official_pad_reads_one_after_the_eighth_read() {
let p = ports("test-ninth");
p.pad().set(0, buttons::NONE);
strobe(&p);
for _ in 0..8 {
assert_eq!(p.shared.read_port(0, 0x40, true) & 1, 0);
}
for _ in 0..4 {
assert_eq!(p.shared.read_port(0, 0x40, true) & 1, 1);
}
}
#[test]
fn a_high_strobe_reloads_forever() {
let p = ports("test-strobe");
p.pad().set(0, buttons::A);
p.shared.write_strobe(1, 10);
for _ in 0..16 {
assert_eq!(p.shared.read_port(0, 0x40, true) & 1, 1);
}
p.pad().set(0, buttons::NONE);
p.shared.write_strobe(0, 14);
assert_eq!(
p.shared.read_port(0, 0x40, true) & 1,
0,
"A was released first"
);
}
#[test]
fn the_upper_bits_are_open_bus() {
let p = ports("test-openbus");
p.pad().set(0, buttons::A);
strobe(&p);
assert_eq!(p.shared.read_port(0, 0x40, true), 0x40 | 1);
}
#[test]
fn the_two_ports_are_independent() {
let p = ports("test-two");
p.pad().set(0, buttons::A);
p.pad().set(1, buttons::RIGHT);
strobe(&p);
assert_eq!(p.shared.read_port(0, 0x40, true) & 1, 1);
assert_eq!(p.shared.read_port(1, 0x40, true) & 1, 0);
for _ in 0..6 {
let _ = p.shared.read_port(1, 0x40, true);
}
assert_eq!(
p.shared.read_port(1, 0x40, true) & 1,
1,
"Right, the eighth bit"
);
}
#[test]
fn a_debug_read_does_not_clock_the_controller() {
let p = ports("test-debug");
p.pad().set(0, buttons::A);
strobe(&p);
for _ in 0..8 {
assert_eq!(p.shared.read_port(0, 0x40, false), 0x40 | 1, "still A");
}
assert_eq!(p.shared.read_port(0, 0x40, true) & 1, 1);
}
#[test]
fn the_ports_answer_through_an_address_space() {
let p = ports("test-space");
p.pad().set(0, buttons::SELECT);
let space = AddressSpace::new("cpu", 16);
{
let mut topo = space.topology();
topo.map(p.region(PORT1).expect("port1"), 0x4016)
.expect("maps");
topo.map(p.region(PORT2).expect("port2"), 0x4017)
.expect("maps");
}
let wr = |v: u64| {
space
.write(0x4016, Width::U8, v, MemAttrs::DEFAULT)
.expect("writable")
};
let rd = || {
space
.read(0x4016, Width::U8, MemAttrs::DEFAULT)
.expect("readable") as u8
};
wr(1);
wr(0);
let mut out = 0u8;
for _ in 0..8 {
out = (out << 1) | (rd() & 1);
}
assert_eq!(out, buttons::SELECT);
let before = space
.read(0x4016, Width::U8, MemAttrs::DEBUG)
.expect("readable");
assert_eq!(
space
.read(0x4016, Width::U8, MemAttrs::DEBUG)
.expect("readable"),
before
);
assert!(p.region("").is_none());
}
#[test]
fn state_round_trips() {
let p = ports("test-state");
p.pad().set(0, buttons::B | buttons::DOWN);
strobe(&p);
let _ = p.shared.read_port(0, 0x40, true);
let mut shape = MachineShape::new();
shape.add_device("ports", CLASS_NAME).expect("unique path");
let mut writer = StateWriter::new(shape);
let mut chunk = writer
.chunk("ports", CLASS_NAME, STATE_VERSION)
.expect("one chunk");
p.save(&mut chunk).expect("saves");
let bytes = writer.to_vec().expect("encodes");
let other = ports("test-state-2");
let reader = StateReader::new(&bytes).expect("decodes");
let chunk = reader
.load("ports", CLASS_NAME, STATE_VERSION, &Migrations::new())
.expect("finds the chunk");
other.load(&mut chunk.reader()).expect("loads");
let restored = *other.shared.regs.lock();
let original = *p.shared.regs.lock();
assert_eq!(restored, original);
for _ in 0..7 {
assert_eq!(
other.shared.read_port(0, 0x40, true) & 1,
p.shared.read_port(0, 0x40, true) & 1
);
}
}
#[test]
fn a_reset_clears_the_latch_but_not_the_players_thumb() {
let p = ports("test-reset");
p.pad().set(0, buttons::A);
p.shared.write_strobe(1, 10);
p.reset(ResetKind::Cold);
assert!(!p.shared.regs.lock().strobe);
assert_eq!(p.pad().get(0), buttons::A, "the host still holds A");
assert_eq!(sequence(&p), buttons::A);
}
#[test]
fn the_pad_table_hands_the_same_port_to_both_ends() {
pads::close("test-table");
let host = pads::open("test-table");
let device = NesPorts::new(&Props::new().with("pads", "test-table")).expect("constructs");
assert_eq!(device.pad_name(), "test-table");
host.set(0, buttons::UP);
assert_eq!(sequence(&device), buttons::UP);
assert!(pads::names().iter().any(|n| n == "test-table"));
assert!(pads::get("test-table").is_some());
assert!(pads::close("test-table"));
}
#[test]
fn an_unknown_property_is_refused() {
let e = NesPorts::new(&Props::new().with("padz", "x")).expect_err("typo");
assert!(alloc::format!("{e}").contains("padz"), "{e}");
}
#[test]
fn a_one_cycle_strobe_reaches_the_pads_only_across_a_put() {
for (raise, expected) in [(9u64, true), (10, false)] {
let p = NesPorts::with_pad_phase(pads::open("t"), String::from("t"), 0);
p.shared.regs.lock().shift = [0x00, 0x00];
p.pad().set(0, buttons::A);
p.shared.write_strobe(1, raise);
p.shared.write_strobe(0, raise + 1);
assert_eq!(
p.shared.regs.lock().shift[0] != 0,
expected,
"raised on cycle {raise}, which is a {}",
if is_get(raise, 0) { "get" } else { "put" }
);
}
}
#[test]
fn a_strobe_held_across_more_than_one_cycle_always_reaches_the_pads() {
let p = NesPorts::with_pad_phase(pads::open("t2"), String::from("t2"), 0);
p.shared.regs.lock().shift = [0x00, 0x00];
p.pad().set(0, buttons::A);
p.shared.write_strobe(1, 9);
p.shared.write_strobe(0, 11);
assert_ne!(p.shared.regs.lock().shift[0], 0);
}
}