commit 3652df31f431c345b9a2dde8374ebd87257720fb Author: Marto Date: Fri Apr 25 21:06:33 2025 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac74b04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/zig-out/ +/.zig-cache/ +/.idea/ \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..133df7b --- /dev/null +++ b/build.zig @@ -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); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..2ee16ee --- /dev/null +++ b/build.zig.zon @@ -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 `, 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", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..8b5a929 --- /dev/null +++ b/src/main.zig @@ -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]); +}