summaryrefslogtreecommitdiff
path: root/src/devices/screen_device.rs
diff options
context:
space:
mode:
authorBen Bridle <bridle.benjamin@gmail.com>2024-10-28 20:25:01 +1300
committerBen Bridle <bridle.benjamin@gmail.com>2024-10-28 20:29:12 +1300
commit1a830a3d1b9d99653322d5ae49ea8165de7ed9d0 (patch)
tree798e77b6fcf2438b1c2538a67efe856a2f7cb979 /src/devices/screen_device.rs
parent03c4b069e1806af256730639cefdae115b24401a (diff)
downloadbedrock-pc-1a830a3d1b9d99653322d5ae49ea8165de7ed9d0.zip
Rewrite emulatorv1.0.0-alpha1
This is a complete rewrite and restructure of the entire emulator project, as part of the effort in locking down the Bedrock specification and in creating much better tooling for creating and using Bedrock programs. This commit adds a command-line argument scheme, an embedded assembler, a headless emulator for use in non-graphical environments, deferred window creation for programs that do not access the screen device, and new versions of phosphor and bedrock-core. The new version of phosphor supports multi-window programs, which will make it possible to implement program forking in the system device later on, and the new version of bedrock-core implements the final core specification.
Diffstat (limited to 'src/devices/screen_device.rs')
-rw-r--r--src/devices/screen_device.rs485
1 files changed, 485 insertions, 0 deletions
diff --git a/src/devices/screen_device.rs b/src/devices/screen_device.rs
new file mode 100644
index 0000000..64f3815
--- /dev/null
+++ b/src/devices/screen_device.rs
@@ -0,0 +1,485 @@
+use crate::*;
+
+use bedrock_core::*;
+use geometry::*;
+use phosphor::*;
+
+type Sprite = [[u8; 8]; 8];
+#[derive(Clone, Copy)]
+pub enum Layer { Fg, Bg }
+
+
+pub struct ScreenDevice {
+ pub wake: bool,
+ pub accessed: bool,
+
+ /// Each byte represents a screen pixel, left-to-right and top-to-bottom.
+ // Only the bottom four bits of each byte are used.
+ // TODO: Consider using the high bit of each pixel byte as a dirty bit.
+ pub fg: Vec<u8>,
+ pub bg: Vec<u8>,
+ pub dirty: bool,
+
+ pub cursor: ScreenPosition,
+ pub vector: ScreenPosition,
+
+ pub dimensions: ScreenDimensions,
+ pub dirty_dimensions: bool,
+ pub width_write: u16,
+ pub height_write: u16,
+ pub fixed_width: Option<u16>,
+ pub fixed_height: Option<u16>,
+
+ pub palette_write: u16,
+ pub palette: [Colour; 16],
+ pub colours: u16,
+ pub sprite: SpriteBuffer,
+}
+
+impl ScreenDevice {
+ pub fn new(config: &EmulatorConfig) -> Self {
+ let area = config.dimensions.area_usize();
+
+ Self {
+ wake: false,
+ accessed: false,
+
+ fg: vec![0; area],
+ bg: vec![0; area],
+ dirty: false,
+
+ cursor: ScreenPosition::ZERO,
+ vector: ScreenPosition::ZERO,
+
+ dimensions: config.dimensions,
+ dirty_dimensions: true,
+ width_write: 0,
+ height_write: 0,
+ fixed_width: None,
+ fixed_height: None,
+
+ palette_write: 0,
+ palette: [Colour::BLACK; 16],
+ colours: 0,
+ sprite: SpriteBuffer::new(),
+ }
+ }
+
+ /// External resize.
+ pub fn resize(&mut self, dimensions: phosphor::Dimensions) {
+ // Replace dimensions with fixed dimensions.
+ let screen_dimensions = ScreenDimensions {
+ width: match self.fixed_width {
+ Some(fixed_width) => fixed_width,
+ None => dimensions.width as u16,
+ },
+ height: match self.fixed_height {
+ Some(fixed_height) => fixed_height,
+ None => dimensions.height as u16,
+ },
+ };
+ let old_dimensions = self.dimensions;
+ if self.dimensions != screen_dimensions {
+ self.dimensions = screen_dimensions;
+ self.resize_layers(old_dimensions);
+ self.wake = true;
+ }
+ }
+
+ /// Internal resize.
+ fn resize_width(&mut self) {
+ self.fixed_width = Some(self.width_write);
+ self.dirty_dimensions = true;
+ let old_dimensions = self.dimensions;
+ if self.dimensions.width != self.width_write {
+ self.dimensions.width = self.width_write;
+ self.resize_layers(old_dimensions);
+ }
+ }
+
+ /// Internal resize.
+ fn resize_height(&mut self) {
+ self.fixed_height = Some(self.height_write);
+ self.dirty_dimensions = true;
+ let old_dimensions = self.dimensions;
+ if self.dimensions.height != self.height_write {
+ self.dimensions.height = self.height_write;
+ self.resize_layers(old_dimensions);
+ }
+ }
+
+ fn resize_layers(&mut self, old_dimensions: ScreenDimensions) {
+ use std::cmp::{min, Ordering};
+
+ let old_width = old_dimensions.width as usize;
+ let old_height = old_dimensions.height as usize;
+ let new_width = self.dimensions.width as usize;
+ let new_height = self.dimensions.height as usize;
+ let new_area = self.dimensions.area_usize();
+ let y_range = 0..min(old_height, new_height);
+ let new_colour = match self.fg.last() {
+ None | Some(0) => *self.bg.last().unwrap_or(&0),
+ Some(colour) => *colour,
+ };
+
+ match new_width.cmp(&old_width) {
+ Ordering::Less => {
+ for y in y_range {
+ let src = y * old_width;
+ let dest = y * new_width;
+ let len = new_width;
+ self.fg.copy_within(src..src+len, dest);
+ self.bg.copy_within(src..src+len, dest);
+ }
+ self.fg.resize(new_area, 0);
+ self.bg.resize(new_area, new_colour);
+ },
+ Ordering::Greater => {
+ self.fg.resize(new_area, 0);
+ self.bg.resize(new_area, new_colour);
+ for y in y_range.rev() {
+ let src = y * old_width;
+ let dest = y * new_width;
+ let len = old_width;
+ self.fg.copy_within(src..src+len, dest);
+ self.bg.copy_within(src..src+len, dest);
+ self.fg[dest+len..dest+new_width].fill(0);
+ self.bg[dest+len..dest+new_width].fill(new_colour);
+ }
+ },
+ Ordering::Equal => {
+ self.fg.resize(new_area, 0);
+ self.bg.resize(new_area, new_colour);
+ },
+ };
+
+ self.dirty = true;
+ }
+
+ pub fn set_palette(&mut self) {
+ let i = (self.palette_write >> 12 ) as usize;
+ let r = (self.palette_write >> 8 & 0xf) as u8 * 17;
+ let g = (self.palette_write >> 4 & 0xf) as u8 * 17;
+ let b = (self.palette_write & 0xf) as u8 * 17;
+ let colour = Colour::from_rgb(r, g, b);
+ if self.palette[i] != colour {
+ self.palette[i] = colour;
+ self.dirty = true;
+ }
+ }
+
+ pub fn draw_dispatch(&mut self, draw: u8) {
+ match draw >> 4 {
+ 0x0 => self.op_draw_pixel(Layer::Bg, draw),
+ 0x1 => self.op_draw_sprite(Layer::Bg, draw),
+ 0x2 => self.op_fill_layer(Layer::Bg, draw),
+ 0x3 => self.op_draw_sprite(Layer::Bg, draw),
+ 0x4 => self.op_draw_line(Layer::Bg, draw),
+ 0x5 => self.op_draw_line(Layer::Bg, draw),
+ 0x6 => self.op_draw_rect(Layer::Bg, draw),
+ 0x7 => self.op_draw_rect(Layer::Bg, draw),
+ 0x8 => self.op_draw_pixel(Layer::Fg, draw),
+ 0x9 => self.op_draw_sprite(Layer::Fg, draw),
+ 0xA => self.op_fill_layer(Layer::Fg, draw),
+ 0xB => self.op_draw_sprite(Layer::Fg, draw),
+ 0xC => self.op_draw_line(Layer::Fg, draw),
+ 0xD => self.op_draw_line(Layer::Fg, draw),
+ 0xE => self.op_draw_rect(Layer::Fg, draw),
+ 0xF => self.op_draw_rect(Layer::Fg, draw),
+ _ => unreachable!(),
+ }
+ self.vector = self.cursor;
+ self.dirty = true;
+ }
+
+ pub fn move_cursor(&mut self, value: u8) {
+ let distance = (value & 0x3f) as u16;
+ match value >> 6 {
+ 0b00 => self.cursor.x = self.cursor.x.wrapping_add(distance),
+ 0b01 => self.cursor.y = self.cursor.y.wrapping_add(distance),
+ 0b10 => self.cursor.x = self.cursor.x.wrapping_sub(distance),
+ 0b11 => self.cursor.y = self.cursor.y.wrapping_sub(distance),
+ _ => unreachable!(),
+ };
+ }
+
+ /// Colour must already be masked by 0xf.
+ pub fn draw_pixel(&mut self, layer: Layer, x: u16, y: u16, colour: u8) {
+ if x < self.dimensions.width && y < self.dimensions.height {
+ let index = x as usize + (self.dimensions.width as usize * y as usize);
+ match layer {
+ Layer::Fg => self.fg[index] = colour,
+ Layer::Bg => self.bg[index] = colour,
+ };
+ }
+ }
+
+ fn op_draw_pixel(&mut self, layer: Layer, draw: u8) {
+ self.draw_pixel(layer, self.cursor.x, self.cursor.y, draw & 0xf);
+ }
+
+ fn op_fill_layer(&mut self, layer: Layer, draw: u8) {
+ match layer {
+ Layer::Fg => self.fg.fill(draw & 0xf),
+ Layer::Bg => self.bg.fill(draw & 0xf),
+ }
+ }
+
+ fn op_draw_sprite(&mut self, layer: Layer, draw: u8) {
+ let sprite = match draw & 0x20 != 0 {
+ true => self.sprite.read_2bit_sprite(draw),
+ false => self.sprite.read_1bit_sprite(draw),
+ };
+ let colours = [
+ (self.colours >> 12 & 0x000f) as u8,
+ (self.colours >> 8 & 0x000f) as u8,
+ (self.colours >> 4 & 0x000f) as u8,
+ (self.colours & 0x000f) as u8,
+ ];
+ let cx = self.cursor.x;
+ let cy = self.cursor.y;
+
+ if draw & 0x08 != 0 {
+ // Draw sprite with transparent background
+ for y in 0..8 {
+ for x in 0..8 {
+ let index = sprite[y as usize][x as usize] as usize;
+ if index != 0 {
+ let px = cx.wrapping_add(x);
+ let py = cy.wrapping_add(y);
+ self.draw_pixel(layer, px, py, colours[index]);
+ }
+ }
+ }
+ } else {
+ // Draw sprite with opaque background
+ for y in 0..8 {
+ for x in 0..8 {
+ let index = sprite[y as usize][x as usize] as usize;
+ let px = cx.wrapping_add(x);
+ let py = cy.wrapping_add(y);
+ self.draw_pixel(layer, px, py, colours[index]);
+ }
+ }
+ }
+ }
+
+ fn op_draw_line(&mut self, layer: Layer, draw: u8) {
+ let mut x: i16 = self.cursor.x as i16;
+ let mut y: i16 = self.cursor.y as i16;
+ let x_end: i16 = self.vector.x as i16;
+ let y_end: i16 = self.vector.y as i16;
+
+ let dx: i16 = (x_end - x).abs();
+ let dy: i16 = -(y_end - y).abs();
+ let sx: i16 = if x < x_end { 1 } else { -1 };
+ let sy: i16 = if y < y_end { 1 } else { -1 };
+ let mut e1: i16 = dx + dy;
+
+ if draw & 0x10 != 0 {
+ // Draw 1-bit textured line.
+ let sprite = self.sprite.read_1bit_sprite(draw);
+ let c1 = (self.colours >> 8 & 0xf) as u8;
+ let c0 = (self.colours >> 12 & 0xf) as u8;
+ let opaque = draw & 0x08 == 0;
+ loop {
+ let sprite_pixel = sprite[(y % 8) as usize][(x % 8) as usize];
+ if sprite_pixel != 0 { self.draw_pixel(layer, x as u16, y as u16, c1); }
+ else if opaque { self.draw_pixel(layer, x as u16, y as u16, c0); }
+ if x == x_end && y == y_end { break; }
+ let e2 = e1 << 1;
+ if e2 >= dy { e1 += dy; x += sx; }
+ if e2 <= dx { e1 += dx; y += sy; }
+ }
+ } else {
+ // Draw solid line.
+ let colour = draw & 0xf;
+ loop {
+ self.draw_pixel(layer, x as u16, y as u16, colour);
+ if x == x_end && y == y_end { break; }
+ let e2 = e1 << 1;
+ if e2 >= dy { e1 += dy; x += sx; }
+ if e2 <= dx { e1 += dx; y += sy; }
+ }
+ }
+ }
+
+ fn op_draw_rect(&mut self, layer: Layer, draw: u8) {
+ macro_rules! clamp {
+ ($v:expr, $max:expr) => {
+ if $v > 0x7fff { 0 } else if $v > $max { $max } else { $v }
+ };
+ }
+ macro_rules! out_of_bounds {
+ ($axis:ident, $max:expr) => {{
+ let c = self.cursor.$axis;
+ let v = self.vector.$axis;
+ c >= $max && v >= $max && (c >= 0x8000) == (v >= 0x8000)
+ }};
+ }
+
+ let out_of_bounds_x = out_of_bounds!(x, self.dimensions.width);
+ let out_of_bounds_y = out_of_bounds!(y, self.dimensions.height);
+ if out_of_bounds_x || out_of_bounds_y { return; }
+
+ // Get bounding box.
+ let mut l = clamp!(self.vector.x, self.dimensions.width -1);
+ let mut r = clamp!(self.cursor.x, self.dimensions.width -1);
+ let mut t = clamp!(self.vector.y, self.dimensions.height -1);
+ let mut b = clamp!(self.cursor.y, self.dimensions.height -1);
+ if l > r { std::mem::swap(&mut l, &mut r) };
+ if t > b { std::mem::swap(&mut t, &mut b) };
+
+ if draw & 0x10 != 0 {
+ // Draw 1-bit textured rectangle.
+ let sprite = self.sprite.read_1bit_sprite(draw);
+ let c1 = (self.colours >> 8 & 0xf) as u8;
+ let c0 = (self.colours >> 12 & 0xf) as u8;
+ let opaque = draw & 0x08 == 0;
+ for y in t..=b {
+ for x in l..=r {
+ let sprite_colour = sprite[(y % 8) as usize][(x % 8) as usize];
+ if sprite_colour != 0 { self.draw_pixel(layer, x, y, c1); }
+ else if opaque { self.draw_pixel(layer, x, y, c0); }
+ }
+ }
+ } else {
+ // Draw solid rectangle.
+ let colour = draw & 0xf;
+ for y in t..=b {
+ for x in l..=r {
+ self.draw_pixel(layer, x, y, colour);
+ }
+ }
+ }
+ }
+}
+
+impl Device for ScreenDevice {
+ fn read(&mut self, port: u8) -> u8 {
+ self.accessed = true;
+ match port {
+ 0x0 => read_h!(self.dimensions.width),
+ 0x1 => read_l!(self.dimensions.width),
+ 0x2 => read_h!(self.dimensions.height),
+ 0x3 => read_l!(self.dimensions.height),
+ 0x4 => read_h!(self.cursor.x),
+ 0x5 => read_l!(self.cursor.x),
+ 0x6 => read_h!(self.cursor.y),
+ 0x7 => read_l!(self.cursor.y),
+ 0x8 => 0,
+ 0x9 => 0,
+ 0xa => 0,
+ 0xb => 0,
+ 0xc => 0,
+ 0xd => 0,
+ 0xe => 0,
+ 0xf => 0,
+ _ => unreachable!(),
+ }
+ }
+
+ fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+ self.accessed = true;
+ match port {
+ 0x0 => write_h!(self.width_write, value),
+ 0x1 => { write_l!(self.width_write, value); self.resize_width(); },
+ 0x2 => write_h!(self.height_write, value),
+ 0x3 => { write_l!(self.height_write, value); self.resize_height(); },
+ 0x4 => write_h!(self.cursor.x, value),
+ 0x5 => write_l!(self.cursor.x, value),
+ 0x6 => write_h!(self.cursor.y, value),
+ 0x7 => write_l!(self.cursor.y, value),
+ 0x8 => write_h!(self.palette_write, value),
+ 0x9 => { write_l!(self.palette_write, value); self.set_palette(); },
+ 0xa => write_h!(self.colours, value),
+ 0xb => write_l!(self.colours, value),
+ 0xc => self.sprite.push_byte(value),
+ 0xd => self.sprite.push_byte(value),
+ 0xe => self.draw_dispatch(value),
+ 0xf => self.move_cursor(value),
+ _ => unreachable!(),
+ };
+ return None;
+ }
+
+ fn wake(&mut self) -> bool {
+ self.accessed = true;
+ std::mem::take(&mut self.wake)
+ }
+}
+
+impl HasDimensions<u16> for ScreenDevice {
+ fn dimensions(&self) -> ScreenDimensions {
+ self.dimensions
+ }
+}
+
+
+pub struct SpriteBuffer {
+ pub mem: [u8; 16],
+ pub pointer: usize,
+}
+
+impl SpriteBuffer {
+ pub fn new() -> Self {
+ Self {
+ mem: [0; 16],
+ pointer: 0,
+ }
+ }
+
+ pub fn push_byte(&mut self, byte: u8) {
+ self.mem[self.pointer] = byte;
+ self.pointer = (self.pointer + 1) % 16;
+ }
+
+ pub fn read_1bit_sprite(&self, draw: u8) -> Sprite {
+ macro_rules! c {
+ ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
+ ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
+ }
+ let mut sprite = [[0; 8]; 8];
+ let mut p = match draw & 0x02 != 0 {
+ true => self.pointer,
+ false => self.pointer + 8,
+ };
+ match draw & 0x07 {
+ 0x0 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
+ 0x1 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } },
+ 0x2 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
+ 0x3 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } },
+ 0x4 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
+ 0x5 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } },
+ 0x6 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
+ 0x7 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } },
+ _ => unreachable!(),
+ }
+ return sprite;
+ }
+
+ pub fn read_2bit_sprite(&self, draw: u8) -> Sprite {
+ macro_rules! c {
+ ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
+ ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
+ }
+ let mut sprite = [[0; 8]; 8];
+ let mut p = match draw & 0x02 != 0 {
+ true => self.pointer,
+ false => self.pointer + 8,
+ };
+ let mut s = p + 8;
+ match draw & 0x07 {
+ 0x0 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x1 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x2 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x3 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x4 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x5 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x6 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x7 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ _ => unreachable!(),
+ }
+ return sprite;
+ }
+}