feat: ITS ALIVE
This commit is contained in:
parent
40223a179d
commit
c70756069b
12 changed files with 395 additions and 38 deletions
50
src/main.zig
50
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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{};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
25
src/octochip/components/wrapping_stack.zig
Normal file
25
src/octochip/components/wrapping_stack.zig
Normal 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];
|
||||
}
|
||||
};
|
||||
}
|
||||
1
src/octochip/machine/digits.bin
Normal file
1
src/octochip/machine/digits.bin
Normal file
|
|
@ -0,0 +1 @@
|
|||
ð<EFBFBD><EFBFBD><EFBFBD>ð ` pðð€ðððð<><C3B0>ðð€ððð€ð<E282AC>ðð @@ð<>ð<EFBFBD>ðð<C3B0>ððð<C3B0>ð<EFBFBD><C3B0>à<EFBFBD>à<EFBFBD>àð€€€ðà<C3B0><C3A0><EFBFBD>àð€ð€ðð€ð€€
|
||||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
pub const Instruction = @import("instruction.zig").Instruction;
|
||||
pub const Machine = @import("machine.zig").Machine;
|
||||
|
|
|
|||
BIN
src/rom.ch8
Normal file
BIN
src/rom.ch8
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue