commit e3fee7e08292ed00c0aaa25d88e2a08123de28a8 Author: sloven-c Date: Tue Nov 18 07:43:19 2025 +0100 Initial commit diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..8290f37 --- /dev/null +++ b/build.zig @@ -0,0 +1,131 @@ +const std = @import("std"); + +// Although this function looks imperative, it does not perform the build +// directly and instead it mutates the build graph (`b`) that will be then +// executed by an external runner. The functions in `std.Build` implement a DSL +// for defining build steps and express dependencies between them, allowing the +// build runner to parallelize the build automatically (and the cache system to +// know when a step doesn't need to be re-run). +pub fn build(b: *std.Build) void { + // Standard target options allow 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(.{}); + // It's also possible to define more custom flags to toggle optional features + // of this build script using `b.option()`. All defined flags (including + // target and optimize options) will be listed when running `zig build --help` + // in this directory. + + // Here we define an executable. An executable needs to have a root module + // which needs to expose a `main` function. While we could add a main function + // to the module defined above, it's sometimes preferable to split business + // logic and the CLI into two separate modules. + // + // If your goal is to create a Zig library for others to use, consider if + // it might benefit from also exposing a CLI tool. A parser library for a + // data serialization format could also bundle a CLI syntax checker, for example. + // + // If instead your goal is to create an executable, consider if users might + // be interested in also being able to embed the core functionality of your + // program in their own executable in order to avoid the overhead involved in + // subprocessing your CLI tool. + // + // If neither case applies to you, feel free to delete the declaration you + // don't need and to put everything under a single module. + const exe = b.addExecutable(.{ + .name = "chip8", + .root_module = b.createModule(.{ + // b.createModule defines a new module just like b.addModule but, + // unlike b.addModule, it does not expose the module to consumers of + // this package, which is why in this case we don't have to give it a name. + .root_source_file = b.path("src/main.zig"), + // Target and optimization levels must be explicitly wired in when + // defining an executable or library (in the root module), and you + // can also hardcode a specific target for an executable or library + // definition if desireable (e.g. firmware for embedded devices). + .target = target, + .optimize = optimize, + // List of modules available for import in source files part of the + // root module. + .imports = &.{ + // Here "chip8" is the name you will use in your source code to + // import this module (e.g. `@import("chip8")`). The name is + // repeated because you are allowed to rename your imports, which + // can be extremely useful in case of collisions (which can happen + // importing modules from different packages). + }, + }), + }); + + const sdl3 = b.dependency("sdl3", .{ + .target = target, + .optimize = optimize, + }); + + exe.root_module.addImport("sdl3", sdl3.module("sdl3")); + + // This declares intent for the executable to be installed into the + // install prefix when running `zig build` (i.e. when executing the default + // step). By default the install prefix is `zig-out/` but can be overridden + // by passing `--prefix` or `-p`. + b.installArtifact(exe); + + // This creates a top level step. Top level steps have a name and can be + // invoked by name when running `zig build` (e.g. `zig build run`). + // This will evaluate the `run` step rather than the default step. + // For a top level step to actually do something, it must depend on other + // steps (e.g. a Run step, as we will see in a moment). + const run_step = b.step("run", "Run the app"); + + // This creates a RunArtifact step in the build graph. A RunArtifact step + // invokes an executable compiled by Zig. Steps will only be executed by the + // runner if invoked directly by the user (in the case of top level steps) + // or if another step depends on it, so it's up to you to define when and + // how this Run step will be executed. In our case we want to run it when + // the user runs `zig build run`, so we create a dependency link. + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + // By making the run step depend on the default step, it will be run from the + // installation directory rather than directly from within the cache directory. + 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); + } + + // Creates an executable that will run `test` blocks from the executable's + // root module. Note that test executables only test one module at a time, + // hence why we have to create two separate ones. + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + // A run step that will run the second test executable. + const run_exe_tests = b.addRunArtifact(exe_tests); + + // A top level step for running all tests. dependOn can be called multiple + // times and since the two run steps do not depend on one another, this will + // make the two of them run in parallel. + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_exe_tests.step); + + // Just like flags, top level steps are also listed in the `--help` menu. + // + // The Zig build system is entirely implemented in userland, which means + // that it cannot hook into private compiler APIs. All compilation work + // orchestrated by the build system will result in other Zig compiler + // subcommands being invoked with the right flags defined. You can observe + // these invocations when one fails (or you pass a flag to increase + // verbosity) to validate assumptions and diagnose problems. + // + // Lastly, the Zig build system is relatively simple and self-contained, + // and reading its source code will allow you to master it. +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..d4c3020 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,48 @@ +.{ + // 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, + // 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 = 0xa61914ab9792a718, // 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.15.2", + // 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 = .{ + .sdl3 = .{ + .url = "git+https://github.com/Gota7/zig-sdl3?ref=v0.1.5#014b7bcb2899f3ed9c945c4abfcfe1b25d75bfeb", + .hash = "sdl3-0.1.5-NmT1QxARJgAH1Wp0cMBJDAc9vD7weufTkIwVa5rehA2q", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/chip8.zig b/src/chip8.zig new file mode 100644 index 0000000..03e2db1 --- /dev/null +++ b/src/chip8.zig @@ -0,0 +1,165 @@ +const std = @import("std"); +const structure = @import("stack.zig"); +const sdl3 = @import("sdl3"); +const str = @import("structs.zig"); + +pub const Emulator = struct { + const Self = @This(); + + const w_pixels = 64; + const h_pixels = 32; + const scale = 25; + + const memory_size = 4096; + const StackType = structure.Stack(u16, 32); + + const program_start = 0x200; + const font_start = 0x050; + + display: [h_pixels][w_pixels]u1, + memory: [memory_size]u8, // programs start at 0x200 address + pc: u16, + i: u16, + // Each call to the stack pushes 2B, and stack should be 64B big + // 2B x 32 = 64B + stack: StackType, + timers: struct { + delay: u8, + sound: u8, + }, + registers: [16]u8, + + pub fn init(allocator: std.mem.Allocator) !Self { + return .{ + .display = std.mem.zeroes([h_pixels][w_pixels]u1), + .memory = try initMemory(allocator), + .pc = 0, + .i = 0, + // stack init + .stack = StackType.init(), + .timers = .{ + // u8 max + .delay = 0xFF, + .sound = 0xFF, + }, + .registers = std.mem.zeroes([16]u8), + }; + } + + pub fn destroy(self: *Self) void { + self.stack.destroy(); + } + + fn initMemory(allocator: std.mem.Allocator) ![memory_size]u8 { + var memory = 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 + }; + + // loading font data into the memory + for (font_data, font_start..) |value, i| { + memory[i] = value; + } + + // reading game data + const filename = getFileName(allocator) catch |err| { + std.debug.print("Failed to parse the game filename: {}\n", .{err}); + return err; + }; + + const file = readFile(allocator, filename) catch |err| { + std.debug.print("Failed to read the file '{s}: {}\n", .{ filename, err }); + return err; + }; + defer allocator.free(file); + + // loading game data into the memory + for (file, program_start..) |value, i| { + memory[i] = value; + } + + return memory; + } + + // despite Linux not needing to use allocators to extract arguments we're using them to ensure + // our emulator will work on windows and WASI (Web Assembly System Interface) + // which should come in handy should we ever want to port it online (I'm not writing this shit in JavaScript) + fn getFileName(allocator: std.mem.Allocator) ![]u8 { + const args = try std.process.argsAlloc(allocator); + + return if (args.len <= 1) error.NoArgs else args[1]; + } + + fn readFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 { + const file = try std.fs.cwd().openFile(filename, .{ .mode = .read_only }); + defer file.close(); + + const len = try file.getEndPos(); + const buffer = try allocator.alloc(u8, len); + // we will free it in the caller function + + _ = try file.read(buffer); + + return buffer; + } + + pub fn printMemory(self: *Self) void { + var space = false; + + for (self.memory, 1..) |value, i| { + const suffix: u8 = if (space) ' ' else 0x0; + const newline: u8 = if (i % 16 == 0) '\n' else 0x0; + + std.debug.print("{X:0>2}{c}{c}", .{ value, suffix, newline }); + + space = !space; + } + } + + pub fn returnDisplayData(_: *Self) str.DisplayData { + return .{ + .screen_width = w_pixels, + .screen_height = h_pixels, + .scale = scale, + }; + } + + // TODO implement some sort of tracking system when display actually changes so we don't call SDL for nothing + // one idea is to make a sort of array that tracks modified/changed "pixels" + pub fn draw(self: *Self, surface: *sdl3.surface.Surface) !void { + for (self.display, 0..) |_, i| { + for (self.display[0], 0..) |value, j| { + const x = i * w_pixels; + const y = j * h_pixels; + const width = w_pixels; + const height = h_pixels; + + const area: sdl3.rect.IRect = .{ + .h = height * scale, + .w = width * scale, + .x = @intCast(x * scale), + .y = @intCast(y * scale), + }; + + const color: u8 = if (value == 1) str.WHITE else str.BLACK; + try surface.fillRect(area, surface.mapRgb(color, color, color)); + } + } + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..cbf5731 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,56 @@ +const std = @import("std"); +const sdl3 = @import("sdl3"); +const chip8 = @import("chip8.zig"); +const str = @import("structs.zig"); + +pub fn main() !void { + const fps = 60; + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const allocator = arena.allocator(); + + // chip8 init + var emulator = try chip8.Emulator.init(allocator); + defer emulator.destroy(); + + emulator.printMemory(); + + defer sdl3.shutdown(); + + // initialise SDL with subsystems you need here + const init_flags = sdl3.InitFlags{ .video = true }; + try sdl3.init(init_flags); + defer sdl3.quit(init_flags); + + // initial window setup + const res = emulator.returnDisplayData(); + const window = try sdl3.video.Window.init("CHIP-8 Emulator", res.screen_width * res.scale, res.screen_height * res.scale, .{}); + defer window.deinit(); + + // useful for limiting the fps and getting the delta time + var fps_capper = sdl3.extras.FramerateCapper(f32){ .mode = .{ .limited = fps } }; + + var quit = false; + while (!quit) { + // delay to limit the fps, returned delta time not needed + _ = fps_capper.delay(); + + // update logic + var surface = try window.getSurface(); + // set background (black) + try surface.fillRect(null, surface.mapRgb(str.BLACK, str.BLACK, str.BLACK)); + try emulator.draw(&surface); + try window.updateSurface(); + + // event logic + while (sdl3.events.poll()) |event| { + switch (event) { + .quit => quit = true, + .terminating => quit = true, + else => {}, + } + } + } +} diff --git a/src/stack.zig b/src/stack.zig new file mode 100644 index 0000000..b40b3d7 --- /dev/null +++ b/src/stack.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +pub fn Stack(comptime T: type, comptime size: usize) type { + return struct { + array: [size]T, + i: ?usize, + + const Self = @This(); + + pub fn init() Self { + return .{ + .array = std.mem.zeroes([size]T), + .i = null, + }; + } + + // this function might as well not exist but might as well set it + // nothing else is necessary, because stack will only read what is set at i + // and if i gets increased it will only get so when we insert new number + pub fn destroy(self: *Self) void { + self.i = null; + } + + pub fn push(self: *Self, value: T) !void { + if (self.i) |index| { + if (index >= self.array.len - 1) return error.StackFull; + // index points at the last currently stored element + self.i = index + 1; + } else { + // if stack is empty set it to point at first element + self.i = 0; + } + + self.array[self.i.?] = value; + } + + pub fn pop(self: *Self) ?T { + if (self.i) |index| { + // set the i pointer to the previous element (null if we're popping the only element so far) + self.i = if (self.i == 0) null else self.i - 1; + return self.array[index]; + } + + return null; // stack is empty we have nothing to return + } + }; +} diff --git a/src/structs.zig b/src/structs.zig new file mode 100644 index 0000000..4797dde --- /dev/null +++ b/src/structs.zig @@ -0,0 +1,8 @@ +pub const DisplayData = struct { + screen_width: usize, + screen_height: usize, + scale: usize, +}; + +pub const BLACK = 0x0; +pub const WHITE = 0xFF;