Initial Commit
This commit is contained in:
commit
19077200cd
|
@ -0,0 +1,4 @@
|
||||||
|
/zig-cache
|
||||||
|
/zig-out
|
||||||
|
/bin
|
||||||
|
/.vscode
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "third_party/SDL.zig"]
|
||||||
|
path = third_party/SDL.zig
|
||||||
|
url = https://github.com/MasterQ32/SDL.zig
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"augusterame.zls-vscode",
|
||||||
|
"tiehuis.zig",
|
||||||
|
"vadimcn.vscode-lldb"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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)]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
};
|
|
@ -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));
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 5118ef94e93d35cbf7888cb71472fa1c18fadac7
|
Loading…
Reference in New Issue