pub mod disasm;
mod exec;
pub mod isa;
#[cfg(test)]
mod tests;
#[cfg(all(test, feature = "std"))]
mod conformance;
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, Initiator, PropertySpec, RealizeCtx, ResetKind, SinkPin,
};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::registry::Registry;
use crate::core::sched::{Budget, Consumed};
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region as MmioRegion, RegionRef,
RequesterId,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{self, AtomicBool, AtomicU8, AtomicU32, LockRank, Ordering};
use crate::core::value::Width;
use crate::core::wire::{FanIn, Level, Resolve, WireId, WireSink};
use exec::{Exec, Mode, State};
pub use exec::VECTORS;
pub mod flags {
pub const Z: u8 = 0x80;
pub const N: u8 = 0x40;
pub const H: u8 = 0x20;
pub const C: u8 = 0x10;
pub const STORED: u8 = 0xf0;
}
pub mod interrupt {
pub const VBLANK: u8 = 0;
pub const STAT: u8 = 1;
pub const TIMER: u8 = 2;
pub const SERIAL: u8 = 3;
pub const JOYPAD: u8 = 4;
pub const MASK: u8 = 0x1f;
pub const PINS: [&str; 5] = ["vblank", "stat", "timer", "serial", "joypad"];
}
pub const MAX_INSTRUCTION_CYCLES: u64 = 6;
pub const IF_ADDRESS: u64 = 0xff0f;
pub const IE_ADDRESS: u64 = 0xffff;
pub const IF_REGION: &str = "if";
pub const IE_REGION: &str = "ie";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Regs {
pub a: u8,
pub f: u8,
pub b: u8,
pub c: u8,
pub d: u8,
pub e: u8,
pub h: u8,
pub l: u8,
pub sp: u16,
pub pc: u16,
}
impl Regs {
#[must_use]
pub const fn new() -> Regs {
Regs {
a: 0,
f: 0,
b: 0,
c: 0,
d: 0,
e: 0,
h: 0,
l: 0,
sp: 0,
pc: 0,
}
}
#[must_use]
pub const fn post_boot_dmg() -> Regs {
Regs {
a: 0x01,
f: 0xb0,
b: 0x00,
c: 0x13,
d: 0x00,
e: 0xd8,
h: 0x01,
l: 0x4d,
sp: 0xfffe,
pc: 0x0100,
}
}
#[inline]
#[must_use]
pub const fn hl(&self) -> u16 {
((self.h as u16) << 8) | self.l as u16
}
#[inline]
pub const fn set_hl(&mut self, value: u16) {
self.h = (value >> 8) as u8;
self.l = value as u8;
}
#[inline]
#[must_use]
pub const fn af(&self) -> u16 {
((self.a as u16) << 8) | (self.f & flags::STORED) as u16
}
#[inline]
#[must_use]
pub const fn bc(&self) -> u16 {
((self.b as u16) << 8) | self.c as u16
}
#[inline]
#[must_use]
pub const fn de(&self) -> u16 {
((self.d as u16) << 8) | self.e as u16
}
#[inline]
#[must_use]
pub const fn flag(&self, mask: u8) -> bool {
self.f & mask != 0
}
}
impl fmt::Display for Regs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"A:{:02x} F:{:02x} B:{:02x} C:{:02x} D:{:02x} E:{:02x} H:{:02x} L:{:02x} \
SP:{:04x} PC:{:04x}",
self.a, self.f, self.b, self.c, self.d, self.e, self.h, self.l, self.sp, self.pc
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Reg {
A,
F,
B,
C,
D,
E,
H,
L,
Sp,
Pc,
}
impl Reg {
pub const ALL: &'static [Reg] = &[
Reg::A,
Reg::F,
Reg::B,
Reg::C,
Reg::D,
Reg::E,
Reg::H,
Reg::L,
Reg::Sp,
Reg::Pc,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Reg::A => "a",
Reg::F => "f",
Reg::B => "b",
Reg::C => "c",
Reg::D => "d",
Reg::E => "e",
Reg::H => "h",
Reg::L => "l",
Reg::Sp => "sp",
Reg::Pc => "pc",
}
}
#[must_use]
pub const fn width(self) -> Width {
match self {
Reg::Sp | Reg::Pc => Width::U16,
_ => Width::U8,
}
}
#[must_use]
pub const fn get(self, regs: &Regs) -> u16 {
match self {
Reg::A => regs.a as u16,
Reg::F => regs.f as u16,
Reg::B => regs.b as u16,
Reg::C => regs.c as u16,
Reg::D => regs.d as u16,
Reg::E => regs.e as u16,
Reg::H => regs.h as u16,
Reg::L => regs.l as u16,
Reg::Sp => regs.sp,
Reg::Pc => regs.pc,
}
}
pub const fn set(self, regs: &mut Regs, value: u16) {
match self {
Reg::A => regs.a = value as u8,
Reg::F => regs.f = (value as u8) & flags::STORED,
Reg::B => regs.b = value as u8,
Reg::C => regs.c = value as u8,
Reg::D => regs.d = value as u8,
Reg::E => regs.e = value as u8,
Reg::H => regs.h = value as u8,
Reg::L => regs.l = value as u8,
Reg::Sp => regs.sp = value,
Reg::Pc => regs.pc = value,
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Reg> {
Reg::ALL.iter().copied().find(|r| r.name() == name)
}
}
impl fmt::Display for Reg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Config {
pub post_boot: bool,
pub requester: RequesterId,
}
impl Config {
pub const DMG: Config = Config {
post_boot: true,
requester: RequesterId::ANONYMOUS,
};
#[must_use]
pub const fn with_requester(mut self, id: RequesterId) -> Self {
self.requester = id;
self
}
}
impl Default for Config {
fn default() -> Self {
Config::DMG
}
}
#[derive(Debug, Default)]
pub(crate) struct Lines {
request: AtomicU8,
enable: AtomicU8,
joypad_active: AtomicBool,
reset_req: AtomicBool,
}
impl Lines {
fn pending(&self) -> u8 {
self.request.load(Ordering::Acquire) & self.enable.load(Ordering::Acquire) & interrupt::MASK
}
fn request(&self, bit: u8) {
self.request.fetch_or(1 << bit, Ordering::AcqRel);
}
fn clear_request(&self, bit: u8) {
self.request.fetch_and(!(1 << bit), Ordering::AcqRel);
}
fn read_if(&self) -> u8 {
self.request.load(Ordering::Acquire) | !interrupt::MASK
}
fn write_if(&self, value: u8) {
self.request
.store(value & interrupt::MASK, Ordering::Release);
}
fn read_ie(&self) -> u8 {
self.enable.load(Ordering::Acquire)
}
fn write_ie(&self, value: u8) {
self.enable.store(value, Ordering::Release);
}
fn stop_wake(&self) -> bool {
self.joypad_active.load(Ordering::Acquire)
}
fn set_joypad_active(&self, active: bool) {
self.joypad_active.store(active, Ordering::Release);
}
fn request_reset(&self) {
self.reset_req.store(true, Ordering::Release);
}
fn take_reset_request(&self) -> bool {
self.reset_req.swap(false, Ordering::AcqRel)
}
fn snapshot(&self) -> (u8, u8, bool, bool) {
(
self.request.load(Ordering::Acquire),
self.enable.load(Ordering::Acquire),
self.joypad_active.load(Ordering::Acquire),
self.reset_req.load(Ordering::Acquire),
)
}
fn restore(&self, (request, enable, joypad, reset): (u8, u8, bool, bool)) {
self.request.store(request, Ordering::Release);
self.enable.store(enable, Ordering::Release);
self.joypad_active.store(joypad, Ordering::Release);
self.reset_req.store(reset, Ordering::Release);
}
}
#[derive(Debug)]
struct Session {
state: State,
space: Option<Arc<AddressSpace>>,
}
#[derive(Debug)]
pub struct Sm83 {
cfg: Config,
lines: Arc<Lines>,
session: sync::Mutex<Session>,
requester: AtomicU32,
if_region: RegionRef,
ie_region: RegionRef,
pins: sync::Mutex<Vec<Arc<InterruptPin>>>,
reset_pin: sync::Mutex<Option<Arc<ResetPin>>>,
}
impl Sm83 {
#[must_use]
pub fn new(cfg: Config) -> Sm83 {
let lines = Arc::new(Lines::default());
let if_region = Arc::new(MmioRegion::io(
"sm83.if",
1,
Arc::new(RegPort {
lines: Arc::clone(&lines),
which: WhichReg::If,
}) as Arc<dyn MemOps>,
));
let ie_region = Arc::new(MmioRegion::io(
"sm83.ie",
1,
Arc::new(RegPort {
lines: Arc::clone(&lines),
which: WhichReg::Ie,
}) as Arc<dyn MemOps>,
));
Sm83 {
cfg,
lines,
session: sync::Mutex::with_rank(
LockRank::BUS,
Session {
state: State::new(),
space: None,
},
),
requester: AtomicU32::new(cfg.requester.0),
if_region,
ie_region,
pins: sync::Mutex::new(Vec::new()),
reset_pin: sync::Mutex::new(None),
}
}
pub fn from_props(props: &Props) -> Result<Sm83> {
let mut r = props.reader();
let post_boot = r.or("post-boot", true)?;
let _ = r.or_enum("engine", "interp", &["interp"])?;
r.finish()?;
Ok(Sm83::new(Config {
post_boot,
requester: RequesterId::ANONYMOUS,
}))
}
#[must_use]
pub fn config(&self) -> Config {
self.cfg
}
fn effective_config(&self) -> Config {
Config {
requester: RequesterId(self.requester.load(Ordering::Relaxed)),
..self.cfg
}
}
pub fn set_requester(&self, id: RequesterId) {
self.requester.store(id.0, Ordering::Relaxed);
}
pub fn attach_space(&self, space: Arc<AddressSpace>) {
self.session.lock().space = Some(space);
}
#[must_use]
pub fn space(&self) -> Option<Arc<AddressSpace>> {
self.session.lock().space.clone()
}
#[must_use]
pub fn regs(&self) -> Regs {
self.session.lock().state.regs
}
pub fn set_regs(&self, regs: Regs) {
let mut session = self.session.lock();
session.state.regs = regs;
session.state.regs.f &= flags::STORED;
}
#[must_use]
pub fn reg(&self, reg: Reg) -> u16 {
reg.get(&self.session.lock().state.regs)
}
pub fn set_reg(&self, reg: Reg, value: u16) {
reg.set(&mut self.session.lock().state.regs, value);
}
#[must_use]
pub fn cycles(&self) -> u64 {
self.session.lock().state.cycles
}
#[must_use]
pub fn ime(&self) -> bool {
self.session.lock().state.ime
}
pub fn set_ime(&self, on: bool) {
let mut session = self.session.lock();
session.state.ime = on;
session.state.ei_pending = false;
}
#[must_use]
pub fn is_halted(&self) -> bool {
self.session.lock().state.mode == Mode::Halted
}
#[must_use]
pub fn is_stopped(&self) -> bool {
self.session.lock().state.mode == Mode::Stopped
}
#[must_use]
pub fn is_locked(&self) -> bool {
self.session.lock().state.mode == Mode::Locked
}
#[must_use]
pub fn interrupt_flags(&self) -> u8 {
self.lines.read_if()
}
#[must_use]
pub fn interrupt_enable(&self) -> u8 {
self.lines.read_ie()
}
pub fn set_interrupt_enable(&self, value: u8) {
self.lines.write_ie(value);
}
pub fn request_interrupt(&self, bit: u8) {
if bit < 5 {
self.lines.request(bit);
}
}
pub fn clear_interrupt(&self, bit: u8) {
if bit < 5 {
self.lines.clear_request(bit);
}
}
#[must_use]
pub fn pending_interrupts(&self) -> u8 {
self.lines.pending()
}
#[must_use]
pub fn bus_faults(&self) -> (u64, u16) {
let s = self.session.lock();
(s.state.faults, s.state.last_fault)
}
pub fn step(&self) -> u64 {
let cfg = self.effective_config();
let mut session = self.session.lock();
if self.lines.take_reset_request() {
session.state = State::new();
if cfg.post_boot {
session.state.regs = Regs::post_boot_dmg();
}
}
let Session { state, space } = &mut *session;
let Some(space) = space.clone() else {
return 0;
};
Exec::new(state, &space, &cfg, &self.lines).step()
}
pub fn run(&self, budget: u64) -> u64 {
let mut used = 0;
while used < budget {
let n = self.step();
if n == 0 {
break;
}
used += n;
}
used
}
pub fn run_budget(&self, ticks: u64) -> u64 {
let owed = self.session.lock().state.debt;
if owed >= ticks {
self.session.lock().state.debt = owed - ticks;
return ticks;
}
let allowance = ticks - owed;
let mut used = 0u64;
while used < allowance {
let n = self.step();
if n == 0 {
break;
}
used += n;
}
if used >= allowance {
self.session.lock().state.debt = used - allowance;
ticks
} else {
self.session.lock().state.debt = 0;
owed + used
}
}
#[must_use]
pub fn cycle_debt(&self) -> u64 {
self.session.lock().state.debt
}
#[must_use]
pub fn disassemble(&self, pc: u16, count: usize) -> Vec<disasm::Disassembled> {
let Some(space) = self.space() else {
return Vec::new();
};
disasm::disassemble_run(pc, count, |addr| {
space
.read(u64::from(addr), Width::U8, MemAttrs::DEBUG)
.ok()
.map(|v| v as u8)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WhichReg {
If,
Ie,
}
#[derive(Debug)]
struct RegPort {
lines: Arc<Lines>,
which: WhichReg,
}
impl MemOps for RegPort {
fn read(&self, _offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
*byte = match self.which {
WhichReg::If => self.lines.read_if(),
WhichReg::Ie => self.lines.read_ie(),
};
Ok(())
}
fn write(&self, _offset: u64, src: &[u8], _attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
match self.which {
WhichReg::If => self.lines.write_if(*value),
WhichReg::Ie => self.lines.write_ie(*value),
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "cpu.sm83",
version: 1,
summary: "Sharp SM83 8-bit CPU core (Game Boy), M-cycle-accurate interpreter",
properties: &[
PropertySpec {
name: "post-boot",
kind: ValueKind::Bool,
required: false,
summary: "start at $0100 with the register file a DMG boot ROM would leave behind",
},
PropertySpec {
name: "engine",
kind: ValueKind::Str,
required: false,
summary: "which execution engine; only `interp` exists until phase 5",
},
],
construct: |props| Ok(Box::new(Sm83::from_props(props)?)),
};
pub fn register(reg: &mut Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for Sm83 {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
IF_REGION => Some(Arc::clone(&self.if_region)),
IE_REGION => Some(Arc::clone(&self.ie_region)),
_ => None,
}
}
fn sink(&self, port: &str, sources: &[WireId]) -> Option<SinkPin> {
if port == "reset" {
let pin = Arc::new(ResetPin::new(Arc::clone(&self.lines), sources));
*self.reset_pin.lock() = Some(Arc::clone(&pin));
return Some(SinkPin { sink: pin, line: 0 });
}
let bit = interrupt::PINS.iter().position(|p| *p == port)? as u8;
let pin = Arc::new(InterruptPin::new(Arc::clone(&self.lines), bit, sources));
self.pins.lock().push(Arc::clone(&pin));
Some(SinkPin { sink: pin, line: 0 })
}
fn is_runnable(&self) -> bool {
true
}
fn run(&self, budget: Budget) -> Consumed {
Consumed::new(self.run_budget(budget.ticks))
}
fn reset(&self, kind: ResetKind) {
let mut session = self.session.lock();
session.state = State::new();
if self.cfg.post_boot {
session.state.regs = Regs::post_boot_dmg();
}
drop(session);
if kind == ResetKind::Cold {
self.lines.restore((0, 0, false, false));
} else {
self.lines.write_if(0);
self.lines.write_ie(0);
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.session.lock().state;
w.write_u8(state.regs.a)?;
w.write_u8(state.regs.f)?;
w.write_u8(state.regs.b)?;
w.write_u8(state.regs.c)?;
w.write_u8(state.regs.d)?;
w.write_u8(state.regs.e)?;
w.write_u8(state.regs.h)?;
w.write_u8(state.regs.l)?;
w.write_u16(state.regs.sp)?;
w.write_u16(state.regs.pc)?;
w.write_u64(state.cycles)?;
w.write_bool(state.ime)?;
w.write_bool(state.ei_pending)?;
w.write_u8(state.mode.tag())?;
w.write_bool(state.halt_bug)?;
w.write_u64(state.debt)?;
w.write_u64(state.faults)?;
w.write_u16(state.last_fault)?;
let (request, enable, joypad, reset) = self.lines.snapshot();
w.write_u8(request)?;
w.write_u8(enable)?;
w.write_bool(joypad)?;
w.write_bool(reset)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let mut state = State::new();
state.regs.a = r.read_u8()?;
state.regs.f = r.read_u8()? & flags::STORED;
state.regs.b = r.read_u8()?;
state.regs.c = r.read_u8()?;
state.regs.d = r.read_u8()?;
state.regs.e = r.read_u8()?;
state.regs.h = r.read_u8()?;
state.regs.l = r.read_u8()?;
state.regs.sp = r.read_u16()?;
state.regs.pc = r.read_u16()?;
state.cycles = r.read_u64()?;
state.ime = r.read_bool()?;
state.ei_pending = r.read_bool()?;
let tag = r.read_u8()?;
state.mode = Mode::from_tag(tag)
.ok_or_else(|| Error::State(alloc::format!("unknown SM83 run mode tag {tag}")))?;
state.halt_bug = r.read_bool()?;
state.debt = r.read_u64()?;
state.faults = r.read_u64()?;
state.last_fault = r.read_u16()?;
let request = r.read_u8()?;
let enable = r.read_u8()?;
let joypad = r.read_bool()?;
let reset = r.read_bool()?;
self.session.lock().state = state;
self.lines.restore((request, enable, joypad, reset));
Ok(())
}
}
impl Initiator for Sm83 {
fn requester(&self) -> RequesterId {
RequesterId(self.requester.load(Ordering::Relaxed))
}
}
impl crate::machine::Instance for Sm83 {
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(
"an SM83 needs an address space to fetch from (`space = cpubus`)",
),
})?;
self.attach_space(Arc::clone(space));
self.set_requester(ctx.requester());
Ok(())
}
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| Ok(Arc::new(Sm83::from_props(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
let mut schema = ClassSchema::new(CLASS.name)
.prop(PropSchema::new("post-boot", ValueKind::Bool))
.prop(PropSchema::new("engine", ValueKind::Str).values(&["interp"]))
.port("reset", PortDir::In)
.region(IF_REGION)
.region(IE_REGION);
for pin in interrupt::PINS {
schema = schema.port(pin, PortDir::In);
}
schema
}
#[derive(Debug)]
pub struct InterruptPin {
lines: Arc<Lines>,
bit: u8,
inputs: FanIn,
resolve: Resolve,
last: AtomicBool,
}
impl InterruptPin {
#[must_use]
pub fn for_cpu(cpu: &Sm83, bit: u8, sources: &[WireId]) -> InterruptPin {
InterruptPin::new(Arc::clone(&cpu.lines), bit, sources)
}
fn new(lines: Arc<Lines>, bit: u8, sources: &[WireId]) -> InterruptPin {
InterruptPin {
lines,
bit,
inputs: FanIn::new(sources),
resolve: Resolve::Or,
last: AtomicBool::new(false),
}
}
#[must_use]
pub fn bit(&self) -> u8 {
self.bit
}
#[must_use]
pub fn inputs(&self) -> &FanIn {
&self.inputs
}
}
impl WireSink for InterruptPin {
fn set_level(&self, src: WireId, _line: u32, level: Level) {
self.inputs.set(src, level);
let now = self.inputs.resolve(self.resolve).is_high();
let was = self.last.swap(now, Ordering::AcqRel);
if now && !was {
self.lines.request(self.bit);
}
if self.bit == interrupt::JOYPAD {
self.lines.set_joypad_active(now);
}
}
}
#[derive(Debug)]
pub struct ResetPin {
lines: Arc<Lines>,
inputs: FanIn,
resolve: Resolve,
}
impl ResetPin {
fn new(lines: Arc<Lines>, sources: &[WireId]) -> ResetPin {
ResetPin {
lines,
inputs: FanIn::new(sources),
resolve: Resolve::Or,
}
}
#[must_use]
pub fn inputs(&self) -> &FanIn {
&self.inputs
}
}
impl WireSink for ResetPin {
fn set_level(&self, src: WireId, _line: u32, level: Level) {
self.inputs.set(src, level);
if self.inputs.resolve(self.resolve).is_high() {
self.lines.request_reset();
}
}
}
#[must_use]
pub fn describe_isa() -> String {
disasm::describe_isa()
}