@@ -79,3 +79,42 @@ PUSH 00 PUSH 12 DEO # x = 0x00
PUSH 00 PUSH 13 DEO # y = 0x00
PUSH 30 PUSH 13 DEO # fills the foreground with transparent color
```
+
+The **`sprite` port** is used to draw sprites (or tiles). A sprite a 8x8 pixel image, with 4 bits per pixel. Writing to this port will take the sprite addressed by the **`address` port** paint it at the coordinates set by the **`x` and `y` ports**.
+
+
+
+
7
+
6
+
5
+
4
+
3
+
2
+
1
+
0
+
+
+
flip x
+
flip y
+
1bpp
+
layer
+
color
+
+
+
+> **TODO**: `flip x`, `flip y`, `1bpp` and `color` are currently unimplemented as per 2024-07-18.
+
+Sprite example:
+
+```
+00777700
+07777770
+67177176
+7f7777f7
+77111177
+77728777
+76777767
+76077067
+```
+
+![Sprite screenshot](../docs/sprite_screenshot.png)
diff --git a/coco-vm/src/video.rs b/coco-vm/src/video.rs
index 9d8dc37..6fc4a95 100644
--- a/coco-vm/src/video.rs
+++ b/coco-vm/src/video.rs
@@ -16,6 +16,8 @@ impl VideoPorts {
const X: u8 = 0x02;
const Y: u8 = 0x03;
const PIXEL: u8 = 0x04;
+ const ADDRESS: u8 = 0x08;
+ const SPRITE: u8 = 0x0a;
}
pub const SCREEN_WIDTH: u8 = 192;
@@ -60,12 +62,20 @@ impl VideoDevice {
}
#[inline]
- fn xy(&self, ports: &mut [u8]) -> (u8, u8) {
+ fn xy(&self, ports: &[u8]) -> (u8, u8) {
let x = cmp::min(ports[VideoPorts::X as usize], (SCREEN_WIDTH - 1) as u8);
let y = cmp::min(ports[VideoPorts::Y as usize], (SCREEN_HEIGHT - 1) as u8);
(x, y)
}
+ #[inline]
+ fn address(&self, ports: &[u8]) -> u16 {
+ let hi = ports[VideoPorts::ADDRESS as usize];
+ let lo = ports[VideoPorts::ADDRESS.wrapping_add(1) as usize];
+
+ u16::from_be_bytes([hi, lo])
+ }
+
fn deo_pixel(&mut self, cpu: &mut Cpu) {
self.is_dirty = true;
@@ -105,6 +115,44 @@ impl VideoDevice {
self.layer(layer)[i] = color;
}
+ fn deo_sprite(&mut self, cpu: &mut Cpu) {
+ self.is_dirty = true;
+ let ports = cpu.device_page::();
+ let sprite_port = ports[VideoPorts::SPRITE as usize];
+
+ let (x, y) = self.xy(ports);
+ let addr = self.address(ports);
+ let sprite_data = self.sprite_data(addr, cpu);
+ let layer = (sprite_port & 0b0001_0000) >> 4;
+
+ for spr_y in 0..8 {
+ for spr_x in 0..8 {
+ let spr_pixel = sprite_data[spr_y as usize * 8 + spr_x as usize];
+ let _x = x + spr_x;
+ let _y = y + spr_y;
+
+ if _x >= SCREEN_WIDTH || _y >= SCREEN_HEIGHT {
+ continue;
+ }
+ self.put_pixel(_x, _y, spr_pixel, layer);
+ }
+ }
+ }
+
+ fn sprite_data(&self, base_addr: u16, cpu: &Cpu) -> [Pixel; 64] {
+ let mut addr = base_addr;
+ let mut res = [0x00; 64];
+ for row in 0..8 as usize {
+ for chunk in 0..4 as usize {
+ let pixel_data = cpu.ram_peek_byte(addr.wrapping_add(chunk as u16));
+ res[row * 8 + chunk * 2 + 0] = (0b1111_0000 & pixel_data) >> 4;
+ res[row * 8 + chunk * 2 + 1] = 0b0000_1111 & pixel_data;
+ }
+ addr = addr.wrapping_add(4);
+ }
+ res
+ }
+
#[inline]
fn layer(&mut self, i: u8) -> &mut VideoBuffer {
&mut self.layers[i as usize]
@@ -117,6 +165,10 @@ impl Device for VideoDevice {
VideoPorts::X => {}
VideoPorts::Y => {}
VideoPorts::PIXEL => self.deo_pixel(cpu),
+ VideoPorts::ADDRESS => {}
+ VideoPorts::SPRITE => {
+ self.deo_sprite(cpu);
+ }
_ => {}
}
}
diff --git a/coco-vm/tests/vm_cpu_test.rs b/coco-vm/tests/vm_cpu_test.rs
index 7db5b96..3d986d7 100644
--- a/coco-vm/tests/vm_cpu_test.rs
+++ b/coco-vm/tests/vm_cpu_test.rs
@@ -113,3 +113,20 @@ fn test_deo_video_pixel_fill_with_flip() {
[0x00; VIDEO_BUFFER_LEN - IDX]
);
}
+
+#[test]
+fn test_deo_sprite() {
+ let rom = [
+ PUSH2, 0x01, 0x0c, PUSH, 0x18, DEO2, PUSH, 0x00, PUSH, 0x1a, DEO, BRK, 0x11, 0x11, 0x11,
+ 0x11, 0x10, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x01, 0x10, 0x00,
+ 0x00, 0x01, 0x10, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x01, 0x11, 0x11, 0x11, 0x11,
+ ];
+ let mut cpu = Cpu::new(&rom);
+ let mut vm = Vm::new();
+
+ let _ = vm.on_reset(&mut cpu);
+ let buffer = vm.pixels();
+ // println!("{:?}", buffer);
+
+ assert_eq!(buffer[0..8], [0x01; 8]);
+}
diff --git a/docs/sprite_screenshot.png b/docs/sprite_screenshot.png
new file mode 100644
index 0000000..5272cd4
Binary files /dev/null and b/docs/sprite_screenshot.png differ