use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
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, RamStore, Region, RegionRef,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireSource};
use crate::host::display::{PixelFormat, Scanout, Surface, SurfaceInfo};
use crate::machine::realize::Instance;
use crate::machine::validate::ClassSchema;
pub const CLASS_NAME: &str = "pc.video";
const STATE_VERSION: u32 = 2;
pub const CRTC_WINDOW_LEN: u64 = 2;
pub const STATUS_WINDOW_LEN: u64 = 1;
pub const MODE_WINDOW_LEN: u64 = 2;
pub const VGA_WINDOW_LEN: u64 = 16;
pub const VRAM_LEN: u64 = 32 * 1024;
const CRTC_REGISTERS: usize = 18;
const ATTR_REGISTERS: usize = 21;
const SEQ_REGISTERS: usize = 5;
const GC_REGISTERS: usize = 9;
const DAC_ENTRIES: usize = 256;
const DAC_COMPONENTS: usize = 3;
const NO_EVENT: u64 = u64::MAX;
const VSYNC_LINES: u64 = 16;
const MODE_VIDEO_ENABLE: u8 = 0x08;
const MODE_BLINK: u8 = 0x20;
const STATUS_DISPLAY_ENABLE: u8 = 0x01;
const STATUS_VSYNC: u8 = 0x08;
const MISC_COLOUR: u8 = 0x01;
const DOT_CLOCK_25MHZ: u64 = 25_175_000;
const DOT_CLOCK_28MHZ: u64 = 28_322_000;
const CRTC_WRITE_MASK: [u8; CRTC_REGISTERS] = [
0xff, 0xff, 0xff, 0xff, 0x7f, 0x1f, 0x7f, 0x7f, 0x03, 0x1f, 0x7f, 0x1f, 0x3f, 0xff, 0x3f, 0xff, 0x00, 0x00, ];
const CRTC_DEFAULTS: [u8; CRTC_REGISTERS] = [
99, 80, 82, 0x0f, 27, 1, 25, 25, 0, 15, 14, 15, 0, 0, 0, 0, 0, 0, ];
const FONT_CELL_HEIGHT: usize = 16;
const FONT_FIRST: u8 = 0x20;
static FONT_ASCII: [[u8; FONT_CELL_HEIGHT]; 95] = [
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x28, 0x28, 0x7c, 0x28, 0x7c, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x3c, 0x50, 0x38, 0x14, 0x78, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x60, 0x64, 0x08, 0x10, 0x20, 0x4c, 0x0c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x30, 0x48, 0x50, 0x20, 0x54, 0x48, 0x34, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x20, 0x20, 0x20, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x20, 0x10, 0x08, 0x08, 0x08, 0x10, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x38, 0x7c, 0x38, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x10, 0x20, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08, 0x10, 0x20, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x4c, 0x54, 0x64, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x30, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x04, 0x08, 0x10, 0x20, 0x7c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x08, 0x10, 0x08, 0x04, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x08, 0x18, 0x28, 0x48, 0x7c, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x40, 0x78, 0x04, 0x04, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x40, 0x78, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x04, 0x08, 0x10, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x44, 0x38, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x44, 0x3c, 0x04, 0x08, 0x30, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x20, 0x40, 0x20, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x20, 0x10, 0x08, 0x04, 0x08, 0x10, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x04, 0x08, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x5c, 0x54, 0x5c, 0x40, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x28, 0x44, 0x44, 0x7c, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x78, 0x44, 0x44, 0x78, 0x44, 0x44, 0x78, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x40, 0x40, 0x40, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x70, 0x48, 0x44, 0x44, 0x44, 0x48, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x40, 0x40, 0x78, 0x40, 0x40, 0x7c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x40, 0x5c, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x7c, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x1c, 0x08, 0x08, 0x08, 0x08, 0x48, 0x30, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x48, 0x50, 0x60, 0x50, 0x48, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x6c, 0x54, 0x54, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x64, 0x54, 0x4c, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x78, 0x44, 0x44, 0x78, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x44, 0x44, 0x54, 0x48, 0x34, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x78, 0x44, 0x44, 0x78, 0x50, 0x48, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x3c, 0x40, 0x40, 0x38, 0x04, 0x04, 0x78, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x44, 0x44, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x54, 0x54, 0x6c, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x28, 0x10, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x28, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x04, 0x08, 0x10, 0x20, 0x40, 0x7c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x20, 0x20, 0x20, 0x20, 0x20, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x20, 0x10, 0x08, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x78, 0x44, 0x44, 0x44, 0x78, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x40, 0x40, 0x40, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x3c, 0x44, 0x44, 0x44, 0x3c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x7c, 0x40, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x20, 0x78, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x44, 0x44, 0x3c, 0x04, 0x44, 0x38, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x78, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x48, 0x30, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x48, 0x50, 0x60, 0x50, 0x48, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x54, 0x54, 0x54, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x44, 0x44, 0x78, 0x40, 0x40, 0x40, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x44, 0x44, 0x3c, 0x04, 0x04, 0x04, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x60, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x40, 0x38, 0x04, 0x78, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x20, 0x20, 0x78, 0x20, 0x20, 0x24, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x4c, 0x34, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x54, 0x54, 0x54, 0x28, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x3c, 0x04, 0x44, 0x38, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x08, 0x10, 0x20, 0x7c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x0c, 0x10, 0x10, 0x20, 0x10, 0x10, 0x0c, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x60, 0x10, 0x10, 0x08, 0x10, 0x10, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00,
], [
0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
], ];
static FONT_EXTRA: &[(u8, [u8; FONT_CELL_HEIGHT])] = &[
(
0x07,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x38, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0x10,
[
0x00, 0x00, 0x00, 0x00, 0x40, 0x60, 0x70, 0x78, 0x70, 0x60, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0x11,
[
0x00, 0x00, 0x00, 0x00, 0x04, 0x0c, 0x1c, 0x3c, 0x1c, 0x0c, 0x04, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0x18,
[
0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x54, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0x19,
[
0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x54, 0x38, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0x1a,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x08, 0x7c, 0x08, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0x1b,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x7c, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xb0,
[
0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00, 0x88, 0x00,
0x22, 0x00,
],
),
(
0xb1,
[
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55,
],
),
(
0xb2,
[
0xff, 0xaa, 0xff, 0xaa, 0xff, 0xaa, 0xff, 0xaa, 0xff, 0xaa, 0xff, 0xaa, 0xff, 0xaa,
0xff, 0xaa,
],
),
(
0xb3,
[
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10,
],
),
(
0xb4,
[
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xf0, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10,
],
),
(
0xb9,
[
0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0xf8, 0x20, 0xf8, 0x28, 0x28, 0x28, 0x28, 0x28,
0x28, 0x28,
],
),
(
0xba,
[
0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
0x28, 0x28,
],
),
(
0xbb,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x20, 0xe8, 0x28, 0x28, 0x28, 0x28, 0x28,
0x28, 0x28,
],
),
(
0xbc,
[
0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0xf8, 0x20, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xbf,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10,
],
),
(
0xc0,
[
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xc1,
[
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xc2,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10,
],
),
(
0xc3,
[
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10,
],
),
(
0xc4,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xc5,
[
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xff, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10,
],
),
(
0xc8,
[
0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x3f, 0x20, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xc9,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x20, 0x2f, 0x28, 0x28, 0x28, 0x28, 0x28,
0x28, 0x28,
],
),
(
0xca,
[
0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0xef, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xcb,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xef, 0x28, 0x28, 0x28, 0x28, 0x28,
0x28, 0x28,
],
),
(
0xcc,
[
0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x3f, 0x08, 0x3f, 0x28, 0x28, 0x28, 0x28, 0x28,
0x28, 0x28,
],
),
(
0xcd,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xce,
[
0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0xef, 0x00, 0xef, 0x28, 0x28, 0x28, 0x28, 0x28,
0x28, 0x28,
],
),
(
0xd9,
[
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
(
0xda,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10,
],
),
(
0xdb,
[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff,
],
),
(
0xdc,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff,
],
),
(
0xdd,
[
0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
0xf0, 0xf0,
],
),
(
0xde,
[
0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
0x0f, 0x0f,
],
),
(
0xdf,
[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
),
];
static FONT_MISSING: [u8; FONT_CELL_HEIGHT] = [
0x00, 0x00, 0x00, 0x00, 0x7c, 0x44, 0x44, 0x44, 0x44, 0x44, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
];
static FONT_BLANK: [u8; FONT_CELL_HEIGHT] = [0; FONT_CELL_HEIGHT];
#[must_use]
fn glyph(code: u8) -> &'static [u8; FONT_CELL_HEIGHT] {
if (FONT_FIRST..0x7f).contains(&code) {
return &FONT_ASCII[(code - FONT_FIRST) as usize];
}
match FONT_EXTRA.binary_search_by_key(&code, |(c, _)| *c) {
Ok(i) => &FONT_EXTRA[i].1,
Err(_) if code < FONT_FIRST || code == 0x7f => &FONT_BLANK,
Err(_) => &FONT_MISSING,
}
}
const CGA_PALETTE_6BIT: [[u8; 3]; 16] = [
[0x00, 0x00, 0x00], [0x00, 0x00, 0x2a], [0x00, 0x2a, 0x00], [0x00, 0x2a, 0x2a], [0x2a, 0x00, 0x00], [0x2a, 0x00, 0x2a], [0x2a, 0x15, 0x00], [0x2a, 0x2a, 0x2a], [0x15, 0x15, 0x15], [0x15, 0x15, 0x3f], [0x15, 0x3f, 0x15], [0x15, 0x3f, 0x3f], [0x3f, 0x15, 0x15], [0x3f, 0x15, 0x3f], [0x3f, 0x3f, 0x15], [0x3f, 0x3f, 0x3f], ];
#[inline]
#[must_use]
const fn expand6(v: u8) -> u8 {
(v << 2) | (v >> 4)
}
#[derive(Debug, Clone)]
struct Vga {
misc: u8,
enable: u8,
attr_index: u8,
attr_data_next: bool,
attr: [u8; ATTR_REGISTERS],
seq_index: u8,
seq: [u8; SEQ_REGISTERS],
gc_index: u8,
gc: [u8; GC_REGISTERS],
dac_mask: u8,
dac_read: u8,
dac_write: u8,
dac_read_sub: u8,
dac_write_sub: u8,
dac_reading: bool,
dac: [[u8; 3]; DAC_ENTRIES],
}
impl Vga {
fn new() -> Vga {
let mut attr = [0u8; ATTR_REGISTERS];
let mut i = 0;
while i < 16 {
attr[i] = i as u8;
i += 1;
}
attr[16] = 0x0c;
attr[18] = 0x0f;
let mut dac = [[0u8; 3]; DAC_ENTRIES];
let mut i = 0;
while i < CGA_PALETTE_6BIT.len() {
dac[i] = CGA_PALETTE_6BIT[i];
i += 1;
}
Vga {
misc: 0x67,
enable: 0x01,
attr_index: 0x20,
attr_data_next: false,
attr,
seq_index: 0,
seq: [0x03, 0x00, 0x03, 0x00, 0x02],
gc_index: 0,
gc: [0; GC_REGISTERS],
dac_mask: 0xff,
dac_read: 0,
dac_write: 0,
dac_read_sub: 0,
dac_write_sub: 0,
dac_reading: false,
dac,
}
}
}
#[derive(Debug, Clone)]
struct State {
ticks: u64,
frame_start: u64,
frames: u64,
crtc_index: u8,
crtc: [u8; CRTC_REGISTERS],
mode: u8,
colour: u8,
vga: Vga,
}
impl State {
fn new() -> State {
State {
ticks: 0,
frame_start: 0,
frames: 0,
crtc_index: 0,
crtc: CRTC_DEFAULTS,
mode: 0x29,
colour: 0x00,
vga: Vga::new(),
}
}
fn cell_height(&self) -> u64 {
u64::from(self.crtc[9] & 0x1f) + 1
}
fn chars_per_line(&self) -> u64 {
u64::from(self.crtc[0]) + 1
}
fn lines_per_frame(&self) -> u64 {
(u64::from(self.crtc[4] & 0x7f) + 1) * self.cell_height() + u64::from(self.crtc[5] & 0x1f)
}
fn ticks_per_frame(&self) -> u64 {
self.chars_per_line() * self.lines_per_frame()
}
fn columns(&self) -> u64 {
u64::from(self.crtc[1])
}
fn rows(&self) -> u64 {
u64::from(self.crtc[6] & 0x7f)
}
fn vsync_start_line(&self) -> u64 {
u64::from(self.crtc[7] & 0x7f) * self.cell_height()
}
fn start_address(&self) -> u64 {
(u64::from(self.crtc[12] & 0x3f) << 8) | u64::from(self.crtc[13])
}
fn cursor_address(&self) -> u64 {
(u64::from(self.crtc[14] & 0x3f) << 8) | u64::from(self.crtc[15])
}
fn char_width(&self) -> u32 {
if self.vga.seq[1] & 0x01 != 0 { 8 } else { 9 }
}
fn dot_clock_hz(&self, override_hz: u64) -> u64 {
if override_hz != 0 {
return override_hz;
}
match (self.vga.misc >> 2) & 0x03 {
0 => DOT_CLOCK_25MHZ,
_ => DOT_CLOCK_28MHZ,
}
}
fn position(&self) -> u64 {
let per = self.ticks_per_frame();
if per == 0 {
return 0;
}
(self.ticks - self.frame_start) % per
}
fn in_vsync(&self) -> bool {
let per = self.ticks_per_frame();
if per == 0 {
return false;
}
let chars = self.chars_per_line();
let start = (self.vsync_start_line() * chars).min(per);
let end = (start + VSYNC_LINES * chars).min(per);
let pos = self.position();
pos >= start && pos < end
}
fn status(&self) -> u8 {
let per = self.ticks_per_frame();
if per == 0 {
return 0;
}
let chars = self.chars_per_line();
let pos = self.position();
let line = pos / chars;
let column = pos % chars;
let mut value = 0;
if column >= self.columns() || line >= self.rows() * self.cell_height() {
value |= STATUS_DISPLAY_ENABLE;
}
if self.in_vsync() {
value |= STATUS_VSYNC;
}
value
}
fn next_event(&self) -> u64 {
let per = self.ticks_per_frame();
if per == 0 {
return NO_EVENT;
}
let chars = self.chars_per_line();
let start = (self.vsync_start_line() * chars).min(per);
let end = (start + VSYNC_LINES * chars).min(per);
let pos = self.position();
for candidate in [start, end, per] {
if candidate > pos {
return self.ticks + (candidate - pos);
}
}
self.ticks + per
}
fn renormalize(&mut self) {
let per = self.ticks_per_frame();
if per == 0 {
self.frame_start = self.ticks;
return;
}
let elapsed = self.ticks.saturating_sub(self.frame_start);
if elapsed >= per {
self.frame_start += (elapsed / per) * per;
}
}
fn video_enabled(&self) -> bool {
self.mode & MODE_VIDEO_ENABLE != 0
&& self.vga.seq[1] & 0x20 == 0
&& self.vga.attr_index & 0x20 != 0
}
fn blink_enabled(&self) -> bool {
self.mode & MODE_BLINK != 0 || self.vga.attr[16] & 0x08 != 0
}
fn dac_index(&self, colour: u8) -> u8 {
let p = self.vga.attr[(colour & 0x0f) as usize] & 0x3f;
let high = if self.vga.attr[16] & 0x80 != 0 {
self.vga.attr[20] & 0x03
} else {
(p >> 4) & 0x03
};
let index = (p & 0x0f) | (high << 4) | ((self.vga.attr[20] & 0x0c) << 4);
index & self.vga.dac_mask
}
fn rgb(&self, colour: u8) -> [u8; 3] {
let entry = self.vga.dac[self.dac_index(colour) as usize];
[expand6(entry[0]), expand6(entry[1]), expand6(entry[2])]
}
}
struct Shared {
state: Mutex<State>,
vram: Arc<RamStore>,
ticks: AtomicU64,
frames: AtomicU64,
next_event: AtomicU64,
dot_clock_hz: u64,
lazy: Mutex<Option<LazyHandle>>,
vsync: Mutex<Option<WireSource>>,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Shared");
s.field("dot_clock_hz", &self.dot_clock_hz);
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl Shared {
fn publish(&self, state: &State) {
self.ticks.store(state.ticks, Ordering::Relaxed);
self.frames.store(state.frames, Ordering::Relaxed);
self.next_event.store(state.next_event(), Ordering::Relaxed);
}
fn drive_vsync(&self, level: Level) {
let pin = self.vsync.lock().clone();
if let Some(pin) = pin {
pin.set(level);
}
}
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);
}
fn advance_to(&self, target: u64) {
let changed = {
let mut state = self.state.lock();
if target <= state.ticks {
return;
}
let before = state.in_vsync();
let per = state.ticks_per_frame();
match (target - state.frame_start).checked_div(per) {
None => {
state.ticks = target;
state.frame_start = target;
}
Some(whole) => {
state.frames += whole;
state.frame_start += whole * per;
state.ticks = target;
}
}
let after = state.in_vsync();
self.publish(&state);
if before == after { None } else { Some(after) }
};
if let Some(level) = changed {
self.drive_vsync(Level::from_bool(level));
}
}
fn touched(&self, state: &mut State) {
state.renormalize();
self.publish(state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Window {
Crtc,
CrtcColour,
CrtcMono,
Status,
Mode,
Vga,
}
#[derive(Debug)]
struct Port {
shared: Arc<Shared>,
window: Window,
}
impl Port {
fn decoded(&self, state: &State) -> bool {
let colour = state.vga.misc & MISC_COLOUR != 0;
match self.window {
Window::CrtcColour => colour,
Window::CrtcMono => !colour,
_ => true,
}
}
fn read_register(&self, offset: u64, debug: bool) -> u8 {
let mut state = self.shared.state.lock();
if !self.decoded(&state) {
return 0xff;
}
match self.window {
Window::Crtc | Window::CrtcColour | Window::CrtcMono => match offset & 1 {
0 => state.crtc_index,
_ => {
let index = (state.crtc_index & 0x1f) as usize;
match index {
12..=15 => state.crtc[index],
_ => 0,
}
}
},
Window::Status => {
let value = state.status();
if !debug {
state.vga.attr_data_next = false;
}
value
}
Window::Mode => match offset & 1 {
0 => state.mode,
_ => state.colour,
},
Window::Vga => Self::read_vga(&mut state, offset, debug),
}
}
fn read_vga(state: &mut State, offset: u64, debug: bool) -> u8 {
match offset & 0x0f {
0x0 => state.vga.attr_index,
0x1 => {
let index = (state.vga.attr_index & 0x1f) as usize;
if index < ATTR_REGISTERS {
state.vga.attr[index]
} else {
0
}
}
0x2 => 0,
0x3 => state.vga.enable,
0x4 => state.vga.seq_index,
0x5 => {
let index = state.vga.seq_index as usize;
if index < SEQ_REGISTERS {
state.vga.seq[index]
} else {
0
}
}
0x6 => state.vga.dac_mask,
0x7 => u8::from(state.vga.dac_reading) * 3,
0x8 => state.vga.dac_write,
0x9 => {
let value =
state.vga.dac[state.vga.dac_read as usize][state.vga.dac_read_sub as usize];
if !debug {
Self::step_dac_read(state);
}
value
}
0xa | 0xb => 0xff,
0xc => state.vga.misc,
0xd => 0xff,
0xe => state.vga.gc_index,
_ => {
let index = state.vga.gc_index as usize;
if index < GC_REGISTERS {
state.vga.gc[index]
} else {
0
}
}
}
}
fn step_dac_read(state: &mut State) {
state.vga.dac_read_sub += 1;
if state.vga.dac_read_sub >= 3 {
state.vga.dac_read_sub = 0;
state.vga.dac_read = state.vga.dac_read.wrapping_add(1);
}
}
fn write_register(&self, offset: u64, value: u8) {
let mut state = self.shared.state.lock();
if !self.decoded(&state) {
return;
}
match self.window {
Window::Crtc | Window::CrtcColour | Window::CrtcMono => match offset & 1 {
0 => state.crtc_index = value & 0x1f,
_ => {
let index = (state.crtc_index & 0x1f) as usize;
if index < CRTC_REGISTERS {
state.crtc[index] = value & CRTC_WRITE_MASK[index];
self.shared.touched(&mut state);
}
}
},
Window::Status => {}
Window::Mode => match offset & 1 {
0 => state.mode = value,
_ => state.colour = value,
},
Window::Vga => {
Self::write_vga(&mut state, offset, value);
self.shared.touched(&mut state);
}
}
}
fn write_vga(state: &mut State, offset: u64, value: u8) {
match offset & 0x0f {
0x0 => {
if state.vga.attr_data_next {
let index = (state.vga.attr_index & 0x1f) as usize;
if index < ATTR_REGISTERS {
state.vga.attr[index] = value & attr_write_mask(index);
}
} else {
state.vga.attr_index = value & 0x3f;
}
state.vga.attr_data_next = !state.vga.attr_data_next;
}
0x1 => {}
0x2 => state.vga.misc = value,
0x3 => state.vga.enable = value & 0x01,
0x4 => state.vga.seq_index = value & 0x07,
0x5 => {
let index = state.vga.seq_index as usize;
if index < SEQ_REGISTERS {
state.vga.seq[index] = value;
}
}
0x6 => state.vga.dac_mask = value,
0x7 => {
state.vga.dac_read = value;
state.vga.dac_read_sub = 0;
state.vga.dac_reading = true;
}
0x8 => {
state.vga.dac_write = value;
state.vga.dac_write_sub = 0;
state.vga.dac_reading = false;
}
0x9 => {
let index = state.vga.dac_write as usize;
let sub = state.vga.dac_write_sub as usize;
state.vga.dac[index][sub] = value & 0x3f;
state.vga.dac_write_sub += 1;
if state.vga.dac_write_sub >= 3 {
state.vga.dac_write_sub = 0;
state.vga.dac_write = state.vga.dac_write.wrapping_add(1);
}
}
0xa..=0xd => {}
0xe => state.vga.gc_index = value & 0x0f,
_ => {
let index = state.vga.gc_index as usize;
if index < GC_REGISTERS {
state.vga.gc[index] = value;
}
}
}
}
}
fn attr_write_mask(index: usize) -> u8 {
match index {
0..=15 => 0x3f,
18..=20 => 0x0f,
_ => 0xff,
}
}
impl MemOps for Port {
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.read_register(offset, 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);
self.write_register(offset, *value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
#[derive(Debug)]
pub struct Video {
shared: Arc<Shared>,
crtc: RegionRef,
crtc_colour: RegionRef,
crtc_mono: RegionRef,
status: RegionRef,
mode: RegionRef,
vga: RegionRef,
vram: RegionRef,
}
fn read_store(store: &RamStore) -> Result<alloc::vec::Vec<u8>> {
let len = usize::try_from(store.len())
.map_err(|_| Error::State(String::from("RAM larger than the host address space")))?;
let mut buf = alloc::vec![0u8; len];
store
.read_at(0, &mut buf)
.map_err(|e| Error::State(alloc::format!("cannot read the character buffer: {e}")))?;
Ok(buf)
}
impl Video {
pub fn new(props: &Props) -> Result<Video> {
let mut r = props.reader();
let dot_clock = r.or_range("dot-clock", 0, 0..=1_000_000_000)?;
r.finish()?;
Ok(Video::with_dot_clock(dot_clock))
}
#[must_use]
pub fn default_device() -> Video {
Video::with_dot_clock(0)
}
#[must_use]
pub fn with_dot_clock(dot_clock_hz: u64) -> Video {
let shared = Arc::new(Shared {
state: Mutex::with_rank(LockRank::DEVICE, State::new()),
vram: Arc::new(RamStore::new(VRAM_LEN)),
ticks: AtomicU64::new(0),
frames: AtomicU64::new(0),
next_event: AtomicU64::new(NO_EVENT),
dot_clock_hz,
lazy: Mutex::with_rank(LockRank::LEAF, None),
vsync: Mutex::with_rank(LockRank::LEAF, None),
});
shared.publish(&shared.state.lock());
let port = |window, len| -> RegionRef {
Arc::new(Region::io(
CLASS_NAME,
len,
Arc::new(Port {
shared: Arc::clone(&shared),
window,
}) as Arc<dyn MemOps>,
))
};
let vram = Arc::new(Region::ram("pc.video.vram", Arc::clone(&shared.vram)));
Video {
crtc: port(Window::Crtc, CRTC_WINDOW_LEN),
crtc_colour: port(Window::CrtcColour, CRTC_WINDOW_LEN),
crtc_mono: port(Window::CrtcMono, CRTC_WINDOW_LEN),
status: port(Window::Status, STATUS_WINDOW_LEN),
mode: port(Window::Mode, MODE_WINDOW_LEN),
vga: port(Window::Vga, VGA_WINDOW_LEN),
vram,
shared,
}
}
#[must_use]
pub fn vram(&self) -> &Arc<RamStore> {
&self.shared.vram
}
#[must_use]
pub fn scanout(&self) -> VideoScanout {
VideoScanout {
shared: Arc::clone(&self.shared),
}
}
#[must_use]
pub fn ticks(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
#[must_use]
pub fn frames(&self) -> u64 {
self.shared.frames.load(Ordering::Relaxed)
}
pub fn advance_to(&self, target: u64) {
self.shared.advance_to(target);
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
#[must_use]
pub fn status(&self) -> u8 {
self.shared.state.lock().status()
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "MC6845 CRTC with a text-mode character generator and a VGA register file",
properties: &[PropertySpec {
name: "dot-clock",
kind: ValueKind::Uint,
required: false,
summary: "the pixel clock in Hz, overriding the clock select bits (default: follow them)",
}],
construct: |props| Ok(Box::new(Video::new(props)?)),
};
impl Device for Video {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
let level = {
let mut state = self.shared.state.lock();
*state = State::new();
self.shared.publish(&state);
state.in_vsync()
};
self.shared.drive_vsync(Level::from_bool(level));
}
fn region(&self, name: &str) -> Option<RegionRef> {
let region = match name {
"" | "crtc" | "regs" => &self.crtc,
"crtc-colour" => &self.crtc_colour,
"crtc-mono" => &self.crtc_mono,
"status" => &self.status,
"mode" => &self.mode,
"vga" => &self.vga,
"vram" => &self.vram,
_ => return None,
};
Some(Arc::clone(region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != "vsync" {
return Err(Error::Config {
at: port.to_string(),
message: String::from("a CRTC drives one pin, `vsync`"),
});
}
*self.shared.vsync.lock() = Some(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == "vsync" {
let level = self.shared.state.lock().in_vsync();
self.shared.drive_vsync(Level::from_bool(level));
}
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
Video::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
match self.shared.next_event.load(Ordering::Relaxed) {
NO_EVENT => None,
tick => Some(tick),
}
}
fn attach_lazy(&self, handle: LazyHandle) {
Video::attach_lazy(self, handle);
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.shared.state.lock();
w.write_u64(state.ticks)?;
w.write_u64(state.frame_start)?;
w.write_u64(state.frames)?;
w.write_u8(state.crtc_index)?;
for byte in state.crtc {
w.write_u8(byte)?;
}
w.write_u8(state.mode)?;
w.write_u8(state.colour)?;
w.write_u8(state.vga.misc)?;
w.write_u8(state.vga.enable)?;
w.write_u8(state.vga.attr_index)?;
w.write_bool(state.vga.attr_data_next)?;
for byte in state.vga.attr {
w.write_u8(byte)?;
}
w.write_u8(state.vga.seq_index)?;
for byte in state.vga.seq {
w.write_u8(byte)?;
}
w.write_u8(state.vga.gc_index)?;
for byte in state.vga.gc {
w.write_u8(byte)?;
}
w.write_u8(state.vga.dac_mask)?;
w.write_u8(state.vga.dac_read)?;
w.write_u8(state.vga.dac_write)?;
w.write_u8(state.vga.dac_read_sub)?;
w.write_u8(state.vga.dac_write_sub)?;
w.write_bool(state.vga.dac_reading)?;
for entry in state.vga.dac {
for component in entry {
w.write_u8(component)?;
}
}
w.write_bytes(&read_store(&self.shared.vram)?)?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let mut state = State::new();
state.ticks = r.read_u64()?;
state.frame_start = r.read_u64()?;
state.frames = r.read_u64()?;
state.crtc_index = r.read_u8()?;
for i in 0..CRTC_REGISTERS {
state.crtc[i] = r.read_u8()?;
}
state.mode = r.read_u8()?;
state.colour = r.read_u8()?;
state.vga.misc = r.read_u8()?;
state.vga.enable = r.read_u8()?;
state.vga.attr_index = r.read_u8()?;
state.vga.attr_data_next = r.read_bool()?;
for i in 0..ATTR_REGISTERS {
state.vga.attr[i] = r.read_u8()?;
}
state.vga.seq_index = r.read_u8()?;
for i in 0..SEQ_REGISTERS {
state.vga.seq[i] = r.read_u8()?;
}
state.vga.gc_index = r.read_u8()?;
for i in 0..GC_REGISTERS {
state.vga.gc[i] = r.read_u8()?;
}
state.vga.dac_mask = r.read_u8()?;
state.vga.dac_read = r.read_u8()?;
state.vga.dac_write = r.read_u8()?;
state.vga.dac_read_sub = r.read_u8()?;
state.vga.dac_write_sub = r.read_u8()?;
state.vga.dac_reading = r.read_bool()?;
for i in 0..DAC_ENTRIES {
for c in 0..DAC_COMPONENTS {
state.vga.dac[i][c] = r.read_u8()?;
}
}
let vram = r.read_bytes()?;
if vram.len() as u64 != VRAM_LEN {
return Err(Error::State(alloc::format!(
"snapshot has {} byte(s) of character buffer, this adapter has {VRAM_LEN}",
vram.len()
)));
}
self.shared.vram.write_at(0, vram).map_err(|e| {
Error::State(alloc::format!("cannot restore the character buffer: {e}"))
})?;
if state.frame_start > state.ticks {
return Err(Error::State(alloc::format!(
"snapshot has a frame beginning at {} after the current tick {}",
state.frame_start,
state.ticks
)));
}
for (what, sub) in [
("read", state.vga.dac_read_sub),
("write", state.vga.dac_write_sub),
] {
if usize::from(sub) >= DAC_COMPONENTS {
return Err(Error::State(alloc::format!(
"snapshot has the DAC {what} sub-index at {sub}, past the \
{DAC_COMPONENTS} components of an entry"
)));
}
}
let level = {
let mut current = self.shared.state.lock();
*current = state;
current.renormalize();
self.shared.publish(¤t);
current.in_vsync()
};
self.shared.drive_vsync(Level::from_bool(level));
Ok(())
}
}
impl Instance for Video {}
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Video::new(props)?)))
}
#[must_use]
pub fn schema() -> ClassSchema {
use crate::machine::validate::{PortDir, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("dot-clock", ValueKind::Uint).range(0, 1_000_000_000))
.region("")
.region("regs")
.region("crtc")
.region("crtc-colour")
.region("crtc-mono")
.region("status")
.region("mode")
.region("vga")
.region("vram")
.port("vsync", PortDir::Out)
}
#[derive(Debug, Clone)]
pub struct VideoScanout {
shared: Arc<Shared>,
}
impl VideoScanout {
#[must_use]
pub fn new(video: &Video) -> VideoScanout {
video.scanout()
}
}
const CURSOR_BLINK_FRAMES: u64 = 8;
const TEXT_BLINK_FRAMES: u64 = 16;
impl Scanout for VideoScanout {
fn info(&self) -> SurfaceInfo {
let state = self.shared.state.lock();
let width = state.columns() as u32 * state.char_width();
let height = (state.rows() * state.cell_height()) as u32;
SurfaceInfo::new(width, height, PixelFormat::RGBA8888)
}
fn frame_counter(&self) -> u64 {
self.shared.frames.load(Ordering::Relaxed)
}
fn frame_period_ns(&self) -> u64 {
let state = self.shared.state.lock();
let per = state.ticks_per_frame();
let dot_hz = state.dot_clock_hz(self.shared.dot_clock_hz);
if per == 0 || dot_hz == 0 {
return 0;
}
per.saturating_mul(u64::from(state.char_width()))
.saturating_mul(1_000_000_000)
/ dot_hz
}
fn capture(&self, dst: &mut Surface) -> u64 {
let state = self.shared.state.lock().clone();
let width = state.columns() as u32 * state.char_width();
let height = (state.rows() * state.cell_height()) as u32;
dst.reshape(dst.format(), width, height);
let serial = state.frames;
if !state.video_enabled() {
dst.fill([0, 0, 0]);
dst.set_serial(serial);
return serial;
}
let cell_height = state.cell_height();
let char_width = state.char_width();
let columns = state.columns();
let blink = state.blink_enabled();
let text_visible = (state.frames / TEXT_BLINK_FRAMES).is_multiple_of(2);
let cursor_visible = match (state.crtc[10] >> 5) & 0x03 {
0 => true,
1 => false,
2 => (state.frames / CURSOR_BLINK_FRAMES).is_multiple_of(2),
_ => (state.frames / (2 * CURSOR_BLINK_FRAMES)).is_multiple_of(2),
};
let cursor_at = state.cursor_address();
let cursor_first = u64::from(state.crtc[10] & 0x1f);
let cursor_last = u64::from(state.crtc[11] & 0x1f);
let line_graphics = state.vga.attr[16] & 0x04 != 0;
for row in 0..state.rows() {
for column in 0..columns {
let address = (state.start_address() + row * columns + column) & 0x3fff;
let offset = address * 2;
let code = self.shared.vram.read_u8(offset).unwrap_or(0);
let attribute = self.shared.vram.read_u8(offset + 1).unwrap_or(0);
let mut foreground = attribute & 0x0f;
let background = if blink {
(attribute >> 4) & 0x07
} else {
(attribute >> 4) & 0x0f
};
let blinking = blink && attribute & 0x80 != 0;
if blinking && !text_visible {
foreground = background;
}
let fg = state.rgb(foreground);
let bg = state.rgb(background);
let bitmap = glyph(code);
let cursor_here = cursor_visible
&& address == cursor_at
&& cursor_first <= cursor_last;
for line in 0..cell_height {
let source = (line * FONT_CELL_HEIGHT as u64) / cell_height;
let bits = bitmap[source as usize];
let on_cursor = cursor_here && line >= cursor_first && line <= cursor_last;
let y = (row * cell_height + line) as u32;
for dot in 0..char_width {
let lit = if on_cursor {
true
} else if dot < 8 {
bits & (0x80 >> dot) != 0
} else if line_graphics && (0xc0..=0xdf).contains(&code) {
bits & 0x01 != 0
} else {
false
};
let x = column as u32 * char_width + dot;
dst.put(x, y, if lit { fg } else { bg });
}
}
}
}
dst.set_serial(serial);
serial
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
const ADDRESS: u64 = 0;
const DATA: u64 = 1;
fn device() -> Video {
Video::default_device()
}
fn port(video: &Video, window: Window) -> Port {
Port {
shared: Arc::clone(&video.shared),
window,
}
}
fn peek(video: &Video, window: Window, offset: u64) -> u8 {
let mut byte = [0u8; 1];
port(video, window)
.read(offset, &mut byte, MemAttrs::DEFAULT)
.expect("a byte read is legal");
byte[0]
}
fn peek_debug(video: &Video, window: Window, offset: u64) -> u8 {
let mut byte = [0u8; 1];
port(video, window)
.read(offset, &mut byte, MemAttrs::DEBUG)
.expect("a byte read is legal");
byte[0]
}
fn poke(video: &Video, window: Window, offset: u64, value: u8) {
port(video, window)
.write(offset, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn crtc(video: &Video, index: u8, value: u8) {
poke(video, Window::Crtc, ADDRESS, index);
poke(video, Window::Crtc, DATA, value);
}
fn write_text(video: &Video, address: u64, text: &str, attr: u8) {
write_bytes(video, address, text.as_bytes(), attr);
}
fn write_bytes(video: &Video, address: u64, codes: &[u8], attr: u8) {
for (i, byte) in codes.iter().enumerate() {
let offset = (address + i as u64) * 2;
video.vram().write_u8(offset, *byte).unwrap();
video.vram().write_u8(offset + 1, attr).unwrap();
}
}
fn hide_cursor(video: &Video) {
crtc(video, 10, 0x20);
}
fn captured(video: &Video) -> Surface {
let scanout = video.scanout();
let mut surface = Surface::for_scanout(&scanout);
scanout.capture(&mut surface);
surface
}
#[test]
fn the_address_register_selects_which_register_a_write_lands_in() {
let video = device();
crtc(&video, 12, 0x11);
crtc(&video, 13, 0x22);
let state = video.shared.state.lock();
assert_eq!(state.crtc[12], 0x11);
assert_eq!(state.crtc[13], 0x22);
assert_eq!(state.crtc[14], CRTC_DEFAULTS[14]);
}
#[test]
fn registers_12_to_17_read_back_and_0_to_11_do_not() {
let video = device();
crtc(&video, 0, 0x5a);
poke(&video, Window::Crtc, ADDRESS, 0);
assert_eq!(peek(&video, Window::Crtc, DATA), 0);
assert_eq!(video.shared.state.lock().crtc[0], 0x5a, "but it was stored");
for (index, value) in [(12u8, 0x0a), (13, 0xbc), (14, 0x01), (15, 0x23)] {
crtc(&video, index, value);
poke(&video, Window::Crtc, ADDRESS, index);
assert_eq!(
peek(&video, Window::Crtc, DATA),
value & CRTC_WRITE_MASK[index as usize]
);
}
poke(&video, Window::Crtc, ADDRESS, 16);
assert_eq!(peek(&video, Window::Crtc, DATA), 0);
poke(&video, Window::Crtc, DATA, 0xff);
assert_eq!(
peek(&video, Window::Crtc, DATA),
0,
"and a write is refused"
);
poke(&video, Window::Crtc, ADDRESS, 20);
assert_eq!(peek(&video, Window::Crtc, DATA), 0);
assert_eq!(peek(&video, Window::Crtc, ADDRESS), 20);
}
#[test]
fn moving_the_start_address_scrolls_the_picture() {
let video = device();
hide_cursor(&video);
write_text(&video, 0, "TOP", 0x07);
write_text(&video, 80, "NEXT", 0x07);
let first = captured(&video);
crtc(&video, 12, 0);
crtc(&video, 13, 80);
let scrolled = captured(&video);
assert_ne!(first.hash(), scrolled.hash(), "the picture moved");
let reference = device();
hide_cursor(&reference);
write_text(&reference, 0, "NEXT", 0x07);
let expect = captured(&reference);
for y in 0..16u32 {
for x in 0..(4 * 9) as u32 {
assert_eq!(scrolled.get(x, y), expect.get(x, y), "at {x},{y}");
}
}
}
#[test]
fn the_cursor_sits_where_r14_and_r15_name_it() {
let video = device();
let address = 80 + 3;
write_bytes(&video, address, b" ", 0x07);
crtc(&video, 14, (address >> 8) as u8);
crtc(&video, 15, (address & 0xff) as u8);
crtc(&video, 10, 14);
crtc(&video, 11, 15);
let surface = captured(&video);
let x = 3 * 9 + 1;
let top = 16;
assert_eq!(
surface.get(x, top + 14),
Some([0xaa, 0xaa, 0xaa]),
"the cursor's first raster is lit in the foreground colour"
);
assert_eq!(surface.get(x, top + 15), Some([0xaa, 0xaa, 0xaa]));
assert_eq!(
surface.get(x, top + 13),
Some([0, 0, 0]),
"and the raster above it is not"
);
crtc(&video, 10, 14 | 0x20);
let hidden = captured(&video);
assert_eq!(hidden.get(x, top + 14), Some([0, 0, 0]));
assert_eq!(hidden.get(x, top + 15), Some([0, 0, 0]));
}
#[test]
fn the_retrace_bit_follows_the_clock_and_a_debug_read_does_not_advance_it() {
let video = device();
let chars = 100u64;
let vsync_start = 400 * chars;
assert_eq!(peek(&video, Window::Status, 0) & STATUS_VSYNC, 0);
video.advance_to(chars * 10);
assert_eq!(
peek(&video, Window::Status, 0) & STATUS_VSYNC,
0,
"line 10 is displayed"
);
assert_eq!(
peek(&video, Window::Status, 0) & STATUS_DISPLAY_ENABLE,
0,
"and the beam is inside the displayed columns"
);
video.advance_to(chars * 10 + 90);
assert_eq!(
peek(&video, Window::Status, 0) & STATUS_DISPLAY_ENABLE,
STATUS_DISPLAY_ENABLE,
"past column 80 is horizontal blanking"
);
video.advance_to(vsync_start + 5);
assert_eq!(
peek(&video, Window::Status, 0) & STATUS_VSYNC,
STATUS_VSYNC,
"inside the sixteen-line sync pulse"
);
video.advance_to(vsync_start + VSYNC_LINES * chars + 1);
assert_eq!(
peek(&video, Window::Status, 0) & STATUS_VSYNC,
0,
"and out the other side"
);
let before = video.ticks();
let _ = peek_debug(&video, Window::Status, 0);
assert_eq!(video.ticks(), before);
}
#[test]
fn a_debug_read_of_the_status_register_leaves_the_flip_flop_alone() {
let video = device();
poke(&video, Window::Vga, 0x0, 0x00);
assert!(video.shared.state.lock().vga.attr_data_next);
let _ = peek_debug(&video, Window::Status, 0);
assert!(
video.shared.state.lock().vga.attr_data_next,
"a debugger must not resynchronise the guest's flip-flop"
);
let _ = peek(&video, Window::Status, 0);
assert!(!video.shared.state.lock().vga.attr_data_next);
}
#[test]
fn the_attribute_flip_flop_alternates_between_index_and_data() {
let video = device();
poke(&video, Window::Vga, 0x0, 0x02);
poke(&video, Window::Vga, 0x0, 0x3f);
assert_eq!(peek(&video, Window::Vga, 0x1), 0x3f);
assert_eq!(peek(&video, Window::Vga, 0x0), 0x02, "the index reads back");
let _ = peek(&video, Window::Status, 0);
poke(&video, Window::Vga, 0x0, 0x03);
poke(&video, Window::Vga, 0x0, 0x11);
assert_eq!(peek(&video, Window::Vga, 0x1), 0x11);
assert_eq!(
video.shared.state.lock().vga.attr[2],
0x3f,
"and the earlier register kept its value"
);
}
#[test]
fn the_dac_round_trips_every_entry_and_auto_increments() {
let video = device();
poke(&video, Window::Vga, 0x8, 0);
for i in 0..DAC_ENTRIES {
poke(&video, Window::Vga, 0x9, (i % 64) as u8);
poke(&video, Window::Vga, 0x9, ((i + 1) % 64) as u8);
poke(&video, Window::Vga, 0x9, ((i + 2) % 64) as u8);
}
assert_eq!(video.shared.state.lock().vga.dac_write, 0);
poke(&video, Window::Vga, 0x7, 0);
assert_eq!(peek(&video, Window::Vga, 0x7), 3, "the DAC is in read mode");
for i in 0..DAC_ENTRIES {
assert_eq!(peek(&video, Window::Vga, 0x9), (i % 64) as u8);
assert_eq!(peek(&video, Window::Vga, 0x9), ((i + 1) % 64) as u8);
assert_eq!(peek(&video, Window::Vga, 0x9), ((i + 2) % 64) as u8);
}
poke(&video, Window::Vga, 0x8, 0x10);
assert_eq!(peek(&video, Window::Vga, 0x8), 0x10);
assert_eq!(peek(&video, Window::Vga, 0x7), 0, "and back to write mode");
poke(&video, Window::Vga, 0x8, 5);
poke(&video, Window::Vga, 0x9, 0xff);
assert_eq!(video.shared.state.lock().vga.dac[5][0], 0x3f);
}
#[test]
fn misc_output_bit_0_chooses_which_crtc_window_answers() {
let video = device();
poke(&video, Window::CrtcColour, ADDRESS, 12);
assert_eq!(peek(&video, Window::CrtcColour, ADDRESS), 12);
assert_eq!(
peek(&video, Window::CrtcMono, ADDRESS),
0xff,
"the monochrome pair is not decoded"
);
poke(&video, Window::CrtcMono, ADDRESS, 3);
assert_eq!(peek(&video, Window::CrtcColour, ADDRESS), 12, "and inert");
poke(&video, Window::Vga, 0x2, 0x66);
assert_eq!(peek(&video, Window::Vga, 0xc), 0x66, "0x3cc reads it back");
assert_eq!(peek(&video, Window::CrtcColour, ADDRESS), 0xff);
poke(&video, Window::CrtcMono, ADDRESS, 7);
assert_eq!(peek(&video, Window::CrtcMono, ADDRESS), 7);
assert_eq!(peek(&video, Window::Crtc, ADDRESS), 7);
}
#[test]
fn an_attributes_colours_land_in_the_right_pixels() {
let video = device();
hide_cursor(&video);
write_bytes(&video, 0, b" ", 0x1f);
write_bytes(&video, 1, &[0xdb], 0x4e);
write_bytes(&video, 2, b"*", 0x21);
let surface = captured(&video);
assert_eq!(surface.get(0, 0), Some([0x00, 0x00, 0xaa]), "blue ground");
assert_eq!(
surface.get(9, 5),
Some([0xff, 0xff, 0x55]),
"yellow on the block's first column"
);
assert_eq!(surface.get(9 + 8, 5), Some([0xff, 0xff, 0x55]));
assert_eq!(surface.get(18 + 8, 5), Some([0x00, 0xaa, 0x00]));
assert_eq!(
surface.get(18 + 3, 5),
Some([0x00, 0x00, 0xaa]),
"the glyph"
);
}
#[test]
fn a_palette_change_is_visible_in_what_capture_produces() {
let video = device();
hide_cursor(&video);
write_bytes(&video, 0, &[0xdb], 0x0f);
assert_eq!(captured(&video).get(0, 0), Some([0xff, 0xff, 0xff]));
poke(&video, Window::Vga, 0x8, 15);
poke(&video, Window::Vga, 0x9, 0x3f);
poke(&video, Window::Vga, 0x9, 0x00);
poke(&video, Window::Vga, 0x9, 0x00);
assert_eq!(captured(&video).get(0, 0), Some([0xff, 0x00, 0x00]));
poke(&video, Window::Vga, 0x0, 0x0f);
poke(&video, Window::Vga, 0x0, 0x01);
assert_eq!(
captured(&video).get(0, 0),
Some([0x00, 0x00, 0x00]),
"blanked while the palette is being programmed"
);
poke(&video, Window::Vga, 0x0, 0x20);
assert_eq!(captured(&video).get(0, 0), Some([0x00, 0x00, 0xaa]));
}
#[test]
fn capturing_a_known_string_gives_a_stable_hash() {
let video = device();
write_text(&video, 0, "rsemu 0.1 -- PC video", 0x0f);
write_bytes(&video, 80, &[0xc9, 0xcd, 0xcd, 0xbb], 0x1e);
let surface = captured(&video);
assert_eq!(surface.width(), 720);
assert_eq!(surface.height(), 400);
assert_eq!(surface.hash(), 0xe736_85eb_ec2d_b0dd);
}
#[test]
fn the_frame_period_comes_from_the_registers_and_the_clock() {
let video = device();
let scanout = video.scanout();
let expect = 100 * 449 * 9 * 1_000_000_000 / DOT_CLOCK_28MHZ;
assert_eq!(scanout.frame_period_ns(), expect);
crtc(&video, 4, 25);
let lines = 26 * 16 + 1;
assert_eq!(
scanout.frame_period_ns(),
100 * lines * 9 * 1_000_000_000 / DOT_CLOCK_28MHZ
);
poke(&video, Window::Vga, 0x2, 0x63);
assert_eq!(
scanout.frame_period_ns(),
100 * lines * 9 * 1_000_000_000 / DOT_CLOCK_25MHZ
);
poke(&video, Window::Vga, 0x4, 1);
poke(&video, Window::Vga, 0x5, 0x01);
assert_eq!(
scanout.frame_period_ns(),
100 * lines * 8 * 1_000_000_000 / DOT_CLOCK_25MHZ
);
assert_eq!(scanout.info().width, 640, "and the picture narrows");
let fixed = Video::with_dot_clock(DOT_CLOCK_25MHZ);
assert_eq!(
fixed.scanout().frame_period_ns(),
100 * 449 * 9 * 1_000_000_000 / DOT_CLOCK_25MHZ
);
}
#[test]
fn frames_advance_with_the_clock_and_bound_the_next_event() {
let video = device();
let per = 100 * 449;
assert_eq!(video.frames(), 0);
assert_eq!(video.scanout().frame_counter(), 0);
video.advance_to(per * 3 + 7);
assert_eq!(video.frames(), 3);
let next = Device::next_event_tick(&video).expect("a sync edge");
assert!(next > video.ticks() && next <= per * 4);
assert!(Device::is_lazy(&video));
}
#[test]
fn a_snapshot_round_trip_is_byte_identical() {
let saved = device();
crtc(&saved, 12, 0x01);
crtc(&saved, 13, 0x40);
crtc(&saved, 9, 7);
poke(&saved, Window::Mode, 0, 0x2d);
poke(&saved, Window::Mode, 1, 0x3a);
poke(&saved, Window::Vga, 0x2, 0x63);
poke(&saved, Window::Vga, 0x0, 0x05);
poke(&saved, Window::Vga, 0x0, 0x2a);
poke(&saved, Window::Vga, 0x8, 3);
poke(&saved, Window::Vga, 0x9, 0x11);
poke(&saved, Window::Vga, 0x9, 0x22);
poke(&saved, Window::Vga, 0xe, 4);
poke(&saved, Window::Vga, 0xf, 0x0f);
saved.vram().write_u8(0, b'r').unwrap();
saved.vram().write_u8(1, 0x0f).unwrap();
saved.vram().write_u8(VRAM_LEN - 1, 0xa5).unwrap();
saved.advance_to(123_456);
let image = |video: &Video| {
let mut shape = MachineShape::new();
shape.add_device("vga", CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("vga", CLASS.name, CLASS.version).unwrap();
video.save(&mut chunk).unwrap();
}
w.to_vec().unwrap()
};
let bytes = image(&saved);
let restored = device();
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load("vga", CLASS.name, CLASS.version, &Migrations::new())
.unwrap();
restored.load(&mut chunk.reader()).unwrap();
assert_eq!(image(&restored), bytes, "the two save images agree");
assert_eq!(restored.ticks(), 123_456);
assert_eq!(restored.frames(), saved.frames());
assert_eq!(restored.status(), saved.status());
assert_eq!(restored.vram().read_u8(0).unwrap(), b'r');
assert_eq!(restored.vram().read_u8(1).unwrap(), 0x0f);
assert_eq!(restored.vram().read_u8(VRAM_LEN - 1).unwrap(), 0xa5);
}
#[test]
fn a_dac_sub_index_out_of_range_is_diagnosed_rather_than_panicked() {
let image = |video: &Video| {
let mut shape = MachineShape::new();
shape.add_device("vga", CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("vga", CLASS.name, CLASS.version).unwrap();
video.save(&mut chunk).unwrap();
}
w.to_vec().unwrap()
};
let plain = image(&device());
let stepped = device();
poke(&stepped, Window::Vga, 0x9, 0x2a);
let at = plain
.iter()
.zip(image(&stepped).iter())
.position(|(a, b)| a != b)
.expect("writing a component moves the sub-index");
let mut bytes = plain;
bytes[at] = 7;
let restored = device();
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load("vga", CLASS.name, CLASS.version, &Migrations::new())
.unwrap();
assert!(
restored.load(&mut chunk.reader()).is_err(),
"a DAC sub-index of 7 was accepted, and indexes an array of {DAC_COMPONENTS}"
);
}
#[test]
fn a_debug_write_is_refused_and_so_is_a_wide_access() {
let video = device();
assert!(
port(&video, Window::Crtc)
.write(ADDRESS, &[0], MemAttrs::DEBUG)
.is_err()
);
assert!(
port(&video, Window::Crtc)
.read(ADDRESS, &mut [0u8; 2], MemAttrs::DEFAULT)
.is_err()
);
assert!(
port(&video, Window::Vga)
.write(0, &[0u8; 4], MemAttrs::DEFAULT)
.is_err()
);
}
#[test]
fn the_regions_a_machine_file_maps_all_exist() {
let video = device();
for name in [
"",
"crtc",
"crtc-colour",
"crtc-mono",
"status",
"mode",
"vga",
"vram",
] {
assert!(video.region(name).is_some(), "region {name}");
}
assert!(video.region("nonsense").is_none());
assert_eq!(video.region("vram").expect("vram").len(), VRAM_LEN);
}
#[test]
fn properties_are_checked_rather_than_ignored() {
assert!(Video::new(&Props::new().with("dot-clock", 25_175_000u64)).is_ok());
assert!(Video::new(&Props::new().with("dotclock", 1u64)).is_err());
}
#[test]
fn a_code_point_with_no_glyph_is_visibly_missing() {
assert_eq!(glyph(0x01), &FONT_BLANK);
assert_eq!(glyph(0xf0), &FONT_MISSING);
assert_eq!(glyph(b'A'), &FONT_ASCII[(b'A' - FONT_FIRST) as usize]);
for code in [
0xb3u8, 0xba, 0xc4, 0xcd, 0xda, 0xbf, 0xc0, 0xd9, 0xc9, 0xbb, 0xc8, 0xbc,
] {
assert_ne!(glyph(code), &FONT_MISSING, "no glyph for {code:#04x}");
}
}
}