use alloc::boxed::Box;
use alloc::collections::VecDeque;
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::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::{AtomicBool, AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::Width;
pub const CLOCK_DIVIDER: u64 = 16;
pub const SAMPLE_DIVISOR: u64 = CLOCK_DIVIDER * 5;
pub const RING_FRAMES: usize = 16_384;
pub const PORT_REGION: &str = "port";
pub const VOLUME: [i16; 16] = [
8191, 6507, 5168, 4105, 3261, 2590, 2057, 1642, 1298, 1031, 819, 650, 516, 410, 326, 0,
];
const LFSR_SEED: u16 = 0x8000;
const WHITE_TAPS: u16 = 0x0009;
const STATE_VERSION: u32 = 1;
#[derive(Debug, Clone)]
struct Engine {
tone: [u16; 4],
volume: [u8; 4],
counter: [u16; 4],
output: [bool; 3],
lfsr: u16,
latched: u8,
divider: u64,
sample_timer: u64,
ticks: u64,
}
impl Default for Engine {
fn default() -> Engine {
Engine {
tone: [0; 4],
volume: [0x0f; 4],
counter: [0; 4],
output: [false; 3],
lfsr: LFSR_SEED,
latched: 0,
divider: CLOCK_DIVIDER,
sample_timer: SAMPLE_DIVISOR,
ticks: 0,
}
}
}
impl Engine {
fn write(&mut self, value: u8) {
if value & 0x80 != 0 {
self.latched = (value >> 4) & 0x07;
let channel = (self.latched >> 1) as usize;
if self.latched & 1 != 0 {
self.volume[channel] = value & 0x0f;
} else if channel == 3 {
self.write_noise_control(value & 0x0f);
} else {
self.tone[channel] = (self.tone[channel] & 0x3f0) | u16::from(value & 0x0f);
}
return;
}
let channel = (self.latched >> 1) as usize;
if self.latched & 1 != 0 {
self.volume[channel] = value & 0x0f;
} else if channel == 3 {
self.write_noise_control(value & 0x0f);
} else {
self.tone[channel] = (self.tone[channel] & 0x00f) | (u16::from(value & 0x3f) << 4);
}
}
fn write_noise_control(&mut self, value: u8) {
self.tone[3] = u16::from(value & 0x07);
self.lfsr = LFSR_SEED;
}
fn period(&self, channel: usize) -> u16 {
match self.tone[channel] & 0x3ff {
0 => 1,
n => n,
}
}
fn noise_period(&self) -> u16 {
match self.tone[3] & 0x03 {
0 => 0x10,
1 => 0x20,
2 => 0x40,
_ => self.period(2),
}
}
fn step(&mut self) {
for channel in 0..3 {
if self.counter[channel] == 0 {
self.counter[channel] = self.period(channel);
self.output[channel] = !self.output[channel];
}
self.counter[channel] -= 1;
}
if self.counter[3] == 0 {
self.counter[3] = self.noise_period();
let feedback = if self.tone[3] & 0x04 != 0 {
(self.lfsr & WHITE_TAPS).count_ones() as u16 & 1
} else {
self.lfsr & 1
};
self.lfsr = (self.lfsr >> 1) | (feedback << 15);
}
self.counter[3] -= 1;
}
fn sample(&self) -> i16 {
let mut sum = 0i32;
for channel in 0..3 {
let level = i32::from(VOLUME[self.volume[channel] as usize]);
sum += if self.output[channel] { level } else { -level };
}
let level = i32::from(VOLUME[self.volume[3] as usize]);
sum += if self.lfsr & 1 != 0 { level } else { -level };
(sum / 4) as i16
}
}
struct Shared {
engine: Mutex<Engine>,
lazy: Mutex<Option<LazyHandle>>,
ring: Mutex<VecDeque<(i16, i16)>>,
recording: AtomicBool,
ticks: AtomicU64,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shared")
.field("engine", &self.engine)
.finish_non_exhaustive()
}
}
impl Shared {
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);
}
}
pub struct SmsPsg {
shared: Arc<Shared>,
port_region: RegionRef,
}
impl fmt::Debug for SmsPsg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SmsPsg")
.field("engine", &self.shared.engine)
.finish_non_exhaustive()
}
}
impl Default for SmsPsg {
fn default() -> Self {
SmsPsg::new()
}
}
impl SmsPsg {
#[must_use]
pub fn new() -> SmsPsg {
let shared = Arc::new(Shared {
engine: Mutex::with_rank(LockRank::DEVICE, Engine::default()),
lazy: Mutex::new(None),
ring: Mutex::with_rank(LockRank::LEAF, VecDeque::new()),
recording: AtomicBool::new(false),
ticks: AtomicU64::new(0),
});
let port_region = Arc::new(MmioRegion::io(
"sms.psg.port",
2,
Arc::new(PsgPort {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
SmsPsg {
shared,
port_region,
}
}
pub fn from_props(props: &Props) -> Result<SmsPsg> {
let mut r = props.reader();
let record = r.or("record", false)?;
r.finish()?;
let psg = SmsPsg::new();
psg.set_recording(record);
Ok(psg)
}
pub fn set_recording(&self, on: bool) {
self.shared.recording.store(on, Ordering::Relaxed);
}
#[must_use]
pub fn recording(&self) -> bool {
self.shared.recording.load(Ordering::Relaxed)
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
#[must_use]
pub fn take_samples(&self) -> alloc::vec::Vec<(i16, i16)> {
self.shared.ring.lock().drain(..).collect()
}
#[must_use]
pub fn queued_samples(&self) -> usize {
self.shared.ring.lock().len()
}
#[must_use]
pub fn ticks(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
pub fn write(&self, value: u8) {
self.shared.engine.lock().write(value);
}
#[must_use]
pub fn tone(&self, channel: usize) -> u16 {
self.shared.engine.lock().tone[channel & 3]
}
#[must_use]
pub fn volume(&self, channel: usize) -> u8 {
self.shared.engine.lock().volume[channel & 3]
}
#[must_use]
pub fn lfsr(&self) -> u16 {
self.shared.engine.lock().lfsr
}
#[must_use]
pub fn sample(&self) -> i16 {
self.shared.engine.lock().sample()
}
pub fn advance_to(&self, target: u64) {
let mut engine = self.shared.engine.lock();
if target <= engine.ticks {
return;
}
let recording = self.shared.recording.load(Ordering::Relaxed);
while engine.ticks < target {
let step = (target - engine.ticks)
.min(engine.divider)
.min(engine.sample_timer);
engine.ticks += step;
engine.divider -= step;
engine.sample_timer -= step;
if engine.divider == 0 {
engine.divider = CLOCK_DIVIDER;
engine.step();
}
if engine.sample_timer == 0 {
engine.sample_timer = SAMPLE_DIVISOR;
if recording {
let value = engine.sample();
let mut ring = self.shared.ring.lock();
if ring.len() >= RING_FRAMES {
ring.pop_front();
}
ring.push_back((value, value));
}
}
}
self.shared.ticks.store(engine.ticks, Ordering::Relaxed);
}
pub fn advance_by(&self, ticks: u64) {
let target = self.shared.ticks.load(Ordering::Relaxed) + ticks;
self.advance_to(target);
}
}
struct PsgPort {
shared: Arc<Shared>,
}
impl fmt::Debug for PsgPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PsgPort").finish_non_exhaustive()
}
}
impl MemOps for PsgPort {
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(());
}
self.shared.sync(attrs);
self.shared.engine.lock().write(*value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "sms.psg",
version: 1,
summary: "SN76489 PSG: three square channels, one noise channel, one write port",
properties: &[PropertySpec {
name: "record",
kind: ValueKind::Bool,
required: false,
summary: "keep output frames in a ring for a host audio sink to drain",
}],
construct: |props| Ok(Box::new(SmsPsg::from_props(props)?) as Box<dyn Device>),
};
pub fn register(reg: &mut crate::core::Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for SmsPsg {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
(name.is_empty() || name == PORT_REGION).then(|| Arc::clone(&self.port_region))
}
fn reset(&self, _kind: ResetKind) {
let mut engine = self.shared.engine.lock();
let ticks = engine.ticks;
*engine = Engine::default();
engine.ticks = ticks;
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let engine = self.shared.engine.lock();
w.write_u32(STATE_VERSION)?;
for value in engine.tone {
w.write_u16(value)?;
}
for value in engine.volume {
w.write_u8(value)?;
}
for value in engine.counter {
w.write_u16(value)?;
}
for value in engine.output {
w.write_bool(value)?;
}
w.write_u16(engine.lfsr)?;
w.write_u8(engine.latched)?;
w.write_u64(engine.divider)?;
w.write_u64(engine.sample_timer)?;
w.write_u64(engine.ticks)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let version = r.read_u32()?;
if version != STATE_VERSION {
return Err(Error::State(alloc::format!(
"the PSG's snapshot is version {version}, this build writes {STATE_VERSION}"
)));
}
let mut engine = self.shared.engine.lock();
for value in &mut engine.tone {
*value = r.read_u16()?;
}
for value in &mut engine.volume {
*value = r.read_u8()?;
}
for value in &mut engine.counter {
*value = r.read_u16()?;
}
for value in &mut engine.output {
*value = r.read_bool()?;
}
engine.lfsr = r.read_u16()?;
engine.latched = r.read_u8()?;
engine.divider = r.read_u64()?;
engine.sample_timer = r.read_u64()?;
engine.ticks = r.read_u64()?;
self.shared.ticks.store(engine.ticks, Ordering::Relaxed);
Ok(())
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
SmsPsg::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
None
}
fn attach_lazy(&self, handle: LazyHandle) {
SmsPsg::attach_lazy(self, handle);
}
}
impl crate::machine::Instance for SmsPsg {}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| Ok(Arc::new(SmsPsg::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("record", ValueKind::Bool))
.region(PORT_REGION)
}