diff --git a/coco-core/src/lib.rs b/coco-core/src/lib.rs index 8b6e233..b08fc7c 100644 --- a/coco-core/src/lib.rs +++ b/coco-core/src/lib.rs @@ -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(), _ => {} } } @@ -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 @@ -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(); @@ -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 { @@ -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); @@ -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); @@ -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); @@ -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 + } } diff --git a/coco-core/src/opcodes.rs b/coco-core/src/opcodes.rs index 4ba5123..bc3ac74 100644 --- a/coco-core/src/opcodes.rs +++ b/coco-core/src/opcodes.rs @@ -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; diff --git a/coco-core/src/stack.rs b/coco-core/src/stack.rs index 87f9911..60abd01 100644 --- a/coco-core/src/stack.rs +++ b/coco-core/src/stack.rs @@ -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 { diff --git a/coco-ui/index.html b/coco-ui/index.html index 3b461f8..625c6ec 100644 --- a/coco-ui/index.html +++ b/coco-ui/index.html @@ -19,6 +19,7 @@

👻-8

+
diff --git a/coco-ui/index.js b/coco-ui/index.js index 65736b9..82b0e8a 100644 --- a/coco-ui/index.js +++ b/coco-ui/index.js @@ -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(); diff --git a/coco-ui/roms/sprite.rom b/coco-ui/roms/sprite.rom new file mode 100644 index 0000000..7259b1a Binary files /dev/null and b/coco-ui/roms/sprite.rom differ diff --git a/coco-ui/styles.css b/coco-ui/styles.css index ca0e6be..d9e7516 100644 --- a/coco-ui/styles.css +++ b/coco-ui/styles.css @@ -5,6 +5,7 @@ } * { + --width: 768px; /* x4 */ margin: 0; } @@ -12,6 +13,8 @@ body { font-family: monospace; -webkit-font-smoothing: antialiased; min-height: 100vh; + display: grid; + place-content: center; } select, button { @@ -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; } @@ -44,4 +48,5 @@ select, button { max-height: 20vh; border: 1px solid black; padding: 1rem; + max-width: (--width); } diff --git a/coco-vm/README.md b/coco-vm/README.md index c2043cc..8f34150 100644 --- a/coco-vm/README.md +++ b/coco-vm/README.md @@ -29,7 +29,7 @@ Sending a non-zero byte to the **`debug` port** will ouput CPU debug information 0x12x0x1asprite 0x13y0x1b-- 0x14pixel0x1c-- - 0x15read0x1d-- + 0x15--0x1d-- 0x16--0x1e-- 0x17--0x1f-- @@ -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**. + + + + + + + + + + + + + + + + + + + +
76543210
flip xflip y1bpplayercolor
+ +> **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