initial commit
This commit is contained in:
commit
3652df31f4
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/zig-out/
|
||||||
|
/.zig-cache/
|
||||||
|
/.idea/
|
128
build.zig
Normal file
128
build.zig
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Although this function looks imperative, note that its job is to
|
||||||
|
// declaratively construct a build graph that will be executed by an external
|
||||||
|
// runner.
|
||||||
|
pub fn build(b: *std.Build) 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 optimization options allow the person running `zig build` to select
|
||||||
|
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||||
|
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const raylib_dep = b.dependency("raylib_zig", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.shared = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const raylib = raylib_dep.module("raylib");
|
||||||
|
const raylib_artifact = raylib_dep.artifact("raylib");
|
||||||
|
|
||||||
|
// This creates a "module", which represents a collection of source files alongside
|
||||||
|
// some compilation options, such as optimization mode and linked system libraries.
|
||||||
|
// Every executable or library we compile will be based on one or more modules.
|
||||||
|
// const lib_mod = b.createModule(.{
|
||||||
|
// // `root_source_file` is the Zig "entry point" of the module. If a module
|
||||||
|
// // only contains e.g. external object files, you can make this `null`.
|
||||||
|
// // In this case the main source file is merely a path, however, in more
|
||||||
|
// // complicated build scripts, this could be a generated file.
|
||||||
|
// .root_source_file = b.path("src/root.zig"),
|
||||||
|
// .target = target,
|
||||||
|
// .optimize = optimize,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// We will also create a module for our other entry point, 'main.zig'.
|
||||||
|
const exe_mod = b.createModule(.{
|
||||||
|
// `root_source_file` is the Zig "entry point" of the module. If a module
|
||||||
|
// only contains e.g. external object files, you can make this `null`.
|
||||||
|
// In this case the main source file is merely a path, however, in more
|
||||||
|
// complicated build scripts, this could be a generated file.
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modules can depend on one another using the `std.Build.Module.addImport` function.
|
||||||
|
// This is what allows Zig source code to use `@import("foo")` where 'foo' is not a
|
||||||
|
// file path. In this case, we set up `exe_mod` to import `lib_mod`.
|
||||||
|
// exe_mod.addImport("chip8_emulator_lib", lib_mod);
|
||||||
|
|
||||||
|
// Now, we will create a static library based on the module we created above.
|
||||||
|
// This creates a `std.Build.Step.Compile`, which is the build step responsible
|
||||||
|
// for actually invoking the compiler.
|
||||||
|
// const lib = b.addLibrary(.{
|
||||||
|
// .linkage = .static,
|
||||||
|
// .name = "chip8_emulator",
|
||||||
|
// .root_module = lib_mod,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// This declares intent for the library to be installed into the standard
|
||||||
|
// location when the user invokes the "install" step (the default step when
|
||||||
|
// running `zig build`).
|
||||||
|
// b.installArtifact(lib);
|
||||||
|
|
||||||
|
// This creates another `std.Build.Step.Compile`, but this one builds an executable
|
||||||
|
// rather than a static library.
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "chip8_emulator",
|
||||||
|
.root_module = exe_mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
// This declares intent for the executable to be installed into the
|
||||||
|
// standard location when the user invokes the "install" step (the default
|
||||||
|
// step when running `zig build`).
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
exe.linkLibrary(raylib_artifact);
|
||||||
|
exe.root_module.addImport("raylib", raylib);
|
||||||
|
|
||||||
|
// This *creates* a Run step in the build graph, to be executed when another
|
||||||
|
// step is evaluated that depends on it. The next line below will establish
|
||||||
|
// such a dependency.
|
||||||
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
|
|
||||||
|
// By making the run step depend on the install step, it will be run from the
|
||||||
|
// installation directory rather than directly from within the cache directory.
|
||||||
|
// This is not necessary, however, if the application depends on other installed
|
||||||
|
// files, this ensures they will be present and in the expected location.
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
// This allows the user to pass arguments to the application in the build
|
||||||
|
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||||
|
// and can be selected like this: `zig build run`
|
||||||
|
// This will evaluate the `run` step rather than the default, which is "install".
|
||||||
|
const run_step = b.step("run", "Run the app");
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
// Creates a step for unit testing. This only builds the test executable
|
||||||
|
// but does not run it.
|
||||||
|
// const lib_unit_tests = b.addTest(.{
|
||||||
|
// .root_module = lib_mod,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||||
|
|
||||||
|
const exe_unit_tests = b.addTest(.{
|
||||||
|
.root_module = exe_mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
||||||
|
|
||||||
|
// Similar to creating the run step earlier, this exposes a `test` step to
|
||||||
|
// the `zig build --help` menu, providing a way for the user to request
|
||||||
|
// running the unit tests.
|
||||||
|
const test_step = b.step("test", "Run unit tests");
|
||||||
|
// test_step.dependOn(&run_lib_unit_tests.step);
|
||||||
|
test_step.dependOn(&run_exe_unit_tests.step);
|
||||||
|
}
|
52
build.zig.zon
Normal file
52
build.zig.zon
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
.{
|
||||||
|
// This is the default name used by packages depending on this one. For
|
||||||
|
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||||
|
// as the key in the `dependencies` table. Although the user can choose a
|
||||||
|
// different name, most users will stick with this provided value.
|
||||||
|
//
|
||||||
|
// It is redundant to include "zig" in this name because it is already
|
||||||
|
// within the Zig package namespace.
|
||||||
|
.name = .chip8_emulator,
|
||||||
|
|
||||||
|
// This is a [Semantic Version](https://semver.org/).
|
||||||
|
// In a future version of Zig it will be used for package deduplication.
|
||||||
|
.version = "0.0.0",
|
||||||
|
|
||||||
|
// Together with name, this represents a globally unique package
|
||||||
|
// identifier. This field is generated by the Zig toolchain when the
|
||||||
|
// package is first created, and then *never changes*. This allows
|
||||||
|
// unambiguous detection of one package being an updated version of
|
||||||
|
// another.
|
||||||
|
//
|
||||||
|
// When forking a Zig project, this id should be regenerated (delete the
|
||||||
|
// field and run `zig build`) if the upstream project is still maintained.
|
||||||
|
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||||
|
// original project's identity. Thus it is recommended to leave the comment
|
||||||
|
// on the following line intact, so that it shows up in code reviews that
|
||||||
|
// modify the field.
|
||||||
|
.fingerprint = 0x11aa205190765268, // Changing this has security and trust implications.
|
||||||
|
|
||||||
|
// Tracks the earliest Zig version that the package considers to be a
|
||||||
|
// supported use case.
|
||||||
|
.minimum_zig_version = "0.14.0",
|
||||||
|
|
||||||
|
// This field is optional.
|
||||||
|
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||||
|
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||||
|
// Once all dependencies are fetched, `zig build` no longer requires
|
||||||
|
// internet connectivity.
|
||||||
|
.dependencies = .{
|
||||||
|
.raylib_zig = .{
|
||||||
|
.url = "git+https://github.com/Not-Nik/raylib-zig?ref=devel#0de5f8aed0565f2dbd8bc6851499c85df9e73534",
|
||||||
|
.hash = "raylib_zig-5.6.0-dev-KE8REGMrBQCs5X69dptNzjw9Z7MYM1fgdaKrnuKf8zyr",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
// For example...
|
||||||
|
//"LICENSE",
|
||||||
|
//"README.md",
|
||||||
|
},
|
||||||
|
}
|
442
src/main.zig
Normal file
442
src/main.zig
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const rl = @import("raylib");
|
||||||
|
|
||||||
|
const WIDTH = 64;
|
||||||
|
const HEIGHT = 32;
|
||||||
|
const SCALE = 15;
|
||||||
|
const FREQUENCY = 30;
|
||||||
|
|
||||||
|
const MEMORY_SIZE = 4096;
|
||||||
|
const FONT_START = 0x050;
|
||||||
|
const PROGRAM_START = 0x200;
|
||||||
|
const PROGRAM_ALLOC = MEMORY_SIZE - PROGRAM_START - 1;
|
||||||
|
|
||||||
|
const Emulator = struct {
|
||||||
|
screen: [HEIGHT][WIDTH]u8 = clearScreen(),
|
||||||
|
memory: [MEMORY_SIZE]u8 = std.mem.zeroes([MEMORY_SIZE]u8),
|
||||||
|
stack: std.ArrayList(u16),
|
||||||
|
v: [16]u8,
|
||||||
|
i: u16,
|
||||||
|
key_pressed: u5, // 4 bits for tracking the key pressed, 1 bit for tracking if the key IS pressed
|
||||||
|
pc: u16,
|
||||||
|
delay_timer: u8,
|
||||||
|
sound_timer: u8,
|
||||||
|
legacy: bool,
|
||||||
|
legacy_memory: bool,
|
||||||
|
add_index_exception: bool,
|
||||||
|
prng: std.Random.Xoshiro256,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) !Emulator {
|
||||||
|
return Emulator{ .screen = clearScreen(), .memory = try initMemory(allocator), .stack = std.ArrayList(u16).init(allocator), .v = std.mem.zeroes([16]u8), .i = 0, .key_pressed = 0, .pc = PROGRAM_START, .delay_timer = FREQUENCY, .sound_timer = 0, .legacy = true, .legacy_memory = false, .add_index_exception = false, .prng = std.Random.DefaultPrng.init(blk: {
|
||||||
|
var seed: u64 = undefined;
|
||||||
|
try std.posix.getrandom(std.mem.asBytes(&seed));
|
||||||
|
break :blk seed;
|
||||||
|
}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(self: *Emulator) void {
|
||||||
|
self.stack.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instructionCycle(self: *Emulator) !void {
|
||||||
|
const instruction: u16 = @as(u16, self.memory[self.pc]) << 8 | @as(u16, self.memory[self.pc + 1]);
|
||||||
|
var bd: [10]u8 = undefined;
|
||||||
|
_ = try std.fmt.bufPrint(&bd, "{X:0>4}", .{instruction});
|
||||||
|
std.debug.print("Instruction: {s}\n\n", .{bd});
|
||||||
|
const first: u8 = @truncate(instruction >> 12);
|
||||||
|
|
||||||
|
const x: u8 = @truncate((instruction >> 8) & 0xF);
|
||||||
|
const y: u8 = @truncate((instruction >> 4) & 0xF);
|
||||||
|
const n: u8 = @truncate(instruction & 0xF);
|
||||||
|
const nn: u8 = @truncate(instruction & 0xFF);
|
||||||
|
const nnn: u16 = instruction & 0xFFF;
|
||||||
|
|
||||||
|
switch (first) {
|
||||||
|
0x0 => {
|
||||||
|
if (instruction == 0x00E0) {
|
||||||
|
// clear screen
|
||||||
|
self.screen = clearScreen();
|
||||||
|
} else if (instruction == 0x00EE) {
|
||||||
|
// pop the pc from stack, unless the binary is shit there should be no reason to unwrap optional
|
||||||
|
self.pc = self.stack.pop().?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x1 => {
|
||||||
|
// jump to specified address (if old address is identical to new one, return)
|
||||||
|
if (self.pc == nnn) return;
|
||||||
|
self.pc = nnn;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
0x2 => {
|
||||||
|
// pushes current pc onto stack and sets nnn as pc
|
||||||
|
try self.stack.append(self.pc);
|
||||||
|
self.pc = nnn;
|
||||||
|
},
|
||||||
|
0x3 => {
|
||||||
|
// skips an instruction ahead if register x equals to nn
|
||||||
|
if (self.v[x] == nn)
|
||||||
|
self.nextInstruction();
|
||||||
|
},
|
||||||
|
0x4 => {
|
||||||
|
// skips an instruction ahead if register x does not equal to nn
|
||||||
|
if (self.v[x] != nn)
|
||||||
|
self.nextInstruction();
|
||||||
|
},
|
||||||
|
0x5 => {
|
||||||
|
// skips an instruction if register x equals to register y
|
||||||
|
if (n == 0x0) { // we check only last 4 digits are zero
|
||||||
|
if (self.v[x] == self.v[y])
|
||||||
|
self.nextInstruction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x6 => {
|
||||||
|
// sets register x to nn
|
||||||
|
self.v[x] = nn;
|
||||||
|
},
|
||||||
|
0x7 => {
|
||||||
|
// adds nn to register x, doesn't care about overflow
|
||||||
|
const ov = @addWithOverflow(self.v[x], nn);
|
||||||
|
self.v[x] = ov[0];
|
||||||
|
},
|
||||||
|
0x8 => {
|
||||||
|
switch (n) {
|
||||||
|
0x0 => {
|
||||||
|
self.v[x] = self.v[y];
|
||||||
|
},
|
||||||
|
0x1 => {
|
||||||
|
self.v[x] |= self.v[y];
|
||||||
|
},
|
||||||
|
0x2 => {
|
||||||
|
self.v[x] &= self.v[y];
|
||||||
|
},
|
||||||
|
0x3 => {
|
||||||
|
self.v[x] ^= self.v[y];
|
||||||
|
},
|
||||||
|
0x4 => {
|
||||||
|
// adds and stores overflow bit in 0xF register
|
||||||
|
const ov = @addWithOverflow(self.v[x], self.v[y]);
|
||||||
|
self.v[0xF] = ov[1];
|
||||||
|
self.v[x] = ov[0];
|
||||||
|
},
|
||||||
|
0x5 => {
|
||||||
|
// subtracts and inverts the "usual" overflow
|
||||||
|
const ov = @subWithOverflow(self.v[x], self.v[y]);
|
||||||
|
self.v[x] = ov[0];
|
||||||
|
self.v[0xF] = ov[1] ^ 1;
|
||||||
|
},
|
||||||
|
0x6 => {
|
||||||
|
// shifting v[x] right and saving the dropped bit into V[0xF]
|
||||||
|
// if (self.legacy) {
|
||||||
|
// self.v[x] = self.v[y];
|
||||||
|
// }
|
||||||
|
|
||||||
|
self.v[0xF] = self.v[x] & 0x1;
|
||||||
|
self.v[x] >>= 1;
|
||||||
|
},
|
||||||
|
0x7 => {
|
||||||
|
// subtracts and inverts the "usual" overflow
|
||||||
|
const ov = @subWithOverflow(self.v[y], self.v[x]);
|
||||||
|
self.v[x] = ov[0];
|
||||||
|
self.v[0xF] = ov[1] ^ 1;
|
||||||
|
},
|
||||||
|
0xE => {
|
||||||
|
// shifting v[x] left and saving the dropped bit into V[0xF]
|
||||||
|
// if (self.legacy) {
|
||||||
|
// self.v[x] = self.v[y];
|
||||||
|
// }
|
||||||
|
|
||||||
|
self.v[0xF] = (self.v[x] & 0x80) >> 7;
|
||||||
|
self.v[x] <<= 1;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x9 => {
|
||||||
|
// skips an instruction if register x equals to register y
|
||||||
|
if (n == 0x0) { // we check only last 4 digits are zero
|
||||||
|
if (self.v[x] != self.v[y])
|
||||||
|
self.nextInstruction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0xA => {
|
||||||
|
// sets index register to nnn
|
||||||
|
self.i = nnn;
|
||||||
|
},
|
||||||
|
0xB => {
|
||||||
|
// jumps to different address depending on interpreter design, legacy is default
|
||||||
|
const addr: u16 = if (self.legacy) 0x0 else x;
|
||||||
|
self.pc = nnn + addr;
|
||||||
|
|
||||||
|
return; // we do not want any further jumps
|
||||||
|
},
|
||||||
|
0xC => {
|
||||||
|
// gen random number AND with NN => v[x]
|
||||||
|
const rand = self.prng.random();
|
||||||
|
self.v[x] = rand.int(u8) & nn;
|
||||||
|
},
|
||||||
|
0xD => {
|
||||||
|
// draws the screen matrix
|
||||||
|
var x_coord = self.v[x] % WIDTH;
|
||||||
|
var y_coord = self.v[y] % HEIGHT;
|
||||||
|
|
||||||
|
self.v[0xF] = 0;
|
||||||
|
|
||||||
|
for (0..n) |it| {
|
||||||
|
const byte = self.memory[self.i + it];
|
||||||
|
|
||||||
|
var b: i4 = 7;
|
||||||
|
while (b >= 0) : (b -= 1) {
|
||||||
|
const bit_pos: u3 = @intCast(b); // loop condition ensures the number never reaches negative
|
||||||
|
const bit = (byte & (@as(u8, 1) << bit_pos));
|
||||||
|
if (bit != 0) {
|
||||||
|
const old_pixel = self.screen[y_coord][x_coord];
|
||||||
|
self.screen[y_coord][x_coord] ^= 1;
|
||||||
|
if (old_pixel != 0 and self.screen[y_coord][x_coord] == 0)
|
||||||
|
self.v[0xF] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
x_coord += 1;
|
||||||
|
if (x_coord >= WIDTH) break;
|
||||||
|
}
|
||||||
|
x_coord = self.v[x] % WIDTH;
|
||||||
|
y_coord += 1;
|
||||||
|
if (y_coord >= HEIGHT) break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0xE => {
|
||||||
|
// if key is pressed (5th bit)
|
||||||
|
if (self.key_pressed & 0x10 != 0) {
|
||||||
|
const key: u4 = @truncate(self.key_pressed & 0xF);
|
||||||
|
// we check for instruction and if the v[x] matches last 4 bits (1xxxx)
|
||||||
|
if (nn == 0x9E and self.v[x] == key) {
|
||||||
|
self.nextInstruction();
|
||||||
|
} else if (nn == 0xA1 and self.v[x] != key) {
|
||||||
|
self.nextInstruction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0xF => {
|
||||||
|
switch (nn) {
|
||||||
|
// set delay
|
||||||
|
0x07 => self.v[x] = self.delay_timer,
|
||||||
|
0x15 => {
|
||||||
|
// set delay, adjust fps
|
||||||
|
self.delay_timer = self.v[x];
|
||||||
|
rl.setTargetFPS(self.delay_timer);
|
||||||
|
},
|
||||||
|
// set sound timer
|
||||||
|
0x18 => self.sound_timer = self.v[x],
|
||||||
|
0x1E => {
|
||||||
|
// add v[x] to index register
|
||||||
|
const ov = @addWithOverflow(self.i, self.v[x]);
|
||||||
|
self.i = ov[0];
|
||||||
|
if (self.add_index_exception)
|
||||||
|
self.v[0xF] = ov[1];
|
||||||
|
},
|
||||||
|
0x0A => {
|
||||||
|
// this instructions blocks the flow of the program until a key is pressed
|
||||||
|
if (self.key_pressed & 0x10 == 0) return;
|
||||||
|
self.v[x] = self.key_pressed & 0xF;
|
||||||
|
},
|
||||||
|
0x29 => {
|
||||||
|
// point to the starting font location of hexadecimal character
|
||||||
|
self.i = FONT_START + 5 * self.v[x];
|
||||||
|
},
|
||||||
|
0x33 => {
|
||||||
|
// split instruction into digits and save them at apppropriate address
|
||||||
|
var num = self.v[x];
|
||||||
|
|
||||||
|
var it: i3 = 2;
|
||||||
|
while (it >= 0) : (it -= 1) {
|
||||||
|
self.memory[self.i + @as(u16, @intCast(it))] = num % 10;
|
||||||
|
num /= 10;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x55 => {
|
||||||
|
// storing registers into memory
|
||||||
|
var it: usize = 0;
|
||||||
|
while (it <= x) : (it += 1) {
|
||||||
|
self.memory[self.i + it] = self.v[it];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.legacy_memory)
|
||||||
|
self.i = @intCast(it);
|
||||||
|
},
|
||||||
|
0x65 => {
|
||||||
|
// loading registers from the memory
|
||||||
|
var it: usize = 0;
|
||||||
|
while (it <= x) : (it += 1) {
|
||||||
|
self.v[it] = self.memory[self.i + it];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.legacy_memory)
|
||||||
|
self.i = @intCast(it);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nextInstruction();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nextInstruction(self: *Emulator) void {
|
||||||
|
self.pc += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getInput(self: *Emulator) void {
|
||||||
|
var key: u4 = 0x0;
|
||||||
|
switch (rl.getKeyPressed()) {
|
||||||
|
.one => key = 0x1,
|
||||||
|
.two => key = 0x2,
|
||||||
|
.three => key = 0x3,
|
||||||
|
.four => key = 0xC,
|
||||||
|
|
||||||
|
.q => key = 0x4,
|
||||||
|
.w => key = 0x5,
|
||||||
|
.e => key = 0x6,
|
||||||
|
.r => key = 0xD,
|
||||||
|
|
||||||
|
.a => key = 0x7,
|
||||||
|
.s => key = 0x8,
|
||||||
|
.d => key = 0x9,
|
||||||
|
.f => key = 0xE,
|
||||||
|
|
||||||
|
.z => key = 0xA,
|
||||||
|
.x => key = 0x0,
|
||||||
|
.c => key = 0xB,
|
||||||
|
.v => key = 0xF,
|
||||||
|
|
||||||
|
else => {
|
||||||
|
self.key_pressed = 0x0;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.key_pressed = 0x10 | @as(u5, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initMemory(allocator: std.mem.Allocator) ![MEMORY_SIZE]u8 {
|
||||||
|
var mem = std.mem.zeroes([MEMORY_SIZE]u8);
|
||||||
|
|
||||||
|
const font_data = [_]u8{
|
||||||
|
0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
|
||||||
|
0x20, 0x60, 0x20, 0x20, 0x70, // 1
|
||||||
|
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
|
||||||
|
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
|
||||||
|
0x90, 0x90, 0xF0, 0x10, 0x10, // 4
|
||||||
|
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
|
||||||
|
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
|
||||||
|
0xF0, 0x10, 0x20, 0x40, 0x40, // 7
|
||||||
|
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
|
||||||
|
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
|
||||||
|
0xF0, 0x90, 0xF0, 0x90, 0x90, // A
|
||||||
|
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
|
||||||
|
0xF0, 0x80, 0x80, 0x80, 0xF0, // C
|
||||||
|
0xE0, 0x90, 0x90, 0x90, 0xE0, // D
|
||||||
|
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
|
||||||
|
0xF0, 0x80, 0xF0, 0x80, 0x80, // F
|
||||||
|
};
|
||||||
|
|
||||||
|
// load font data into the memory
|
||||||
|
for (font_data, FONT_START..) |value, i| {
|
||||||
|
mem[i] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load program into the memory
|
||||||
|
const instructions = try readFile(allocator);
|
||||||
|
|
||||||
|
// load font data into the memory
|
||||||
|
for (instructions, PROGRAM_START..) |value, i| {
|
||||||
|
mem[i] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(self: *Emulator) void {
|
||||||
|
rl.beginDrawing();
|
||||||
|
defer rl.endDrawing();
|
||||||
|
|
||||||
|
rl.clearBackground(.black);
|
||||||
|
for (0..self.screen.len) |y| {
|
||||||
|
for (0..self.screen[y].len) |x| {
|
||||||
|
if (self.screen[y][x] == 0) continue;
|
||||||
|
|
||||||
|
rl.drawRectangle(@as(i32, @intCast(x)) * SCALE, @as(i32, @intCast(y)) * SCALE, SCALE, SCALE, .white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_instructions(self: *Emulator) void {
|
||||||
|
var i: usize = PROGRAM_START;
|
||||||
|
while (i < self.memory.len) : (i += 2) {
|
||||||
|
if (i != 0 and i % 8 == 0) {
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
}
|
||||||
|
if (self.memory[i] == 0xAA and self.memory[i + 1] == 0xAA) break;
|
||||||
|
std.debug.print("{X:0>2}{X:0>2} ", .{ self.memory[i], self.memory[i + 1] });
|
||||||
|
}
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clearScreen() [HEIGHT][WIDTH]u8 {
|
||||||
|
return std.mem.zeroes([HEIGHT][WIDTH]u8);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const allocator = arena.allocator();
|
||||||
|
|
||||||
|
var chip8 = try Emulator.init(allocator);
|
||||||
|
defer chip8.destroy();
|
||||||
|
|
||||||
|
chip8.print_instructions();
|
||||||
|
|
||||||
|
rl.initWindow(WIDTH * SCALE, HEIGHT * SCALE, "CHIP-8 Emulator");
|
||||||
|
defer rl.closeWindow();
|
||||||
|
|
||||||
|
rl.setTargetFPS(chip8.delay_timer);
|
||||||
|
var cycle_count: usize = 0;
|
||||||
|
|
||||||
|
while (!rl.windowShouldClose()) {
|
||||||
|
cycle_count += 1;
|
||||||
|
std.debug.print("Cycle count: {d}\n", .{cycle_count});
|
||||||
|
chip8.getInput();
|
||||||
|
try chip8.instructionCycle();
|
||||||
|
chip8.draw();
|
||||||
|
// todo sound timer decrease i guess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readFile(allocator: std.mem.Allocator) ![PROGRAM_ALLOC]u8 {
|
||||||
|
const fs = std.fs;
|
||||||
|
const filename = try getArgs(allocator);
|
||||||
|
defer allocator.free(filename);
|
||||||
|
|
||||||
|
var buffer: [PROGRAM_ALLOC]u8 = undefined;
|
||||||
|
|
||||||
|
const file = try fs.cwd().openFile(filename, .{ .mode = .read_only });
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const bytes_read = try file.readAll(&buffer);
|
||||||
|
|
||||||
|
if (PROGRAM_START + bytes_read > MEMORY_SIZE) {
|
||||||
|
std.debug.print("[ERROR] Instruction set size exceeds memory size ({d})!", .{MEMORY_SIZE});
|
||||||
|
return error.MemorySizeExceeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getArgs(allocator: std.mem.Allocator) ![]const u8 {
|
||||||
|
const args = try std.process.argsAlloc(allocator);
|
||||||
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
|
if (args.len < 2) return error.InsufficientArgs;
|
||||||
|
|
||||||
|
return allocator.dupe(u8, args[1]);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user