mod engine;
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::space::{AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult};
use crate::core::state::{ChunkReader, ChunkWriter};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::Width;
use crate::core::wire::{Level, WireSource};
pub use engine::{
DEFAULT_DECAY_DOTS, DOTS_PER_FRAME, DOTS_PER_SCANLINE, Engine, EvalPhase, FRAMEBUFFER_LEN,
NMI_ANNOUNCE_DOTS, PRE_RENDER_SCANLINE, Pixel, SCANLINES_PER_FRAME, SCREEN_HEIGHT,
SCREEN_WIDTH, VBLANK_SCANLINE, WARMUP_DOTS,
};
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 DOT_DIVIDER: u64 = 4;
pub fn add_clock_domain(forest: &mut ClockForest, master: DomainId) -> ClockResult<DomainId> {
forest.add_domain("ppu", master, 1, DOT_DIVIDER)
}
struct Shared {
engine: Mutex<Engine>,
links: Mutex<Links>,
}
#[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);
(result, engine.nmi_active())
};
self.drive_nmi(nmi);
result
}
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));
}
}
}
pub struct NesPpu {
shared: Arc<Shared>,
}
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 warmup = r.or("warmup", true)?;
let decay = r.or("open-bus-decay-dots", DEFAULT_DECAY_DOTS)?;
r.finish()?;
Ok(NesPpu {
shared: Arc::new(Shared {
engine: Mutex::with_rank(LockRank::BUS, Engine::new(warmup, decay)),
links: Mutex::with_rank(LockRank::WIRE, Links::default()),
}),
})
}
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 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);
(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: "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: 1,
summary: "NES / Famicom picture processing unit (RP2C02)",
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<()> {
if self.shared.engine.lock().bus.is_none() {
return Err(ctx.error(
"no PPU address space attached: call attach_bus with the $0000-$3FFF space \
the cartridge provides",
));
}
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 = { self.shared.engine.lock().load(r) };
let nmi = self.shared.engine.lock().nmi_active();
self.shared.drive_nmi(nmi);
result
}
}
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);
};
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);
}
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)
}
}