From c70756069b5b72b79d320ad0b006f264f5592476 Mon Sep 17 00:00:00 2001 From: teesh3rt Date: Mon, 16 Mar 2026 13:06:52 +0200 Subject: [PATCH] feat: ITS ALIVE --- src/main.zig | 50 +++- src/octochip/components/display.zig | 16 +- src/octochip/components/keyboard.zig | 36 +-- src/octochip/components/memory.zig | 13 +- src/octochip/components/registers.zig | 3 +- src/octochip/components/root.zig | 1 + src/octochip/components/wrapping_stack.zig | 25 ++ src/octochip/machine/digits.bin | 1 + src/octochip/machine/instruction.zig | 10 +- src/octochip/machine/machine.zig | 277 ++++++++++++++++++++- src/octochip/machine/root.zig | 1 + src/rom.ch8 | Bin 0 -> 3581 bytes 12 files changed, 395 insertions(+), 38 deletions(-) create mode 100644 src/octochip/components/wrapping_stack.zig create mode 100644 src/octochip/machine/digits.bin create mode 100644 src/rom.ch8 diff --git a/src/main.zig b/src/main.zig index 6c288e0..f2aa4f6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,51 @@ -const std = @import("std"); const octo = @import("octochip"); +const rl = @import("raylib"); + +const PIXEL_SCALE: i32 = 20; pub fn main() !void { - const ins = try octo.machine.Instruction.from_bytes(0xabcd); - std.log.info("instruction: {f}", .{ins}); + var machine = octo.machine.Machine.new(); + try machine.loadRom(@embedFile("./rom.ch8")); + + rl.initWindow(64 * PIXEL_SCALE, 32 * PIXEL_SCALE, "octochip"); + defer rl.closeWindow(); + + while (!rl.windowShouldClose()) { + machine.keyboard.press_or_release_if(0x1, rl.isKeyDown(.one)); + machine.keyboard.press_or_release_if(0x2, rl.isKeyDown(.two)); + machine.keyboard.press_or_release_if(0x3, rl.isKeyDown(.three)); + machine.keyboard.press_or_release_if(0xC, rl.isKeyDown(.four)); + + machine.keyboard.press_or_release_if(0x4, rl.isKeyDown(.q)); + machine.keyboard.press_or_release_if(0x5, rl.isKeyDown(.w)); + machine.keyboard.press_or_release_if(0x6, rl.isKeyDown(.e)); + machine.keyboard.press_or_release_if(0xD, rl.isKeyDown(.r)); + + machine.keyboard.press_or_release_if(0x7, rl.isKeyDown(.a)); + machine.keyboard.press_or_release_if(0x8, rl.isKeyDown(.s)); + machine.keyboard.press_or_release_if(0x9, rl.isKeyDown(.d)); + machine.keyboard.press_or_release_if(0xE, rl.isKeyDown(.f)); + + machine.keyboard.press_or_release_if(0xA, rl.isKeyDown(.z)); + machine.keyboard.press_or_release_if(0x0, rl.isKeyDown(.x)); + machine.keyboard.press_or_release_if(0xB, rl.isKeyDown(.c)); + machine.keyboard.press_or_release_if(0xF, rl.isKeyDown(.v)); + + machine.tick(); + + rl.beginDrawing(); + defer rl.endDrawing(); + + rl.clearBackground(.black); + + for (0..32) |y| { + for (0..64) |x| { + if (machine.display.get(@intCast(x), @intCast(y)) == 0) { + continue; + } + + rl.drawRectangle(@intCast(x * PIXEL_SCALE), @intCast(y * PIXEL_SCALE), PIXEL_SCALE, PIXEL_SCALE, .white); + } + } + } } diff --git a/src/octochip/components/display.zig b/src/octochip/components/display.zig index bee5520..6404a25 100644 --- a/src/octochip/components/display.zig +++ b/src/octochip/components/display.zig @@ -1,7 +1,19 @@ pub const Display = struct { - pixels: [32][64]bool = .{}, + pixels: [32][64]u1 = [_][64]u1{[_]u1{0} ** 64} ** 32, - fn new() @This() { + pub fn new() @This() { return .{}; } + + pub fn clear(self: *@This()) void { + self.pixels = [_][64]u1{[_]u1{0} ** 64} ** 32; + } + + pub fn get(self: *const @This(), x: u8, y: u8) u1 { + return self.pixels[y][x]; + } + + pub fn set(self: *@This(), x: u8, y: u8, value: u1) void { + self.pixels[y][x] = value; + } }; diff --git a/src/octochip/components/keyboard.zig b/src/octochip/components/keyboard.zig index 0f79b7a..7492b5f 100644 --- a/src/octochip/components/keyboard.zig +++ b/src/octochip/components/keyboard.zig @@ -1,40 +1,28 @@ pub const Keyboard = struct { - // FEDCBA9876543210 - pressed: u16 = 0, + // We track each key by its index (0..15) + pressed: [16]bool = [_]bool{false} ** 16, - fn new() @This() { + pub fn new() @This() { return .{}; } - fn released(self: *const @This()) u16 { - return ~self.pressed; + pub fn press(self: *@This(), key: u4) void { + self.pressed[@intCast(key)] = true; } - fn mask(key: u4) u16 { - return (@as(u16, 1) << key); + pub fn release(self: *@This(), key: u4) void { + self.pressed[@intCast(key)] = false; } - fn press(self: *@This(), key: u4) void { - self.pressed |= mask(key); + pub fn press_or_release_if(self: *@This(), key: u4, cond: bool) void { + self.pressed[@intCast(key)] = cond; } - fn release(self: *@This(), key: u4) void { - self.pressed &= ~mask(key); + pub fn is_pressed(self: *const @This(), key: u4) bool { + return self.pressed[@intCast(key)]; } - fn press_or_release_if(self: *@This(), key: u4, cond: bool) void { - if (cond) { - self.press(key); - } else { - self.release(key); - } - } - - fn is_pressed(self: *const @This(), key: u4) bool { - return (self.pressed & mask(key)) != 0; - } - - fn is_released(self: *const @This(), key: u4) bool { + pub fn is_released(self: *const @This(), key: u4) bool { return !self.is_pressed(key); } }; diff --git a/src/octochip/components/memory.zig b/src/octochip/components/memory.zig index fad91e4..1098515 100644 --- a/src/octochip/components/memory.zig +++ b/src/octochip/components/memory.zig @@ -1,5 +1,5 @@ pub const Memory = struct { - memory: [4096]u8 = .{}, + memory: [4096]u8 = [_]u8{0} ** 4096, pub fn new() @This() { return .{}; @@ -12,4 +12,15 @@ pub const Memory = struct { pub fn peek(self: *const @This(), addr: usize) u8 { return self.memory[addr]; } + + pub fn put16(self: *@This(), addr: usize, value: u16) void { + self.memory[addr] = @intCast((value >> 8) & 0xFF); + self.memory[addr + 1] = @intCast(value & 0xFF); + } + + pub fn peek16(self: *const @This(), addr: usize) u16 { + const hi: u16 = @intCast(self.memory[addr]); + const lo: u16 = @intCast(self.memory[addr + 1]); + return (hi << 8) | lo; + } }; diff --git a/src/octochip/components/registers.zig b/src/octochip/components/registers.zig index 41ab7e9..b1d2158 100644 --- a/src/octochip/components/registers.zig +++ b/src/octochip/components/registers.zig @@ -1,10 +1,9 @@ pub const Registers = struct { - V: [16]u8 = .{}, + V: [16]u8 = [_]u8{0} ** 16, DT: u8 = 0, ST: u8 = 0, I: u16 = 0, PC: u16 = 0x200, - SP: u8 = 0, pub fn new() Registers { return Registers{}; diff --git a/src/octochip/components/root.zig b/src/octochip/components/root.zig index 20e6864..94644a6 100644 --- a/src/octochip/components/root.zig +++ b/src/octochip/components/root.zig @@ -2,3 +2,4 @@ pub const Memory = @import("memory.zig").Memory; pub const Registers = @import("registers.zig").Registers; pub const Keyboard = @import("keyboard.zig").Keyboard; pub const Display = @import("display.zig").Display; +pub const WrappingStack = @import("wrapping_stack.zig").WrappingStack; diff --git a/src/octochip/components/wrapping_stack.zig b/src/octochip/components/wrapping_stack.zig new file mode 100644 index 0000000..bdae89d --- /dev/null +++ b/src/octochip/components/wrapping_stack.zig @@ -0,0 +1,25 @@ +// NOTE: used for the machine callstack. +// via WrappingStack(u16, 16) +pub fn WrappingStack(comptime T: type, comptime size: usize) type { + return struct { + stack: [size]T = undefined, + pointer: usize = 0, + + pub fn new() @This() { + return .{ + .stack = [_]T{0} ** size, + .pointer = 0, + }; + } + + pub fn push(self: *@This(), value: T) void { + self.stack[self.pointer] = value; + self.pointer = (self.pointer + 1) % size; + } + + pub fn pop(self: *@This()) T { + self.pointer = (self.pointer + size - 1) % size; + return self.stack[self.pointer]; + } + }; +} diff --git a/src/octochip/machine/digits.bin b/src/octochip/machine/digits.bin new file mode 100644 index 0000000..8280f9b --- /dev/null +++ b/src/octochip/machine/digits.bin @@ -0,0 +1 @@ +ðð ` pðð€ðððððð€ððð€ððð @@ððððððððàààð€€€ðààð€ð€ðð€ð€€ \ No newline at end of file diff --git a/src/octochip/machine/instruction.zig b/src/octochip/machine/instruction.zig index 98c5958..c1fd09b 100644 --- a/src/octochip/machine/instruction.zig +++ b/src/octochip/machine/instruction.zig @@ -129,11 +129,11 @@ pub const Instruction = union(enum) { switch (self) { .CLS => try writer.writeAll("CLS"), .RET => try writer.writeAll("RET"), - .SYS => |v| try writer.print("SYS {x}", .{v.location}), - .JP => |v| try writer.print("JP {x}", .{v.location}), - .CALL => |v| try writer.print("CALL {x}", .{v.subroutine}), - .SE => |v| try writer.print("SE V{X}, {x}", .{ v.register, v.value }), - .SNE => |v| try writer.print("SNE V{X}, {x}", .{ v.register, v.value }), + .SYS => |v| try writer.print("SYS {X}", .{v.location}), + .JP => |v| try writer.print("JP {X}", .{v.location}), + .CALL => |v| try writer.print("CALL {X}", .{v.subroutine}), + .SE => |v| try writer.print("SE V{X}, {X}", .{ v.register, v.value }), + .SNE => |v| try writer.print("SNE V{X}, {X}", .{ v.register, v.value }), .SER => |v| try writer.print("SE V{X}, V{X}", .{ v.first, v.second }), .LD => |v| try writer.print("LD V{X}, {x}", .{ v.register, v.value }), .ADD => |v| try writer.print("ADD V{X}, {x}", .{ v.register, v.value }), diff --git a/src/octochip/machine/machine.zig b/src/octochip/machine/machine.zig index 12e49c8..4a2a3a9 100644 --- a/src/octochip/machine/machine.zig +++ b/src/octochip/machine/machine.zig @@ -1 +1,276 @@ -// the part that actually executes chip8 +const std = @import("std"); + +const c = @import("../components/root.zig"); +const Instruction = @import("./instruction.zig").Instruction; + +pub const Machine = struct { + display: c.Display = c.Display.new(), + keyboard: c.Keyboard = c.Keyboard.new(), + memory: c.Memory = c.Memory.new(), + registers: c.Registers = c.Registers.new(), + stack: c.WrappingStack(u16, 16) = c.WrappingStack(u16, 16).new(), + + const log = std.log.scoped(.machine); + + pub fn new() @This() { + var self: @This() = .{}; + + const digits = @embedFile("./digits.bin"); + @memcpy(self.memory.memory[0x0..digits.len], digits); + + return self; + } + + pub fn loadRom(self: *@This(), rom: []const u8) !void { + if (rom.len > self.memory.memory.len) return error.RomTooBig; + @memcpy(self.memory.memory[0x200..(0x200 + rom.len)], rom); + } + + pub fn tick(self: *@This()) void { + const instruction_raw = self.memory.peek16(self.registers.PC); + const instruction = Instruction.from_bytes(instruction_raw) catch { + log.warn("Invalid instruction: 0x{X}", .{instruction_raw}); + log.warn(" skipping...", .{}); + self.registers.PC += 2; + return; + }; + + log.debug("pc=0x{X} ins={f}", .{ self.registers.PC, instruction }); + + switch (instruction) { + .SYS => { + log.warn("The ROM you are currently running tried to execute a SYS instruction! ({f})", .{instruction}); + log.warn("These are ignored by modern emulators, and this emulator is no exception.", .{}); + log.warn("Take heed: the ROM may now misbehave!", .{}); + self.registers.PC += 2; + }, + .CLS => { + self.display.clear(); + self.registers.PC += 2; + }, + .RET => { + self.registers.PC = self.stack.pop() + 2; + }, + .JP => |jp| { + self.registers.PC = jp.location; + }, + .CALL => |call| { + self.stack.push(self.registers.PC); + self.registers.PC = call.subroutine; + }, + .SE => |se| { + if (self.registers.get(se.register) == se.value) { + self.registers.PC += 4; + } else { + self.registers.PC += 2; + } + }, + .SNE => |sne| { + if (self.registers.get(sne.register) != sne.value) { + self.registers.PC += 4; + } else { + self.registers.PC += 2; + } + }, + .SER => |ser| { + if (self.registers.get(ser.first) == self.registers.get(ser.second)) { + self.registers.PC += 4; + } else { + self.registers.PC += 2; + } + }, + .LD => |ld| { + self.registers.set(ld.register, ld.value); + self.registers.PC += 2; + }, + .ADD => |add| { + const old = self.registers.get(add.register); + self.registers.set(add.register, old +% add.value); + self.registers.PC += 2; + }, + .LDR => |ldr| { + self.registers.set(ldr.first, self.registers.get(ldr.second)); + self.registers.PC += 2; + }, + .OR => |orr| { + const first = self.registers.get(orr.first); + const second = self.registers.get(orr.second); + const result = first | second; + self.registers.set(orr.first, result); + self.registers.PC += 2; + }, + .AND => |andd| { + const first = self.registers.get(andd.first); + const second = self.registers.get(andd.second); + const result = first & second; + self.registers.set(andd.first, result); + self.registers.PC += 2; + }, + .XOR => |xor| { + const first = self.registers.get(xor.first); + const second = self.registers.get(xor.second); + const result = first ^ second; + self.registers.set(xor.first, result); + self.registers.PC += 2; + }, + .ADDR => |addr| { + const first = self.registers.get(addr.first); + const second = self.registers.get(addr.second); + const result = @addWithOverflow(first, second); + + const sum = result[0]; + const carry = result[1]; + + self.registers.set(addr.first, sum); + self.registers.set(0xF, carry); + self.registers.PC += 2; + }, + .SUB => |sub| { + const first = self.registers.get(sub.first); + const second = self.registers.get(sub.second); + self.registers.set(0xF, if (first > second) 1 else 0); + self.registers.set(sub.first, first -% second); + self.registers.PC += 2; + }, + .SHR => |shr| { + const value = self.registers.get(shr.register); + const lsb = value & 0x01; + self.registers.set(0xF, lsb); + self.registers.set(shr.register, value >> 1); + self.registers.PC += 2; + }, + .SUBN => |subn| { + const vy = self.registers.get(subn.second); + const vx = self.registers.get(subn.first); + self.registers.set(0xF, if (vy > vx) 1 else 0); + self.registers.set(subn.first, vy -% vx); + self.registers.PC += 2; + }, + .SHL => |shl| { + const value = self.registers.get(shl.register); + const msb = (value & 0x80) >> 7; + self.registers.set(0xF, msb); + self.registers.set(shl.register, value << 1); + self.registers.PC += 2; + }, + .SNER => |sner| { + if (self.registers.get(sner.first) != self.registers.get(sner.second)) { + self.registers.PC += 4; + } else { + self.registers.PC += 2; + } + }, + .LDI => |ldi| { + self.registers.I = ldi.value; + self.registers.PC += 2; + }, + .JPV0 => |jpv0| { + self.registers.PC = self.registers.get(0x0) + jpv0.add; + }, + .RND => |rnd| { + const r: u8 = std.crypto.random.int(u8); + const value = r & rnd.value; + self.registers.set(rnd.register, value); + self.registers.PC += 2; + }, + .DRW => |drw| { + var VF: u8 = 0; + + const x0: u8 = self.registers.get(drw.x_register); + const y0: u8 = self.registers.get(drw.y_register); + const h: u8 = drw.bytes; + + for (0..h) |row| { + const sprite: u8 = self.memory.peek(self.registers.I + row); + + for (0..8) |col| { + const sprite_pixel: u1 = @intCast((sprite >> @intCast(7 - col)) & 1); + + if (sprite_pixel == 1) { + const x: u8 = @intCast((x0 + col) % 64); + const y: u8 = @intCast((y0 + row) % 32); + + const old_pixel = self.display.get(x, y); + if (old_pixel == 1) { + VF = 1; + } + self.display.pixels[y][x] ^= 1; + } + } + } + + self.registers.set(0xF, VF); + self.registers.PC += 2; + }, + .SKP => |skp| { + if (self.keyboard.is_pressed(@truncate(self.registers.get(skp.key)))) { + self.registers.PC += 4; + } else { + self.registers.PC += 2; + } + }, + .SKNP => |sknp| { + if (!self.keyboard.is_pressed(@truncate(self.registers.get(sknp.key)))) { + self.registers.PC += 4; + } else { + self.registers.PC += 2; + } + }, + .LDDTIN => |lddtin| { + self.registers.set(lddtin.register, self.registers.DT); + self.registers.PC += 2; + }, + .LDK => |ldk| { + for (0..16) |key| { + if (self.keyboard.is_pressed(@intCast(key))) { + self.registers.set(ldk.register, @intCast(key)); + self.registers.PC += 2; + break; + } + } + // If no key is currently pressed, PC is not advanced. + // tick() will re-execute LDK on the next frame until a key is held. + }, + .LDDT => |lddt| { + self.registers.DT = self.registers.get(lddt.register); + self.registers.PC += 2; + }, + .LDST => |ldst| { + self.registers.ST = self.registers.get(ldst.register); + self.registers.PC += 2; + }, + .ADDI => |addi| { + self.registers.I += self.registers.get(addi.register); + self.registers.PC += 2; + }, + .LDF => |ldf| { + self.registers.I = self.registers.get(ldf.register) * 5; + self.registers.PC += 2; + }, + .LDB => |ldb| { + const value: u8 = self.registers.get(ldb.register); + self.memory.put(self.registers.I + 0, value / 100); + self.memory.put(self.registers.I + 1, (value / 10) % 10); + self.memory.put(self.registers.I + 2, value % 10); + self.registers.PC += 2; + }, + .LDRO => |ldro| { + const count = @as(usize, ldro.until_register) + 1; + for (0..count) |register| { + self.memory.put(self.registers.I + register, self.registers.get(@intCast(register))); + } + self.registers.PC += 2; + }, + .LDRI => |ldri| { + const count = @as(usize, ldri.until_register) + 1; + for (0..count) |register| { + self.registers.set(@intCast(register), self.memory.peek(self.registers.I + register)); + } + self.registers.PC += 2; + }, + } + + if (self.registers.DT > 0) self.registers.DT -= 1; + if (self.registers.ST > 0) self.registers.ST -= 1; + } +}; diff --git a/src/octochip/machine/root.zig b/src/octochip/machine/root.zig index 092f533..030d70f 100644 --- a/src/octochip/machine/root.zig +++ b/src/octochip/machine/root.zig @@ -1 +1,2 @@ pub const Instruction = @import("instruction.zig").Instruction; +pub const Machine = @import("machine.zig").Machine; diff --git a/src/rom.ch8 b/src/rom.ch8 new file mode 100644 index 0000000000000000000000000000000000000000..4be77cb54fc90be0bec1ad8d4565a0e0e42c26f6 GIT binary patch literal 3581 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 Yc#*%nX0Ptv6sSIXM)}{l3}Ers0EWGHDF6Tf literal 0 HcmV?d00001