use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, Initiator, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::Props;
use crate::core::sched::{AccessKind, LazyHandle};
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::{AtomicU32, AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::Width;
use crate::core::wire::{Level, WireSource};
pub const DOTS_PER_LINE: u64 = 456;
pub const LINES_PER_FRAME: u64 = 154;
pub const DOTS_PER_FRAME: u64 = DOTS_PER_LINE * LINES_PER_FRAME;
pub const VBLANK_LINE: u8 = 144;
pub const SCREEN_WIDTH: usize = 160;
pub const SCREEN_HEIGHT: usize = 144;
pub const FRAMEBUFFER_LEN: usize = SCREEN_WIDTH * SCREEN_HEIGHT;
pub const OAM_SCAN_DOTS: u64 = 80;
pub const MODE3_MIN_DOTS: u64 = 172;
pub const VRAM_LEN: u64 = 0x2000;
pub const OAM_LEN: u64 = 0xa0;
pub const VRAM_BASE: u64 = 0x8000;
pub const OAM_BASE: u64 = 0xfe00;
pub const REGISTER_BASE: u64 = 0xff40;
pub const REGISTER_LEN: u64 = 12;
pub const VRAM_REGION: &str = "vram";
pub const OAM_REGION: &str = "oam";
pub const REGISTER_REGION: &str = "regs";
pub const VBLANK_PIN: &str = "vblank";
pub const STAT_PIN: &str = "stat";
pub const DMA_BYTES: u64 = 160;
const CLOCKS_PER_MCYCLE: u64 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Mode {
HBlank,
VBlank,
OamScan,
Drawing,
}
impl Mode {
#[must_use]
pub const fn bits(self) -> u8 {
match self {
Mode::HBlank => 0,
Mode::VBlank => 1,
Mode::OamScan => 2,
Mode::Drawing => 3,
}
}
}
pub mod lcdc {
pub const BG_ENABLE: u8 = 0x01;
pub const OBJ_ENABLE: u8 = 0x02;
pub const OBJ_TALL: u8 = 0x04;
pub const BG_MAP: u8 = 0x08;
pub const TILE_DATA: u8 = 0x10;
pub const WINDOW_ENABLE: u8 = 0x20;
pub const WINDOW_MAP: u8 = 0x40;
pub const LCD_ENABLE: u8 = 0x80;
}
pub mod stat {
pub const HBLANK_INT: u8 = 0x08;
pub const VBLANK_INT: u8 = 0x10;
pub const OAM_INT: u8 = 0x20;
pub const LYC_INT: u8 = 0x40;
pub const LYC_EQUAL: u8 = 0x04;
pub const WRITABLE: u8 = 0x78;
}
#[derive(Clone)]
struct Engine {
vram: Vec<u8>,
oam: Vec<u8>,
fb: Vec<u8>,
lcdc: u8,
stat: u8,
scy: u8,
scx: u8,
ly: u8,
lyc: u8,
bgp: u8,
obp0: u8,
obp1: u8,
wy: u8,
wx: u8,
dma_source: u8,
dot: u64,
dots: u64,
frame: u64,
mode3_len: u64,
window_line: u8,
window_active: bool,
dma_remaining: u64,
dma_next_dot: u64,
dma_page: u8,
}
impl fmt::Debug for Engine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Engine")
.field("lcdc", &self.lcdc)
.field("ly", &self.ly)
.field("dot", &self.dot)
.field("mode", &self.mode())
.finish_non_exhaustive()
}
}
impl Engine {
fn new() -> Engine {
Engine {
vram: vec![0; VRAM_LEN as usize],
oam: vec![0; OAM_LEN as usize],
fb: vec![0; FRAMEBUFFER_LEN],
lcdc: lcdc::LCD_ENABLE | lcdc::BG_ENABLE | lcdc::TILE_DATA,
stat: 0,
scy: 0,
scx: 0,
ly: 0,
lyc: 0,
bgp: 0xfc,
obp0: 0xff,
obp1: 0xff,
wy: 0,
wx: 0,
dma_source: 0xff,
dot: 0,
dots: 0,
frame: 0,
mode3_len: MODE3_MIN_DOTS,
window_line: 0,
window_active: false,
dma_remaining: 0,
dma_next_dot: 0,
dma_page: 0,
}
}
fn lcd_on(&self) -> bool {
self.lcdc & lcdc::LCD_ENABLE != 0
}
fn mode(&self) -> Mode {
if !self.lcd_on() {
return Mode::HBlank;
}
if self.ly >= VBLANK_LINE {
return Mode::VBlank;
}
if self.dot < OAM_SCAN_DOTS {
Mode::OamScan
} else if self.dot < OAM_SCAN_DOTS + self.mode3_len {
Mode::Drawing
} else {
Mode::HBlank
}
}
fn visible_ly(&self) -> u8 {
if !self.lcd_on() {
return 0;
}
if self.ly == 153 && self.dot >= 4 {
0
} else {
self.ly
}
}
fn lyc_equal(&self) -> bool {
self.lcd_on() && self.visible_ly() == self.lyc
}
fn read_stat(&self) -> u8 {
0x80 | (self.stat & stat::WRITABLE)
| if self.lyc_equal() { stat::LYC_EQUAL } else { 0 }
| self.mode().bits()
}
fn stat_line(&self) -> bool {
if !self.lcd_on() {
return false;
}
let s = self.stat;
(s & stat::LYC_INT != 0 && self.lyc_equal())
|| match self.mode() {
Mode::HBlank => s & stat::HBLANK_INT != 0,
Mode::VBlank => s & stat::VBLANK_INT != 0,
Mode::OamScan => s & stat::OAM_INT != 0,
Mode::Drawing => false,
}
}
fn vblank_line(&self) -> bool {
self.lcd_on() && self.ly >= VBLANK_LINE
}
fn vram_readable(&self) -> bool {
self.mode() != Mode::Drawing
}
fn oam_readable(&self) -> bool {
!matches!(self.mode(), Mode::Drawing | Mode::OamScan) && self.dma_remaining == 0
}
fn compute_mode3(&self) -> u64 {
let mut len = MODE3_MIN_DOTS + u64::from(self.scx & 7);
if self.window_visible_on_line() {
len += 6;
}
if self.lcdc & lcdc::OBJ_ENABLE != 0 {
let objects = self.objects_on_line();
let mut last_tile = u16::MAX;
for obj in &objects {
let tile = (u16::from(obj.x).wrapping_add(u16::from(self.scx))) / 8;
if tile == last_tile {
len += 6;
} else {
len += 11 - u64::from(obj.x.wrapping_add(self.scx) & 7).min(5);
last_tile = tile;
}
}
}
len.min(DOTS_PER_LINE - OAM_SCAN_DOTS - 4)
}
fn window_visible_on_line(&self) -> bool {
self.lcdc & lcdc::WINDOW_ENABLE != 0
&& self.lcdc & lcdc::BG_ENABLE != 0
&& self.ly >= self.wy
&& self.wx <= 166
}
fn vram_byte(&self, addr: u16) -> u8 {
self.vram[(addr as usize) & (VRAM_LEN as usize - 1)]
}
fn shade(palette: u8, index: u8) -> u8 {
(palette >> (index * 2)) & 3
}
fn tile_pixel(&self, tile_addr: u16, x: u8, y: u8) -> u8 {
let row = tile_addr + u16::from(y % 8) * 2;
let lo = self.vram_byte(row);
let hi = self.vram_byte(row + 1);
let bit = 7 - (x % 8);
((hi >> bit) & 1) << 1 | ((lo >> bit) & 1)
}
fn tile_address(&self, index: u8) -> u16 {
if self.lcdc & lcdc::TILE_DATA != 0 {
0x8000 + u16::from(index) * 16
} else {
(0x9000i32 + i32::from(index as i8) * 16) as u16
}
}
fn render_line(&mut self) {
let ly = self.ly;
if usize::from(ly) >= SCREEN_HEIGHT {
return;
}
let row = usize::from(ly) * SCREEN_WIDTH;
let mut bg_index = [0u8; SCREEN_WIDTH];
let bg_on = self.lcdc & lcdc::BG_ENABLE != 0;
let window_here = self.window_visible_on_line();
let mut window_drawn = false;
#[allow(clippy::needless_range_loop)]
for x in 0..SCREEN_WIDTH {
let index = if !bg_on {
0
} else if window_here && (x as i16) >= i16::from(self.wx) - 7 {
window_drawn = true;
let wx = (x as i16 - (i16::from(self.wx) - 7)) as u16;
let wy = u16::from(self.window_line);
let map = if self.lcdc & lcdc::WINDOW_MAP != 0 {
0x9c00
} else {
0x9800
};
let tile = self.vram_byte(map + (wy / 8) * 32 + (wx / 8));
self.tile_pixel(self.tile_address(tile), wx as u8, wy as u8)
} else {
let bx = (x as u16 + u16::from(self.scx)) & 0xff;
let by = (u16::from(ly) + u16::from(self.scy)) & 0xff;
let map = if self.lcdc & lcdc::BG_MAP != 0 {
0x9c00
} else {
0x9800
};
let tile = self.vram_byte(map + (by / 8) * 32 + (bx / 8));
self.tile_pixel(self.tile_address(tile), bx as u8, by as u8)
};
bg_index[x] = index;
self.fb[row + x] = Engine::shade(self.bgp, index);
}
if window_drawn {
self.window_line = self.window_line.wrapping_add(1);
self.window_active = true;
}
if self.lcdc & lcdc::OBJ_ENABLE == 0 {
return;
}
let height: u8 = if self.lcdc & lcdc::OBJ_TALL != 0 {
16
} else {
8
};
let mut objects = self.objects_on_line();
objects.sort_by_key(|o| core::cmp::Reverse(o.x));
for obj in objects {
let mut line = ly.wrapping_add(16).wrapping_sub(obj.y);
if obj.attrs & 0x40 != 0 {
line = height - 1 - line;
}
let tile = if height == 16 {
(obj.tile & 0xfe) + u8::from(line >= 8)
} else {
obj.tile
};
let addr = 0x8000 + u16::from(tile) * 16;
let palette = if obj.attrs & 0x10 != 0 {
self.obp1
} else {
self.obp0
};
for px in 0..8u8 {
let sx = i16::from(obj.x) - 8 + i16::from(px);
if sx < 0 || sx >= SCREEN_WIDTH as i16 {
continue;
}
let sx = sx as usize;
let tx = if obj.attrs & 0x20 != 0 { 7 - px } else { px };
let index = self.tile_pixel(addr, tx, line % 8);
if index == 0 {
continue;
}
if obj.attrs & 0x80 != 0 && bg_index[sx] != 0 {
continue;
}
self.fb[row + sx] = Engine::shade(palette, index);
}
}
}
fn objects_on_line(&self) -> Vec<Object> {
let height: i16 = if self.lcdc & lcdc::OBJ_TALL != 0 {
16
} else {
8
};
let ly = i16::from(self.ly);
let mut out = Vec::with_capacity(10);
for i in 0..40usize {
let y = self.oam[i * 4];
let top = i16::from(y) - 16;
if ly < top || ly >= top + height {
continue;
}
out.push(Object {
y,
x: self.oam[i * 4 + 1],
tile: self.oam[i * 4 + 2],
attrs: self.oam[i * 4 + 3],
});
if out.len() == 10 {
break;
}
}
out
}
fn step_dot(&mut self) -> bool {
self.dots += 1;
if !self.lcd_on() {
return false;
}
let entering_mode3 = self.ly < VBLANK_LINE && self.dot == OAM_SCAN_DOTS - 1;
self.dot += 1;
if entering_mode3 {
self.mode3_len = self.compute_mode3();
self.render_line();
}
if self.dot < DOTS_PER_LINE {
return false;
}
self.dot = 0;
self.ly += 1;
if u64::from(self.ly) < LINES_PER_FRAME {
if self.ly < VBLANK_LINE {
self.mode3_len = MODE3_MIN_DOTS;
}
return false;
}
self.ly = 0;
self.frame += 1;
self.window_line = 0;
self.window_active = false;
true
}
fn next_boundary(&self) -> Option<u64> {
if !self.lcd_on() {
return None;
}
let candidates: [u64; 4] = if self.ly < VBLANK_LINE {
[
OAM_SCAN_DOTS,
OAM_SCAN_DOTS + self.mode3_len,
DOTS_PER_LINE,
DOTS_PER_LINE,
]
} else if self.ly == 153 {
[4, DOTS_PER_LINE, DOTS_PER_LINE, DOTS_PER_LINE]
} else {
[DOTS_PER_LINE; 4]
};
candidates
.into_iter()
.filter(|c| *c > self.dot)
.min()
.map(|c| c - self.dot)
}
fn read_register(&self, index: u8) -> u8 {
match index {
0x00 => self.lcdc,
0x01 => self.read_stat(),
0x02 => self.scy,
0x03 => self.scx,
0x04 => self.visible_ly(),
0x05 => self.lyc,
0x06 => self.dma_source,
0x07 => self.bgp,
0x08 => self.obp0,
0x09 => self.obp1,
0x0a => self.wy,
0x0b => self.wx,
_ => 0xff,
}
}
fn write_register(&mut self, index: u8, value: u8) -> Option<u8> {
match index {
0x00 => {
let was_on = self.lcd_on();
self.lcdc = value;
let now_on = self.lcd_on();
if was_on && !now_on {
self.ly = 0;
self.dot = 0;
self.window_line = 0;
self.window_active = false;
} else if !was_on && now_on {
self.ly = 0;
self.dot = 0;
self.mode3_len = self.compute_mode3();
}
}
0x01 => self.stat = value & stat::WRITABLE,
0x02 => self.scy = value,
0x03 => self.scx = value,
0x04 => {}
0x05 => self.lyc = value,
0x06 => {
self.dma_source = value;
return Some(value);
}
0x07 => self.bgp = value,
0x08 => self.obp0 = value,
0x09 => self.obp1 = value,
0x0a => self.wy = value,
0x0b => self.wx = value,
_ => {}
}
None
}
}
#[derive(Debug, Clone, Copy)]
struct Object {
y: u8,
x: u8,
tile: u8,
attrs: u8,
}
pub const ENGINE_LOCK_RANK: LockRank = LockRank::new(0x4800);
struct Shared {
engine: Mutex<Engine>,
links: Mutex<Links>,
lazy: Mutex<Option<LazyHandle>>,
space: Mutex<Option<Arc<AddressSpace>>>,
dots: AtomicU64,
next_event: AtomicU64,
requester: AtomicU32,
}
#[derive(Debug, Default)]
struct Links {
vblank: Option<WireSource>,
stat: Option<WireSource>,
}
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 publish(&self, engine: &Engine) {
self.dots.store(engine.dots, Ordering::Relaxed);
let event = match engine.next_boundary() {
Some(delta) => engine.dots + delta,
None => u64::MAX,
};
self.next_event.store(event, Ordering::Relaxed);
}
fn drive(&self, vblank: bool, stat: bool) {
let (v, s) = {
let links = self.links.lock();
(links.vblank.clone(), links.stat.clone())
};
if let Some(v) = v {
v.set(Level::from_bool(vblank));
}
if let Some(s) = s {
s.set(Level::from_bool(stat));
}
}
fn with_engine<R>(&self, f: impl FnOnce(&mut Engine) -> R) -> R {
let (result, vblank, stat) = {
let mut engine = self.engine.lock();
let result = f(&mut engine);
self.publish(&engine);
(result, engine.vblank_line(), engine.stat_line())
};
self.drive(vblank, stat);
result
}
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 GbPpu {
shared: Arc<Shared>,
vram_region: RegionRef,
oam_region: RegionRef,
regs_region: RegionRef,
}
impl fmt::Debug for GbPpu {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GbPpu")
.field("engine", &self.shared.engine)
.finish_non_exhaustive()
}
}
impl Default for GbPpu {
fn default() -> Self {
GbPpu::new()
}
}
impl GbPpu {
#[must_use]
pub fn new() -> GbPpu {
let engine = Engine::new();
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),
space: Mutex::new(None),
dots: AtomicU64::new(0),
next_event: AtomicU64::new(OAM_SCAN_DOTS),
requester: AtomicU32::new(0),
});
let vram_region = Arc::new(MmioRegion::io(
"gb.ppu.vram",
VRAM_LEN,
Arc::new(VideoRam {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
let oam_region = Arc::new(MmioRegion::io(
"gb.ppu.oam",
OAM_LEN,
Arc::new(ObjectRam {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
let regs_region = Arc::new(MmioRegion::io(
"gb.ppu.regs",
REGISTER_LEN,
Arc::new(LcdPort {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
GbPpu {
shared,
vram_region,
oam_region,
regs_region,
}
}
pub fn from_props(props: &Props) -> Result<GbPpu> {
props.reader().finish()?;
Ok(GbPpu::new())
}
pub fn attach_space(&self, space: Arc<AddressSpace>) {
*self.shared.space.lock() = Some(space);
}
pub fn attach_vblank(&self, source: WireSource) {
self.shared.links.lock().vblank = Some(source);
}
pub fn attach_stat(&self, source: WireSource) {
self.shared.links.lock().stat = Some(source);
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
#[must_use]
pub fn dots(&self) -> u64 {
self.shared.dots.load(Ordering::Relaxed)
}
#[must_use]
pub fn frame(&self) -> u64 {
self.shared.engine.lock().frame
}
#[must_use]
pub fn position(&self) -> (u8, u64) {
let engine = self.shared.engine.lock();
(engine.ly, engine.dot)
}
#[must_use]
pub fn mode(&self) -> Mode {
self.shared.engine.lock().mode()
}
#[must_use]
pub fn read_register(&self, index: u8) -> u8 {
self.shared.engine.lock().read_register(index)
}
pub fn write_register(&self, index: u8, value: u8) {
let page = self.shared.with_engine(|e| e.write_register(index, value));
if let Some(page) = page {
self.start_dma(page);
}
}
#[must_use]
pub fn peek_vram(&self, offset: u64) -> u8 {
let engine = self.shared.engine.lock();
engine.vram[(offset as usize) % engine.vram.len()]
}
pub fn poke_vram(&self, offset: u64, value: u8) {
let mut engine = self.shared.engine.lock();
let len = engine.vram.len();
engine.vram[(offset as usize) % len] = value;
}
#[must_use]
pub fn peek_oam(&self, offset: u64) -> u8 {
let engine = self.shared.engine.lock();
engine.oam[(offset as usize) % engine.oam.len()]
}
pub fn poke_oam(&self, offset: u64, value: u8) {
let mut engine = self.shared.engine.lock();
let len = engine.oam.len();
engine.oam[(offset as usize) % len] = value;
}
pub fn with_framebuffer<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
let engine = self.shared.engine.lock();
f(&engine.fb)
}
#[must_use]
pub fn pixel(&self, x: usize, y: usize) -> Option<u8> {
if x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT {
return None;
}
Some(self.shared.engine.lock().fb[y * SCREEN_WIDTH + x])
}
fn start_dma(&self, page: u8) {
let mut engine = self.shared.engine.lock();
engine.dma_page = page;
engine.dma_remaining = DMA_BYTES;
engine.dma_next_dot = engine.dots + CLOCKS_PER_MCYCLE;
}
pub fn advance_to(&self, target: u64) {
let pending = self.pump_dma(target);
let (vblank, stat) = {
let mut engine = self.shared.engine.lock();
for (index, byte) in pending {
let len = engine.oam.len();
engine.oam[(index as usize) % len] = byte;
}
while engine.dots < target {
if !engine.lcd_on() {
engine.dots = target;
break;
}
engine.step_dot();
}
self.shared.publish(&engine);
(engine.vblank_line(), engine.stat_line())
};
self.shared.drive(vblank, stat);
}
fn pump_dma(&self, target: u64) -> Vec<(u64, u8)> {
let (page, first, count) = {
let engine = self.shared.engine.lock();
if engine.dma_remaining == 0 || engine.dma_next_dot > target {
return Vec::new();
}
let span = target - engine.dma_next_dot;
let count = (1 + span / CLOCKS_PER_MCYCLE).min(engine.dma_remaining);
let first = DMA_BYTES - engine.dma_remaining;
(engine.dma_page, first, count)
};
let space = self.shared.space.lock().clone();
let attrs = MemAttrs::DEFAULT
.with_requester(RequesterId(self.shared.requester.load(Ordering::Relaxed)));
let mut out = Vec::with_capacity(count as usize);
for i in 0..count {
let index = first + i;
let addr = (u64::from(page) << 8) | index;
let byte = if (VRAM_BASE..VRAM_BASE + VRAM_LEN).contains(&addr) {
self.shared.engine.lock().vram[(addr - VRAM_BASE) as usize]
} else {
match &space {
Some(space) => space.read(addr, Width::U8, attrs).unwrap_or(0xff) as u8,
None => 0xff,
}
};
out.push((index, byte));
}
{
let mut engine = self.shared.engine.lock();
engine.dma_remaining -= count;
engine.dma_next_dot += count * CLOCKS_PER_MCYCLE;
}
out
}
pub fn advance_by(&self, dots: u64) {
let target = self.shared.dots.load(Ordering::Relaxed) + dots;
self.advance_to(target);
}
}
struct VideoRam {
shared: Arc<Shared>,
}
impl fmt::Debug for VideoRam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VideoRam").finish_non_exhaustive()
}
}
impl MemOps for VideoRam {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
let engine = self.shared.engine.lock();
*byte = if attrs.debug || engine.vram_readable() {
engine.vram[(offset as usize) % engine.vram.len()]
} else {
0xff
};
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
let mut engine = self.shared.engine.lock();
if !attrs.debug && !engine.vram_readable() {
return Ok(());
}
let len = engine.vram.len();
engine.vram[(offset as usize) % len] = *value;
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
struct ObjectRam {
shared: Arc<Shared>,
}
impl fmt::Debug for ObjectRam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ObjectRam").finish_non_exhaustive()
}
}
impl MemOps for ObjectRam {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
let engine = self.shared.engine.lock();
*byte = if attrs.debug || engine.oam_readable() {
engine.oam[(offset as usize) % engine.oam.len()]
} else {
0xff
};
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
let mut engine = self.shared.engine.lock();
if !attrs.debug && !engine.oam_readable() {
return Ok(());
}
let len = engine.oam.len();
engine.oam[(offset as usize) % len] = *value;
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
struct LcdPort {
shared: Arc<Shared>,
}
impl fmt::Debug for LcdPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LcdPort").finish_non_exhaustive()
}
}
impl MemOps for LcdPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
*byte = self.shared.engine.lock().read_register(offset as u8);
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 page = self
.shared
.with_engine(|e| e.write_register(offset as u8, *value));
if let Some(page) = page {
let mut engine = self.shared.engine.lock();
engine.dma_page = page;
engine.dma_remaining = DMA_BYTES;
engine.dma_next_dot = engine.dots + CLOCKS_PER_MCYCLE;
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "gb.ppu",
version: 1,
summary: "Game Boy LCD controller: VRAM, OAM, $FF40-$FF4B, OAM DMA",
properties: &[],
construct: |props| Ok(Box::new(GbPpu::from_props(props)?) as Box<dyn Device>),
};
pub fn register(reg: &mut crate::core::Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for GbPpu {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, ctx: &mut RealizeCtx<'_>) -> Result<()> {
self.shared
.requester
.store(ctx.requester().0, Ordering::Relaxed);
let (vblank, stat) = {
let engine = self.shared.engine.lock();
(engine.vblank_line(), engine.stat_line())
};
self.shared.drive(vblank, stat);
Ok(())
}
fn unrealize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
self.shared.drive(false, false);
let mut links = self.shared.links.lock();
links.vblank = None;
links.stat = None;
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
VRAM_REGION => Some(Arc::clone(&self.vram_region)),
OAM_REGION => Some(Arc::clone(&self.oam_region)),
REGISTER_REGION => Some(Arc::clone(&self.regs_region)),
_ => None,
}
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
match port {
VBLANK_PIN => self.attach_vblank(source),
STAT_PIN => self.attach_stat(source),
_ => {
return Err(Error::Config {
at: String::from(port),
message: alloc::format!(
"the LCD controller drives `{VBLANK_PIN}` and `{STAT_PIN}`, nothing else"
),
});
}
}
Ok(())
}
fn announce(&self, port: &str) {
let (vblank, stat) = {
let engine = self.shared.engine.lock();
(engine.vblank_line(), engine.stat_line())
};
match port {
VBLANK_PIN | STAT_PIN => self.shared.drive(vblank, stat),
_ => {}
}
}
fn reset(&self, _kind: ResetKind) {
self.shared.with_engine(|e| *e = Engine::new());
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let engine = self.shared.engine.lock();
w.write_bytes(&engine.vram)?;
w.write_bytes(&engine.oam)?;
w.write_bytes(&engine.fb)?;
for byte in [
engine.lcdc,
engine.stat,
engine.scy,
engine.scx,
engine.ly,
engine.lyc,
engine.bgp,
engine.obp0,
engine.obp1,
engine.wy,
engine.wx,
engine.dma_source,
engine.window_line,
engine.dma_page,
] {
w.write_u8(byte)?;
}
w.write_bool(engine.window_active)?;
w.write_u64(engine.dot)?;
w.write_u64(engine.dots)?;
w.write_u64(engine.frame)?;
w.write_u64(engine.mode3_len)?;
w.write_u64(engine.dma_remaining)?;
w.write_u64(engine.dma_next_dot)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let vram = r.read_bytes()?;
let oam = r.read_bytes()?;
let fb = r.read_bytes()?;
if vram.len() as u64 != VRAM_LEN
|| oam.len() as u64 != OAM_LEN
|| fb.len() != FRAMEBUFFER_LEN
{
return Err(Error::State(String::from(
"the LCD controller's snapshot has the wrong memory sizes",
)));
}
let (vblank, stat) = {
let mut engine = self.shared.engine.lock();
engine.vram.copy_from_slice(vram);
engine.oam.copy_from_slice(oam);
engine.fb.copy_from_slice(fb);
engine.lcdc = r.read_u8()?;
engine.stat = r.read_u8()?;
engine.scy = r.read_u8()?;
engine.scx = r.read_u8()?;
engine.ly = r.read_u8()?;
engine.lyc = r.read_u8()?;
engine.bgp = r.read_u8()?;
engine.obp0 = r.read_u8()?;
engine.obp1 = r.read_u8()?;
engine.wy = r.read_u8()?;
engine.wx = r.read_u8()?;
engine.dma_source = r.read_u8()?;
engine.window_line = r.read_u8()?;
engine.dma_page = r.read_u8()?;
engine.window_active = r.read_bool()?;
engine.dot = r.read_u64()?;
engine.dots = r.read_u64()?;
engine.frame = r.read_u64()?;
engine.mode3_len = r.read_u64()?;
engine.dma_remaining = r.read_u64()?;
engine.dma_next_dot = r.read_u64()?;
self.shared.publish(&engine);
(engine.vblank_line(), engine.stat_line())
};
self.shared.drive(vblank, stat);
Ok(())
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.dots.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
GbPpu::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
let event = self.shared.next_event.load(Ordering::Relaxed);
(event != u64::MAX).then_some(event)
}
fn attach_lazy(&self, handle: LazyHandle) {
GbPpu::attach_lazy(self, handle);
}
}
impl Initiator for GbPpu {
fn requester(&self) -> RequesterId {
RequesterId(self.shared.requester.load(Ordering::Relaxed))
}
}
impl crate::machine::Instance for GbPpu {
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(
"the LCD controller needs the CPU's address space to run an OAM DMA through: \
add `space = cpubus` to the object",
),
})?;
self.attach_space(Arc::clone(space));
self.shared
.requester
.store(ctx.requester().0, Ordering::Relaxed);
Ok(())
}
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| Ok(Arc::new(GbPpu::from_props(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir};
ClassSchema::new(CLASS.name)
.port(VBLANK_PIN, PortDir::Out)
.port(STAT_PIN, PortDir::Out)
.region(VRAM_REGION)
.region(OAM_REGION)
.region(REGISTER_REGION)
}