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, 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::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::Width;
use crate::core::wire::{Level, WireSource};
pub const DOTS_PER_LINE: u64 = 342;
pub const HBLANK_DOT: u64 = 256;
pub const SCREEN_WIDTH: usize = 256;
pub const SCREEN_HEIGHT: usize = 240;
pub const FRAMEBUFFER_LEN: usize = SCREEN_WIDTH * SCREEN_HEIGHT;
pub const VRAM_LEN: usize = 0x4000;
pub const CRAM_LEN: usize = 32;
pub const REGISTER_COUNT: usize = 11;
pub const PORT_REGION: &str = "ports";
pub const COUNTER_REGION: &str = "counters";
pub const IRQ_PIN: &str = "irq";
const STATE_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TvRegion {
#[default]
Ntsc,
Pal,
}
impl TvRegion {
#[must_use]
pub fn from_name(name: &str) -> Option<TvRegion> {
match name {
"ntsc" => Some(TvRegion::Ntsc),
"pal" => Some(TvRegion::Pal),
_ => None,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
TvRegion::Ntsc => "ntsc",
TvRegion::Pal => "pal",
}
}
#[must_use]
pub const fn lines_per_frame(self) -> u16 {
match self {
TvRegion::Ntsc => 262,
TvRegion::Pal => 313,
}
}
const fn vcounter_runs(self, height: u16) -> &'static [(u8, u16)] {
match (self, height) {
(TvRegion::Ntsc, 192) => &[(0x00, 0xdb), (0xd5, 0x2b)],
(TvRegion::Ntsc, 224) => &[(0x00, 0xeb), (0xe5, 0x1b)],
(TvRegion::Ntsc, _) => &[(0x00, 0x100), (0x00, 0x06)],
(TvRegion::Pal, 192) => &[(0x00, 0xf3), (0xba, 0x46)],
(TvRegion::Pal, 224) => &[(0x00, 0x100), (0x00, 0x03), (0xca, 0x36)],
(TvRegion::Pal, _) => &[(0x00, 0x100), (0x00, 0x0b), (0xd2, 0x2e)],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VdpMode {
GraphicsI,
Text,
GraphicsII,
Multicolor,
Mode4 {
height: u16,
},
}
impl VdpMode {
#[must_use]
pub const fn active_height(self) -> u16 {
match self {
VdpMode::Mode4 { height } => height,
_ => 192,
}
}
#[must_use]
pub const fn is_mode4(self) -> bool {
matches!(self, VdpMode::Mode4 { .. })
}
}
const TMS_PALETTE: [u8; 16] = [
0x00, 0x00, 0x18, 0x1d, 0x35, 0x35, 0x16, 0x3d, 0x17, 0x17, 0x1a, 0x2b, 0x18, 0x26, 0x2a, 0x3f, ];
#[derive(Debug, Clone, Copy)]
struct Sprite {
x: i32,
pattern: usize,
zoom: bool,
}
struct Engine {
vram: Vec<u8>,
cram: Vec<u8>,
regs: [u8; REGISTER_COUNT],
addr: u16,
code: u8,
latch: bool,
first: u8,
buffer: u8,
status: u8,
line_counter: u8,
line_irq: bool,
vscroll_latch: u8,
region: TvRegion,
dot: u64,
line: u16,
dots: u64,
frame: u64,
fb: Vec<u8>,
}
impl fmt::Debug for Engine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Engine")
.field("mode", &self.mode())
.field("line", &self.line)
.field("dot", &self.dot)
.field("status", &self.status)
.field("regs", &self.regs)
.finish_non_exhaustive()
}
}
impl Engine {
fn new(region: TvRegion) -> Engine {
let mut engine = Engine {
vram: vec![0; VRAM_LEN],
cram: vec![0; CRAM_LEN],
regs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff],
addr: 0,
code: 0,
latch: false,
first: 0,
buffer: 0,
status: 0,
line_counter: 0xff,
line_irq: false,
vscroll_latch: 0,
region,
dot: 0,
line: 0,
dots: 0,
frame: 0,
fb: vec![0; FRAMEBUFFER_LEN],
};
engine.line_start();
engine
}
fn mode(&self) -> VdpMode {
let m1 = self.regs[1] & 0x10 != 0;
let m2 = self.regs[0] & 0x02 != 0;
let m3 = self.regs[1] & 0x08 != 0;
let m4 = self.regs[0] & 0x04 != 0;
if m4 {
let height = match (m1, m2, m3) {
(true, false, true) => 224,
(false, true, true) => 240,
_ => 192,
};
VdpMode::Mode4 { height }
} else if m1 {
VdpMode::Text
} else if m2 {
VdpMode::GraphicsII
} else if m3 {
VdpMode::Multicolor
} else {
VdpMode::GraphicsI
}
}
fn active_height(&self) -> u16 {
self.mode().active_height()
}
fn display_on(&self) -> bool {
self.regs[1] & 0x40 != 0
}
fn backdrop(&self) -> u8 {
if self.mode().is_mode4() {
self.cram[16 + (self.regs[7] & 0x0f) as usize] & 0x3f
} else {
TMS_PALETTE[(self.regs[7] & 0x0f) as usize]
}
}
fn irq_line(&self) -> bool {
let frame = self.status & 0x80 != 0 && self.regs[1] & 0x20 != 0;
let line = self.line_irq && self.regs[0] & 0x10 != 0;
frame || line
}
fn vcounter(&self) -> u8 {
let runs = self.region.vcounter_runs(self.active_height());
let mut line = self.line;
for &(first, count) in runs {
if line < count {
return first.wrapping_add(line as u8);
}
line -= count;
}
0xff
}
fn hcounter(&self) -> u8 {
let half = (self.dot / 2) as u8;
if half <= 0x93 {
half
} else {
0xe9u8.wrapping_add(half - 0x94)
}
}
fn next_boundary(&self) -> u64 {
if self.dot < HBLANK_DOT {
HBLANK_DOT - self.dot
} else {
DOTS_PER_LINE - self.dot
}
}
fn line_start(&mut self) {
if self.line == 0 {
self.vscroll_latch = self.regs[9];
}
let height = self.active_height();
if self.line < height {
self.render_line(self.line);
}
}
fn hblank(&mut self) {
let height = self.active_height();
if self.line <= height {
if self.line_counter == 0 {
self.line_counter = self.regs[10];
self.line_irq = true;
} else {
self.line_counter -= 1;
}
} else {
self.line_counter = self.regs[10];
}
if self.line == height {
self.status |= 0x80;
}
}
fn next_line(&mut self) {
self.line += 1;
if self.line >= self.region.lines_per_frame() {
self.line = 0;
self.frame += 1;
}
}
fn advance_to(&mut self, target: u64) {
while self.dots < target {
let step = (target - self.dots).min(self.next_boundary());
self.dot += step;
self.dots += step;
if self.dot == HBLANK_DOT {
self.hblank();
} else if self.dot == DOTS_PER_LINE {
self.dot = 0;
self.next_line();
self.line_start();
}
}
}
fn read_status(&mut self) -> u8 {
let value = self.peek_status();
self.status &= 0x1f;
self.line_irq = false;
self.latch = false;
value
}
fn peek_status(&self) -> u8 {
(self.status & 0xe0) | 0x1f
}
fn write_control(&mut self, value: u8) {
if !self.latch {
self.first = value;
self.latch = true;
self.addr = (self.addr & 0x3f00) | u16::from(value);
return;
}
self.latch = false;
self.code = (value >> 6) & 0x03;
self.addr = ((u16::from(value) & 0x3f) << 8) | u16::from(self.first);
match self.code {
0 => {
self.buffer = self.vram[self.addr as usize & 0x3fff];
self.addr = (self.addr + 1) & 0x3fff;
}
2 => {
let index = (value & 0x0f) as usize;
if index < REGISTER_COUNT {
self.regs[index] = self.first;
}
}
_ => {}
}
}
fn read_data(&mut self) -> u8 {
let value = self.buffer;
self.buffer = self.vram[self.addr as usize & 0x3fff];
self.addr = (self.addr + 1) & 0x3fff;
self.latch = false;
value
}
fn write_data(&mut self, value: u8) {
if self.code == 3 {
self.cram[self.addr as usize % CRAM_LEN] = value;
} else {
self.vram[self.addr as usize & 0x3fff] = value;
}
self.buffer = value;
self.addr = (self.addr + 1) & 0x3fff;
self.latch = false;
}
fn fb_row(&mut self, line: u16) -> &mut [u8] {
let start = line as usize * SCREEN_WIDTH;
&mut self.fb[start..start + SCREEN_WIDTH]
}
fn render_line(&mut self, line: u16) {
if !self.display_on() {
let colour = self.backdrop();
self.fb_row(line).fill(colour);
return;
}
match self.mode() {
VdpMode::Mode4 { .. } => self.render_mode4(line),
VdpMode::GraphicsI => self.render_tms_tiles(line, false),
VdpMode::GraphicsII => self.render_tms_tiles(line, true),
VdpMode::Multicolor => self.render_multicolor(line),
VdpMode::Text => self.render_text(line),
}
}
fn name_table_base(&self, height: u16) -> usize {
if height == 192 {
((self.regs[2] as usize) & 0x0e) << 10
} else {
(((self.regs[2] as usize) & 0x0c) << 10) | 0x700
}
}
fn render_mode4(&mut self, line: u16) {
let height = self.active_height();
let backdrop = self.backdrop();
let mut row = [backdrop; SCREEN_WIDTH];
let mut bg_priority = [false; SCREEN_WIDTH];
let nt_base = self.name_table_base(height);
let rows = if height == 192 { 28u16 } else { 32u16 };
let nt_height = rows * 8;
let hscroll = if self.regs[0] & 0x40 != 0 && line < 16 {
0
} else {
self.regs[8]
};
let vscroll = self.vscroll_latch;
for x in 0..SCREEN_WIDTH {
let locked = self.regs[0] & 0x80 != 0 && x >= 192;
let src_y = if locked {
line
} else {
(line + u16::from(vscroll)) % nt_height
};
let src_x = (x as u8).wrapping_sub(hscroll);
let col = (src_x / 8) as usize;
let tile_row = (src_y / 8) as usize;
let entry = nt_base + (tile_row * 32 + col) * 2;
let low = self.vram[entry & 0x3fff];
let high = self.vram[(entry + 1) & 0x3fff];
let word = u16::from(low) | (u16::from(high) << 8);
let tile = (word & 0x1ff) as usize;
let hflip = word & 0x200 != 0;
let vflip = word & 0x400 != 0;
let palette = if word & 0x800 != 0 { 16 } else { 0 };
let priority = word & 0x1000 != 0;
let mut px = src_x % 8;
let mut py = (src_y % 8) as u8;
if hflip {
px = 7 - px;
}
if vflip {
py = 7 - py;
}
let index = self.tile_pixel(tile * 32 + py as usize * 4, px);
row[x] = self.cram[palette + index as usize] & 0x3f;
bg_priority[x] = priority && index != 0;
}
self.draw_mode4_sprites(line, &mut row, &bg_priority);
if self.regs[0] & 0x20 != 0 {
row[..8].fill(backdrop);
}
self.fb_row(line).copy_from_slice(&row);
}
fn tile_pixel(&self, base: usize, x: u8) -> u8 {
let bit = 7 - x;
let mut index = 0u8;
for plane in 0..4 {
let byte = self.vram[(base + plane) & 0x3fff];
index |= ((byte >> bit) & 1) << plane;
}
index
}
fn draw_mode4_sprites(
&mut self,
line: u16,
row: &mut [u8; SCREEN_WIDTH],
bg_priority: &[bool; SCREEN_WIDTH],
) {
let sat = ((self.regs[5] as usize) & 0x7e) << 7;
let pattern_base = ((self.regs[6] as usize) & 0x04) << 11;
let tall = self.regs[1] & 0x02 != 0;
let zoom = self.regs[1] & 0x01 != 0;
let shift = if self.regs[0] & 0x08 != 0 { 8 } else { 0 };
let base_height: u16 = if tall { 16 } else { 8 };
let height = if zoom { base_height * 2 } else { base_height };
let terminates = self.active_height() == 192;
let mut chosen: [Option<Sprite>; 8] = [None; 8];
let mut count = 0usize;
for i in 0..64usize {
let y = self.vram[(sat + i) & 0x3fff];
if y == 0xd0 && terminates {
break;
}
let top = u16::from(y).wrapping_add(1) & 0xff;
let delta = line.wrapping_sub(top) & 0xff;
if delta >= height {
continue;
}
if count == 8 {
self.status |= 0x40;
break;
}
let x = i32::from(self.vram[(sat + 0x80 + i * 2) & 0x3fff]) - shift;
let mut tile = self.vram[(sat + 0x81 + i * 2) & 0x3fff] as usize;
if tall {
tile &= 0xfe;
}
let py = if zoom { delta / 2 } else { delta };
chosen[count] = Some(Sprite {
x,
pattern: pattern_base + tile * 32 + py as usize * 4,
zoom,
});
count += 1;
}
let mut claimed = [false; SCREEN_WIDTH];
for sprite in chosen.iter().flatten() {
for i in 0..8i32 {
let index = self.tile_pixel(sprite.pattern, i as u8);
if index == 0 {
continue;
}
let width = if sprite.zoom { 2 } else { 1 };
for step in 0..width {
let x = sprite.x + i * width + step;
if !(0..SCREEN_WIDTH as i32).contains(&x) {
continue;
}
let x = x as usize;
if claimed[x] {
self.status |= 0x20;
continue;
}
claimed[x] = true;
if bg_priority[x] {
continue;
}
row[x] = self.cram[16 + index as usize] & 0x3f;
}
}
}
}
fn render_tms_tiles(&mut self, line: u16, graphics_ii: bool) {
let backdrop = self.backdrop();
let mut row = [backdrop; SCREEN_WIDTH];
let nt = ((self.regs[2] as usize) & 0x0f) << 10;
let tile_row = (line / 8) as usize;
let py = (line % 8) as usize;
for col in 0..32usize {
let name = self.vram[(nt + tile_row * 32 + col) & 0x3fff] as usize;
let (pattern, colour) = if graphics_ii {
let bank = (tile_row / 8) * 0x100;
let pattern_base = ((self.regs[4] as usize) & 0x04) << 11;
let pattern_mask = (((self.regs[4] as usize) & 0x03) << 8) | 0xff;
let colour_base = ((self.regs[3] as usize) & 0x80) << 6;
let colour_mask = (((self.regs[3] as usize) & 0x7f) << 3) | 0x07;
(
pattern_base + ((bank + name) & pattern_mask) * 8 + py,
colour_base + ((bank + name) & colour_mask) * 8 + py,
)
} else {
let pattern_base = ((self.regs[4] as usize) & 0x07) << 11;
let colour_base = (self.regs[3] as usize) << 6;
(pattern_base + name * 8 + py, colour_base + name / 8)
};
let bits = self.vram[pattern & 0x3fff];
let pair = self.vram[colour & 0x3fff];
for x in 0..8usize {
let on = bits & (0x80 >> x) != 0;
let index = if on { pair >> 4 } else { pair & 0x0f };
row[col * 8 + x] = if index == 0 {
backdrop
} else {
TMS_PALETTE[index as usize]
};
}
}
self.draw_tms_sprites(line, &mut row);
self.fb_row(line).copy_from_slice(&row);
}
fn render_multicolor(&mut self, line: u16) {
let backdrop = self.backdrop();
let mut row = [backdrop; SCREEN_WIDTH];
let nt = ((self.regs[2] as usize) & 0x0f) << 10;
let pattern_base = ((self.regs[4] as usize) & 0x07) << 11;
let tile_row = (line / 8) as usize;
let offset = ((line / 4) % 2) as usize + ((tile_row % 4) * 2);
for col in 0..32usize {
let name = self.vram[(nt + tile_row * 32 + col) & 0x3fff] as usize;
let byte = self.vram[(pattern_base + name * 8 + offset) & 0x3fff];
for half in 0..2usize {
let index = if half == 0 { byte >> 4 } else { byte & 0x0f };
let value = if index == 0 {
backdrop
} else {
TMS_PALETTE[index as usize]
};
let start = col * 8 + half * 4;
row[start..start + 4].fill(value);
}
}
self.draw_tms_sprites(line, &mut row);
self.fb_row(line).copy_from_slice(&row);
}
fn render_text(&mut self, line: u16) {
let backdrop = self.backdrop();
let mut row = [backdrop; SCREEN_WIDTH];
let nt = ((self.regs[2] as usize) & 0x0f) << 10;
let pattern_base = ((self.regs[4] as usize) & 0x07) << 11;
let fg = TMS_PALETTE[(self.regs[7] >> 4) as usize];
let bg = TMS_PALETTE[(self.regs[7] & 0x0f) as usize];
let tile_row = (line / 8) as usize;
let py = (line % 8) as usize;
for col in 0..40usize {
let name = self.vram[(nt + tile_row * 40 + col) & 0x3fff] as usize;
let bits = self.vram[(pattern_base + name * 8 + py) & 0x3fff];
for x in 0..6usize {
row[8 + col * 6 + x] = if bits & (0x80 >> x) != 0 { fg } else { bg };
}
}
self.fb_row(line).copy_from_slice(&row);
}
fn draw_tms_sprites(&mut self, line: u16, row: &mut [u8; SCREEN_WIDTH]) {
let sat = ((self.regs[5] as usize) & 0x7f) << 7;
let pattern_base = ((self.regs[6] as usize) & 0x07) << 11;
let large = self.regs[1] & 0x02 != 0;
let zoom = self.regs[1] & 0x01 != 0;
let base = if large { 16u16 } else { 8 };
let height = if zoom { base * 2 } else { base };
let mut drawn = 0usize;
let mut claimed = [false; SCREEN_WIDTH];
for entry_index in 0..32usize {
let entry = sat + entry_index * 4;
let y = self.vram[entry & 0x3fff];
if y == 0xd0 {
break;
}
let top = u16::from(y).wrapping_add(1) & 0xff;
let delta = line.wrapping_sub(top) & 0xff;
if delta >= height {
continue;
}
if drawn == 4 {
self.status |= 0x40;
break;
}
drawn += 1;
let colour_byte = self.vram[(entry + 3) & 0x3fff];
let colour = colour_byte & 0x0f;
if colour == 0 {
continue;
}
let x = i32::from(self.vram[(entry + 1) & 0x3fff])
- if colour_byte & 0x80 != 0 { 32 } else { 0 };
let name = self.vram[(entry + 2) & 0x3fff] as usize & if large { 0xfc } else { 0xff };
let py = if zoom { delta / 2 } else { delta } as usize;
let value = TMS_PALETTE[colour as usize];
let width = if large { 16usize } else { 8 };
for i in 0..width {
let half = (i / 8) * 16;
let byte = self.vram[(pattern_base + name * 8 + half + (py % 8)) & 0x3fff];
if byte & (0x80 >> (i % 8)) == 0 {
continue;
}
let scale = if zoom { 2i32 } else { 1 };
for step in 0..scale {
let sx = x + i as i32 * scale + step;
if !(0..SCREEN_WIDTH as i32).contains(&sx) {
continue;
}
let sx = sx as usize;
if claimed[sx] {
self.status |= 0x20;
continue;
}
claimed[sx] = true;
row[sx] = value;
}
}
}
}
}
struct Shared {
engine: Mutex<Engine>,
irq: Mutex<Option<WireSource>>,
lazy: Mutex<Option<LazyHandle>>,
dots: AtomicU64,
next_event: 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 publish(&self, engine: &Engine) {
self.dots.store(engine.dots, Ordering::Relaxed);
self.next_event
.store(engine.dots + engine.next_boundary(), Ordering::Relaxed);
}
fn drive(&self, level: bool) {
let source = self.irq.lock().clone();
if let Some(source) = source {
source.set(Level::from_bool(level));
}
}
fn with_engine<R>(&self, f: impl FnOnce(&mut Engine) -> R) -> R {
let (result, irq) = {
let mut engine = self.engine.lock();
let result = f(&mut engine);
self.publish(&engine);
(result, engine.irq_line())
};
self.drive(irq);
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 SmsVdp {
shared: Arc<Shared>,
port_region: RegionRef,
counter_region: RegionRef,
}
impl fmt::Debug for SmsVdp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SmsVdp")
.field("engine", &self.shared.engine)
.finish_non_exhaustive()
}
}
impl Default for SmsVdp {
fn default() -> Self {
SmsVdp::new(TvRegion::Ntsc)
}
}
impl SmsVdp {
#[must_use]
pub fn new(region: TvRegion) -> SmsVdp {
let engine = Engine::new(region);
let next = engine.dots + engine.next_boundary();
let shared = Arc::new(Shared {
engine: Mutex::with_rank(LockRank::DEVICE, engine),
irq: Mutex::with_rank(LockRank::WIRE, None),
lazy: Mutex::new(None),
dots: AtomicU64::new(0),
next_event: AtomicU64::new(next),
});
let port_region = Arc::new(MmioRegion::io(
"sms.vdp.ports",
2,
Arc::new(VdpPorts {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
let counter_region = Arc::new(MmioRegion::io(
"sms.vdp.counters",
2,
Arc::new(VdpCounters {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
SmsVdp {
shared,
port_region,
counter_region,
}
}
pub fn from_props(props: &Props) -> Result<SmsVdp> {
let mut r = props.reader();
let name = r.or_str("region", "ntsc")?;
let region = TvRegion::from_name(name).ok_or_else(|| Error::Config {
at: String::from("region"),
message: alloc::format!("`{name}` is not a television region; use `ntsc` or `pal`"),
})?;
r.finish()?;
Ok(SmsVdp::new(region))
}
#[must_use]
pub fn tv_region(&self) -> TvRegion {
self.shared.engine.lock().region
}
pub fn attach_irq(&self, source: WireSource) {
*self.shared.irq.lock() = Some(source);
let level = self.shared.engine.lock().irq_line();
self.shared.drive(level);
}
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) -> (u16, u64) {
let engine = self.shared.engine.lock();
(engine.line, engine.dot)
}
#[must_use]
pub fn mode(&self) -> VdpMode {
self.shared.engine.lock().mode()
}
#[must_use]
pub fn active_height(&self) -> u16 {
self.shared.engine.lock().active_height()
}
#[must_use]
pub fn register(&self, index: usize) -> u8 {
let engine = self.shared.engine.lock();
engine.regs.get(index).copied().unwrap_or(0)
}
#[must_use]
pub fn peek_status(&self) -> u8 {
self.shared.engine.lock().peek_status()
}
#[must_use]
pub fn peek_vram(&self, offset: usize) -> u8 {
self.shared.engine.lock().vram[offset & 0x3fff]
}
pub fn poke_vram(&self, offset: usize, value: u8) {
self.shared.engine.lock().vram[offset & 0x3fff] = value;
}
#[must_use]
pub fn peek_cram(&self, index: usize) -> u8 {
self.shared.engine.lock().cram[index % CRAM_LEN]
}
pub fn poke_cram(&self, index: usize, value: u8) {
self.shared.engine.lock().cram[index % CRAM_LEN] = value;
}
#[must_use]
pub fn irq_line(&self) -> bool {
self.shared.engine.lock().irq_line()
}
#[must_use]
pub fn vcounter(&self) -> u8 {
self.shared.engine.lock().vcounter()
}
#[must_use]
pub fn hcounter(&self) -> u8 {
self.shared.engine.lock().hcounter()
}
pub fn read_port(&self, offset: u64) -> u8 {
self.shared.with_engine(|e| {
if offset & 1 == 0 {
e.read_data()
} else {
e.read_status()
}
})
}
pub fn write_port(&self, offset: u64, value: u8) {
self.shared.with_engine(|e| {
if offset & 1 == 0 {
e.write_data(value);
} else {
e.write_control(value);
}
});
}
pub fn with_framebuffer<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
f(&self.shared.engine.lock().fb)
}
#[must_use]
pub fn pixel(&self, x: usize, y: usize) -> Option<u8> {
(x < SCREEN_WIDTH && y < SCREEN_HEIGHT)
.then(|| self.shared.engine.lock().fb[y * SCREEN_WIDTH + x])
}
pub fn advance_to(&self, target: u64) {
self.shared.with_engine(|e| e.advance_to(target));
}
pub fn advance_by(&self, dots: u64) {
let target = self.shared.dots.load(Ordering::Relaxed) + dots;
self.advance_to(target);
}
}
struct VdpPorts {
shared: Arc<Shared>,
}
impl fmt::Debug for VdpPorts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VdpPorts").finish_non_exhaustive()
}
}
impl MemOps for VdpPorts {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
if attrs.debug {
let engine = self.shared.engine.lock();
*byte = if offset & 1 == 0 {
engine.buffer
} else {
engine.peek_status()
};
return Ok(());
}
*byte = self.shared.with_engine(|e| {
if offset & 1 == 0 {
e.read_data()
} else {
e.read_status()
}
});
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.with_engine(|e| {
if offset & 1 == 0 {
e.write_data(*value);
} else {
e.write_control(*value);
}
});
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
struct VdpCounters {
shared: Arc<Shared>,
}
impl fmt::Debug for VdpCounters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VdpCounters").finish_non_exhaustive()
}
}
impl MemOps for VdpCounters {
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 offset & 1 == 0 {
engine.vcounter()
} else {
engine.hcounter()
};
Ok(())
}
fn write(&self, _offset: u64, src: &[u8], _attrs: MemAttrs) -> MemResult {
let [_] = src else {
return Err(BusError::BadAccess);
};
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "sms.vdp",
version: 1,
summary: "Sega 315-5124 VDP: mode 4 and the TMS9918A modes, 16 KiB VRAM, 32 colours",
properties: &[PropertySpec {
name: "region",
kind: ValueKind::Str,
required: false,
summary: "television standard: `ntsc` (262 lines) or `pal` (313)",
}],
construct: |props| Ok(Box::new(SmsVdp::from_props(props)?) as Box<dyn Device>),
};
pub fn register(reg: &mut crate::core::Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for SmsVdp {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
let level = self.shared.engine.lock().irq_line();
self.shared.drive(level);
Ok(())
}
fn unrealize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
self.shared.drive(false);
*self.shared.irq.lock() = None;
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
PORT_REGION => Some(Arc::clone(&self.port_region)),
COUNTER_REGION => Some(Arc::clone(&self.counter_region)),
_ => None,
}
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != IRQ_PIN {
return Err(Error::Config {
at: String::from(port),
message: alloc::format!("the VDP drives only `{IRQ_PIN}`"),
});
}
self.attach_irq(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == IRQ_PIN {
let level = self.shared.engine.lock().irq_line();
self.shared.drive(level);
}
}
fn reset(&self, _kind: ResetKind) {
let region = self.tv_region();
self.shared.with_engine(|e| {
let dots = e.dots;
*e = Engine::new(region);
e.dots = dots;
});
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let engine = self.shared.engine.lock();
w.write_u32(STATE_VERSION)?;
w.write_bytes(&engine.vram)?;
w.write_bytes(&engine.cram)?;
w.write_bytes(&engine.regs)?;
w.write_u16(engine.addr)?;
w.write_u8(engine.code)?;
w.write_bool(engine.latch)?;
w.write_u8(engine.first)?;
w.write_u8(engine.buffer)?;
w.write_u8(engine.status)?;
w.write_u8(engine.line_counter)?;
w.write_bool(engine.line_irq)?;
w.write_u8(engine.vscroll_latch)?;
w.write_u64(engine.dot)?;
w.write_u16(engine.line)?;
w.write_u64(engine.dots)?;
w.write_u64(engine.frame)?;
w.write_bytes(&engine.fb)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let version = r.read_u32()?;
if version != STATE_VERSION {
return Err(Error::State(alloc::format!(
"the VDP's snapshot is version {version}, this build writes {STATE_VERSION}"
)));
}
let vram = r.read_bytes()?;
let cram = r.read_bytes()?;
let regs = r.read_bytes()?;
let addr = r.read_u16()?;
let code = r.read_u8()?;
let latch = r.read_bool()?;
let first = r.read_u8()?;
let buffer = r.read_u8()?;
let status = r.read_u8()?;
let line_counter = r.read_u8()?;
let line_irq = r.read_bool()?;
let vscroll_latch = r.read_u8()?;
let dot = r.read_u64()?;
let line = r.read_u16()?;
let dots = r.read_u64()?;
let frame = r.read_u64()?;
let fb = r.read_bytes()?;
if vram.len() != VRAM_LEN
|| cram.len() != CRAM_LEN
|| regs.len() != REGISTER_COUNT
|| fb.len() != FRAMEBUFFER_LEN
{
return Err(Error::State(String::from(
"the VDP's snapshot has the wrong memory sizes",
)));
}
let irq = {
let mut engine = self.shared.engine.lock();
engine.vram.copy_from_slice(vram);
engine.cram.copy_from_slice(cram);
engine.regs.copy_from_slice(regs);
engine.addr = addr;
engine.code = code;
engine.latch = latch;
engine.first = first;
engine.buffer = buffer;
engine.status = status;
engine.line_counter = line_counter;
engine.line_irq = line_irq;
engine.vscroll_latch = vscroll_latch;
engine.dot = dot;
engine.line = line;
engine.dots = dots;
engine.frame = frame;
engine.fb.copy_from_slice(fb);
self.shared.publish(&engine);
engine.irq_line()
};
self.shared.drive(irq);
Ok(())
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.dots.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
SmsVdp::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) {
SmsVdp::attach_lazy(self, handle);
}
}
impl crate::machine::Instance for SmsVdp {}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| Ok(Arc::new(SmsVdp::from_props(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
ClassSchema::new(CLASS.name)
.prop(PropSchema::new("region", ValueKind::Str).values(&["ntsc", "pal"]))
.port(IRQ_PIN, PortDir::Out)
.region(PORT_REGION)
.region(COUNTER_REGION)
}