pub const VRAM_WORDS: usize = 0x8000;
pub const FB_WIDTH: usize = 512;
pub const FB_HEIGHT: usize = 242;
pub const ACTIVE_WIDTH: usize = 256;
pub const ACTIVE_HEIGHT: usize = 239;
pub const VBLANK_LINE: u16 = ACTIVE_HEIGHT as u16;
const REG_MAWR: u8 = 0x00; const REG_MARR: u8 = 0x01; const REG_VRR_VWR: u8 = 0x02; const REG_CR: u8 = 0x05; const REG_RCR: u8 = 0x06; const REG_BXR: u8 = 0x07; const REG_BYR: u8 = 0x08; const REG_MWR: u8 = 0x09; const REG_DCR: u8 = 0x0F; const REG_SOUR: u8 = 0x10; const REG_DESR: u8 = 0x11; const REG_LENR: u8 = 0x12; const REG_DVSSR: u8 = 0x13;
const ST_COLLISION: u8 = 1 << 0; const ST_OVERFLOW: u8 = 1 << 1; const ST_RASTER: u8 = 1 << 2; const ST_SATB_DONE: u8 = 1 << 3; const ST_DMA_DONE: u8 = 1 << 4; const ST_VBLANK: u8 = 1 << 5;
const CR_COLLISION_IRQ: u16 = 1 << 0;
const CR_OVERFLOW_IRQ: u16 = 1 << 1;
const CR_RASTER_IRQ: u16 = 1 << 2;
const CR_VBLANK_IRQ: u16 = 1 << 3;
const CR_SPRITE_ENABLE: u16 = 1 << 6; const CR_BG_ENABLE: u16 = 1 << 7;
const DCR_DMA_IRQ: u16 = 1 << 0; const DCR_SATB_IRQ: u16 = 1 << 1; const DCR_SOUR_DEC: u16 = 1 << 2; const DCR_DESR_DEC: u16 = 1 << 3; const DCR_SATB_AUTO: u16 = 1 << 4;
const SPRITES_PER_LINE: usize = 16;
#[derive(Clone, Copy, Debug)]
pub struct VdcEvent {
pub scanline: u16,
pub kind: VdcEventKind,
}
#[derive(Clone, Copy, Debug)]
pub enum VdcEventKind {
RcrWrite(u16),
CrWrite(u16),
RasterMatch { rcr: u16, irq: bool },
Vblank { irq: bool },
}
#[derive(Clone)]
pub struct Vdc {
vram: Vec<u16>,
satb: [u16; 256],
registers: [u16; 0x20],
selected: u8,
status: u8,
irq: bool,
scanline: u16,
bg_y: u16,
pub framebuffer: Vec<u16>,
trace_enabled: bool,
events: Vec<VdcEvent>,
traced_rcr: u16,
traced_cr: u16,
}
impl Default for Vdc {
fn default() -> Self {
Self::new()
}
}
impl Vdc {
#[must_use]
pub fn new() -> Self {
Self {
vram: vec![0; VRAM_WORDS],
satb: [0; 256],
registers: [0; 0x20],
selected: 0,
status: 0,
irq: false,
scanline: 0,
bg_y: 0,
framebuffer: vec![0; FB_WIDTH * FB_HEIGHT],
trace_enabled: false,
events: Vec::new(),
traced_rcr: 0,
traced_cr: 0,
}
}
fn address_increment(&self) -> u16 {
match (self.registers[REG_CR as usize] >> 11) & 0x03 {
0b00 => 1,
0b01 => 32,
0b10 => 64,
_ => 128,
}
}
pub fn read(&mut self, offset: u16) -> u8 {
match offset & 0x03 {
0x00 => {
let value = self.status;
self.status &= !(ST_COLLISION
| ST_OVERFLOW
| ST_RASTER
| ST_SATB_DONE
| ST_DMA_DONE
| ST_VBLANK);
self.irq = false;
value
}
0x01 => 0xFF,
0x02 => {
let addr = self.registers[REG_MARR as usize] as usize % VRAM_WORDS;
(self.vram[addr] & 0xFF) as u8
}
_ => {
let addr = self.registers[REG_MARR as usize] as usize % VRAM_WORDS;
let value = (self.vram[addr] >> 8) as u8;
let inc = self.address_increment();
self.registers[REG_MARR as usize] =
self.registers[REG_MARR as usize].wrapping_add(inc);
value
}
}
}
pub fn write(&mut self, offset: u16, value: u8) {
match offset & 0x03 {
0x00 => self.selected = value & 0x1F,
0x01 => {}
0x02 => self.write_register_low(value),
_ => self.write_register_high(value),
}
if self.trace_enabled {
self.trace_register_write();
}
}
fn trace_register_write(&mut self) {
match self.selected {
REG_RCR => {
let value = self.registers[REG_RCR as usize] & 0x03FF;
if value != self.traced_rcr {
self.traced_rcr = value;
self.events.push(VdcEvent {
scanline: self.scanline,
kind: VdcEventKind::RcrWrite(value),
});
}
}
REG_CR => {
let value = self.registers[REG_CR as usize];
if value != self.traced_cr {
self.traced_cr = value;
self.events.push(VdcEvent {
scanline: self.scanline,
kind: VdcEventKind::CrWrite(value),
});
}
}
_ => {}
}
}
fn write_register_low(&mut self, value: u8) {
let reg = self.selected as usize;
if reg < self.registers.len() {
self.registers[reg] = (self.registers[reg] & 0xFF00) | u16::from(value);
}
}
fn write_register_high(&mut self, value: u8) {
let reg = self.selected as usize;
if reg < self.registers.len() {
self.registers[reg] = (self.registers[reg] & 0x00FF) | (u16::from(value) << 8);
}
match self.selected {
REG_VRR_VWR => {
let addr = self.registers[REG_MAWR as usize] as usize % VRAM_WORDS;
self.vram[addr] = self.registers[REG_VRR_VWR as usize];
let inc = self.address_increment();
self.registers[REG_MAWR as usize] =
self.registers[REG_MAWR as usize].wrapping_add(inc);
}
REG_BYR => self.bg_y = self.registers[REG_BYR as usize] & 0x01FF,
REG_LENR => self.run_vram_dma(),
REG_DVSSR => self.run_satb_dma(),
_ => {}
}
}
fn run_vram_dma(&mut self) {
let dcr = self.registers[REG_DCR as usize];
let src_step: u16 = if dcr & DCR_SOUR_DEC != 0 { u16::MAX } else { 1 };
let dst_step: u16 = if dcr & DCR_DESR_DEC != 0 { u16::MAX } else { 1 };
let mut src = self.registers[REG_SOUR as usize];
let mut dst = self.registers[REG_DESR as usize];
let count = u32::from(self.registers[REG_LENR as usize]) + 1;
for _ in 0..count {
self.vram[dst as usize % VRAM_WORDS] = self.vram[src as usize % VRAM_WORDS];
src = src.wrapping_add(src_step);
dst = dst.wrapping_add(dst_step);
}
self.registers[REG_SOUR as usize] = src;
self.registers[REG_DESR as usize] = dst;
self.status |= ST_DMA_DONE;
if dcr & DCR_DMA_IRQ != 0 {
self.irq = true;
}
}
fn run_satb_dma(&mut self) {
let base = self.registers[REG_DVSSR as usize] as usize;
for i in 0..256 {
self.satb[i] = self.vram[(base + i) % VRAM_WORDS];
}
self.status |= ST_SATB_DONE;
if self.registers[REG_DCR as usize] & DCR_SATB_IRQ != 0 {
self.irq = true;
}
}
pub fn step_scanline(&mut self) -> bool {
self.render_current_line();
self.enter_hblank();
self.advance_scanline()
}
pub fn render_current_line(&mut self) {
if self.scanline == 0 {
self.bg_y = self.registers[REG_BYR as usize] & 0x01FF;
}
if self.scanline < VBLANK_LINE {
self.render_scanline(self.scanline as usize);
self.bg_y = self.bg_y.wrapping_add(1) & 0x01FF;
}
}
pub fn enter_hblank(&mut self) {
let rcr = self.registers[REG_RCR as usize] & 0x03FF;
if rcr >= 64 && self.scanline == rcr - 64 {
self.status |= ST_RASTER;
let irq_enabled = self.registers[REG_CR as usize] & CR_RASTER_IRQ != 0;
if irq_enabled {
self.irq = true;
}
if self.trace_enabled {
self.events.push(VdcEvent {
scanline: self.scanline,
kind: VdcEventKind::RasterMatch {
rcr,
irq: irq_enabled,
},
});
}
}
if self.scanline == VBLANK_LINE {
self.status |= ST_VBLANK;
let irq_enabled = self.registers[REG_CR as usize] & CR_VBLANK_IRQ != 0;
if irq_enabled {
self.irq = true;
}
if self.trace_enabled {
self.events.push(VdcEvent {
scanline: self.scanline,
kind: VdcEventKind::Vblank { irq: irq_enabled },
});
}
if self.registers[REG_DCR as usize] & DCR_SATB_AUTO != 0 {
self.run_satb_dma();
}
}
}
pub fn advance_scanline(&mut self) -> bool {
self.scanline += 1;
if self.scanline >= crate::SCANLINES_PER_FRAME {
self.scanline = 0;
return true;
}
false
}
fn bat_dimensions(&self) -> (usize, usize) {
let mwr = self.registers[REG_MWR as usize];
let width = match (mwr >> 4) & 0x03 {
0 => 32,
1 => 64,
_ => 128,
};
let height = if mwr & 0x40 != 0 { 64 } else { 32 };
(width, height)
}
fn render_scanline(&mut self, line: usize) {
let cr = self.registers[REG_CR as usize];
let mut line_buf = [0u16; ACTIVE_WIDTH];
let mut bg_opaque = [false; ACTIVE_WIDTH];
if cr & CR_BG_ENABLE != 0 {
let (bat_w, bat_h) = self.bat_dimensions();
let scroll_x = self.registers[REG_BXR as usize] & 0x03FF;
let bg_y = self.bg_y as usize % (bat_h * 8);
let tile_row = bg_y / 8;
let fine_y = bg_y % 8;
for (x, slot) in line_buf.iter_mut().enumerate() {
let bg_x = (scroll_x as usize + x) % (bat_w * 8);
let tile_col = bg_x / 8;
let fine_x = bg_x % 8;
let bat = self.vram[(tile_row * bat_w + tile_col) % VRAM_WORDS];
let char_code = (bat & 0x07FF) as usize;
let palette = (bat >> 12) & 0x0F;
let base = char_code * 16;
let plane01 = self.vram[(base + fine_y) % VRAM_WORDS];
let plane23 = self.vram[(base + fine_y + 8) % VRAM_WORDS];
let bit = 7 - fine_x;
let color = ((plane01 >> bit) & 1)
| (((plane01 >> (bit + 8)) & 1) << 1)
| (((plane23 >> bit) & 1) << 2)
| (((plane23 >> (bit + 8)) & 1) << 3);
if color != 0 {
*slot = (palette << 4) | color;
bg_opaque[x] = true;
}
}
}
if cr & CR_SPRITE_ENABLE != 0 {
let (collision, overflow) = self.render_sprites_line(line, &mut line_buf, &bg_opaque);
if collision {
self.status |= ST_COLLISION;
if cr & CR_COLLISION_IRQ != 0 {
self.irq = true;
}
}
if overflow {
self.status |= ST_OVERFLOW;
if cr & CR_OVERFLOW_IRQ != 0 {
self.irq = true;
}
}
}
let start = line * FB_WIDTH;
self.framebuffer[start..start + ACTIVE_WIDTH].copy_from_slice(&line_buf);
for slot in &mut self.framebuffer[start + ACTIVE_WIDTH..start + FB_WIDTH] {
*slot = 0;
}
}
fn render_sprites_line(
&self,
line: usize,
line_buf: &mut [u16; ACTIVE_WIDTH],
bg_opaque: &[bool; ACTIVE_WIDTH],
) -> (bool, bool) {
let mut taken = [false; ACTIVE_WIDTH];
let mut sprite0 = [false; ACTIVE_WIDTH];
let mut on_line = 0usize;
let mut collision = false;
let mut overflow = false;
for index in 0..64 {
let attr = &self.satb[index * 4..index * 4 + 4];
let sy = (attr[0] & 0x03FF) as i32 - 64;
let sx = (attr[1] & 0x03FF) as i32 - 32;
let pattern = (attr[2] >> 1) & 0x03FF;
let flags = attr[3];
let palette = flags & 0x0F;
let in_front = flags & 0x0080 != 0;
let x_flip = flags & 0x0800 != 0;
let y_flip = flags & 0x8000 != 0;
let cells_x = if flags & 0x0100 != 0 { 2 } else { 1 };
let cells_y = match (flags >> 12) & 0x03 {
0 => 1,
3 => 4,
_ => 2,
};
let height = cells_y * 16;
let width = cells_x * 16;
let ly = line as i32 - sy;
if ly < 0 || ly >= height as i32 {
continue;
}
on_line += 1;
if on_line > SPRITES_PER_LINE {
overflow = true;
break;
}
let mut row_in_sprite = ly as usize;
if y_flip {
row_in_sprite = height - 1 - row_in_sprite;
}
let cell_y = row_in_sprite / 16;
let fine_y = row_in_sprite % 16;
for col in 0..width {
let screen_x = sx + col;
if screen_x < 0 || screen_x >= ACTIVE_WIDTH as i32 {
continue;
}
let sxp = screen_x as usize;
let mut col_in_sprite = col;
if x_flip {
col_in_sprite = width - 1 - col_in_sprite;
}
let cell_x = col_in_sprite / 16;
let fine_x = col_in_sprite % 16;
let mut pat = pattern;
if cells_x == 2 {
pat = (pat & !0x01) | cell_x as u16;
}
if cells_y >= 2 {
let mask = (cells_y as u16 - 1) << 1;
pat = (pat & !mask) | ((cell_y as u16) << 1);
}
let base = pat as usize * 64;
let bit = 15 - fine_x;
let p0 = (self.vram[(base + fine_y) % VRAM_WORDS] >> bit) & 1;
let p1 = (self.vram[(base + 16 + fine_y) % VRAM_WORDS] >> bit) & 1;
let p2 = (self.vram[(base + 32 + fine_y) % VRAM_WORDS] >> bit) & 1;
let p3 = (self.vram[(base + 48 + fine_y) % VRAM_WORDS] >> bit) & 1;
let color = p0 | (p1 << 1) | (p2 << 2) | (p3 << 3);
if color == 0 {
continue; }
if index == 0 {
sprite0[sxp] = true;
} else if sprite0[sxp] {
collision = true;
}
if taken[sxp] {
continue; }
taken[sxp] = true;
if in_front || !bg_opaque[sxp] {
line_buf[sxp] = 256 + ((palette << 4) | color);
}
}
}
(collision, overflow)
}
#[must_use]
pub const fn irq(&self) -> bool {
self.irq
}
#[must_use]
pub const fn scanline(&self) -> u16 {
self.scanline
}
#[must_use]
pub const fn status(&self) -> u8 {
self.status
}
#[must_use]
pub const fn control(&self) -> u16 {
self.registers[REG_CR as usize]
}
#[must_use]
pub fn vram(&self) -> &[u16] {
&self.vram
}
#[must_use]
pub const fn rcr(&self) -> u16 {
self.registers[REG_RCR as usize] & 0x03FF
}
#[must_use]
pub fn register(&self, index: usize) -> u16 {
self.registers.get(index).copied().unwrap_or(0)
}
pub fn set_trace(&mut self, on: bool) {
self.trace_enabled = on;
}
pub fn drain_events(&mut self) -> Vec<VdcEvent> {
std::mem::take(&mut self.events)
}
}