From 6d4bcdd66539c1fadbac966bed806a60a8a873f3 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Sun, 12 May 2024 16:00:10 -0700 Subject: [PATCH] cleanup + make memory diffing conditionally compiled --- build.zig | 9 +++++++- src/bindings.zig | 50 ++++++++++++++++++++++++++------------------ src/cpu.zig | 27 +++++++++++++++--------- src/instruction.zig | 31 +++++++++++++++++---------- web-host/src/main.ts | 4 ++-- 5 files changed, 77 insertions(+), 44 deletions(-) diff --git a/build.zig b/build.zig index bc27e36..9fd3ecd 100644 --- a/build.zig +++ b/build.zig @@ -26,7 +26,7 @@ pub fn build(b: *std.Build) void { const memory_size = b.option( u13, "memory_size", - "Size of the CHIP-8's memory. Higher than 4096 is unacceptable; lower than 4096 may break some programs.", + "Size of the CHIP-8's memory. Higher than 4096 is unacceptable; lower than 4096 may break some programs. Default 4096.", ); const column_major_display = b.option( @@ -35,9 +35,16 @@ pub fn build(b: *std.Build) void { "Whether the display should use column-major layout instead of row-major. Default false.", ) orelse false; + const experimental_render_hooks = b.option( + bool, + "experimental_render_hooks", + "Enable functions for tracking draw operations and tracking whether memory used by a draw has been modified", + ) orelse false; + const options = b.addOptions(); options.addOption(?u13, "memory_size", memory_size); options.addOption(bool, "column_major_display", column_major_display); + options.addOption(bool, "experimental_render_hooks", experimental_render_hooks); const options_module = options.createModule(); const native_library = b.addSharedLibrary(.{ diff --git a/src/bindings.zig b/src/bindings.zig index 5f78caa..4bd694c 100644 --- a/src/bindings.zig +++ b/src/bindings.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Cpu = @import("./cpu.zig"); +const build_options = @import("build_options"); fn cpuPtrCast(in: anytype) switch (@TypeOf(in)) { ?*anyopaque => *Cpu, @@ -16,11 +17,11 @@ export const ZIP8_ERR_BAD_RETURN: u16 = @intFromError(error.BadReturn); export const ZIP8_ERR_PROGRAM_TOO_LONG: u16 = @intFromError(error.ProgramTooLong); export const ZIP8_ERR_FLAG_OVERFLOW: u16 = @intFromError(error.FlagOverflow); -export fn zip8GetErrorName(err: u16) callconv(.C) [*:0]const u8 { +export fn zip8GetErrorName(err: u16) [*:0]const u8 { return @errorName(@errorFromInt(err)); } -export fn zip8CpuGetSize() callconv(.C) usize { +export fn zip8CpuGetSize() usize { return @sizeOf(Cpu); } @@ -31,7 +32,7 @@ export fn zip8CpuInit( program_len: usize, seed: u64, flags: u64, -) callconv(.C) c_int { +) c_int { // directly storing in the pointer breaks this on RP2040 for whatever reason // cpuPtrCast(cpu).* = Cpu.init(program.?[0..program_len], seed) catch |e| { Cpu.initInPlace( @@ -46,7 +47,7 @@ export fn zip8CpuInit( return 0; } -export fn zip8CpuCycle(err: ?*u16, cpu: ?*anyopaque) callconv(.C) c_int { +export fn zip8CpuCycle(err: ?*u16, cpu: ?*anyopaque) c_int { cpuPtrCast(cpu).cycle() catch |e| { err.?.* = @intFromError(e); return 1; @@ -54,7 +55,7 @@ export fn zip8CpuCycle(err: ?*u16, cpu: ?*anyopaque) callconv(.C) c_int { return 0; } -export fn zip8CpuSetKeys(cpu: ?*anyopaque, keys: u16) callconv(.C) void { +export fn zip8CpuSetKeys(cpu: ?*anyopaque, keys: u16) void { var new_keys: [16]bool = undefined; for (&new_keys, 0..) |*key, i_usize| { const i: u4 = @intCast(i_usize); @@ -63,27 +64,27 @@ export fn zip8CpuSetKeys(cpu: ?*anyopaque, keys: u16) callconv(.C) void { cpuPtrCast(cpu).setKeys(&new_keys); } -export fn zip8CpuIsWaitingForKey(cpu: ?*const anyopaque) callconv(.C) bool { +export fn zip8CpuIsWaitingForKey(cpu: ?*const anyopaque) bool { return cpuPtrCast(cpu).next_key_register != null; } -export fn zip8CpuTimerTick(cpu: ?*anyopaque) callconv(.C) void { +export fn zip8CpuTimerTick(cpu: ?*anyopaque) void { cpuPtrCast(cpu).timerTick(); } -export fn zip8CpuDisplayIsDirty(cpu: ?*const anyopaque) callconv(.C) bool { +export fn zip8CpuDisplayIsDirty(cpu: ?*const anyopaque) bool { return cpuPtrCast(cpu).display_dirty; } -export fn zip8CpuSetDisplayNotDirty(cpu: ?*anyopaque) callconv(.C) void { +export fn zip8CpuSetDisplayNotDirty(cpu: ?*anyopaque) void { cpuPtrCast(cpu).display_dirty = false; } -export fn zip8CpuGetDisplay(cpu: ?*const anyopaque) callconv(.C) [*]const u8 { +export fn zip8CpuGetDisplay(cpu: ?*const anyopaque) [*]const u8 { return &cpuPtrCast(cpu).display; } -export fn zip8CpuGetInstruction(cpu: ?*const anyopaque) callconv(.C) u16 { +export fn zip8CpuGetInstruction(cpu: ?*const anyopaque) u16 { const mem: []const u8 = &cpuPtrCast(cpu).mem; const pc = cpuPtrCast(cpu).pc; const high = mem[pc]; @@ -91,28 +92,37 @@ export fn zip8CpuGetInstruction(cpu: ?*const anyopaque) callconv(.C) u16 { return (@as(u16, high) << 8) + low; } -export fn zip8CpuGetProgramCounter(cpu: ?*const anyopaque) callconv(.C) u16 { +export fn zip8CpuGetProgramCounter(cpu: ?*const anyopaque) u16 { return cpuPtrCast(cpu).pc; } -export fn zip8CpuGetFlags(cpu: ?*const anyopaque) callconv(.C) u64 { +export fn zip8CpuGetFlags(cpu: ?*const anyopaque) u64 { return std.mem.nativeToLittle(u64, @as(u64, @bitCast(cpuPtrCast(cpu).flags))); } -export fn zip8CpuFlagsAreDirty(cpu: ?*const anyopaque) callconv(.C) bool { +export fn zip8CpuFlagsAreDirty(cpu: ?*const anyopaque) bool { return cpuPtrCast(cpu).flags_dirty; } -export fn zip8CpuSetFlagsNotDirty(cpu: ?*anyopaque) callconv(.C) void { +export fn zip8CpuSetFlagsNotDirty(cpu: ?*anyopaque) void { cpuPtrCast(cpu).flags_dirty = false; } -export fn zip8CpuGetDrawBytes(cpu: ?*const anyopaque) callconv(.C) usize { - return cpuPtrCast(cpu).draw_bytes_this_frame; -} +comptime { + if (build_options.experimental_render_hooks) { + const render_hook_exports = struct { + fn zip8CpuGetDrawBytes(cpu: ?*const anyopaque) callconv(.C) usize { + return cpuPtrCast(cpu).draw_bytes_this_frame; + } -export fn zip8CpuResetDrawBytes(cpu: ?*anyopaque) callconv(.C) void { - cpuPtrCast(cpu).draw_bytes_this_frame = 0; + fn zip8CpuResetDrawBytes(cpu: ?*anyopaque) callconv(.C) void { + cpuPtrCast(cpu).draw_bytes_this_frame = 0; + } + }; + + @export(render_hook_exports.zip8CpuGetDrawBytes, .{ .name = "zip8CpuGetDrawBytes" }); + @export(render_hook_exports.zip8CpuResetDrawBytes, .{ .name = "zip8CpuResetDrawBytes" }); + } } comptime { diff --git a/src/cpu.zig b/src/cpu.zig index 9054033..01aeaa5 100644 --- a/src/cpu.zig +++ b/src/cpu.zig @@ -44,11 +44,18 @@ stack: std.BoundedArray(u12, 16), rand: std.rand.DefaultPrng, /// 4 KiB of memory -mem: [memory_size]u8 = .{0x00} ** memory_size, +mem: [memory_size]u8, /// track which memory has not been synchronized to clients -mem_dirty: [memory_size / 8]u8 = .{0xff} ** (memory_size / 8), +mem_dirty: if (build_options.experimental_render_hooks) + [memory_size / 8]u8 +else + void = + if (build_options.experimental_render_hooks) + .{0xff} ** (memory_size / 8) + else {}, -draw_bytes_this_frame: usize = 0, +draw_bytes_this_frame: if (build_options.experimental_render_hooks) usize else void = + if (build_options.experimental_render_hooks) 0 else {}, /// display is 64x32 stored row-major display: [display_width * display_height / 8]u8 = .{0} ** (display_width * display_height / 8), @@ -75,14 +82,16 @@ flags_dirty: bool = false, pub fn initInPlace(cpu: *Cpu, program: []const u8, seed: u64, flags: [8]u8) error{ProgramTooLong}!void { cpu.* = Cpu{ .rand = std.rand.DefaultPrng.init(seed), - .stack = std.BoundedArray(u12, stack_size).init(0) catch unreachable, + .stack = std.BoundedArray(u12, stack_size){}, .flags = flags, + .mem = undefined, }; if (program.len > (memory_size - initial_pc)) { return error.ProgramTooLong; } - @memcpy(cpu.mem[font_base_address..].ptr, font_data); - @memcpy(cpu.mem[initial_pc..].ptr, program); + @memset(&cpu.mem, 0); + @memcpy((&cpu.mem)[font_base_address..].ptr, font_data); + @memcpy((&cpu.mem)[initial_pc..].ptr, program); log.info("cpu init", .{}); } @@ -94,9 +103,7 @@ pub fn init(program: []const u8, seed: u64, flags: [8]u8) error{ProgramTooLong}! } pub fn cycle(self: *Cpu) !void { - const hi: u8 = @as([*]u8, @ptrCast(&self.mem))[self.pc]; - const lo: u8 = @as([*]u8, @ptrCast(&self.mem))[self.pc + 1]; - const opcode: u16 = (@as(u16, hi) << 8) + lo; + const opcode = std.mem.readInt(u16, (&self.mem)[self.pc..][0..2], .big); const inst = Instruction.decode(opcode); log.debug("{any}", .{inst}); if (inst.exec(self) catch |e| { @@ -164,7 +171,7 @@ pub fn getPixel(self: *const Cpu, x: u8, y: u8) u1 { const byte_index = pixel_index / 8; const bit_index: u3 = @truncate(pixel_index); - return @truncate(@as([*]const u8, @ptrCast(&self.display))[byte_index] >> bit_index); + return @truncate((&self.display)[byte_index] >> bit_index); } test "Cpu.init" { diff --git a/src/instruction.zig b/src/instruction.zig index d311f58..bebb8aa 100644 --- a/src/instruction.zig +++ b/src/instruction.zig @@ -3,6 +3,7 @@ const Instruction = @This(); const std = @import("std"); const Cpu = @import("./cpu.zig"); +const build_options = @import("build_options"); /// 4-bit elements of opcode, most significant first nibbles: [4]u4, @@ -116,7 +117,9 @@ fn op00EX(self: Instruction, cpu: *Cpu) !?u12 { @memset(&cpu.display, 0); cpu.display_dirty = true; draw_log.info("clear", .{}); - cpu.draw_bytes_this_frame += 4; + if (build_options.experimental_render_hooks) { + cpu.draw_bytes_this_frame += 4; + } return null; }, 0x00EE => { @@ -670,19 +673,23 @@ fn opDraw(self: Instruction, cpu: *Cpu) !?u12 { } 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 (build_options.experimental_render_hooks) { + 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; + if (build_options.experimental_render_hooks) { + 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; } - draw_log.info("sprite: {} rows at ({}, {})", .{ sprite.len, x_start, y_start }); - cpu.draw_bytes_this_frame += 4; return null; } @@ -947,7 +954,9 @@ 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)); + if (build_options.experimental_render_hooks) { + cpu.mem_dirty[cpu.I / 8] |= (@as(u8, 1) << @truncate(cpu.I)); + } } return null; } @@ -982,7 +991,7 @@ test "FX55 store registers" { /// FX65: load values from memory starting at I into registers [V0, VX]; set I to I + X + 1 fn opLoad(self: Instruction, cpu: *Cpu) !?u12 { for (0..(@as(u8, self.regX) + 1)) |offset| { - cpu.V[offset] = @as([*]const u8, @ptrCast(&cpu.mem))[cpu.I]; + cpu.V[offset] = (&cpu.mem)[cpu.I]; cpu.I +%= 1; } return null; diff --git a/web-host/src/main.ts b/web-host/src/main.ts index 54ed1dc..a5fb35f 100644 --- a/web-host/src/main.ts +++ b/web-host/src/main.ts @@ -92,7 +92,7 @@ async function run(rom: ArrayBuffer) { cpu.setKeys(keys); } - cpu.resetDrawBytes(); + // cpu.resetDrawBytes(); if (!cpu.isWaitingForKey()) { for (let i = 0; i < instructionsPerTick && !halt; i++) { @@ -119,7 +119,7 @@ async function run(rom: ArrayBuffer) { } } - console.log(cpu.getDrawBytes()); + // console.log(cpu.getDrawBytes()); } tick();