mod engine;
mod region;
mod regs;
#[cfg(test)]
mod tests;
use alloc::boxed::Box;
use alloc::sync::Arc;
use core::fmt;
use crate::core::clock::{ClockForest, ClockResult, DomainId};
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, AddressSpace, MemAttrs, MemOps, MemResult, Region as MmioRegion, RegionRef,
};
use crate::core::state::{ChunkReader, ChunkWriter};
use crate::core::sync::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::Width;
use crate::core::wire::{Level, WireSource};
use crate::machine::realize::{BindCtx, Instance};
pub use engine::{
DEFAULT_DECAY_DOTS, DOTS_PER_FRAME, DOTS_PER_SCANLINE, Engine, EvalPhase, FRAMEBUFFER_LEN,
PRE_RENDER_SCANLINE, Pixel, SCANLINES_PER_FRAME, SCREEN_HEIGHT, SCREEN_WIDTH, VBLANK_SCANLINE,
WARMUP_DOTS,
};
pub use region::{BORDER_BLACK, Geometry, RESET_LOCKOUT_CPU_CYCLES, Region};
pub use regs::{
CTRL_BG_TABLE, CTRL_INCREMENT, CTRL_MASTER, CTRL_NAMETABLE, CTRL_NMI, CTRL_SPRITE_16,
CTRL_SPRITE_TABLE, IoLatch, MASK_BG, MASK_BG_LEFT, MASK_EMPHASIS_B, MASK_EMPHASIS_G,
MASK_EMPHASIS_R, MASK_GREYSCALE, MASK_RENDERING, MASK_SPRITE, MASK_SPRITE_LEFT, OAMADDR,
OAMDATA, PPUADDR, PPUCTRL, PPUDATA, PPUMASK, PPUSCROLL, PPUSTATUS, SPRITE_ATTR_IMPLEMENTED,
SPRITE_BEHIND, SPRITE_FLIP_X, SPRITE_FLIP_Y, SPRITE_PALETTE, STATUS_DRIVEN, STATUS_OVERFLOW,
STATUS_SPRITE0, STATUS_VBLANK,
};
pub const REGISTER_BASE: u64 = 0x2000;
pub const REGISTER_WINDOW_LEN: u64 = 0x2000;
pub const REGISTER_REGION: &str = "regs";
pub const NMI_PIN: &str = "nmi";
pub fn add_clock_domain(
forest: &mut ClockForest,
master: DomainId,
region: Region,
) -> ClockResult<DomainId> {
forest.add_domain("ppu", master, 1, region.dot_divider())
}
pub const ENGINE_LOCK_RANK: LockRank = LockRank::new(0x4800);
struct Shared {
engine: Mutex<Engine>,
links: Mutex<Links>,
lazy: Mutex<Option<LazyHandle>>,
dots: AtomicU64,
next_event: AtomicU64,
}
#[derive(Debug, Default)]
struct Links {
nmi: Option<WireSource>,
clock: Option<DomainId>,
}
impl Shared {
fn with_engine<R>(&self, f: impl FnOnce(&mut Engine) -> R) -> R {
let (result, nmi) = {
let mut engine = self.engine.lock();
let result = f(&mut engine);
self.publish(&engine);
(result, engine.nmi_active())
};
self.drive_nmi(nmi);
result
}
fn publish(&self, engine: &Engine) {
self.dots.store(engine.dots, Ordering::Relaxed);
self.next_event
.store(engine.next_event_dot(), Ordering::Relaxed);
}
fn drive_nmi(&self, active: bool) {
let source = {
let links = self.links.lock();
links.nmi.clone()
};
if let Some(source) = source {
source.set(Level::from_bool(active));
}
}
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 NesPpu {
shared: Arc<Shared>,
regs: RegionRef,
}
impl fmt::Debug for NesPpu {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NesPpu")
.field("engine", &self.shared.engine)
.finish()
}
}
impl NesPpu {
pub fn new(props: &Props) -> Result<NesPpu> {
let mut r = props.reader();
let name = r.or_enum("region", Region::Ntsc.name(), Region::NAMES)?;
let warmup = r.or("warmup", true)?;
let decay = r.or("open-bus-decay-dots", DEFAULT_DECAY_DOTS)?;
r.finish()?;
let region = Region::from_name(name).ok_or_else(|| {
crate::core::Error::Property(alloc::format!("unknown `region` `{name}`"))
})?;
let engine = Engine::new(region, warmup, decay);
let next_event = engine.next_event_dot();
let shared = Arc::new(Shared {
engine: Mutex::with_rank(ENGINE_LOCK_RANK, engine),
links: Mutex::with_rank(LockRank::WIRE, Links::default()),
lazy: Mutex::new(None),
dots: AtomicU64::new(0),
next_event: AtomicU64::new(next_event),
});
let regs = Arc::new(MmioRegion::io(
"nes.ppu.regs",
REGISTER_WINDOW_LEN,
Arc::new(PpuPort {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
Ok(NesPpu { shared, regs })
}
pub fn tv_region(&self) -> Region {
self.shared.engine.lock().tv_region()
}
pub fn geometry(&self) -> Geometry {
self.shared.engine.lock().geometry()
}
pub fn attach_bus(&self, space: Arc<AddressSpace>) {
self.shared.engine.lock().bus = Some(space);
}
pub fn attach_nmi(&self, source: WireSource) {
self.shared.links.lock().nmi = Some(source);
}
pub fn attach_clock(&self, domain: DomainId) {
self.shared.links.lock().clock = Some(domain);
}
pub fn clock_domain(&self) -> Option<DomainId> {
self.shared.links.lock().clock
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
pub fn next_event_dot(&self) -> u64 {
self.shared.next_event.load(Ordering::Relaxed)
}
pub fn port(&self) -> Arc<PpuPort> {
Arc::new(PpuPort {
shared: Arc::clone(&self.shared),
})
}
pub fn dots(&self) -> u64 {
self.shared.engine.lock().dots
}
pub fn frame(&self) -> u64 {
self.shared.engine.lock().frame
}
pub fn position(&self) -> (u16, u16) {
let engine = self.shared.engine.lock();
(engine.scanline, engine.dot)
}
pub fn advance_to(&self, target: u64) {
loop {
let (reached, nmi) = {
let mut engine = self.shared.engine.lock();
let entry = engine.nmi_active();
let reached = engine.run_to(target, entry);
self.shared.publish(&engine);
(reached, engine.nmi_active())
};
self.shared.drive_nmi(nmi);
if reached {
return;
}
}
}
pub fn advance_by(&self, dots: u64) {
let target = self.shared.engine.lock().dots + dots;
self.advance_to(target);
}
pub fn read_register(&self, index: u8) -> u8 {
self.shared.with_engine(|e| e.read_register(index, false))
}
pub fn write_register(&self, index: u8, value: u8) {
self.shared.with_engine(|e| e.write_register(index, value));
}
pub fn oam_dma_write(&self, value: u8) {
self.shared
.with_engine(|e| e.write_register(regs::OAMDATA, value));
}
pub fn oam_addr(&self) -> u8 {
self.shared.engine.lock().oam_addr
}
pub fn peek_oam(&self, addr: u8) -> u8 {
self.shared.engine.lock().oam[usize::from(addr)]
}
pub fn poke_oam(&self, addr: u8, value: u8) {
self.shared.engine.lock().write_oam(addr, value);
}
pub fn peek_palette(&self, addr: u16) -> u8 {
self.shared.engine.lock().palette_read(addr)
}
pub fn poke_palette(&self, addr: u16, value: u8) {
self.shared.engine.lock().palette_write(addr, value);
}
pub fn with_framebuffer<R>(&self, f: impl FnOnce(&[Pixel]) -> R) -> R {
let engine = self.shared.engine.lock();
f(&engine.fb)
}
pub fn pixel(&self, x: usize, y: usize) -> Option<Pixel> {
if x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT {
return None;
}
Some(self.shared.engine.lock().fb[y * SCREEN_WIDTH + x])
}
pub fn with_engine<R>(&self, f: impl FnOnce(&mut Engine) -> R) -> R {
self.shared.with_engine(f)
}
}
static PPU_PROPERTIES: &[PropertySpec] = &[
PropertySpec {
name: "region",
kind: ValueKind::Str,
required: false,
summary: "console variant: `ntsc` (RP2C02), `pal` (RP2C07) or `dendy` (UA6538)",
},
PropertySpec {
name: "warmup",
kind: ValueKind::Bool,
required: false,
summary: "honour the ~29658-CPU-cycle lockout on $2000/$2001/$2005/$2006 writes after reset",
},
PropertySpec {
name: "open-bus-decay-dots",
kind: ValueKind::Uint,
required: false,
summary: "how many PPU dots a bit of the I/O latch holds its charge",
},
];
pub static NES_PPU_CLASS: DeviceClass = DeviceClass {
name: "nes.ppu",
version: 4,
summary: "NES / Famicom picture processing unit (RP2C02 / RP2C07 / UA6538)",
properties: PPU_PROPERTIES,
construct: |props| Ok(Box::new(NesPpu::new(props)?) as Box<dyn Device>),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&NES_PPU_CLASS)
}
impl Device for NesPpu {
fn class(&self) -> &'static DeviceClass {
&NES_PPU_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
let nmi = self.shared.engine.lock().nmi_active();
self.shared.drive_nmi(nmi);
Ok(())
}
fn unrealize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
self.shared.drive_nmi(false);
self.shared.links.lock().nmi = None;
Ok(())
}
fn reset(&self, kind: ResetKind) {
self.shared.with_engine(|e| match kind {
ResetKind::Cold => e.reset_cold(),
ResetKind::Warm | ResetKind::Bus => e.reset_warm(),
});
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
self.shared.engine.lock().save(w)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let (result, nmi) = {
let mut engine = self.shared.engine.lock();
let result = engine.load(r);
self.shared.publish(&engine);
let nmi = engine.nmi_active();
(result, nmi)
};
self.shared.drive_nmi(nmi);
result
}
fn region(&self, name: &str) -> Option<RegionRef> {
(name.is_empty() || name == REGISTER_REGION).then(|| Arc::clone(&self.regs))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != NMI_PIN {
return Err(crate::core::Error::Config {
at: alloc::string::String::from(port),
message: alloc::format!("the PPU drives only `{NMI_PIN}`"),
});
}
self.attach_nmi(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == NMI_PIN {
let nmi = self.shared.engine.lock().nmi_active();
self.shared.drive_nmi(nmi);
}
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.dots.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
NesPpu::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
Some(self.shared.next_event.load(Ordering::Relaxed))
}
fn sampled_every_cycle(&self) -> bool {
true
}
fn attach_lazy(&self, handle: LazyHandle) {
NesPpu::attach_lazy(self, handle);
}
}
impl Instance for NesPpu {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| crate::core::Error::Config {
at: alloc::string::String::from(ctx.path()),
message: alloc::string::String::from(
"the PPU needs an address space of its own: add `space = ppubus` to the object \
and map the cartridge's pattern tables and the console's nametables into it",
),
})?;
self.attach_bus(Arc::clone(space));
if let Some(domain) = ctx.domain() {
self.attach_clock(domain);
}
Ok(())
}
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(NES_PPU_CLASS.name, |props| {
Ok(Arc::new(NesPpu::new(props)?))
})
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
ClassSchema::new(NES_PPU_CLASS.name)
.prop(PropSchema::new("region", ValueKind::Str).values(Region::NAMES))
.prop(PropSchema::new("warmup", ValueKind::Bool))
.prop(PropSchema::new("open-bus-decay-dots", ValueKind::Uint))
.port(NMI_PIN, PortDir::Out)
.region(REGISTER_REGION)
}
pub struct PpuPort {
shared: Arc<Shared>,
}
impl fmt::Debug for PpuPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PpuPort").finish_non_exhaustive()
}
}
impl MemOps for PpuPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
let index = (offset & 7) as u8;
*byte = self
.shared
.with_engine(|e| e.read_register(index, attrs.debug));
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Err(BusError::BadAccess);
}
self.shared.sync(attrs);
let index = (offset & 7) as u8;
self.shared.with_engine(|e| e.write_register(index, *value));
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}