Skip to content

Commit

Permalink
Add basic sprite drawing (#8)
Browse files Browse the repository at this point in the history
* Implement PUSH2 opcode

* Implement DEO2 opcode

* Implement Sprite DEO
  • Loading branch information
belen-albeza authored Jul 18, 2024
1 parent 86e8f7b commit 28d4058
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 10 deletions.
69 changes: 65 additions & 4 deletions coco-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ impl Cpu {
let op = self.read_byte();
match op {
opcodes::BRK => break,
opcodes::PUSH => self.op_push(),
opcodes::DEO => self.op_deo(machine),
opcodes::DEO2 => self.op_deo2(machine),
opcodes::PUSH => self.op_push(),
opcodes::PUSH2 => self.op_push2(),
_ => {}
}
}
Expand All @@ -74,6 +76,12 @@ impl Cpu {
&mut self.devices[(D::BASE as usize)..(D::BASE as usize + 0x10)]
}

/// Returns a chunk of memory
#[inline]
pub fn ram_peek_byte(&self, addr: u16) -> u8 {
self.ram[addr as usize]
}

/// Returns the current value for the program counter (PC)
pub fn pc(&self) -> u16 {
self.pc
Expand All @@ -86,12 +94,27 @@ impl Cpu {
res
}

#[inline]
fn read_short(&mut self) -> u16 {
let hi = self.ram[self.pc as usize];
let lo = self.ram[self.pc.wrapping_add(1) as usize];
self.pc = self.pc.wrapping_add(2);

u16::from_be_bytes([hi, lo])
}

#[inline]
fn op_push(&mut self) {
let value = self.read_byte();
self.stack.push_byte(value);
}

#[inline]
fn op_push2(&mut self) {
let value = self.read_short();
self.stack.push_short(value);
}

#[inline]
fn op_deo(&mut self, machine: &mut impl Machine) {
let target = self.stack.pop_byte();
Expand All @@ -103,6 +126,20 @@ impl Cpu {
// callback for I/O
machine.deo(self, target);
}

#[inline]
fn op_deo2(&mut self, machine: &mut impl Machine) {
let target = self.stack.pop_byte();

// write short value to device port
let value = self.stack.pop_short();
let [hi, lo] = value.to_be_bytes();
self.devices[target as usize] = hi;
self.devices[target.wrapping_add(1) as usize] = lo;

// callback for I/0
machine.deo(self, target);
}
}

impl fmt::Display for Cpu {
Expand Down Expand Up @@ -153,7 +190,7 @@ mod tests {
}

#[test]
pub fn run_wraps_pc_at_the_end_of_ram() {
fn run_wraps_pc_at_the_end_of_ram() {
let mut rom = zeroed_memory();
rom[rom.len() - 1] = 0x01;
let mut cpu = Cpu::new(&rom);
Expand All @@ -165,7 +202,7 @@ mod tests {
}

#[test]
pub fn push_opcode() {
fn push_opcode() {
let rom = rom_from(&[PUSH, 0xab, BRK]);
let mut cpu = Cpu::new(&rom);

Expand All @@ -176,7 +213,18 @@ mod tests {
}

#[test]
pub fn deo_opcode() {
fn push2_opcode() {
let rom = rom_from(&[PUSH2, 0xab, 0xcd, BRK]);
let mut cpu = Cpu::new(&rom);

let pc = cpu.run(0x100, &mut AnyMachine {});
assert_eq!(pc, 0x104);
assert_eq!(cpu.stack.len(), 2);
assert_eq!(cpu.stack.short_at(0), 0xabcd);
}

#[test]
fn deo_opcode() {
let rom = rom_from(&[PUSH, 0xab, PUSH, 0x02, DEO, BRK]);
let mut cpu = Cpu::new(&rom);

Expand All @@ -186,4 +234,17 @@ mod tests {
assert_eq!(cpu.devices[0x02], 0xab);
// TODO: check AnyMachine.deo has been called with 0xab as target arg
}

#[test]
fn deo2_opcode() {
let rom = rom_from(&[PUSH2, 0xab, 0xcd, PUSH, 0x00, DEO2, BRK]);
let mut cpu = Cpu::new(&rom);

let pc = cpu.run(0x100, &mut AnyMachine {});
assert_eq!(pc, 0x107);
assert_eq!(cpu.stack.len(), 0);
assert_eq!(cpu.devices[0x00], 0xab);
assert_eq!(cpu.devices[0x01], 0xcd);
// TODO: check AnyMachine.deo has been called with 0xab as target arg
}
}
2 changes: 2 additions & 0 deletions coco-core/src/opcodes.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub const BRK: u8 = 0x00;
pub const DEO: u8 = 0x17;
pub const DEO2: u8 = 0x37;
pub const PUSH: u8 = 0x80;
pub const PUSH2: u8 = 0xa0;
19 changes: 19 additions & 0 deletions coco-core/src/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,34 @@ impl Stack {
self.data[self.index as usize] = x;
}

pub fn push_short(&mut self, x: u16) {
let [hi, lo] = x.to_be_bytes();
self.push_byte(hi);
self.push_byte(lo);
}

pub fn pop_byte(&mut self) -> u8 {
let res = self.data[self.index as usize];
self.index = self.index.wrapping_sub(1);
res
}

pub fn pop_short(&mut self) -> u16 {
let lo = self.pop_byte();
let hi = self.pop_byte();
u16::from_be_bytes([hi, lo])
}

pub fn byte_at(&self, i: u8) -> u8 {
return self.data[i as usize];
}

pub fn short_at(&self, i: u8) -> u16 {
let hi = self.data[i as usize];
let lo = self.data[i.wrapping_add(1) as usize];

u16::from_be_bytes([hi, lo])
}
}

impl fmt::Display for Stack {
Expand Down
1 change: 1 addition & 0 deletions coco-ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ <h1>👻-8</h1>
<option value="deo_system_debug.rom">Debug</option>
<option value="put_pixel.rom">Pixel (single)</option>
<option value="pixel_fill.rom">Pixel (fill)</option>
<option value="sprite.rom">Sprite</option>
</select>

<div>
Expand Down
2 changes: 1 addition & 1 deletion coco-ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async function main() {
const _ = await initWasm("./vendor/coco_ui_bg.wasm");
const romSelector = document.querySelector("#coco-rom-selector");

const defaultRom = "pixel_fill.rom";
const defaultRom = "sprite.rom";
setupRomSelector(romSelector, defaultRom);
setupControls();

Expand Down
Binary file added coco-ui/roms/sprite.rom
Binary file not shown.
11 changes: 8 additions & 3 deletions coco-ui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
}

* {
--width: 768px; /* x4 */
margin: 0;
}

body {
font-family: monospace;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
display: grid;
place-content: center;
}

select, button {
Expand All @@ -22,15 +25,16 @@ select, button {
.wrapper {
padding: 1rem;
display: grid;
place-content: center;
grid-template-rows: auto 1fr auto;
row-gap: 1rem;
height: 100vh;
/* height: 100vh; */
width: var(--width);
}

.coco-video {
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 768px; /* x4 */
width: 100%;
background: black;
}

Expand All @@ -44,4 +48,5 @@ select, button {
max-height: 20vh;
border: 1px solid black;
padding: 1rem;
max-width: (--width);
}
41 changes: 40 additions & 1 deletion coco-vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Sending a non-zero byte to the **`debug` port** will ouput CPU debug information
<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>0x15</code></th><td>--</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>
Expand Down Expand Up @@ -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**.

<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>1bpp</td>
<td>layer</td>
<td colspan="4">color</td>
</tr>
</table>

> **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)
54 changes: 53 additions & 1 deletion coco-vm/src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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::<VideoPorts>();
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]
Expand All @@ -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);
}
_ => {}
}
}
Expand Down
17 changes: 17 additions & 0 deletions coco-vm/tests/vm_cpu_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Binary file added docs/sprite_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 28d4058

Please sign in to comment.