diff --git a/.gitignore b/.gitignore index 4c82b07..124b99c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ zig-cache zig-out +.vscode diff --git a/build.zig b/build.zig index 3416c49..5351043 100644 --- a/build.zig +++ b/build.zig @@ -13,7 +13,9 @@ pub fn build(b: *std.Build) void { // 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 optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = .ReleaseSmall, + }); const use_avr_gcc = b.option( bool, @@ -44,23 +46,27 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - native_library.addModule("build_options", options_module); - const native_library_install = b.addInstallLibFile(native_library.getEmittedBin(), "libzip8.a"); + native_library.root_module.addImport("build_options", options_module); + const native_library_install = b.addInstallLibFile( + native_library.getEmittedBin(), + b.fmt("libzip8{s}", .{target.result.dynamicLibSuffix()}), + ); - const wasm_library = b.addSharedLibrary(.{ + const wasm_library = b.addExecutable(.{ .name = "zip8", // 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 = .{ .path = "src/main.zig" }, - .target = .{ + .target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .cpu_features_add = std.Target.wasm.featureSet(&.{.bulk_memory}), - }, + }), .optimize = optimize, }); + wasm_library.entry = .disabled; + wasm_library.root_module.addImport("build_options", options_module); wasm_library.rdynamic = true; - wasm_library.addModule("build_options", options_module); const wasm_step = b.step("wasm", "Build WebAssembly library"); @@ -91,40 +97,36 @@ pub fn build(b: *std.Build) void { // 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 = .{ .path = "src/main.zig" }, - .target = .{ + .target = b.resolveTargetQuery(.{ .cpu_arch = .thumb, .os_tag = .freestanding, .abi = .eabi, .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m0plus }, - }, + }), .optimize = optimize, }); - m0plus_library.addModule("build_options", options_module); + m0plus_library.root_module.addImport("build_options", options_module); const m0plus_library_install = b.addInstallLibFile(m0plus_library.getEmittedBin(), "cortex-m0plus/libzip8.a"); // build a static library for AVR const atmega4809_library = b.addStaticLibrary(.{ .name = "zip8", .root_source_file = .{ .path = "src/main.zig" }, - .target = .{ + .target = b.resolveTargetQuery(.{ .cpu_arch = .avr, .os_tag = .freestanding, .cpu_model = .{ .explicit = &std.Target.avr.cpu.atmega4809 }, .ofmt = if (use_avr_gcc) .c else null, - }, + }), .optimize = optimize, }); - atmega4809_library.addModule("build_options", options_module); + atmega4809_library.root_module.addImport("build_options", options_module); const atmega4809_library_file = if (use_avr_gcc) bin: { - // we use this to get the lib_dir from our zig installation as that directory contains zig.h - const zig_lib_dir_cmd = b.addSystemCommand(&.{ "sh", "-c", "$0 env | jq -r .lib_dir", b.zig_exe }); - const zig_lib_dir_file = zig_lib_dir_cmd.captureStdOut(); - const gcc_cmd = b.addSystemCommand(&.{ "sh", "-c", - "avr-gcc -c -Wno-incompatible-pointer-types -Wno-builtin-declaration-mismatch -mmcu=atmega4809 $0 -I $(cat $1) $2 -o $3", + "avr-gcc -c -Wno-incompatible-pointer-types -Wno-builtin-declaration-mismatch -mmcu=atmega4809 $0 -I $1 $2 -o $3", switch (optimize) { .Debug => "-g", .ReleaseSafe => "-O3", @@ -132,8 +134,9 @@ pub fn build(b: *std.Build) void { .ReleaseSmall => "-Oz", }, }); - gcc_cmd.addFileArg(zig_lib_dir_file); + gcc_cmd.addArg(b.lib_dir); gcc_cmd.addFileArg(atmega4809_library.getEmittedBin()); + const avr_object_path = gcc_cmd.addOutputFileArg("zip8.o"); const ar_cmd = b.addSystemCommand(&.{ "ar", "-rcs" }); @@ -150,8 +153,8 @@ pub fn build(b: *std.Build) void { const write_files_step = b.addWriteFiles(); _ = write_files_step.addCopyFile(m0plus_library.getEmittedBin(), "zip8/src/cortex-m0plus/libzip8.a"); _ = write_files_step.addCopyFile(atmega4809_library_file, "zip8/src/atmega4809/libzip8.a"); - _ = write_files_step.addCopyFile(std.build.LazyPath.relative("src/zip8.h"), "zip8/src/zip8.h"); - _ = write_files_step.addCopyFile(std.build.LazyPath.relative("library.properties"), "zip8/library.properties"); + _ = write_files_step.addCopyFile(b.path("src/zip8.h"), "zip8/src/zip8.h"); + _ = write_files_step.addCopyFile(b.path("library.properties"), "zip8/library.properties"); const zip_step = b.addSystemCommand(&.{ "sh", "-c", "cd $0; zip -r $1 zip8" }); zip_step.addDirectoryArg(write_files_step.getDirectory()); const zip_output = zip_step.addOutputFileArg("zip8.zip"); @@ -174,12 +177,12 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - exe_tests.addModule("build_options", options_module); + exe_tests.root_module.addImport("build_options", options_module); // 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"); - const test_run_cmd = b.addRunArtifact(exe_tests.step.cast(std.build.Step.Compile).?); + const test_run_cmd = b.addRunArtifact(exe_tests.step.cast(std.Build.Step.Compile).?); test_step.dependOn(&test_run_cmd.step); } diff --git a/src/bindings.zig b/src/bindings.zig index 4aa3cd8..afa0679 100644 --- a/src/bindings.zig +++ b/src/bindings.zig @@ -107,18 +107,28 @@ export fn zip8CpuSetFlagsNotDirty(cpu: ?*anyopaque) callconv(.C) void { cpuPtrCast(cpu).flags_dirty = false; } -fn zip8CpuAlloc() callconv(.C) ?[*]u8 { - return (std.heap.wasm_allocator.alignedAlloc(u8, @alignOf(Cpu), @sizeOf(Cpu)) catch return null).ptr; +export fn zip8CpuGetDrawBytes(cpu: ?*const anyopaque) callconv(.C) usize { + return cpuPtrCast(cpu).draw_bytes_this_frame; } -fn wasmAlloc(n: usize) callconv(.C) ?[*]u8 { - return (std.heap.wasm_allocator.alignedAlloc(u8, @import("builtin").target.maxIntAlignment(), n) catch return null).ptr; +export fn zip8CpuResetDrawBytes(cpu: ?*anyopaque) callconv(.C) void { + cpuPtrCast(cpu).draw_bytes_this_frame = 0; } comptime { if (@import("builtin").target.isWasm()) { - @export(zip8CpuAlloc, .{ .name = "zip8CpuAlloc" }); - @export(wasmAlloc, .{ .name = "wasmAlloc" }); + const wasm_only_functions = struct { + fn zip8CpuAlloc() callconv(.C) ?[*]u8 { + return (std.heap.wasm_allocator.alignedAlloc(u8, @alignOf(Cpu), @sizeOf(Cpu)) catch return null).ptr; + } + + fn wasmAlloc(n: usize) callconv(.C) ?[*]u8 { + return (std.heap.wasm_allocator.alignedAlloc(u8, @import("builtin").target.maxIntAlignment(), n) catch return null).ptr; + } + }; + + @export(wasm_only_functions.zip8CpuAlloc, .{ .name = "zip8CpuAlloc" }); + @export(wasm_only_functions.wasmAlloc, .{ .name = "wasmAlloc" }); } } diff --git a/src/cpu.zig b/src/cpu.zig index b01d6a5..9054033 100644 --- a/src/cpu.zig +++ b/src/cpu.zig @@ -36,8 +36,6 @@ const log = if (@import("builtin").is_test) struct { V: [16]u8 = .{0} ** 16, /// 12-bit register I for indexing memory I: u12 = 0, -/// 4 KiB of memory -mem: [memory_size]u8 = [_]u8{0} ** memory_size, /// program counter pc: u12 = initial_pc, /// call stack @@ -45,6 +43,13 @@ stack: std.BoundedArray(u12, 16), /// random number generator to use rand: std.rand.DefaultPrng, +/// 4 KiB of memory +mem: [memory_size]u8 = .{0x00} ** memory_size, +/// track which memory has not been synchronized to clients +mem_dirty: [memory_size / 8]u8 = .{0xff} ** (memory_size / 8), + +draw_bytes_this_frame: usize = 0, + /// display is 64x32 stored row-major display: [display_width * display_height / 8]u8 = .{0} ** (display_width * display_height / 8), /// whether the contents of the screen have changed since the last time this flag was set to false diff --git a/src/instruction.zig b/src/instruction.zig index 6dab780..d311f58 100644 --- a/src/instruction.zig +++ b/src/instruction.zig @@ -23,6 +23,8 @@ opcode: u16, const testing_seed = 1337; +const draw_log = std.log.scoped(.draw); + /// split up an opcode into useful parts pub fn decode(opcode: u16) Instruction { const nibbles = [4]u4{ @@ -113,6 +115,8 @@ fn op00EX(self: Instruction, cpu: *Cpu) !?u12 { 0x00E0 => { @memset(&cpu.display, 0); cpu.display_dirty = true; + draw_log.info("clear", .{}); + cpu.draw_bytes_this_frame += 4; return null; }, 0x00EE => { @@ -639,20 +643,16 @@ test "CXNN random" { try std.testing.expectEqual(@as(u8, 0x00), cpu.V[0x0]); } -export fn draw(ptr: ?*anyopaque, xReg: u8, yReg: u8, rows: u8) callconv(.C) void { - const cpu: *Cpu = @alignCast(@ptrCast(ptr.?)); - const inst = Instruction.decode(0xd000 | (@as(u16, xReg) << 8) | (yReg << 4) | rows); - _ = inst.exec(cpu) catch unreachable; - cpu.pc +%= 2; -} - /// DXYN: draw an 8xN sprite from memory starting at I at (VX, VY); set VF to 1 if any pixel was /// turned off, 0 otherwise fn opDraw(self: Instruction, cpu: *Cpu) !?u12 { cpu.V[0xF] = 0; - const sprite: []const u8 = cpu.mem[(cpu.I)..(cpu.I + self.low4)]; + const sprite: []const u8 = cpu.mem[cpu.I..][0..self.low4]; const x_start = cpu.V[self.regX] % Cpu.display_width; const y_start = cpu.V[self.regY] % Cpu.display_height; + + var any_bytes_dirty = false; + for (sprite, 0..) |row, y_sprite| { for (0..8) |x_sprite| { const pixel: u1 = @truncate(row >> @intCast(7 - x_sprite)); @@ -668,8 +668,21 @@ fn opDraw(self: Instruction, cpu: *Cpu) !?u12 { cpu.invertPixel(x, y); cpu.display_dirty = true; } + + const mem_index = y_sprite + cpu.I; + if ((cpu.mem_dirty[mem_index / 8] >> @truncate(mem_index)) & 0x01 != 0) { + any_bytes_dirty = true; + cpu.mem_dirty[mem_index / 8] ^= (@as(u8, 1) << @truncate(mem_index)); + } } } + + if (any_bytes_dirty) { + draw_log.info("untaint: [{}, {})", .{ cpu.I, cpu.I + self.low4 }); + cpu.draw_bytes_this_frame += self.low4; + } + draw_log.info("sprite: {} rows at ({}, {})", .{ sprite.len, x_start, y_start }); + cpu.draw_bytes_this_frame += 4; return null; } @@ -934,6 +947,7 @@ fn opStore(self: Instruction, cpu: *Cpu) !?u12 { for (0..(@as(u8, self.regX) + 1)) |offset| { cpu.mem[cpu.I] = cpu.V[offset]; cpu.I +%= 1; + cpu.mem_dirty[cpu.I / 8] |= (@as(u8, 1) << @truncate(cpu.I)); } return null; } diff --git a/src/main.zig b/src/main.zig index d826693..623eb66 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,25 +10,26 @@ pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_ while (true) {} } -pub const std_options = struct { - pub const log_level = .info; +pub fn logFn( + comptime level: std.log.Level, + comptime scope: @Type(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + const level_prefix = comptime level.asText(); + const prefix = comptime level_prefix ++ switch (scope) { + .default => ": ", + else => " (" ++ @tagName(scope) ++ "): ", + }; - pub fn logFn( - comptime level: std.log.Level, - comptime scope: @TypeOf(.EnumLiteral), - comptime format: []const u8, - args: anytype, - ) void { - const level_prefix = comptime level.asText(); - const prefix = comptime level_prefix ++ switch (scope) { - .default => ": ", - else => " (" ++ @tagName(scope) ++ "): ", - }; + var buf: [1024:0]u8 = undefined; + const string = std.fmt.bufPrintZ(&buf, prefix ++ format, args) catch &buf; + zip8Log(string.ptr, string.len); +} - var buf: [1024:0]u8 = undefined; - const string = std.fmt.bufPrintZ(&buf, prefix ++ format, args) catch &buf; - zip8Log(string.ptr, string.len); - } +pub const std_options = std.Options{ + .log_level = .info, + .logFn = logFn, }; comptime { diff --git a/web-host/src/cpu.ts b/web-host/src/cpu.ts index 046efb6..a8c54dd 100644 --- a/web-host/src/cpu.ts +++ b/web-host/src/cpu.ts @@ -31,6 +31,8 @@ interface Zip8Exports { zip8CpuFlagsAreDirty(cpuPtr: number): boolean; zip8CpuSetFlagsNotDirty(cpuPtr: number): void; zip8CpuAlloc(): number; + zip8CpuGetDrawBytes(cpuPtr: number): number; + zip8CpuResetDrawBytes(cpuPtr: number): void; wasmAlloc(size: number): number; } @@ -202,4 +204,12 @@ export default class CPU { } return flags; } + + getDrawBytes(): number { + return this.exports.zip8CpuGetDrawBytes(this.cpuPtr); + } + + resetDrawBytes() { + this.exports.zip8CpuResetDrawBytes(this.cpuPtr); + } } diff --git a/web-host/src/main.ts b/web-host/src/main.ts index b08b374..54ed1dc 100644 --- a/web-host/src/main.ts +++ b/web-host/src/main.ts @@ -92,6 +92,8 @@ async function run(rom: ArrayBuffer) { cpu.setKeys(keys); } + cpu.resetDrawBytes(); + if (!cpu.isWaitingForKey()) { for (let i = 0; i < instructionsPerTick && !halt; i++) { try { @@ -116,6 +118,8 @@ async function run(rom: ArrayBuffer) { localStorage.setItem(key, JSON.stringify(flags)); } } + + console.log(cpu.getDrawBytes()); } tick();