commit 40223a179ddfc09c84724c351439b44e2689e3c8 Author: teesh3rt Date: Sat Mar 14 17:04:18 2026 +0200 init: abstractions over chip8 stuff diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33228ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +### Nix ### +# Ignore build outputs from performing a nix-build or `nix build` command +result +result-* + +# Ignore automatically generated direnv output +.direnv +### End of Nix ### + +### Zig ### +.zig-cache/ +zig-out/ +*.o +### End of Zig ### + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..dfa97bb --- /dev/null +++ b/build.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const mod = b.createModule(.{ + .root_source_file = b.path("./src/octochip/root.zig"), + .target = target, + .optimize = optimize, + }); + + const raylib_dep = b.dependency("raylib_zig", .{ + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "octochip", + .root_module = b.createModule(.{ + .root_source_file = b.path("./src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "octochip", .module = mod }, + .{ .name = "raylib", .module = raylib_dep.module("raylib") }, + }, + }), + }); + + b.installArtifact(exe); + + const run_step = b.step("run", "Run octochip"); + const run_exe = b.addRunArtifact(exe); + run_step.dependOn(&run_exe.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..f1637b7 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .octochip, + .version = "0.0.0", + .fingerprint = 0xd60bf7ea9ec78472, + .minimum_zig_version = "0.15.2", + .dependencies = .{ + .raylib_zig = .{ + .url = "git+https://github.com/raylib-zig/raylib-zig#cd71c85d571027ac8033357f83b124ee051825b3", + .hash = "raylib_zig-5.6.0-dev-KE8REENOBQC-m5nK7M2b5aKSIubJPbPLUYcRhT7aT3RN", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ce8f5be --- /dev/null +++ b/flake.lock @@ -0,0 +1,77 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1772408722, + "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "import-tree": { + "locked": { + "lastModified": 1772999353, + "narHash": "sha256-dPb0WxUhFaz6wuR3B6ysqFJpsu8txKDPZvS47AT2XLI=", + "owner": "vic", + "repo": "import-tree", + "rev": "545a4df146fce44d155573e47f5a777985acf912", + "type": "github" + }, + "original": { + "owner": "vic", + "repo": "import-tree", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1773122722, + "narHash": "sha256-FIqHByVqxCprNjor1NqF80F2QQoiiyqanNNefdlvOg4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "62dc67aa6a52b4364dd75994ec00b51fbf474e50", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1772328832, + "narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "import-tree": "import-tree", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..50b151a --- /dev/null +++ b/flake.nix @@ -0,0 +1,11 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + import-tree.url = "github:vic/import-tree"; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake {inherit inputs;} + (inputs.import-tree ./packages/nix); +} diff --git a/packages/nix/shell.nix b/packages/nix/shell.nix new file mode 100644 index 0000000..033d505 --- /dev/null +++ b/packages/nix/shell.nix @@ -0,0 +1,19 @@ +{...}: { + perSystem = {pkgs, ...}: { + devShells.default = pkgs.mkShell { + packages = [ + pkgs.zig + + pkgs.libGLX + pkgs.libx11 + pkgs.libxcursor + pkgs.libxext + pkgs.libxfixes + pkgs.libxi + pkgs.libxinerama + pkgs.libxrandr + pkgs.libxrender + ]; + }; + }; +} diff --git a/packages/nix/systems.nix b/packages/nix/systems.nix new file mode 100644 index 0000000..138a0e2 --- /dev/null +++ b/packages/nix/systems.nix @@ -0,0 +1,3 @@ +{...}: { + systems = ["x86_64-linux"]; +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..6c288e0 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +const octo = @import("octochip"); + +pub fn main() !void { + const ins = try octo.machine.Instruction.from_bytes(0xabcd); + std.log.info("instruction: {f}", .{ins}); +} diff --git a/src/octochip/components/display.zig b/src/octochip/components/display.zig new file mode 100644 index 0000000..bee5520 --- /dev/null +++ b/src/octochip/components/display.zig @@ -0,0 +1,7 @@ +pub const Display = struct { + pixels: [32][64]bool = .{}, + + fn new() @This() { + return .{}; + } +}; diff --git a/src/octochip/components/keyboard.zig b/src/octochip/components/keyboard.zig new file mode 100644 index 0000000..0f79b7a --- /dev/null +++ b/src/octochip/components/keyboard.zig @@ -0,0 +1,40 @@ +pub const Keyboard = struct { + // FEDCBA9876543210 + pressed: u16 = 0, + + fn new() @This() { + return .{}; + } + + fn released(self: *const @This()) u16 { + return ~self.pressed; + } + + fn mask(key: u4) u16 { + return (@as(u16, 1) << key); + } + + fn press(self: *@This(), key: u4) void { + self.pressed |= mask(key); + } + + fn release(self: *@This(), key: u4) void { + self.pressed &= ~mask(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 { + return !self.is_pressed(key); + } +}; diff --git a/src/octochip/components/memory.zig b/src/octochip/components/memory.zig new file mode 100644 index 0000000..fad91e4 --- /dev/null +++ b/src/octochip/components/memory.zig @@ -0,0 +1,15 @@ +pub const Memory = struct { + memory: [4096]u8 = .{}, + + pub fn new() @This() { + return .{}; + } + + pub fn put(self: *@This(), addr: usize, value: u8) void { + self.memory[addr] = value; + } + + pub fn peek(self: *const @This(), addr: usize) u8 { + return self.memory[addr]; + } +}; diff --git a/src/octochip/components/registers.zig b/src/octochip/components/registers.zig new file mode 100644 index 0000000..41ab7e9 --- /dev/null +++ b/src/octochip/components/registers.zig @@ -0,0 +1,20 @@ +pub const Registers = struct { + V: [16]u8 = .{}, + DT: u8 = 0, + ST: u8 = 0, + I: u16 = 0, + PC: u16 = 0x200, + SP: u8 = 0, + + pub fn new() Registers { + return Registers{}; + } + + pub fn get(self: *const Registers, index: u4) u8 { + return self.V[index]; + } + + pub fn set(self: *Registers, index: u4, value: u8) void { + self.V[index] = value; + } +}; diff --git a/src/octochip/components/root.zig b/src/octochip/components/root.zig new file mode 100644 index 0000000..20e6864 --- /dev/null +++ b/src/octochip/components/root.zig @@ -0,0 +1,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; diff --git a/src/octochip/machine/instruction.zig b/src/octochip/machine/instruction.zig new file mode 100644 index 0000000..98c5958 --- /dev/null +++ b/src/octochip/machine/instruction.zig @@ -0,0 +1,167 @@ +const std = @import("std"); + +// NOTE: Some instructions have X and then XR +// It means that X operates on values (xkk) and XR operates on registers Vx and Vy +// An instance of this can be found with SE and SER + +// NOTE: Packed structs for instructions. +// Variants for E--- and F--- can simply use Xxkk with pattern matching +const Xnnn = packed struct { nnn: u12, X: u4 }; +const Xxkk = packed struct { kk: u8, x: u4, X: u4 }; +const XxyY = packed struct { Y: u4, y: u4, x: u4, X: u4 }; +const Xxyn = packed struct { n: u4, y: u4, x: u4, X: u4 }; + +pub const Instruction = union(enum) { + SYS: struct { location: u12 }, + CLS: void, + RET: void, + JP: struct { location: u12 }, + CALL: struct { subroutine: u12 }, + SE: struct { register: u4, value: u8 }, + SNE: struct { register: u4, value: u8 }, + SER: struct { first: u4, second: u4 }, + LD: struct { register: u4, value: u8 }, + ADD: struct { register: u4, value: u8 }, + LDR: struct { first: u4, second: u4 }, + OR: struct { first: u4, second: u4 }, + AND: struct { first: u4, second: u4 }, + XOR: struct { first: u4, second: u4 }, + ADDR: struct { first: u4, second: u4 }, + SUB: struct { first: u4, second: u4 }, + SHR: struct { register: u4 }, + SUBN: struct { first: u4, second: u4 }, + SHL: struct { register: u4 }, + SNER: struct { first: u4, second: u4 }, + LDI: struct { value: u12 }, + JPV0: struct { add: u12 }, + RND: struct { register: u4, value: u8 }, + DRW: struct { x_register: u4, y_register: u4, bytes: u4 }, + SKP: struct { key: u4 }, + SKNP: struct { key: u4 }, + LDDTIN: struct { register: u4 }, + LDK: struct { register: u4 }, + LDDT: struct { register: u4 }, + LDST: struct { register: u4 }, + ADDI: struct { register: u4 }, + LDF: struct { register: u4 }, + LDB: struct { register: u4 }, + LDRO: struct { until_register: u4 }, + LDRI: struct { until_register: u4 }, + + pub fn from_bytes(instruction: u16) !@This() { + switch (instruction) { + 0x00E0 => return .CLS, + 0x00EE => return .RET, + else => {}, + } + + const xnnn: Xnnn = @bitCast(instruction); + const xxkk: Xxkk = @bitCast(instruction); + const xxyy: XxyY = @bitCast(instruction); + const xxyn: Xxyn = @bitCast(instruction); + + switch (xnnn.X) { + 0x0 => return .{ .SYS = .{ .location = xnnn.nnn } }, + 0x1 => return .{ .JP = .{ .location = xnnn.nnn } }, + 0x2 => return .{ .CALL = .{ .subroutine = xnnn.nnn } }, + 0xA => return .{ .LDI = .{ .value = xnnn.nnn } }, + 0xB => return .{ .JPV0 = .{ .add = xnnn.nnn } }, + else => {}, + } + + switch (xxkk.X) { + 0x3 => return .{ .SE = .{ .register = xxkk.x, .value = xxkk.kk } }, + 0x4 => return .{ .SNE = .{ .register = xxkk.x, .value = xxkk.kk } }, + 0x6 => return .{ .LD = .{ .register = xxkk.x, .value = xxkk.kk } }, + 0x7 => return .{ .ADD = .{ .register = xxkk.x, .value = xxkk.kk } }, + 0xC => return .{ .RND = .{ .register = xxkk.x, .value = xxkk.kk } }, + 0xE => switch (xxkk.kk) { + 0x9E => return .{ .SKP = .{ .key = xxkk.x } }, + 0xA1 => return .{ .SKNP = .{ .key = xxkk.x } }, + else => return error.InvalidInstruction, + }, + 0xF => switch (xxkk.kk) { + 0x07 => return .{ .LDDTIN = .{ .register = xxkk.x } }, + 0x0A => return .{ .LDK = .{ .register = xxkk.x } }, + 0x15 => return .{ .LDDT = .{ .register = xxkk.x } }, + 0x18 => return .{ .LDST = .{ .register = xxkk.x } }, + 0x1E => return .{ .ADDI = .{ .register = xxkk.x } }, + 0x29 => return .{ .LDF = .{ .register = xxkk.x } }, + 0x33 => return .{ .LDB = .{ .register = xxkk.x } }, + 0x55 => return .{ .LDRO = .{ .until_register = xxkk.x } }, + 0x65 => return .{ .LDRI = .{ .until_register = xxkk.x } }, + else => return error.InvalidInstruction, + }, + + else => {}, + } + + switch (xxyy.X) { + 0x5 => return .{ .SER = .{ .first = xxyy.x, .second = xxyy.y } }, + 0x8 => switch (xxyy.Y) { + 0x0 => return .{ .LDR = .{ .first = xxyy.x, .second = xxyy.y } }, + 0x1 => return .{ .OR = .{ .first = xxyy.x, .second = xxyy.y } }, + 0x2 => return .{ .AND = .{ .first = xxyy.x, .second = xxyy.y } }, + 0x3 => return .{ .XOR = .{ .first = xxyy.x, .second = xxyy.y } }, + 0x4 => return .{ .ADDR = .{ .first = xxyy.x, .second = xxyy.y } }, + 0x5 => return .{ .SUB = .{ .first = xxyy.x, .second = xxyy.y } }, + 0x6 => return .{ .SHR = .{ .register = xxyy.x } }, + 0x7 => return .{ .SUBN = .{ .first = xxyy.x, .second = xxyy.y } }, + 0xE => return .{ .SHL = .{ .register = xxyy.x } }, + else => return error.InvalidInstruction, + }, + 0x9 => return .{ .SNER = .{ .first = xxyy.x, .second = xxyy.y } }, + else => {}, + } + + switch (xxyn.X) { + 0xD => return .{ .DRW = .{ .x_register = xxyn.x, .y_register = xxyn.y, .bytes = xxyn.n } }, + else => {}, + } + + return error.InvalidInstruction; + } + + pub fn format( + self: @This(), + writer: anytype, + ) !void { + 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 }), + .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 }), + .LDR => |v| try writer.print("LD V{X}, V{X}", .{ v.first, v.second }), + .OR => |v| try writer.print("OR V{X}, V{X}", .{ v.first, v.second }), + .AND => |v| try writer.print("AND V{X}, V{X}", .{ v.first, v.second }), + .XOR => |v| try writer.print("XOR V{X}, V{X}", .{ v.first, v.second }), + .ADDR => |v| try writer.print("ADD V{X}, V{X}", .{ v.first, v.second }), + .SUB => |v| try writer.print("SUB V{X}, V{X}", .{ v.first, v.second }), + .SHR => |v| try writer.print("SHR V{X}", .{v.register}), + .SUBN => |v| try writer.print("SUBN V{X}, V{X}", .{ v.first, v.second }), + .SHL => |v| try writer.print("SHL V{X}", .{v.register}), + .SNER => |v| try writer.print("SNE V{X}, V{X}", .{ v.first, v.second }), + .LDI => |v| try writer.print("LD I, {x}", .{v.value}), + .JPV0 => |v| try writer.print("JP V0, {x}", .{v.add}), + .RND => |v| try writer.print("RND V{X}, {x}", .{ v.register, v.value }), + .DRW => |v| try writer.print("DRW V{X}, V{X}, {}", .{ v.x_register, v.y_register, v.bytes }), + .SKP => |v| try writer.print("SKP V{X}", .{v.key}), + .SKNP => |v| try writer.print("SKNP V{X}", .{v.key}), + .LDDTIN => |v| try writer.print("LD V{X}, DT", .{v.register}), + .LDK => |v| try writer.print("LD V{X}, K", .{v.register}), + .LDDT => |v| try writer.print("LD DT, V{X}", .{v.register}), + .LDST => |v| try writer.print("LD ST, V{X}", .{v.register}), + .ADDI => |v| try writer.print("ADD I, V{X}", .{v.register}), + .LDF => |v| try writer.print("LD F, V{X}", .{v.register}), + .LDB => |v| try writer.print("LD B, V{X}", .{v.register}), + .LDRO => |v| try writer.print("LD [I], V0..V{X}", .{v.until_register}), + .LDRI => |v| try writer.print("LD V0..V{X}, [I]", .{v.until_register}), + } + } +}; diff --git a/src/octochip/machine/machine.zig b/src/octochip/machine/machine.zig new file mode 100644 index 0000000..12e49c8 --- /dev/null +++ b/src/octochip/machine/machine.zig @@ -0,0 +1 @@ +// the part that actually executes chip8 diff --git a/src/octochip/machine/root.zig b/src/octochip/machine/root.zig new file mode 100644 index 0000000..092f533 --- /dev/null +++ b/src/octochip/machine/root.zig @@ -0,0 +1 @@ +pub const Instruction = @import("instruction.zig").Instruction; diff --git a/src/octochip/root.zig b/src/octochip/root.zig new file mode 100644 index 0000000..3ca2335 --- /dev/null +++ b/src/octochip/root.zig @@ -0,0 +1,2 @@ +pub const components = @import("components/root.zig"); +pub const machine = @import("machine/root.zig");