init: abstractions over chip8 stuff

This commit is contained in:
Teesh 2026-03-14 17:04:18 +02:00
commit 40223a179d
18 changed files with 443 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

15
.gitignore vendored Normal file
View file

@ -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 ###

36
build.zig Normal file
View file

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

17
build.zig.zon Normal file
View file

@ -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",
},
}

77
flake.lock generated Normal file
View file

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

11
flake.nix Normal file
View file

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

19
packages/nix/shell.nix Normal file
View file

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

3
packages/nix/systems.nix Normal file
View file

@ -0,0 +1,3 @@
{...}: {
systems = ["x86_64-linux"];
}

7
src/main.zig Normal file
View file

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

View file

@ -0,0 +1,7 @@
pub const Display = struct {
pixels: [32][64]bool = .{},
fn new() @This() {
return .{};
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
// the part that actually executes chip8

View file

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

2
src/octochip/root.zig Normal file
View file

@ -0,0 +1,2 @@
pub const components = @import("components/root.zig");
pub const machine = @import("machine/root.zig");