From 19077200cdcb87b131e95eeec0a678f5acb18f3a Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Tue, 26 Oct 2021 19:48:22 -0300 Subject: [PATCH] Initial Commit --- .gitignore | 4 + .gitmodules | 3 + .vscode/extensions.json | 7 ++ build.zig | 34 +++++ src/chip8.zig | 60 +++++++++ src/cpu.zig | 266 ++++++++++++++++++++++++++++++++++++++++ src/display.zig | 46 +++++++ src/instruction.zig | 36 ++++++ src/main.zig | 89 ++++++++++++++ src/scheduler.zig | 122 ++++++++++++++++++ src/util.zig | 24 ++++ third_party/SDL.zig | 1 + 12 files changed, 692 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .vscode/extensions.json create mode 100644 build.zig create mode 100644 src/chip8.zig create mode 100644 src/cpu.zig create mode 100644 src/display.zig create mode 100644 src/instruction.zig create mode 100644 src/main.zig create mode 100644 src/scheduler.zig create mode 100644 src/util.zig create mode 160000 third_party/SDL.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e1758e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/zig-cache +/zig-out +/bin +/.vscode \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9b85bf4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/SDL.zig"] + path = third_party/SDL.zig + url = https://github.com/MasterQ32/SDL.zig diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..163c8d9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "augusterame.zls-vscode", + "tiehuis.zig", + "vadimcn.vscode-lldb" + ] +} \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..21416d5 --- /dev/null +++ b/build.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const Sdk = @import("third_party/SDL.zig/Sdk.zig"); + +pub fn build(b: *std.build.Builder) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard release options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. + const mode = b.standardReleaseOptions(); + + const sdk = Sdk.init(b); + + const exe = b.addExecutable("zig8", "src/main.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); + + sdk.link(exe, .dynamic); + exe.addPackage(sdk.getNativePackage("sdl2")); + + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/src/chip8.zig b/src/chip8.zig new file mode 100644 index 0000000..c332aff --- /dev/null +++ b/src/chip8.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const cpu = @import("cpu.zig"); +const Scheduler = @import("scheduler.zig").Scheduler; +const Display = @import("display.zig").Display; +const Allocator = std.mem.Allocator; +const Cpu = cpu.Cpu; + +const FONT_SET: [80]u8 = [_]u8{ + 0xF0, 0x90, 0x90, 0x90, 0xF0, + 0x20, 0x60, 0x20, 0x20, 0x70, + 0xF0, 0x10, 0xF0, 0x80, 0xF0, + 0xF0, 0x10, 0xF0, 0x10, 0xF0, + 0x90, 0x90, 0xF0, 0x10, 0x10, + 0xF0, 0x80, 0xF0, 0x10, 0xF0, + 0xF0, 0x80, 0xF0, 0x90, 0xF0, + 0xF0, 0x10, 0x20, 0x40, 0x40, + 0xF0, 0x90, 0xF0, 0x90, 0xF0, + 0xF0, 0x90, 0xF0, 0x10, 0xF0, + 0xF0, 0x90, 0xF0, 0x90, 0x90, + 0xE0, 0x90, 0xE0, 0x90, 0xE0, + 0xF0, 0x80, 0x80, 0x80, 0xF0, + 0xE0, 0x90, 0x90, 0x90, 0xE0, + 0xF0, 0x80, 0xF0, 0x80, 0xF0, + 0xF0, 0x80, 0xF0, 0x80, 0x80, +}; + +pub const Chip8 = struct { + cpu: Cpu, + mem: [0x1000]u8, + disp: Display, + + pub fn fromFile(alloc: *Allocator, scheduler: *Scheduler, path: []const u8) !Chip8 { + const file = try std.fs.cwd().openFile(path, .{ .read = true }); + defer file.close(); + + // Read file into allocated buffer + const size = try file.getEndPos(); + const rom_buf = try file.readToEndAlloc(alloc, size); + defer alloc.free(rom_buf); + + var chip8 = Chip8{ + .cpu = Cpu.new(scheduler), + .mem = [_]u8{0x00} ** 0x1000, + .disp = Display.new(), + }; + + std.mem.copy(u8, chip8.mem[0x0200..], rom_buf); // Copy ROM + std.mem.copy(u8, chip8.mem[0x50..0xA0], &FONT_SET); // Copy FONT_SET + + return chip8; + } + + pub fn step(self: *Chip8) u64 { + return cpu.step(&self.cpu, &self.disp, &self.mem); + } + + pub fn getDisplay(self: *Chip8) *const Display { + return &self.disp; + } +}; diff --git a/src/cpu.zig b/src/cpu.zig new file mode 100644 index 0000000..bb07bd0 --- /dev/null +++ b/src/cpu.zig @@ -0,0 +1,266 @@ +const std = @import("std"); +const disp = @import("display.zig"); +const util = @import("util.zig"); +const sched = @import("scheduler.zig"); +const Chip8 = @import("chip8.zig").Chip8; +const Instruction = @import("instruction.zig").Instruction; +const Point = disp.Point; +const Display = disp.Display; +const Scheduler = sched.Scheduler; +const EventKind = sched.EventKind; + +const OPS_PER_HZ = sched.OPS_PER_HZ; +const panic = std.debug.panic; +var prng = std.rand.DefaultPrng.init(1337); + +pub const Cpu = struct { + i: u16, + pc: u16, + sp: u16, + v: [16]u8, + stack: [16]u16, + scheduler: *Scheduler, + + pub fn new(scheduler: *Scheduler) Cpu { + return .{ + .i = 0x0000, + .pc = 0x0200, + .sp = 0x0000, + .v = [_]u8{0x00} ** 16, + .stack = [_]u16{0x0000} ** 16, + .scheduler = scheduler, + }; + } +}; + +pub fn step(cpu: *Cpu, display: *Display, mem: *[0x1000]u8) u64 { + const opcode = fetch(cpu, mem); + + // std.log.debug("0x{X:}", .{opcode}); + execute(cpu, display, mem, decode(opcode)); + + return 1; +} + +fn fetch(cpu: *Cpu, mem: *[0x1000]u8) u16 { + const high: u16 = mem[cpu.pc]; + const low: u16 = mem[cpu.pc + 1]; + + cpu.pc += 2; + + return high << 8 | low; +} + +fn decode(opcode: u16) Instruction { + const n1: u4 = @intCast(u4, (opcode & 0xF000) >> 12); + const n2: u4 = @intCast(u4, (opcode & 0x0F00) >> 8); + const n3: u4 = @intCast(u4, (opcode & 0x00F0) >> 4); + const n4: u4 = @intCast(u4, (opcode & 0x000F) >> 0); + + return switch (n1) { + 0x0 => switch (n4) { + 0x0 => return Instruction.CLS, // 0x00E0 | CLS + 0xE => return Instruction.RET, // 0x00EE | RET + else => panic("0x{X:} is an unknown 0x0-prefixed opcode", .{opcode}), + }, + 0x1 => Instruction{ .JP = util.calc12bit(n2, n3, n4) }, // 0x1nnn | JP u12 + 0x2 => Instruction{ .CALL = util.calc12bit(n2, n3, n4) }, // 0x2nnn | CALL u12 + 0x3 => Instruction{ .SE_Vx = .{ .x = n2, .kk = util.calc8bit(n3, n4) } }, // 0x3xkk | SE Vx, u8 + 0x4 => Instruction{ .SNE_Vx = .{ .x = n2, .kk = util.calc8bit(n3, n4) } }, // 0x4xkk | SNE Vx, u8 + 0x5 => Instruction{ .SE_Vx_Vy = .{ .x = n2, .y = n3 } }, // 0x5xy0 | SE Vx, Vy + 0x6 => Instruction{ .LD_Vx = .{ .x = n2, .kk = util.calc8bit(n3, n4) } }, // 0x6xkk | LD Vx, u8 + 0x7 => Instruction{ .ADD_Vx = .{ .x = n2, .kk = util.calc8bit(n3, n4) } }, // 0x7xkk | ADD Vx, u8 + 0x8 => switch (n4) { + 0x0 => return Instruction{ .LD_Vx_Vy = .{ .x = n2, .y = n3 } }, // 0x8xy0 | LD Vx, Vy + 0x1 => return Instruction{ .OR = .{ .x = n2, .y = n3 } }, // 0x8xy1 | OR Vx, Vy + 0x2 => return Instruction{ .AND = .{ .x = n2, .y = n3 } }, // 0x8xy2 | AND Vx, Vy + 0x3 => return Instruction{ .XOR = .{ .x = n2, .y = n3 } }, // 0x8xy3 | XOR Vx, Vy + 0x4 => return Instruction{ .ADD_Vx_Vy = .{ .x = n2, .y = n3 } }, // 0x8xy4 | ADD Vx, Vy + 0x5 => return Instruction{ .SUB = .{ .x = n2, .y = n3 } }, // 0x8xy5 | SUB Vx, Vy + 0x6 => return Instruction{ .SHR = .{ .x = n2, .y = n3 } }, // 0x8xy6 | SHR Vx, Vy + 0x7 => return Instruction{ .SUBN = .{ .x = n2, .y = n3 } }, // 0x8xy7 | SUBN Vx, Vy + 0xE => return Instruction{ .SHL = .{ .x = n2, .y = n3 } }, // 0x8xyE | SHL Vx, Vy + else => panic("0x{X:} is an unknown 0x8-prefixed opcode", .{opcode}), + }, + 0x9 => Instruction{ .SNE_Vx_Vy = .{ .x = n2, .y = n3 } }, // 0x9xy0 | SNE Vx, Vy + 0xA => Instruction{ .LD_I = util.calc12bit(n2, n3, n4) }, // 0xAnnn | LD I, u12 + 0xB => Instruction{ .JP_V0 = util.calc12bit(n2, n3, n4) }, // 0xBnnn | JP V0, u12 + 0xC => Instruction{ .RND = .{ .x = n2, .kk = util.calc8bit(n3, n4) } }, // 0xCxkk | RND Vx, u8 + 0xD => Instruction{ .DRW = .{ .x = n2, .y = n3, .n = n4 } }, // 0xDxyn | DRW Vx, Vy, u4 + 0xE => switch (n3) { + 0x9 => return Instruction{ .SKP = n2 }, // 0xEx9E | SKP Vx + 0xA => return Instruction{ .SKNP = n2 }, // 0xExA1 | SKNP Vx + else => panic("0x{X:} is an unknown 0xE-prefixed opcode", .{opcode}), + }, + 0xF => switch (n3) { + 0x0 => switch (n4) { + 0x7 => return Instruction{ .LD_Vx_DT = n2 }, // 0xFx07 | LD Vx, DT + 0xA => return Instruction{ .LD_Vx_K = n2 }, // 0xFx0A | LD Vx, K + else => panic("0x{X:} is an unknown 0xFx0-prefixed opcode", .{opcode}), + }, + 0x1 => switch (n4) { + 0x5 => return Instruction{ .LD_DT_Vx = n2 }, // 0xFx15 | LD DT, Vx + 0x8 => return Instruction{ .LD_ST_Vx = n2 }, // 0xFx18 | LD ST, Vx + 0xE => return Instruction{ .ADD_I_Vx = n2 }, // 0xFx1E | ADD I, Vx + else => panic("0x{X:} is an unknown 0xFx1-prefixed opcode", .{opcode}), + }, + 0x2 => return Instruction{ .LD_F_Vx = n2 }, // 0xFx29 | LD F, Vx + 0x3 => return Instruction{ .LD_B_Vx = n2 }, // 0xFx33 | LD B, Vx + 0x5 => return Instruction{ .LD_IPTR_Vx = n2 }, // 0xFx55 | LD [I], Vx + 0x6 => return Instruction{ .LD_Vx_IPTR = n2 }, // 0xFx64 | LD Vx, [I] + else => panic("0x{X:} is an unknown 0xFx-prefixed opcode", .{opcode}), + }, + }; +} + +fn execute(cpu: *Cpu, display: *Display, mem: *[0x1000]u8, instr: Instruction) void { + const scheduler = cpu.scheduler; + + switch (instr) { + .CLS => { + display.buf = [_]u8{0x00} ** (disp.WIDTH * disp.HEIGHT); + scheduler.push(EventKind.RequestRedraw, scheduler.now()); + }, + .RET => { + cpu.pc = cpu.stack[cpu.sp]; + cpu.sp -= 1; + }, + .JP => |addr| { + cpu.pc = addr; + }, + .CALL => |addr| { + cpu.sp += 1; + cpu.stack[cpu.sp] = cpu.pc; + cpu.pc = addr; + }, + .SE_Vx => |args| { + if (cpu.v[args.x] == args.kk) { + cpu.pc += 2; + } + }, + .SNE_Vx => |args| { + if (cpu.v[args.x] != args.kk) { + cpu.pc += 2; + } + }, + .SE_Vx_Vy => |args| { + if (cpu.v[args.x] == cpu.v[args.y]) { + cpu.pc += 2; + } + }, + .LD_Vx => |args| { + cpu.v[args.x] = args.kk; + }, + .ADD_Vx => |args| { + cpu.v[args.x] +%= args.kk; + }, + .LD_Vx_Vy => |args| { + cpu.v[args.x] = cpu.v[args.y]; + }, + .OR => |args| { + cpu.v[args.x] |= cpu.v[args.y]; + }, + .AND => |args| { + cpu.v[args.x] &= cpu.v[args.y]; + }, + .XOR => |args| { + cpu.v[args.x] ^= cpu.v[args.y]; + }, + .ADD_Vx_Vy => |args| { + const did_overflow = @addWithOverflow(u8, cpu.v[args.x], cpu.v[args.y], &cpu.v[args.x]); + cpu.v[0xF] = @boolToInt(did_overflow); + }, + .SUB => |args| { + cpu.v[0xF] = @boolToInt(cpu.v[args.x] > cpu.v[args.y]); + cpu.v[args.x] -%= cpu.v[args.y]; + }, + .SHR => |args| { + cpu.v[0xF] = @boolToInt((cpu.v[args.x] & 0x01) == 0x01); + cpu.v[args.x] >>= 1; + }, + .SUBN => |args| { + cpu.v[0xF] = @boolToInt(cpu.v[args.y] > cpu.v[args.x]); + cpu.v[args.x] = cpu.v[args.y] -% cpu.v[args.x]; + }, + .SHL => |args| { + cpu.v[0xF] = @boolToInt(((cpu.v[args.x] >> 7) & 0x01) == 0x01); + cpu.v[args.x] <<= 1; + }, + .SNE_Vx_Vy => |args| { + if (cpu.v[args.x] != cpu.v[args.y]) { + cpu.pc += 2; + } + }, + .LD_I => |addr| { + cpu.i = addr; + }, + .JP_V0 => |addr| { + cpu.pc = addr + cpu.v[0]; + }, + .RND => |args| { + cpu.v[args.x] = prng.random.int(u8) & args.kk; + }, + .DRW => |args| { + const x = args.x; + const y = args.y; + const n = args.n; + + const draw_pos = Point{ .x = cpu.v[x], .y = cpu.v[y] }; + const sprite_data = mem[cpu.i..(cpu.i + n)]; + + // Draw Sprite + const collision = disp.drawSprite(display, &draw_pos, &sprite_data); + + cpu.v[0xF] = @boolToInt(collision); + scheduler.push(EventKind.RequestRedraw, scheduler.now()); + }, + .SKP => |x| { + std.log.debug("TODO: SKP V{}", .{x}); + }, + .SKNP => |x| { + std.log.debug("TODO: SKNP V{}", .{x}); + }, + .LD_Vx_DT => |x| { + if (scheduler.find(EventKind.DelayTimer)) |e| { + const num = (e.timestamp - scheduler.now()) / OPS_PER_HZ; + cpu.v[x] = @intCast(u8, num); + } else { + cpu.v[x] = 0; + } + }, + .LD_Vx_K => |x| { + std.log.debug("TODO: LD V{}, K", .{x}); + }, + .LD_DT_Vx => |x| { + const when = scheduler.now() + OPS_PER_HZ * @as(u64, cpu.v[x]); + scheduler.ReplaceOrPush(EventKind.DelayTimer, when); + }, + .LD_ST_Vx => |x| { + const when = scheduler.now() + OPS_PER_HZ * @as(u64, cpu.v[x]); + scheduler.ReplaceOrPush(EventKind.SoundTimer, when); + }, + .ADD_I_Vx => |x| { + const sum: u16 = cpu.i + cpu.v[x]; + cpu.v[0xF] = @boolToInt(sum >= 0x1000); + cpu.i = sum & 0x0FFF; + }, + .LD_F_Vx => |x| { + cpu.i = 0x50 + (5 * cpu.v[x]); + }, + .LD_B_Vx => |x| { + var value = cpu.v[x]; + + mem[cpu.i] = value / 100; + mem[cpu.i + 1] = (value / 10) % 10; + mem[cpu.i + 2] = value % 10; + }, + .LD_IPTR_Vx => |x| { + const upper = x + 1; + std.mem.copy(u8, mem[(cpu.i)..(cpu.i + upper)], cpu.v[0..upper]); + }, + .LD_Vx_IPTR => |x| { + const upper = x + 1; + std.mem.copy(u8, cpu.v[0..upper], mem[cpu.i..(cpu.i + upper)]); + }, + } +} diff --git a/src/display.zig b/src/display.zig new file mode 100644 index 0000000..0dd224e --- /dev/null +++ b/src/display.zig @@ -0,0 +1,46 @@ +pub const WIDTH: usize = 64; +pub const HEIGHT: usize = 32; +pub const PIXELBUF_LEN = WIDTH * HEIGHT * @sizeOf(u32); +pub const SCALE = 10; + +pub const Display = struct { + buf: [WIDTH * HEIGHT]u8, + + pub fn new() Display { + return .{ .buf = [_]u8{0x00} ** (HEIGHT * WIDTH) }; + } +}; + +pub const Point = struct { + x: u16, + y: u16, +}; + +pub fn drawSprite(disp: *Display, pos: *const Point, data: *const []u8) bool { + var set_vf: bool = false; + + for (data.*) |byte, y_offset| { + var offset_count: u8 = 0; + + while (offset_count < 8) : (offset_count += 1) { + const x_bit_offset = @intCast(u3, offset_count); + const x = @intCast(u8, pos.x + (7 - x_bit_offset)); + const y = @intCast(u8, pos.y + y_offset); + + const temp = (byte >> x_bit_offset) & 0x01; + const bit = @intCast(u1, temp); + + const i = WIDTH * y + x; + + if (i >= disp.buf.len) break; + + if (bit == 0x1 and disp.buf[i] == 0x01) { + set_vf = true; + } + + disp.buf[i] ^= bit; + } + } + + return set_vf; +} diff --git a/src/instruction.zig b/src/instruction.zig new file mode 100644 index 0000000..7f7ed22 --- /dev/null +++ b/src/instruction.zig @@ -0,0 +1,36 @@ +pub const Instruction = union(enum) { + CLS: void, // 0x00E0 + RET: void, // 0x00EE + JP: u12, // 0x1nnn + CALL: u12, // 0x2nnn + SE_Vx: struct { x: u4, kk: u8 }, // 0x3xkk + SNE_Vx: struct { x: u4, kk: u8 }, // 0x4xkk + SE_Vx_Vy: struct { x: u4, y: u4 }, // 0x5xy0 + LD_Vx: struct { x: u4, kk: u8 }, // 0x6xkk + ADD_Vx: struct { x: u4, kk: u8 }, // 0x7xkk + LD_Vx_Vy: struct { x: u4, y: u4 }, // 0x8xy0 + OR: struct { x: u4, y: u4 }, // 0x8xy1 + AND: struct { x: u4, y: u4 }, // 0x8xy2 + XOR: struct { x: u4, y: u4 }, // 0x8xy3 + ADD_Vx_Vy: struct { x: u4, y: u4 }, // 0x8xy4 + SUB: struct { x: u4, y: u4 }, // 0x8xy5 + SHR: struct { x: u4, y: u4 }, // 0x8xy6 + SUBN: struct { x: u4, y: u4 }, // 0x8xy7 + SHL: struct { x: u4, y: u4 }, // 0x8xyE + SNE_Vx_Vy: struct { x: u4, y: u4 }, // 0x9xy0 + LD_I: u12, // 0xAnnn + JP_V0: u12, // 0xBnnn + RND: struct { x: u4, kk: u8 }, // 0xCxkk + DRW: struct { x: u4, y: u4, n: u4 }, // 0xDxyn + SKP: u4, // 0xEx9E + SKNP: u4, // 0xExA1 + LD_Vx_DT: u4, // 0xFx07 + LD_Vx_K: u4, // 0xFx0A + LD_DT_Vx: u4, // 0xFx15 + LD_ST_Vx: u4, // 0xFx18 + ADD_I_Vx: u4, // 0xFx1E + LD_F_Vx: u4, // 0xFx29 + LD_B_Vx: u4, // 0xFx33 + LD_IPTR_Vx: u4, // 0xFx55 + LD_Vx_IPTR: u4, // 0xFx65 +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..2d2fed6 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,89 @@ +const std = @import("std"); +const SDL = @import("sdl2"); +const sched = @import("scheduler.zig"); +const emu = @import("chip8.zig"); +const display = @import("display.zig"); + +const Timer = std.time.Timer; +const Chip8 = emu.Chip8; +const Display = display.Display; +const Scheduler = sched.Scheduler; + +const OPS_PER_HZ = sched.OPS_PER_HZ; +const CHIP8_WIDTH = display.WIDTH; +const CHIP8_HEIGHT = display.HEIGHT; +const WINDOW_WIDTH = CHIP8_WIDTH * display.SCALE; +const WINDOW_HEIGHT = CHIP8_HEIGHT * display.SCALE; +const PIXELBUF_LEN = display.PIXELBUF_LEN; + +pub fn main() anyerror!void { + const heap = std.heap.page_allocator; + var arg_it = std.process.args(); + + // Skip process name + _ = arg_it.skip(); + + const os_string = try arg_it.next(heap) orelse { + std.debug.warn("Expected first argument to be path to CHIP-8 rom\n", .{}); + return; + }; + const rom_path = try std.fs.path.resolve(heap, &[_][]const u8{os_string}); + + if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) { + sdlPanic(); + } + defer SDL.SDL_Quit(); + + const window = SDL.SDL_CreateWindow( + "Zig CHIP-8 Emulator", + SDL.SDL_WINDOWPOS_CENTERED, + SDL.SDL_WINDOWPOS_CENTERED, + WINDOW_WIDTH, + WINDOW_HEIGHT, + SDL.SDL_WINDOW_SHOWN, + ) orelse sdlPanic(); + defer _ = SDL.SDL_DestroyWindow(window); + + const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED) orelse sdlPanic(); + defer _ = SDL.SDL_DestroyRenderer(renderer); + + const texture = SDL.SDL_CreateTexture(renderer, SDL.SDL_PIXELFORMAT_RGBA8888, SDL.SDL_TEXTUREACCESS_STATIC, 64, 32); + defer SDL.SDL_DestroyTexture(texture); + + var pixels: [PIXELBUF_LEN]u8 = [_]u8{0x00} ** PIXELBUF_LEN; + + var scheduler = Scheduler.new(heap, &pixels); + var chip8 = try Chip8.fromFile(heap, &scheduler, rom_path); + + const frametime = 1_000_000_000 / OPS_PER_HZ; + var timer = Timer.start() catch unreachable; + + emuloop: while (true) { + var e: SDL.SDL_Event = undefined; + while (SDL.SDL_PollEvent(&e) != 0) { + if (e.type == SDL.SDL_QUIT) { + break :emuloop; + } + } + + _ = SDL.SDL_UpdateTexture(texture, null, &pixels, CHIP8_WIDTH * @sizeOf(u32)); + _ = SDL.SDL_RenderCopy(renderer, texture, null, null); + SDL.SDL_RenderPresent(renderer); + + sched.run_until(&scheduler, &chip8, scheduler.now() + OPS_PER_HZ); + + while (true) { + const diff = timer.read(); + + if (diff >= frametime) { + timer.reset(); + break; + } + } + } +} + +fn sdlPanic() noreturn { + const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; + @panic(std.mem.sliceTo(str, 0)); +} diff --git a/src/scheduler.zig b/src/scheduler.zig new file mode 100644 index 0000000..794c2ac --- /dev/null +++ b/src/scheduler.zig @@ -0,0 +1,122 @@ +const std = @import("std"); +const disp = @import("display.zig"); +const Chip8 = @import("chip8.zig").Chip8; +const Order = std.math.Order; +const Allocator = std.mem.Allocator; +const PriorityQueue = std.PriorityQueue; +const Display = disp.Display; + +pub const OPS_PER_HZ = 500; + +const PIXELBUF_LEN = disp.PIXELBUF_LEN; + +pub const Scheduler = struct { + pixels_ptr: *[PIXELBUF_LEN]u8, + timestamp: u64, + queue: PriorityQueue(Event), + + pub fn new(alloc: *Allocator, pixels_ptr: *[PIXELBUF_LEN]u8) Scheduler { + var queue = Scheduler{ + .timestamp = 0, + .pixels_ptr = pixels_ptr, + .queue = PriorityQueue(Event).init(alloc, less_than), + }; + + queue.pushEvent(.{ + .kind = EventKind.HeatDeath, + .timestamp = std.math.maxInt(u64), + }); + + return queue; + } + + pub fn now(self: *const Scheduler) u64 { + return self.timestamp; + } + + pub fn push(self: *Scheduler, kind: EventKind, when: u64) void { + self.pushEvent(Event{ .kind = kind, .timestamp = when }); + } + + pub fn ReplaceOrPush(self: *Scheduler, kind: EventKind, when: u64) void { + if (self.find(kind)) |event| { + self.queue.update(event, Event{ .kind = kind, .timestamp = when }) catch unreachable; + } else { + self.push(kind, when); + } + } + + fn pushEvent(self: *Scheduler, event: Event) void { + self.queue.add(event) catch unreachable; + } + + pub fn find(self: *Scheduler, kind: EventKind) ?Event { + var it = self.queue.iterator(); + + while (it.next()) |e| { + if (e.kind == kind) return e; + } + + return null; + } + + fn handleEvent(self: *Scheduler, chip8: *Chip8, event: Event) void { + switch (event.kind) { + .HeatDeath => { + std.debug.panic("Reached u64 overflow somehow", .{}); + }, + .RequestRedraw => draw(&chip8.disp, self.pixels_ptr), + .DelayTimer => { + std.log.debug("{} | Delay Timer Expire", .{self.timestamp}); + }, + .SoundTimer => { + std.log.debug("{} | Sound Timer Expire", .{self.timestamp}); + }, + } + } +}; + +const Event = struct { + kind: EventKind, + timestamp: u64, +}; + +pub const EventKind = enum { + HeatDeath, + RequestRedraw, + SoundTimer, + DelayTimer, +}; + +fn less_than(a: Event, b: Event) Order { + return std.math.order(a.timestamp, b.timestamp); +} + +pub fn run_until(sched: *Scheduler, chip8: *Chip8, timestamp: u64) void { + while (sched.timestamp <= timestamp) { + sched.timestamp += chip8.step(); + + const should_handle = if (sched.queue.peek()) |e| sched.timestamp >= e.timestamp else false; + + if (should_handle) { + const event = sched.queue.remove(); + sched.handleEvent(chip8, event); + } + } +} + +fn draw(display: *const Display, buf: *[PIXELBUF_LEN]u8) void { + const WHITE = [_]u8{ 0xFF, 0xFF, 0xFF, 0xFF }; + const BLACK = [_]u8{ 0xFF, 0x00, 0x00, 0x00 }; + var i: usize = 0; + + while (i < display.buf.len) : (i += 1) { + var px_i = i * 4; + + if (display.buf[i] == 0x01) { + std.mem.copy(u8, buf[px_i..(px_i + 4)], &WHITE); + } else { + std.mem.copy(u8, buf[px_i..(px_i + 4)], &BLACK); + } + } +} diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..896d928 --- /dev/null +++ b/src/util.zig @@ -0,0 +1,24 @@ +const std = @import("std"); +const expect = std.testing.expect; + +pub fn calc12bit(nib2: u4, nib3: u4, nib4: u4) u12 { + return @as(u12, nib2) << 8 | @as(u12, nib3) << 4 | @as(u12, nib4); +} + +pub fn calc8bit(nib_a: u4, nib_b: u4) u8 { + return @as(u8, nib_a) << 4 | @as(u8, nib_b); +} + +test "calc12bit works" { + const left: u12 = 0xABC; + const right: u12 = calc12bit(0xA, 0xB, 0xC); + + expect(left == right); +} + +test "calc8bit works" { + const left: u8 = 0xAB; + const right: u12 = calc8bit(0xA, 0xB); + + expect(left == right); +} diff --git a/third_party/SDL.zig b/third_party/SDL.zig new file mode 160000 index 0000000..5118ef9 --- /dev/null +++ b/third_party/SDL.zig @@ -0,0 +1 @@ +Subproject commit 5118ef94e93d35cbf7888cb71472fa1c18fadac7