Skip to content

Commit

Permalink
Add video/x, video/y and video/pixel DEO (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
belen-albeza authored Jul 17, 2024
1 parent 9ddc0fe commit 0d522d7
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 18 deletions.
1 change: 1 addition & 0 deletions coco-ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ <h1>👻-8</h1>
<select name="rom" id="coco-rom-selector">
<option value="empty.rom">Empty</option>
<option value="deo_system_debug.rom">Debug</option>
<option value="put_pixel.rom">Pixel</option>
</select>
</p>
</main>
Expand Down
2 changes: 1 addition & 1 deletion coco-ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async function main() {
const _ = await initWasm("./vendor/coco_ui_bg.wasm");
const romSelector = document.querySelector("#coco-rom-selector");

const defaultRom = "deo_system_debug.rom";
const defaultRom = "put_pixel.rom";
await setupRomSelector(romSelector, defaultRom);

const rom = await fetchRom(`/roms/${defaultRom}`);
Expand Down
Binary file added coco-ui/roms/put_pixel.rom
Binary file not shown.
6 changes: 2 additions & 4 deletions coco-ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub fn run_rom(rom: &[u8]) -> Result<Output> {

let ctx = canvas_context();
let mut canvas_buffer: DisplayBuffer = [0; SCREEN_WIDTH * SCREEN_HEIGHT * 4];
render(&vm.borrow(), &ctx, &mut canvas_buffer);

*g.borrow_mut() = Some(Closure::new(move || {
vm.borrow_mut().on_video(&mut cpu.borrow_mut());
render(&vm.borrow(), &ctx, &mut canvas_buffer);
Expand All @@ -73,10 +75,6 @@ pub fn run_rom(rom: &[u8]) -> Result<Output> {
fn render(vm: &Vm, ctx: &web_sys::CanvasRenderingContext2d, buffer: &mut DisplayBuffer) {
let (bg, fg) = vm.pixels();

// clear background
ctx.set_fill_style(&JsValue::from("#000000"));
ctx.fill_rect(0.0, 0.0, SCREEN_WIDTH as f64, SCREEN_HEIGHT as f64);

// update buffer and copy its pixel to the canvas
update_display_buffer(buffer, bg, fg);
let image_data = image_data(buffer.as_slice(), SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32);
Expand Down
81 changes: 81 additions & 0 deletions coco-vm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# coco-vm

The COCO-8 virtual machine is built on top of a COCO-8 CPU and uses devices in the fashion of [Varvara](https://wiki.xxiivv.com/site/varvara.html) and NES.

The COCO-8 CPU has a 256-byte device page, that contains 16 devices with 16 bytes for ports. Some of the ports take just one byte, but others take a short (2 bytes).

| Address | Device |
| ------- | ------------------------ |
| `0x00` | [System](#system-device) |
| `0x10` | Video |

## System device

<table>
<tr><th><code>0x00</code></th><td rowspan="2"><i>unused*</i></td></tr>
<tr><th><code>0x01</code></th></tr>
<tr><th><code>0x02</code></th><td>debug</td></tr>
</table>

Instead of a customizable vector, the system is always to be assumed to have `0x100` as the address of its vector (which is the reset vector and it's called when the ROM is booted).

Sending a non-zero byte to the **`debug` port** will ouput CPU debug information.

## Video device

<table>
<tr><th><code>0x10</code></th><td rowspan="2">vector*</td><th><code>0x18</code></th><td rowspan="2">address*</td></tr>
<tr><th><code>0x11</code></th><th><code>0x19</code></th></tr>
<tr><th><code>0x12</code></th><td>x</td><th><code>0x1a</code></th><td>sprite</td></tr>
<tr><th><code>0x13</code></th><td>y</td><th><code>0x1b</code></th><td>--</td></tr>
<tr><th><code>0x14</code></th><td>pixel</td><th><code>0x1c</code></th><td>--</td></tr>
<tr><th><code>0x15</code></th><td>read</td><th><code>0x1d</code></th><td>--</td></tr>
<tr><th><code>0x16</code></th><td>--</td><th><code>0x1e</code></th><td>--</td></tr>
<tr><th><code>0x17</code></th><td>--</td><th><code>0x1f</code></th><td>--</td></tr>
</table>

The **screen <code>vector\*</code>** is called at a rate of 60 fps, and it's meant to run any drawing operations.

The ports `x*` and `y*` contain the X and Y coordinates used by the drawing or buffer reading operations: `pixel`, `read` and `sprite`.

The **`pixel` port** is used to put pixels into the video buffer. It follows this layout:

<table>
<tr>
<th><code>7</code></th>
<th><code>6</code></th>
<th><code>5</code></th>
<th><code>4</code></th>
<th><code>3</code></th>
<th><code>2</code></th>
<th><code>1</code></th>
<th><code>0</code></th>
</tr>
<tr>
<td>flip x</td>
<td>flip y</td>
<td>fill</td>
<td>layer</td>
<td colspan="4">color</td>
</tr>
</table>

- `color` is an index in the 16-color COCO-8 palette, and can range from `0` to `f`.
- `layer` indicates the layer to put the pixel in; background is `0` and foreground is `1`. Color `0` in the foreground will be considered transparent.
- `fill` sets wether to draw a single pixel, `0`, or to use the `x` and `y` coordinates as the position of a rectangle to fill, `1`. By default it takes the bottom right quadrant.
- `flip x` will use a quadrant on the left for filling. It has no effect when `fill` is `0`.
- `flip y` will use a quadrant on the top for filling. It has no effect when `fill` is `0`.

Some examples:

```uxn
PUSH 60 PUSH 12 DEO # x = 0x60
PUSH 48 PUSH 13 DEO # y = 0x48
PUSH 18 PUSH DEO # put pixel with color 0x8 in the foreground
```

```uxn
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
```
13 changes: 8 additions & 5 deletions coco-vm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
mod system;
mod video;

use coco_core::{Cpu, Machine};
use system::SystemDevice;
use video::{VideoBuffer, VideoDevice};
use coco_core::{Cpu, Machine, Ports};
use system::{SystemDevice, SystemPorts};
use video::{VideoBuffer, VideoDevice, VideoPorts};

pub use video::{SCREEN_HEIGHT, SCREEN_WIDTH};

trait Device {
#[allow(dead_code)]
fn dei(&mut self, cpu: &mut Cpu, target: u8);
fn deo(&mut self, cpu: &mut Cpu, target: u8);
}
Expand All @@ -34,10 +35,12 @@ pub struct Vm {
}

impl Machine for Vm {
fn dei(&mut self, cpu: &mut Cpu, target: u8) {}
fn dei(&mut self, _: &mut Cpu, _: u8) {}
fn deo(&mut self, cpu: &mut Cpu, target: u8) {
let offset = target & 0x0f;
match target & 0xf0 {
0x00 => self.system.deo(cpu, target),
SystemPorts::BASE => self.system.deo(cpu, offset),
VideoPorts::BASE => self.video.deo(cpu, offset),
_ => unimplemented!(),
}
}
Expand Down
4 changes: 2 additions & 2 deletions coco-vm/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ impl Device for SystemDevice {
match target {
SystemPorts::VECTOR => {}
SystemPorts::DEBUG => self.debug(cpu),
_ => panic!("Unimplemented device port {}", target),
_ => {}
}
}

fn dei(&mut self, cpu: &mut coco_core::Cpu, target: u8) {}
fn dei(&mut self, _: &mut coco_core::Cpu, _: u8) {}
}
67 changes: 62 additions & 5 deletions coco-vm/src/video.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
use core::cmp;

use super::Device;
use coco_core::{Cpu, Ports};

#[derive(Debug)]
pub struct VideoPorts {}

impl Ports for VideoPorts {
const BASE: u8 = 0x10;
}

impl VideoPorts {
#[allow(dead_code)]
const VECTOR: u8 = 0x00;
const X: u8 = 0x02;
const Y: u8 = 0x03;
const PIXEL: u8 = 0x04;
}

pub const SCREEN_WIDTH: usize = 192;
pub const SCREEN_HEIGHT: usize = 144;
const VIDEO_BUFFER_LEN: usize = SCREEN_WIDTH * SCREEN_HEIGHT;
Expand All @@ -13,13 +33,50 @@ pub struct VideoDevice {

impl VideoDevice {
pub fn new() -> Self {
let mut buffer = [0x00 as Pixel; VIDEO_BUFFER_LEN];
for i in 0..VIDEO_BUFFER_LEN {
buffer[i] = (i % 0x10) as Pixel;
}
Self {
background: [0x00; VIDEO_BUFFER_LEN],
foreground: buffer,
foreground: [0x00; VIDEO_BUFFER_LEN],
}
}

#[inline]
fn xy(&self, ports: &mut [u8]) -> (u8, u8) {
(ports[VideoPorts::X as usize], ports[VideoPorts::Y as usize])
}

fn deo_pixel(&mut self, cpu: &mut Cpu) {
let ports = cpu.device_page::<VideoPorts>();
let pixel = ports[VideoPorts::PIXEL as usize];

let (x, y) = self.xy(ports);
let color = pixel & 0x0f;
let layer = (pixel & 0b0001_0000) >> 4;

self.put_pixel(x, y, color, layer == 0x01);
}

fn put_pixel(&mut self, x: u8, y: u8, color: u8, is_foreground: bool) {
let x = cmp::min(x as usize, SCREEN_WIDTH - 1);
let y = cmp::min(y as usize, SCREEN_HEIGHT - 1);
let i = y * SCREEN_WIDTH + x;

if is_foreground {
self.foreground[i] = color;
} else {
self.background[i] = color;
}
}
}

impl Device for VideoDevice {
fn deo(&mut self, cpu: &mut Cpu, target: u8) {
match target {
VideoPorts::X => {}
VideoPorts::Y => {}
VideoPorts::PIXEL => self.deo_pixel(cpu),
_ => {}
}
}

fn dei(&mut self, _: &mut Cpu, _: u8) {}
}
16 changes: 15 additions & 1 deletion coco-vm/tests/vm_cpu_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use coco_core::opcodes::*;
use coco_core::Cpu;
use coco_vm::Vm;
use coco_vm::{Vm, SCREEN_WIDTH};

#[test]
fn test_deo_system_debug() {
Expand All @@ -13,3 +13,17 @@ fn test_deo_system_debug() {
let expected_sys_output = "WRK: [ff]\nRET: []".to_string();
assert_eq!(output.sys_stdout, expected_sys_output);
}

#[test]
fn test_deo_video_pixel_put() {
let rom = [
PUSH, 0x01, PUSH, 0x12, DEO, PUSH, 0x01, PUSH, 0x13, DEO, PUSH, 0x08, PUSH, 0x14, DEO, BRK,
];
let mut cpu = Cpu::new(&rom);
let mut vm = Vm::new();

let _ = vm.on_reset(&mut cpu);
let (bg, _) = vm.pixels();

assert_eq!(bg[0x01 * SCREEN_WIDTH + 0x01], 0x08);
}

0 comments on commit 0d522d7

Please sign in to comment.