feat: ITS ALIVE

This commit is contained in:
Teesh 2026-03-16 13:06:52 +02:00
parent 40223a179d
commit c70756069b
12 changed files with 395 additions and 38 deletions

View file

@ -1,7 +1,51 @@
const std = @import("std");
const octo = @import("octochip"); const octo = @import("octochip");
const rl = @import("raylib");
const PIXEL_SCALE: i32 = 20;
pub fn main() !void { pub fn main() !void {
const ins = try octo.machine.Instruction.from_bytes(0xabcd); var machine = octo.machine.Machine.new();
std.log.info("instruction: {f}", .{ins}); 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);
}
}
}
} }

View file

@ -1,7 +1,19 @@
pub const Display = struct { 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 .{}; 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;
}
}; };

View file

@ -1,40 +1,28 @@
pub const Keyboard = struct { pub const Keyboard = struct {
// FEDCBA9876543210 // We track each key by its index (0..15)
pressed: u16 = 0, pressed: [16]bool = [_]bool{false} ** 16,
fn new() @This() { pub fn new() @This() {
return .{}; return .{};
} }
fn released(self: *const @This()) u16 { pub fn press(self: *@This(), key: u4) void {
return ~self.pressed; self.pressed[@intCast(key)] = true;
} }
fn mask(key: u4) u16 { pub fn release(self: *@This(), key: u4) void {
return (@as(u16, 1) << key); self.pressed[@intCast(key)] = false;
} }
fn press(self: *@This(), key: u4) void { pub fn press_or_release_if(self: *@This(), key: u4, cond: bool) void {
self.pressed |= mask(key); self.pressed[@intCast(key)] = cond;
} }
fn release(self: *@This(), key: u4) void { pub fn is_pressed(self: *const @This(), key: u4) bool {
self.pressed &= ~mask(key); return self.pressed[@intCast(key)];
} }
fn press_or_release_if(self: *@This(), key: u4, cond: bool) void { pub fn is_released(self: *const @This(), key: u4) bool {
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 {
return !self.is_pressed(key); return !self.is_pressed(key);
} }
}; };

View file

@ -1,5 +1,5 @@
pub const Memory = struct { pub const Memory = struct {
memory: [4096]u8 = .{}, memory: [4096]u8 = [_]u8{0} ** 4096,
pub fn new() @This() { pub fn new() @This() {
return .{}; return .{};
@ -12,4 +12,15 @@ pub const Memory = struct {
pub fn peek(self: *const @This(), addr: usize) u8 { pub fn peek(self: *const @This(), addr: usize) u8 {
return self.memory[addr]; 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;
}
}; };

View file

@ -1,10 +1,9 @@
pub const Registers = struct { pub const Registers = struct {
V: [16]u8 = .{}, V: [16]u8 = [_]u8{0} ** 16,
DT: u8 = 0, DT: u8 = 0,
ST: u8 = 0, ST: u8 = 0,
I: u16 = 0, I: u16 = 0,
PC: u16 = 0x200, PC: u16 = 0x200,
SP: u8 = 0,
pub fn new() Registers { pub fn new() Registers {
return Registers{}; return Registers{};

View file

@ -2,3 +2,4 @@ pub const Memory = @import("memory.zig").Memory;
pub const Registers = @import("registers.zig").Registers; pub const Registers = @import("registers.zig").Registers;
pub const Keyboard = @import("keyboard.zig").Keyboard; pub const Keyboard = @import("keyboard.zig").Keyboard;
pub const Display = @import("display.zig").Display; pub const Display = @import("display.zig").Display;
pub const WrappingStack = @import("wrapping_stack.zig").WrappingStack;

View file

@ -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];
}
};
}

View file

@ -0,0 +1 @@
ð<EFBFBD><EFBFBD><EFBFBD>ð ` pðð€ðððð<><C3B0>ðð€ððð€ð<E282AC>ðð @@ð<>ð<EFBFBD>ðð<C3B0>ððð<C3B0>ð<EFBFBD><C3B0>à<EFBFBD>à<EFBFBD>àð€€€ðà<C3B0><C3A0><EFBFBD>àð€ð€ðð€ð€€

View file

@ -129,11 +129,11 @@ pub const Instruction = union(enum) {
switch (self) { switch (self) {
.CLS => try writer.writeAll("CLS"), .CLS => try writer.writeAll("CLS"),
.RET => try writer.writeAll("RET"), .RET => try writer.writeAll("RET"),
.SYS => |v| try writer.print("SYS {x}", .{v.location}), .SYS => |v| try writer.print("SYS {X}", .{v.location}),
.JP => |v| try writer.print("JP {x}", .{v.location}), .JP => |v| try writer.print("JP {X}", .{v.location}),
.CALL => |v| try writer.print("CALL {x}", .{v.subroutine}), .CALL => |v| try writer.print("CALL {X}", .{v.subroutine}),
.SE => |v| try writer.print("SE V{X}, {x}", .{ v.register, v.value }), .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 }), .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 }), .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 }), .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 }), .ADD => |v| try writer.print("ADD V{X}, {x}", .{ v.register, v.value }),

View file

@ -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;
}
};

View file

@ -1 +1,2 @@
pub const Instruction = @import("instruction.zig").Instruction; pub const Instruction = @import("instruction.zig").Instruction;
pub const Machine = @import("machine.zig").Machine;

BIN
src/rom.ch8 Normal file

Binary file not shown.