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 0000000..4be77cb Binary files /dev/null and b/src/rom.ch8 differ