From 3a8c887b706466470f29f4dbeac447e9b4534e09 Mon Sep 17 00:00:00 2001
From: SuperAuguste <19855629+SuperAuguste@users.noreply.github.com>
Date: Tue, 30 Apr 2024 08:51:43 -0400
Subject: [PATCH 1/6] Fix build, improve reloader, API
---
build.zig | 8 +-
samples/feature_test.zig | 79 +++++++--
simulator/src/runtime.ts | 3 +-
simulator/src/ui/app.ts | 7 +-
src/cart/api.zig | 220 ++++++++++++++++--------
src/watch/404.html | 9 -
src/watch/Reloader.zig | 199 ++++++++++-----------
src/watch/main.zig | 154 ++++++++---------
src/watch/watcher/WindowsWatcher.zig | 248 ++++++---------------------
9 files changed, 439 insertions(+), 488 deletions(-)
delete mode 100644 src/watch/404.html
diff --git a/build.zig b/build.zig
index 1d3a8f2..a4e252e 100644
--- a/build.zig
+++ b/build.zig
@@ -64,9 +64,7 @@ pub fn build(b: *Build) void {
//
// TODO: parameterize:
const watch_run = b.addRunArtifact(watch);
- watch_run.addArg("serve");
- watch_run.addArtifactArg(feature_test_cart.wasm);
- watch_run.addArgs(&.{ "--zig-out-bin-dir", "zig-out/bin" });
+ watch_run.addArgs(&.{ "serve", b.graph.zig_exe, "--input-dir", b.pathFromRoot("samples"), "--cart", "zig-out/bin/feature_test.wasm" });
const watch_step = b.step("watch", "");
watch_step.dependOn(&watch_run.step);
@@ -163,8 +161,8 @@ pub fn add_cart(
wasm.entry = .disabled;
wasm.import_memory = true;
- wasm.initial_memory = 2 * 65536;
- wasm.max_memory = 2 * 65536;
+ wasm.initial_memory = 64 * 65536;
+ wasm.max_memory = 64 * 65536;
wasm.stack_size = 14752;
wasm.global_base = 160 * 128 * 2 + 0x1e;
diff --git a/samples/feature_test.zig b/samples/feature_test.zig
index 3cf32c7..75e4cb9 100644
--- a/samples/feature_test.zig
+++ b/samples/feature_test.zig
@@ -22,10 +22,15 @@ fn write_stored_number(number: u64) void {
export fn update() void {
if (offset % (60 * 2) == 0) {
- cart.tone(440, 20, 10, .{
- .channel = .pulse1,
- .duty_cycle = .@"1/8",
- .panning = .left,
+ cart.tone(.{
+ .frequency = 440,
+ .duration = 20,
+ .volume = 10,
+ .flags = .{
+ .channel = .pulse1,
+ .duty_cycle = .@"1/8",
+ .panning = .left,
+ },
});
}
@@ -79,15 +84,59 @@ export fn update() void {
// TODO: blit, blitSub
- cart.line(.{ .red = 0, .green = 63, .blue = 0 }, 50, 50, 70, 70);
-
- cart.hline(.{ .red = 31, .green = 0, .blue = 0 }, 30, 30, 20);
- cart.vline(.{ .red = 31, .green = 0, .blue = 0 }, 30, 30, 20);
-
- cart.oval(.{ .red = 0, .green = 0, .blue = 31 }, .{ .red = 31, .green = 0, .blue = 31 }, 80, 80, 10, 10);
- cart.rect(.{ .red = 31, .green = 31, .blue = 31 }, .{ .red = 0, .green = 63, .blue = 31 }, 100, 100, 10, 10);
-
- cart.text(.{ .red = 0, .green = 0, .blue = 0 }, .{ .red = 31, .green = 63, .blue = 31 }, fbs.getWritten(), 0, 0);
-
- cart.text(.{ .red = 0, .green = 0, .blue = 0 }, .{ .red = 31, .green = 63, .blue = 31 }, "\x80\x81\x82\x83\x84\x85\x86\x87\x88", 0, 120);
+ cart.line(.{
+ .x1 = 50,
+ .y1 = 50,
+ .x2 = 70,
+ .y2 = 70,
+ .color = .{ .red = 0, .green = 63, .blue = 0 },
+ });
+
+ cart.hline(.{
+ .x = 30,
+ .y = 30,
+ .len = 20,
+ .color = .{ .red = 31, .green = 0, .blue = 0 },
+ });
+
+ cart.vline(.{
+ .x = 30,
+ .y = 30,
+ .len = 20,
+ .color = .{ .red = 31, .green = 0, .blue = 0 },
+ });
+
+ cart.oval(.{
+ .x = 80,
+ .y = 80,
+ .width = 10,
+ .height = 10,
+ .stroke_color = .{ .red = 0, .green = 0, .blue = 31 },
+ .fill_color = .{ .red = 31, .green = 0, .blue = 31 },
+ });
+
+ cart.rect(.{
+ .x = 100,
+ .y = 100,
+ .width = 10,
+ .height = 10,
+ .stroke_color = .{ .red = 31, .green = 31, .blue = 31 },
+ .fill_color = .{ .red = 0, .green = 63, .blue = 31 },
+ });
+
+ cart.text(.{
+ .str = fbs.getWritten(),
+ .x = 0,
+ .y = 0,
+ .text_color = .{ .red = 0, .green = 0, .blue = 0 },
+ .background_color = .{ .red = 31, .green = 63, .blue = 31 },
+ });
+
+ cart.text(.{
+ .str = "\x80\x81\x82\x83\x84\x85\x86\x87\x88",
+ .x = 0,
+ .y = 120,
+ .text_color = .{ .red = 0, .green = 0, .blue = 0 },
+ .background_color = .{ .red = 31, .green = 63, .blue = 31 },
+ });
}
diff --git a/simulator/src/runtime.ts b/simulator/src/runtime.ts
index 44dd6d9..230f380 100644
--- a/simulator/src/runtime.ts
+++ b/simulator/src/runtime.ts
@@ -42,7 +42,7 @@ export class Runtime {
this.flashBuffer = new ArrayBuffer(constants.FLASH_PAGE_SIZE);
- this.memory = new WebAssembly.Memory({initial: 2, maximum: 2});
+ this.memory = new WebAssembly.Memory({initial: 64, maximum: 64});
this.data = new DataView(this.memory.buffer);
this.framebuffer = new Framebuffer(this.memory.buffer);
@@ -272,6 +272,7 @@ export class Runtime {
function errorToBlueScreenText(err: Error) {
// hand written messages for specific errors
+ console.log(err);
if (err instanceof WebAssembly.RuntimeError) {
let message;
if (err.message.match(/unreachable/)) {
diff --git a/simulator/src/ui/app.ts b/simulator/src/ui/app.ts
index 5f8e8b6..a3ef818 100644
--- a/simulator/src/ui/app.ts
+++ b/simulator/src/ui/app.ts
@@ -58,6 +58,7 @@ export class App extends LitElement {
}
.help {
+ font-family: wasm4-font;
font-size: 0.9em;
color: #aaa;
}
@@ -114,11 +115,13 @@ export class App extends LitElement {
ws.onopen = w => {
setInterval(() => {
ws.send("spam");
- }, 1000);
+ }, 100);
}
ws.onmessage = async m => {
- await this.resetCart(new Uint8Array(await (await fetch("http://localhost:2468/cart.wasm")).arrayBuffer()), false);
+ if (m.data == "reload") {
+ await this.resetCart(new Uint8Array(await (await fetch("http://localhost:2468/cart.wasm")).arrayBuffer()), false);
+ }
}
function takeScreenshot () {
diff --git a/src/cart/api.zig b/src/cart/api.zig
index fc214d8..0948576 100644
--- a/src/cart/api.zig
+++ b/src/cart/api.zig
@@ -77,7 +77,7 @@ const platform_specific = if (builtin.target.isWasm())
extern fn text(text_color: DisplayColor, background_color: OptionalDisplayColor, str_ptr: [*]const u8, str_len: usize, x: i32, y: i32) void;
extern fn vline(color: DisplayColor, x: i32, y: i32, len: u32) void;
extern fn hline(color: DisplayColor, x: i32, y: i32, len: u32) void;
- extern fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneFlags) void;
+ extern fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneOptions.Flags) void;
extern fn read_flash(offset: u32, dst: [*]u8, len: u32) u32;
extern fn write_flash_page(page: u32, src: [*]const u8) void;
extern fn trace(str_ptr: [*]const u8, str_len: usize) void;
@@ -167,144 +167,228 @@ pub inline fn blit_sub(sprite: [*]const u8, x: i32, y: i32, width: u32, height:
}
}
+pub const LineOptions = struct {
+ x1: i32,
+ y1: i32,
+ x2: i32,
+ y2: i32,
+ color: DisplayColor,
+};
+
/// Draws a line between two points.
-pub inline fn line(color: DisplayColor, x1: i32, y1: i32, x2: i32, y2: i32) void {
+pub inline fn line(options: LineOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.line(color, x1, y1, x2, y2);
+ platform_specific.line(options.color, options.x1, options.y1, options.x2, options.y2);
} else {
asm volatile (" svc #2"
:
- : [x1] "{r0}" (x1),
- [y1] "{r1}" (y1),
- [x2] "{r2}" (x2),
- [y2] "{r3}" (y2),
+ : [x1] "{r0}" (options.x1),
+ [y1] "{r1}" (options.y1),
+ [x2] "{r2}" (options.x2),
+ [y2] "{r3}" (options.y2),
: "memory"
);
}
}
+pub const OvalOptions = struct {
+ x: i32,
+ y: i32,
+ width: u32,
+ height: u32,
+ stroke_color: ?DisplayColor,
+ fill_color: ?DisplayColor,
+};
+
/// Draws an oval (or circle).
-pub inline fn oval(stroke_color: ?DisplayColor, fill_color: ?DisplayColor, x: i32, y: i32, width: u32, height: u32) void {
+pub inline fn oval(options: OvalOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.oval(OptionalDisplayColor.from(stroke_color), OptionalDisplayColor.from(fill_color), x, y, width, height);
+ platform_specific.oval(
+ OptionalDisplayColor.from(options.stroke_color),
+ OptionalDisplayColor.from(options.fill_color),
+ options.x,
+ options.y,
+ options.width,
+ options.height,
+ );
} else {
asm volatile (" svc #3"
:
- : [x] "{r0}" (x),
- [y] "{r1}" (y),
- [width] "{r2}" (width),
- [height] "{r3}" (height),
+ : [x] "{r0}" (options.x),
+ [y] "{r1}" (options.y),
+ [width] "{r2}" (options.width),
+ [height] "{r3}" (options.height),
: "memory"
);
}
}
+pub const RectOptions = struct {
+ x: i32,
+ y: i32,
+ width: u32,
+ height: u32,
+ stroke_color: ?DisplayColor,
+ fill_color: ?DisplayColor,
+};
+
/// Draws a rectangle.
-pub inline fn rect(stroke_color: ?DisplayColor, fill_color: ?DisplayColor, x: i32, y: i32, width: u32, height: u32) void {
+pub inline fn rect(options: RectOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.rect(OptionalDisplayColor.from(stroke_color), OptionalDisplayColor.from(fill_color), x, y, width, height);
+ platform_specific.rect(
+ OptionalDisplayColor.from(options.stroke_color),
+ OptionalDisplayColor.from(options.fill_color),
+ options.x,
+ options.y,
+ options.width,
+ options.height,
+ );
} else {
asm volatile (" svc #4"
:
- : [x] "{r0}" (x),
- [y] "{r1}" (y),
- [width] "{r2}" (width),
- [height] "{r3}" (height),
+ : [x] "{r0}" (options.x),
+ [y] "{r1}" (options.y),
+ [width] "{r2}" (options.width),
+ [height] "{r3}" (options.height),
: "memory"
);
}
}
+pub const TextOptions = struct {
+ str: []const u8,
+ x: i32,
+ y: i32,
+ text_color: DisplayColor,
+ background_color: ?DisplayColor,
+};
+
/// Draws text using the built-in system font.
-pub inline fn text(text_color: DisplayColor, background_color: ?DisplayColor, str: []const u8, x: i32, y: i32) void {
+pub inline fn text(options: TextOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.text(text_color, OptionalDisplayColor.from(background_color), str.ptr, str.len, x, y);
+ platform_specific.text(
+ options.text_color,
+ OptionalDisplayColor.from(options.background_color),
+ options.str.ptr,
+ options.str.len,
+ options.x,
+ options.y,
+ );
} else {
asm volatile (" svc #5"
:
- : [str_ptr] "{r0}" (str.ptr),
- [str_len] "{r1}" (str.len),
- [x] "{r2}" (x),
- [y] "{r3}" (y),
+ : [str_ptr] "{r0}" (options.str.ptr),
+ [str_len] "{r1}" (options.str.len),
+ [x] "{r2}" (options.x),
+ [y] "{r3}" (options.y),
: "memory"
);
}
}
+pub const StraightLineOptions = struct {
+ x: i32,
+ y: i32,
+ len: u32,
+ color: DisplayColor,
+};
+
/// Draws a horizontal line
-pub inline fn hline(color: DisplayColor, x: i32, y: i32, len: u32) void {
+pub inline fn hline(options: StraightLineOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.hline(color, x, y, len);
+ platform_specific.hline(
+ options.color,
+ options.x,
+ options.y,
+ options.len,
+ );
} else {
asm volatile (" svc #7"
:
- : [x] "{r0}" (x),
- [y] "{r1}" (y),
- [len] "{r2}" (len),
+ : [x] "{r0}" (options.x),
+ [y] "{r1}" (options.y),
+ [len] "{r2}" (options.len),
: "memory"
);
}
}
/// Draws a vertical line
-pub inline fn vline(color: DisplayColor, x: i32, y: i32, len: u32) void {
+pub inline fn vline(options: StraightLineOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.vline(color, x, y, len);
+ platform_specific.vline(
+ options.color,
+ options.x,
+ options.y,
+ options.len,
+ );
} else {
asm volatile (" svc #6"
:
- : [x] "{r0}" (x),
- [y] "{r1}" (y),
- [len] "{r2}" (len),
+ : [x] "{r0}" (options.x),
+ [y] "{r1}" (options.y),
+ [len] "{r2}" (options.len),
: "memory"
);
}
}
-pub const ToneFlags = packed struct(u32) {
- pub const Channel = enum(u2) {
- pulse1,
- pulse2,
- triangle,
- noise,
- };
-
- pub const DutyCycle = enum(u2) {
- @"1/8",
- @"1/4",
- @"1/2",
- @"3/4",
- };
-
- pub const Panning = enum(u2) {
- stereo,
- left,
- right,
- };
-
- channel: Channel,
- duty_cycle: DutyCycle,
- panning: Panning,
- padding: u26 = undefined,
-};
-
// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
// │ Sound Functions │
// │ │
// └───────────────────────────────────────────────────────────────────────────┘
+pub const ToneOptions = struct {
+ pub const Flags = packed struct(u32) {
+ pub const Channel = enum(u2) {
+ pulse1,
+ pulse2,
+ triangle,
+ noise,
+ };
+
+ pub const DutyCycle = enum(u2) {
+ @"1/8",
+ @"1/4",
+ @"1/2",
+ @"3/4",
+ };
+
+ pub const Panning = enum(u2) {
+ stereo,
+ left,
+ right,
+ };
+
+ channel: Channel,
+ duty_cycle: DutyCycle,
+ panning: Panning,
+ padding: u26 = undefined,
+ };
+
+ frequency: u32,
+ duration: u32,
+ volume: u32,
+ flags: Flags,
+};
+
/// Plays a sound tone.
-pub inline fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneFlags) void {
+pub inline fn tone(options: ToneOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.tone(frequency, duration, volume, flags);
+ platform_specific.tone(
+ options.frequency,
+ options.duration,
+ options.volume,
+ options.flags,
+ );
} else {
asm volatile (" svc #8"
:
- : [frequency] "{r0}" (frequency),
- [duration] "{r1}" (duration),
- [volume] "{r2}" (volume),
- [flags] "{r3}" (flags),
+ : [frequency] "{r0}" (options.frequency),
+ [duration] "{r1}" (options.duration),
+ [volume] "{r2}" (options.volume),
+ [flags] "{r3}" (options.flags),
);
}
}
diff --git a/src/watch/404.html b/src/watch/404.html
deleted file mode 100644
index 7b98053..0000000
--- a/src/watch/404.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-404
-
-
-404 not found
-
-
diff --git a/src/watch/Reloader.zig b/src/watch/Reloader.zig
index da1edbe..91bc765 100644
--- a/src/watch/Reloader.zig
+++ b/src/watch/Reloader.zig
@@ -4,7 +4,6 @@ const builtin = @import("builtin");
const ws = @import("ws");
const log = std.log.scoped(.watcher);
-const ListenerFn = fn (self: *Reloader, path: []const u8, name: []const u8) void;
const Watcher = switch (builtin.target.os.tag) {
.linux => @import("watcher/LinuxWatcher.zig"),
.macos => @import("watcher/MacosWatcher.zig"),
@@ -15,8 +14,8 @@ const Watcher = switch (builtin.target.os.tag) {
gpa: std.mem.Allocator,
ws_server: ws.Server,
zig_exe: []const u8,
-out_dir_path: []const u8,
watcher: Watcher,
+output_dir_index: usize,
clients_lock: std.Thread.Mutex = .{},
clients: std.AutoArrayHashMapUnmanaged(*ws.Conn, void) = .{},
@@ -24,127 +23,105 @@ clients: std.AutoArrayHashMapUnmanaged(*ws.Conn, void) = .{},
pub fn init(
gpa: std.mem.Allocator,
zig_exe: []const u8,
- out_dir_path: []const u8,
- in_dir_paths: []const []const u8,
+ dirs_to_watch: []const []const u8,
) !Reloader {
const ws_server = try ws.Server.init(gpa, .{});
return .{
.gpa = gpa,
.zig_exe = zig_exe,
- .out_dir_path = out_dir_path,
.ws_server = ws_server,
- .watcher = try Watcher.init(gpa, out_dir_path, in_dir_paths),
+ .watcher = try Watcher.init(gpa, dirs_to_watch),
+ .output_dir_index = dirs_to_watch.len - 1,
};
}
-pub fn listen(self: *Reloader) !void {
- try self.watcher.listen(self.gpa, self);
+pub fn listen(reloader: *Reloader) !void {
+ try reloader.watcher.listen(reloader, onChange);
}
-pub fn onInputChange(self: *Reloader, path: []const u8, name: []const u8) void {
- _ = name;
- _ = path;
- log.debug("re-building!", .{});
- const result = std.ChildProcess.run(.{
- .allocator = self.gpa,
- .argv = &.{ self.zig_exe, "build" },
- }) catch |err| {
- log.err("unable to run zig build: {s}", .{@errorName(err)});
- return;
- };
- defer {
- self.gpa.free(result.stdout);
- self.gpa.free(result.stderr);
- }
-
- if (result.stdout.len > 0) {
- log.info("zig build stdout: {s}", .{result.stdout});
- }
+pub fn onChange(reloader: *Reloader, dir_that_changed: usize) void {
+ if (dir_that_changed == reloader.output_dir_index) {
+ std.log.info("Output changed", .{});
- if (result.stderr.len > 0) {
- std.debug.print("{s}\n\n", .{result.stderr});
- } else {
- std.debug.print("File change triggered a successful build.\n", .{});
- }
+ reloader.clients_lock.lock();
+ defer reloader.clients_lock.unlock();
- self.clients_lock.lock();
- defer self.clients_lock.unlock();
+ var idx: usize = 0;
+ while (idx < reloader.clients.entries.len) {
+ const conn = reloader.clients.entries.get(idx).key;
- var idx: usize = 0;
- while (idx < self.clients.entries.len) {
- const conn = self.clients.entries.get(idx).key;
+ conn.write("reload") catch |err| {
+ log.debug("error writing to websocket: {s}", .{
+ @errorName(err),
+ });
+ reloader.clients.swapRemoveAt(idx);
+ continue;
+ };
- const BuildCommand = struct {
- command: []const u8 = "build",
- err: []const u8,
- };
-
- const cmd: BuildCommand = .{ .err = result.stderr };
-
- var buf = std.ArrayList(u8).init(self.gpa);
- defer buf.deinit();
-
- std.json.stringify(cmd, .{}, buf.writer()) catch {
- log.err("unable to generate ws message", .{});
- return;
- };
-
- conn.write(buf.items) catch |err| {
- log.debug("error writing to websocket: {s}", .{
- @errorName(err),
- });
- self.clients.swapRemoveAt(idx);
- continue;
- };
+ idx += 1;
+ }
+ } else {
+ std.log.info("Input changed", .{});
- idx += 1;
- }
-}
-pub fn onOutputChange(self: *Reloader, path: []const u8, name: []const u8) void {
- if (std.mem.indexOfScalar(u8, name, '.') == null) {
- return;
- }
- log.debug("reload: {s}/{s}!", .{ path, name });
-
- self.clients_lock.lock();
- defer self.clients_lock.unlock();
-
- std.log.info("{d}", .{self.clients.entries.len});
-
- var idx: usize = 0;
- while (idx < self.clients.entries.len) {
- const conn = self.clients.entries.get(idx).key;
-
- const msg_fmt =
- \\{{
- \\ "command":"reload",
- \\ "path":"{s}/{s}"
- \\}}
- ;
-
- var buf: [4096]u8 = undefined;
- const msg = std.fmt.bufPrint(&buf, msg_fmt, .{
- path[self.out_dir_path.len..],
- name,
- }) catch {
- log.err("unable to generate ws message", .{});
+ const result = std.ChildProcess.run(.{
+ .allocator = reloader.gpa,
+ .argv = &.{ reloader.zig_exe, "build" },
+ }) catch |err| {
+ log.err("unable to run zig build: {s}", .{@errorName(err)});
return;
};
-
- conn.write(msg) catch |err| {
- log.debug("error writing to websocket: {s}", .{
- @errorName(err),
- });
- self.clients.swapRemoveAt(idx);
- continue;
- };
-
- idx += 1;
+ defer {
+ reloader.gpa.free(result.stdout);
+ reloader.gpa.free(result.stderr);
+ }
+
+ if (result.stdout.len > 0) {
+ log.info("zig build stdout: {s}", .{result.stdout});
+ }
+
+ if (result.stderr.len > 0) {
+ std.debug.print("{s}\n\n", .{result.stderr});
+ } else {
+ std.debug.print("File change triggered a successful build.\n", .{});
+ }
+
+ reloader.clients_lock.lock();
+ defer reloader.clients_lock.unlock();
+
+ var idx: usize = 0;
+ while (idx < reloader.clients.entries.len) {
+ const conn = reloader.clients.entries.get(idx).key;
+
+ const BuildCommand = struct {
+ command: []const u8 = "build",
+ err: []const u8,
+ };
+
+ const cmd: BuildCommand = .{ .err = result.stderr };
+
+ var buf = std.ArrayList(u8).init(reloader.gpa);
+ defer buf.deinit();
+
+ std.json.stringify(cmd, .{}, buf.writer()) catch {
+ log.err("unable to generate ws message", .{});
+ return;
+ };
+
+ conn.write(buf.items) catch |err| {
+ log.debug("error writing to websocket: {s}", .{
+ @errorName(err),
+ });
+ reloader.clients.swapRemoveAt(idx);
+ continue;
+ };
+
+ idx += 1;
+ }
}
}
-pub fn handleWs(self: *Reloader, stream: std.net.Stream, h: [20]u8) void {
+pub fn handleWs(reloader: *Reloader, stream: std.net.Stream, h: [20]u8) void {
var buf =
("HTTP/1.1 101 Switching Protocols\r\n" ++
"Access-Control-Allow-Origin: *\r\n" ++
@@ -157,13 +134,13 @@ pub fn handleWs(self: *Reloader, stream: std.net.Stream, h: [20]u8) void {
stream.writeAll(&buf) catch @panic("bad");
- // var conn = self.ws_server.newConn(stream);
- const conn = self.gpa.create(ws.Conn) catch @panic("bad");
- conn.* = self.ws_server.newConn(stream);
+ // var conn = reloader.ws_server.newConn(stream);
+ const conn = reloader.gpa.create(ws.Conn) catch @panic("bad");
+ conn.* = reloader.ws_server.newConn(stream);
- var context: Handler.Context = .{ .watcher = self };
+ var context: Handler.Context = .{ .watcher = reloader };
var handler = Handler.init(undefined, conn, &context) catch @panic("bad");
- self.ws_server.handle(Handler, &handler, conn);
+ reloader.ws_server.handle(Handler, &handler, conn);
}
const Handler = struct {
@@ -188,16 +165,16 @@ const Handler = struct {
};
}
- pub fn handle(self: *Handler, message: ws.Message) !void {
- _ = self;
+ pub fn handle(handler: *Handler, message: ws.Message) !void {
+ _ = handler;
_ = message;
}
- pub fn close(self: *Handler) void {
+ pub fn close(handler: *Handler) void {
log.debug("ws connection was closed\n", .{});
- const watcher = self.context.watcher;
+ const watcher = handler.context.watcher;
watcher.clients_lock.lock();
defer watcher.clients_lock.unlock();
- _ = watcher.clients.swapRemove(self.conn);
+ _ = watcher.clients.swapRemove(handler.conn);
}
};
diff --git a/src/watch/main.zig b/src/watch/main.zig
index 5d65e16..a0908f4 100644
--- a/src/watch/main.zig
+++ b/src/watch/main.zig
@@ -3,13 +3,12 @@ const fs = std.fs;
const mime = @import("mime");
const Allocator = std.mem.Allocator;
const Reloader = @import("Reloader.zig");
-const not_found_html = @embedFile("404.html");
const assert = std.debug.assert;
const log = std.log.scoped(.server);
-pub const std_options: std.Options = .{
- .log_level = .err,
-};
+// pub const std_options: std.Options = .{
+// .log_level = .err,
+// };
const usage =
\\usage: zine serve [options]
@@ -31,25 +30,13 @@ const Server = struct {
s.* = undefined;
}
- fn handleRequest(s: *Server, req: *std.http.Server.Request) !bool {
+ fn handleRequest(s: *Server, req: *std.http.Server.Request, cart_path: []const u8) !bool {
var arena_impl = std.heap.ArenaAllocator.init(general_purpose_allocator.allocator());
defer arena_impl.deinit();
const arena = arena_impl.allocator();
var path = req.head.target;
- if (std.mem.indexOf(u8, path, "..")) |_| {
- std.debug.print("'..' not allowed in URLs\n", .{});
- @panic("TODO: check if '..' is fine");
- }
-
- if (std.mem.endsWith(u8, path, "/")) {
- path = try std.fmt.allocPrint(arena, "{s}{s}", .{
- path,
- "index.html",
- });
- }
-
if (std.mem.eql(u8, path, "/ws")) {
var it = req.iterateHeaders();
const key = while (it.next()) |header| {
@@ -85,63 +72,66 @@ const Server = struct {
const mime_type = mime.extension_map.get(ext) orelse
.@"application/octet-stream";
- const file = s.zig_out_bin_dir.openFile(path[1..], .{}) catch |err| switch (err) {
- error.FileNotFound => {
- if (std.mem.endsWith(u8, req.head.target, "/")) {
- try req.respond(not_found_html, .{
- .status = .not_found,
+ if (std.mem.eql(u8, path, "/cart.wasm")) {
+ const file = s.zig_out_bin_dir.openFile(std.fs.path.basename(cart_path), .{}) catch |err| switch (err) {
+ error.FileNotFound => {
+ if (std.mem.endsWith(u8, req.head.target, "/")) {
+ try req.respond("404", .{
+ .status = .not_found,
+ .extra_headers = &.{
+ .{ .name = "content-type", .value = "text/plain" },
+ .{ .name = "connection", .value = "close" },
+ .{ .name = "access-control-allow-origin", .value = "*" },
+ },
+ });
+ log.debug("not found\n", .{});
+ return false;
+ } else {
+ try appendSlashRedirect(arena, req);
+ return false;
+ }
+ },
+ else => {
+ const message = try std.fmt.allocPrint(
+ arena,
+ "error accessing the resource: {s}",
+ .{
+ @errorName(err),
+ },
+ );
+ try req.respond(message, .{
+ .status = .internal_server_error,
.extra_headers = &.{
.{ .name = "content-type", .value = "text/html" },
.{ .name = "connection", .value = "close" },
.{ .name = "access-control-allow-origin", .value = "*" },
},
});
- log.debug("not found\n", .{});
+ log.debug("error: {s}\n", .{@errorName(err)});
return false;
- } else {
+ },
+ };
+ defer file.close();
+
+ const contents = file.readToEndAlloc(arena, std.math.maxInt(usize)) catch |err| switch (err) {
+ error.IsDir => {
try appendSlashRedirect(arena, req);
return false;
- }
- },
- else => {
- const message = try std.fmt.allocPrint(
- arena,
- "error accessing the resource: {s}",
- .{
- @errorName(err),
- },
- );
- try req.respond(message, .{
- .status = .internal_server_error,
- .extra_headers = &.{
- .{ .name = "content-type", .value = "text/html" },
- .{ .name = "connection", .value = "close" },
- .{ .name = "access-control-allow-origin", .value = "*" },
- },
- });
- log.debug("error: {s}\n", .{@errorName(err)});
- return false;
- },
- };
- defer file.close();
+ },
+ else => return err,
+ };
+
+ try req.respond(contents, .{
+ .status = .ok,
+ .extra_headers = &.{
+ .{ .name = "content-type", .value = @tagName(mime_type) },
+ .{ .name = "connection", .value = "close" },
+ .{ .name = "access-control-allow-origin", .value = "*" },
+ },
+ });
+ log.debug("sent file\n", .{});
+ }
- const contents = file.readToEndAlloc(arena, std.math.maxInt(usize)) catch |err| switch (err) {
- error.IsDir => {
- try appendSlashRedirect(arena, req);
- return false;
- },
- else => return err,
- };
-
- try req.respond(contents, .{
- .status = .ok,
- .extra_headers = &.{
- .{ .name = "content-type", .value = @tagName(mime_type) },
- .{ .name = "connection", .value = "close" },
- .{ .name = "access-control-allow-origin", .value = "*" },
- },
- });
- log.debug("sent file\n", .{});
return false;
}
};
@@ -155,11 +145,11 @@ fn appendSlashRedirect(
"{s}/",
.{req.head.target},
);
- try req.respond(not_found_html, .{
+ try req.respond("404", .{
.status = .see_other,
.extra_headers = &.{
.{ .name = "location", .value = location },
- .{ .name = "content-type", .value = "text/html" },
+ .{ .name = "content-type", .value = "text/plain" },
.{ .name = "connection", .value = "close" },
.{ .name = "access-control-allow-origin", .value = "*" },
},
@@ -190,37 +180,39 @@ fn fatal(comptime format: []const u8, args: anytype) noreturn {
}
fn cmdServe(gpa: Allocator, args: []const []const u8) !void {
- var zig_out_bin_path: ?[]const u8 = null;
- var input_dirs: std.ArrayListUnmanaged([]const u8) = .{};
+ std.log.info("{s}", .{args});
+
+ var cart_path: ?[]const u8 = null;
+ var dirs_to_watch: std.ArrayListUnmanaged([]const u8) = .{};
const zig_exe = args[0];
{
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
- if (std.mem.eql(u8, arg, "--zig-out-bin-dir")) {
+ if (std.mem.eql(u8, arg, "--cart")) {
i += 1;
if (i >= args.len) fatal("expected arg after '{s}'", .{arg});
- zig_out_bin_path = args[i];
+ cart_path = args[i];
} else if (std.mem.eql(u8, arg, "--input-dir")) {
i += 1;
if (i >= args.len) fatal("expected arg after '{s}'", .{arg});
- try input_dirs.append(gpa, args[i]);
+ try dirs_to_watch.append(gpa, args[i]);
} else {
fatal("unrecognized arg: '{s}'", .{arg});
}
}
}
- // ensure the path exists. without this, an empty website that
- // doesn't generate a zig-out/ will cause the server to error out
- try fs.cwd().makePath(zig_out_bin_path.?);
+ const zig_out_bin_path = std.fs.path.dirname(cart_path.?).?;
+ try fs.cwd().makePath(zig_out_bin_path);
- var zig_out_bin_dir: fs.Dir = fs.cwd().openDir(zig_out_bin_path.?, .{ .iterate = true }) catch |e|
- fatal("unable to open directory '{s}': {s}", .{ zig_out_bin_path.?, @errorName(e) });
+ var zig_out_bin_dir: fs.Dir = fs.cwd().openDir(zig_out_bin_path, .{ .iterate = true }) catch |e|
+ fatal("unable to open directory '{s}': {s}", .{ zig_out_bin_path, @errorName(e) });
defer zig_out_bin_dir.close();
- var watcher = try Reloader.init(gpa, zig_exe, zig_out_bin_path.?, input_dirs.items);
+ try dirs_to_watch.append(gpa, zig_out_bin_path);
+ var watcher = try Reloader.init(gpa, zig_exe, dirs_to_watch.items);
var server: Server = .{
.watcher = &watcher,
@@ -231,10 +223,10 @@ fn cmdServe(gpa: Allocator, args: []const []const u8) !void {
const watch_thread = try std.Thread.spawn(.{}, Reloader.listen, .{&watcher});
watch_thread.detach();
- try serve(&server, 2468);
+ try serve(&server, cart_path.?, 2468);
}
-fn serve(s: *Server, listen_port: u16) !void {
+fn serve(s: *Server, cart_path: []const u8, listen_port: u16) !void {
const address = try std.net.Address.parseIp("127.0.0.1", listen_port);
var tcp_server = try address.listen(.{
.reuse_port = true,
@@ -245,7 +237,7 @@ fn serve(s: *Server, listen_port: u16) !void {
const server_port = tcp_server.listen_address.in.getPort();
std.debug.assert(server_port == listen_port);
- std.debug.print("\x1b[2K\rSimulator live! Go to [link] to test your cartridge.\n", .{});
+ std.debug.print("\x1b[2K\rSimulator live! Go to https://badgesim.microzig.tech/ to test your cartridge.\n", .{});
var buffer: [1024]u8 = undefined;
accept: while (true) {
@@ -271,7 +263,7 @@ fn serve(s: *Server, listen_port: u16) !void {
continue :accept;
};
- became_websocket = s.handleRequest(&request) catch |err| {
+ became_websocket = s.handleRequest(&request, cart_path) catch |err| {
log.debug("failed request: {s}", .{@errorName(err)});
continue :accept;
};
diff --git a/src/watch/watcher/WindowsWatcher.zig b/src/watch/watcher/WindowsWatcher.zig
index 04e3e30..65df7af 100644
--- a/src/watch/watcher/WindowsWatcher.zig
+++ b/src/watch/watcher/WindowsWatcher.zig
@@ -4,223 +4,79 @@ const std = @import("std");
const windows = std.os.windows;
const Reloader = @import("../Reloader.zig");
-const log = std.log.scoped(.watcher);
+handles: std.ArrayListUnmanaged(windows.HANDLE) = .{},
-const Error = error{ InvalidHandle, QueueFailed, WaitFailed };
+pub fn init(gpa: std.mem.Allocator, paths: []const []const u8) error{ InvalidHandle, OutOfMemory }!WindowsWatcher {
+ var watcher = WindowsWatcher{};
+ errdefer watcher.deinit(gpa);
-const CompletionKey = usize;
-/// Values should be a multiple of `ReadBufferEntrySize`
-const ReadBufferIndex = u32;
-const ReadBufferEntrySize = 1024;
+ try watcher.handles.ensureUnusedCapacity(gpa, paths.len);
-const WatchEntry = struct {
- kind: Kind,
+ var path_buf = std.ArrayListUnmanaged(u8){};
+ defer path_buf.deinit(gpa);
- dir_path: [:0]const u8,
- dir_handle: windows.HANDLE,
+ for (paths) |path| {
+ try path_buf.ensureUnusedCapacity(gpa, path.len + 1);
+ path_buf.appendSliceAssumeCapacity(path);
+ path_buf.appendAssumeCapacity(0);
- overlap: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED),
- buf_idx: ReadBufferIndex,
-
- pub const Kind = enum { input, output };
-};
-
-iocp_port: windows.HANDLE,
-entries: std.AutoHashMap(CompletionKey, WatchEntry),
-read_buffer: []u8,
-
-pub fn init(
- gpa: std.mem.Allocator,
- out_dir_path: []const u8,
- in_dir_paths: []const []const u8,
-) !WindowsWatcher {
- var watcher = WindowsWatcher{
- .iocp_port = windows.INVALID_HANDLE_VALUE,
- .entries = std.AutoHashMap(CompletionKey, WatchEntry).init(gpa),
- .read_buffer = undefined,
- };
- errdefer {
- var iter = watcher.entries.valueIterator();
- while (iter.next()) |entry| {
- windows.CloseHandle(entry.dir_handle);
- gpa.free(entry.dir_path);
- }
- watcher.entries.deinit();
- }
-
- // Doubles as the number of WatchEntries
- var comp_key: CompletionKey = 0;
-
- {
- const out_path = try gpa.dupeZ(u8, out_dir_path);
- try watcher.entries.putNoClobber(
- comp_key,
- try addPath(out_path, comp_key, .output, &watcher.iocp_port),
- );
- comp_key += 1;
- }
-
- for (in_dir_paths) |path| {
- const in_path = try gpa.dupeZ(u8, path);
- try watcher.entries.putNoClobber(
- comp_key,
- try addPath(in_path, comp_key, .input, &watcher.iocp_port),
+ const handle = FindFirstChangeNotificationA(
+ @ptrCast(path_buf.items),
+ windows.TRUE,
+ windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME,
);
- comp_key += 1;
+ if (handle == std.os.windows.INVALID_HANDLE_VALUE) return error.InvalidHandle;
+ watcher.handles.appendAssumeCapacity(handle);
}
- watcher.read_buffer = try gpa.alloc(u8, ReadBufferEntrySize * comp_key);
-
- // Here we need pointers to both the read_buffer and entry overlapped structs,
- // which we can only do after setting up everything else.
- watcher.entries.lockPointers();
- for (0..comp_key) |key| {
- const entry = watcher.entries.getPtr(key).?;
- if (windows.kernel32.ReadDirectoryChangesW(
- entry.dir_handle,
- @ptrCast(@alignCast(&watcher.read_buffer[entry.buf_idx])),
- ReadBufferEntrySize,
- @intFromBool(true),
- windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME,
- null,
- &entry.overlap,
- null,
- ) == 0) {
- log.err("ReadDirectoryChanges error: {s}", .{@tagName(windows.kernel32.GetLastError())});
- return Error.QueueFailed;
- }
- }
return watcher;
}
-fn addPath(
- path: [:0]const u8,
- /// Assumed to increment by 1 after each invocation, starting at 0.
- key: CompletionKey,
- io: WatchEntry.Kind,
- port: *windows.HANDLE,
-) !WatchEntry {
- const dir_handle = CreateFileA(
- path,
- windows.GENERIC_READ, // FILE_LIST_DIRECTORY,
- windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE,
- null,
- windows.OPEN_EXISTING,
- windows.FILE_FLAG_BACKUP_SEMANTICS | windows.FILE_FLAG_OVERLAPPED,
- null,
- );
- if (dir_handle == windows.INVALID_HANDLE_VALUE) {
- log.err(
- "Unable to open directory {s}: {s}",
- .{ path, @tagName(windows.kernel32.GetLastError()) },
- );
- return Error.InvalidHandle;
+pub fn deinit(watcher: *WindowsWatcher, gpa: std.mem.Allocator) void {
+ for (watcher.handles.items) |handle| {
+ windows.CloseHandle(handle);
}
-
- if (port.* == windows.INVALID_HANDLE_VALUE) {
- port.* = try windows.CreateIoCompletionPort(dir_handle, null, key, 0);
- } else {
- _ = try windows.CreateIoCompletionPort(dir_handle, port.*, key, 0);
- }
-
- return .{
- .kind = io,
- .dir_path = path,
- .dir_handle = dir_handle,
- .buf_idx = @intCast(ReadBufferEntrySize * key),
- };
+ watcher.handles.deinit(gpa);
}
pub fn listen(
- self: *WindowsWatcher,
- gpa: std.mem.Allocator,
- reloader: *Reloader,
-) !void {
- _ = gpa;
-
- var dont_care: struct {
- bytes_transferred: windows.DWORD = undefined,
- overlap: ?*windows.OVERLAPPED = undefined,
- } = .{};
-
- var key: CompletionKey = undefined;
- while (true) {
- // Waits here until any of the directory handles associated with the iocp port
- // have been updated.
- const wait_result = windows.GetQueuedCompletionStatus(
- self.iocp_port,
- &dont_care.bytes_transferred,
- &key,
- &dont_care.overlap,
- windows.INFINITE,
- );
- if (wait_result != .Normal) {
- log.err("GetQueuedCompletionStatus error: {s}", .{@tagName(wait_result)});
- return Error.WaitFailed;
- }
-
- const entry = self.entries.getPtr(key) orelse @panic("Invalid CompletionKey");
-
- var info_iter = windows.FileInformationIterator(FILE_NOTIFY_INFORMATION){
- .buf = self.read_buffer[entry.buf_idx..][0..ReadBufferEntrySize],
+ watcher: *WindowsWatcher,
+ context: anytype,
+ callback: fn (@TypeOf(context), changed_handle: usize) void,
+) error{ UnknownWaitStatus, NextChangeFailed, WaitAbandoned, Unexpected }!void {
+ wait_loop: while (true) {
+ const status = windows.WaitForMultipleObjectsEx(watcher.handles.items, false, windows.INFINITE, false) catch |err| switch (err) {
+ error.WaitTimeOut => unreachable,
+ else => |e| return e,
};
- var path_buf: [windows.MAX_PATH]u8 = undefined;
- while (info_iter.next()) |info| {
- const filename: []const u8 = blk: {
- const n = try std.unicode.utf16LeToUtf8(
- &path_buf,
- @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2],
- );
- break :blk path_buf[0..n];
- };
-
- const args = .{ @tagName(entry.kind), entry.dir_path, filename };
- switch (info.Action) {
- windows.FILE_ACTION_ADDED => log.debug("added ({s}) {s}/{s}", args),
- windows.FILE_ACTION_REMOVED => log.debug("removed ({s}) {s}/{s}", args),
- windows.FILE_ACTION_MODIFIED => log.debug("modified ({s}) {s}/{s}", args),
- windows.FILE_ACTION_RENAMED_OLD_NAME => log.debug("renamed_old_name ({s}) {s}/{s}", args),
- windows.FILE_ACTION_RENAMED_NEW_NAME => log.debug("renamed_new_name ({s}) {s}/{s}", args),
- else => log.debug("Unknown Action ({s}) {s}/{s}", args),
- }
- switch (entry.kind) {
- .input => reloader.onInputChange(entry.dir_path, filename),
- .output => reloader.onOutputChange(entry.dir_path, filename),
+ for (watcher.handles.items, 0..) |handle, offset| {
+ if (status == windows.WAIT_OBJECT_0 + offset) {
+ callback(context, offset);
+ // Stop multifiring
+ while (true) {
+ if (FindNextChangeNotification(handle) == windows.FALSE) return error.NextChangeFailed;
+ const status_2 = windows.WaitForMultipleObjectsEx(&.{handle}, false, 10, false) catch |err| switch (err) {
+ error.WaitTimeOut => break,
+ else => |e| return e,
+ };
+ if (status_2 != windows.WAIT_OBJECT_0) return error.UnknownWaitStatus;
+ }
+ if (FindNextChangeNotification(handle) == windows.FALSE) return error.NextChangeFailed;
+ continue :wait_loop;
}
}
- // Re-queue the directory entry
- if (windows.kernel32.ReadDirectoryChangesW(
- entry.dir_handle,
- @ptrCast(@alignCast(&self.read_buffer[entry.buf_idx])),
- ReadBufferEntrySize,
- @intFromBool(true),
- windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME,
- null,
- &entry.overlap,
- null,
- ) == 0) {
- log.err("ReadDirectoryChanges error: {s}", .{@tagName(windows.kernel32.GetLastError())});
- return Error.QueueFailed;
- }
+ return error.UnknownWaitStatus;
}
}
-const FILE_NOTIFY_INFORMATION = extern struct {
- NextEntryOffset: windows.DWORD,
- Action: windows.DWORD,
- FileNameLength: windows.DWORD,
- /// Flexible array member
- FileName: windows.WCHAR,
-};
-
-extern "kernel32" fn CreateFileA(
- lpFileName: windows.LPCSTR,
- dwDesiredAccess: windows.DWORD,
- dwShareMode: windows.DWORD,
- lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES,
- dwCreationDisposition: windows.DWORD,
- dwFlagsAndAttributes: windows.DWORD,
- hTemplateFile: ?windows.HANDLE,
+extern fn FindFirstChangeNotificationA(
+ lpPathName: windows.LPCSTR,
+ bWatchSubtree: windows.BOOL,
+ dwNotifyFilter: windows.DWORD,
) callconv(windows.WINAPI) windows.HANDLE;
+
+extern fn FindNextChangeNotification(
+ handle: windows.HANDLE,
+) callconv(windows.WINAPI) windows.BOOL;
From a0b17149725115d63511065a7cadca27309e2dd9 Mon Sep 17 00:00:00 2001
From: SuperAuguste <19855629+SuperAuguste@users.noreply.github.com>
Date: Tue, 30 Apr 2024 11:35:59 -0400
Subject: [PATCH 2/6] Implement blit
---
build.zig | 2 +-
samples/feature_test.zig | 14 +++++
simulator/src/framebuffer.ts | 47 +++++++++++++++-
simulator/src/runtime.ts | 20 ++-----
src/cart/api.zig | 82 +++++++++++-----------------
src/watch/Reloader.zig | 2 +-
src/watch/watcher/LinuxWatcher.zig | 70 +++++++-----------------
src/watch/watcher/WindowsWatcher.zig | 3 +
8 files changed, 123 insertions(+), 117 deletions(-)
diff --git a/build.zig b/build.zig
index a4e252e..fe5ae08 100644
--- a/build.zig
+++ b/build.zig
@@ -64,7 +64,7 @@ pub fn build(b: *Build) void {
//
// TODO: parameterize:
const watch_run = b.addRunArtifact(watch);
- watch_run.addArgs(&.{ "serve", b.graph.zig_exe, "--input-dir", b.pathFromRoot("samples"), "--cart", "zig-out/bin/feature_test.wasm" });
+ watch_run.addArgs(&.{ "serve", b.graph.zig_exe, "--input-dir", b.pathFromRoot("samples"), "--cart", b.pathFromRoot("zig-out/bin/feature_test.wasm") });
const watch_step = b.step("watch", "");
watch_step.dependOn(&watch_run.step);
diff --git a/samples/feature_test.zig b/samples/feature_test.zig
index 75e4cb9..0d56b3c 100644
--- a/samples/feature_test.zig
+++ b/samples/feature_test.zig
@@ -84,6 +84,20 @@ export fn update() void {
// TODO: blit, blitSub
+ cart.blit(.{
+ .sprite = &.{
+ .{ .red = 31, .green = 0, .blue = 0 },
+ .{ .red = 0, .green = 0, .blue = 31 },
+ .{ .red = 31, .green = 0, .blue = 0 },
+ .{ .red = 0, .green = 0, .blue = 31 },
+ },
+ .x = 40,
+ .y = 40,
+ .width = 2,
+ .height = 2,
+ .flags = .{},
+ });
+
cart.line(.{
.x1 = 50,
.y1 = 50,
diff --git a/simulator/src/framebuffer.ts b/simulator/src/framebuffer.ts
index 674a650..a6c68dc 100644
--- a/simulator/src/framebuffer.ts
+++ b/simulator/src/framebuffer.ts
@@ -238,7 +238,7 @@ export class Framebuffer {
y += 8;
currentX = x;
} else if (charCode >= 32 && charCode <= 255) {
- this.blit([textColor, backgroundColor], FONT, currentX, y, 8, 8, 0, (charCode - 32) << 3, 8);
+ this.blitPalette([textColor, backgroundColor], FONT, currentX, y, 8, 8, 0, (charCode - 32) << 3, 8);
currentX += 8;
} else {
currentX += 8;
@@ -246,7 +246,7 @@ export class Framebuffer {
}
}
- blit (
+ blitPalette (
colors: [number, number] | [number, number, number, number],
sprite: Uint8Array,
dstX: number, dstY: number,
@@ -302,4 +302,47 @@ export class Framebuffer {
}
}
}
+
+ blit(
+ sprite: Uint16Array,
+ dstX: number, dstY: number,
+ width: number, height: number,
+ srcX: number, srcY: number,
+ srcStride: number,
+ flipX: number | boolean = false,
+ flipY: number | boolean = false,
+ rotate: number | boolean = false
+ ) {
+ // Clip rectangle to screen
+ let clipXMin, clipYMin, clipXMax, clipYMax;
+ if (rotate) {
+ flipX = !flipX;
+ clipXMin = Math.max(0, dstY) - dstY;
+ clipYMin = Math.max(0, dstX) - dstX;
+ clipXMax = Math.min(width, HEIGHT - dstY);
+ clipYMax = Math.min(height, WIDTH - dstX);
+ } else {
+ clipXMin = Math.max(0, dstX) - dstX;
+ clipYMin = Math.max(0, dstY) - dstY;
+ clipXMax = Math.min(width, WIDTH - dstX);
+ clipYMax = Math.min(height, HEIGHT - dstY);
+ }
+
+ // Iterate pixels in rectangle
+ for (let y = clipYMin; y < clipYMax; y++) {
+ for (let x = clipXMin; x < clipXMax; x++) {
+ // Calculate sprite target coords
+ const tx = dstX + (rotate ? y : x);
+ const ty = dstY + (rotate ? x : y);
+
+ // Calculate sprite source coords
+ const sx = srcX + (flipX ? width - x - 1 : x);
+ const sy = srcY + (flipY ? height - y - 1 : y);
+
+ const index = sy * srcStride + sx;
+
+ this.drawPoint(sprite[index], tx, ty);
+ }
+ }
+ }
}
diff --git a/simulator/src/runtime.ts b/simulator/src/runtime.ts
index 230f380..a1b9a92 100644
--- a/simulator/src/runtime.ts
+++ b/simulator/src/runtime.ts
@@ -123,7 +123,6 @@ export class Runtime {
text: this.text.bind(this),
blit: this.blit.bind(this),
- blit_sub: this.blitSub.bind(this),
tone: this.apu.tone.bind(this.apu),
@@ -165,20 +164,13 @@ export class Runtime {
this.framebuffer.drawText(textColor, backgroundColor, text, x, y);
}
- blit (colorsPtr: number, spritePtr: number, x: number, y: number, width: number, height: number, flags: number) {
- this.blitSub(colorsPtr, spritePtr, x, y, width, height, 0, 0, width, flags);
- }
-
- blitSub (colorsPtr: number, spritePtr: number, x: number, y: number, width: number, height: number, srcX: number, srcY: number, stride: number, flags: number) {
- const sprite = new Uint8Array(this.memory.buffer, spritePtr);
- const bpp2 = (flags & 1);
- const flipX = (flags & 2);
- const flipY = (flags & 4);
- const rotate = (flags & 8);
-
- const colors = new Uint16Array(this.memory.buffer, colorsPtr);
+ blit (spritePtr: number, x: number, y: number, width: number, height: number, srcX: number, srcY: number, stride: number, flags: number) {
+ const sprite = new Uint16Array(this.memory.buffer, spritePtr);
+ const flipX = (flags & 1);
+ const flipY = (flags & 2);
+ const rotate = (flags & 4);
- this.framebuffer.blit(bpp2 ? [colors[0], colors[1], colors[2], colors[3]] : [colors[0], colors[1]], sprite, x, y, width, height, srcX, srcY, stride, flipX, flipY, rotate);
+ this.framebuffer.blit(sprite, x, y, width, height, srcX, srcY, stride, flipX, flipY, rotate);
}
read_flash (offset: number, dstPtr: number, length: number): number {
diff --git a/src/cart/api.zig b/src/cart/api.zig
index 0948576..4f1b778 100644
--- a/src/cart/api.zig
+++ b/src/cart/api.zig
@@ -69,8 +69,7 @@ pub const framebuffer: *[screen_width * screen_height]DisplayColor = @ptrFromInt
const platform_specific = if (builtin.target.isWasm())
struct {
- extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, flags: BlitFlags) void;
- extern fn blit_sub(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: BlitFlags) void;
+ extern fn blit(sprite: [*]const DisplayColor, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: BlitOptions.Flags) void;
extern fn line(color: DisplayColor, x1: i32, y1: i32, x2: i32, y2: i32) void;
extern fn oval(stroke_color: OptionalDisplayColor, fill_color: OptionalDisplayColor, x: i32, y: i32, width: u32, height: u32) void;
extern fn rect(stroke_color: OptionalDisplayColor, fill_color: OptionalDisplayColor, x: i32, y: i32, width: u32, height: u32) void;
@@ -94,23 +93,42 @@ comptime {
_ = platform_specific;
}
-pub const BitsPerPixel = enum(u1) { one, two };
-pub const BlitFlags = packed struct(u32) {
- bits_per_pixel: BitsPerPixel = .one,
- flip_x: bool = false,
- flip_y: bool = false,
- rotate: bool = false,
- padding: u28 = undefined,
+pub const BlitOptions = struct {
+ pub const Flags = packed struct(u32) {
+ flip_x: bool = false,
+ flip_y: bool = false,
+ rotate: bool = false,
+ padding: u29 = undefined,
+ };
+
+ sprite: [*]const DisplayColor,
+ x: i32,
+ y: i32,
+ width: u32,
+ height: u32,
+ /// x within the sprite atlas.
+ src_x: i32 = 0,
+ /// y within the sprite atlas.
+ src_y: i32 = 0,
+ /// Width of the entire sprite atlas.
+ stride: ?u32 = null,
+ flags: Flags,
};
/// Copies pixels to the framebuffer.
-/// colors.len >= 2 for flags.bits_per_pixel == .one
-/// colors.len >= 4 for flags.bits_per_pixel == .two
-/// TODO: this is super unsafe also blit is just a basic wrapper over blitSub
-pub inline fn blit(colors: [*]const DisplayColor, sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, flags: BlitFlags) void {
- _ = colors;
+pub inline fn blit(options: BlitOptions) void {
if (comptime builtin.target.isWasm()) {
- platform_specific.blit(sprite, x, y, width, height, flags);
+ platform_specific.blit(
+ options.sprite,
+ options.x,
+ options.y,
+ options.width,
+ options.height,
+ options.src_x,
+ options.src_y,
+ options.stride orelse options.width,
+ options.flags,
+ );
} else {
@compileError("TODO");
// const rest: extern struct {
@@ -133,40 +151,6 @@ pub inline fn blit(colors: [*]const DisplayColor, sprite: [*]const u8, x: i32, y
}
}
-/// Copies a subregion within a larger sprite atlas to the framebuffer.
-/// colors.len >= 2 for flags.bits_per_pixel == .one
-/// colors.len >= 4 for flags.bits_per_pixel == .two
-/// TODO: this is super unsafe also blit is just a basic wrapper over blitSub
-pub inline fn blit_sub(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: BlitFlags) void {
- if (comptime builtin.target.isWasm()) {
- platform_specific.blit_sub(sprite, x, y, width, height, src_x, src_y, stride, flags);
- } else {
- const rest: extern struct {
- width: u32,
- height: u32,
- src_x: u32,
- src_y: u32,
- stride: u32,
- flags: u32,
- } = .{
- .width = width,
- .height = height,
- .src_x = src_x,
- .src_y = src_y,
- .stride = stride,
- .flags = flags,
- };
- asm volatile (" svc #1"
- :
- : [sprite] "{r0}" (sprite),
- [x] "{r1}" (x),
- [y] "{r2}" (y),
- [rest] "{r3}" (&rest),
- : "memory"
- );
- }
-}
-
pub const LineOptions = struct {
x1: i32,
y1: i32,
diff --git a/src/watch/Reloader.zig b/src/watch/Reloader.zig
index 91bc765..c753366 100644
--- a/src/watch/Reloader.zig
+++ b/src/watch/Reloader.zig
@@ -37,7 +37,7 @@ pub fn init(
}
pub fn listen(reloader: *Reloader) !void {
- try reloader.watcher.listen(reloader, onChange);
+ try reloader.watcher.listen(reloader.gpa, reloader, onChange);
}
pub fn onChange(reloader: *Reloader, dir_that_changed: usize) void {
diff --git a/src/watch/watcher/LinuxWatcher.zig b/src/watch/watcher/LinuxWatcher.zig
index 95d41be..e4bb522 100644
--- a/src/watch/watcher/LinuxWatcher.zig
+++ b/src/watch/watcher/LinuxWatcher.zig
@@ -16,26 +16,18 @@ children_fds: std.AutoHashMapUnmanaged(std.posix.fd_t, std.ArrayListUnmanaged(st
/// inotify cookie tracker for move events
cookie_fds: std.AutoHashMapUnmanaged(u32, std.posix.fd_t) = .{},
-const TreeKind = enum { input, output };
-
const WatchEntry = struct {
dir_path: []const u8,
name: []const u8,
- kind: TreeKind,
+ dir_index: u32,
};
-pub fn init(
- gpa: std.mem.Allocator,
- out_dir_path: []const u8,
- in_dir_paths: []const []const u8,
-) !LinuxWatcher {
- const notify_fd = try std.posix.inotify_init1(0);
- var self: LinuxWatcher = .{ .notify_fd = notify_fd };
- _ = try self.addTree(gpa, .output, out_dir_path);
- for (in_dir_paths) |p| {
- _ = try self.addTree(gpa, .input, p);
+pub fn init(gpa: std.mem.Allocator, paths: []const []const u8) (std.posix.INotifyInitError || std.posix.INotifyAddWatchError || std.fs.Dir.OpenError || std.mem.Allocator.Error)!LinuxWatcher {
+ var watcher: LinuxWatcher = .{ .notify_fd = try std.posix.inotify_init1(0) };
+ for (paths, 0..) |p, i| {
+ _ = try watcher.addTree(gpa, @intCast(i), p);
}
- return self;
+ return watcher;
}
/// Register `child` with the `parent`
@@ -92,12 +84,12 @@ fn removeChildByName(
fn addTree(
self: *LinuxWatcher,
gpa: std.mem.Allocator,
- tree_kind: TreeKind,
+ dir_index: u32,
root_dir_path: []const u8,
) !std.posix.fd_t {
var root_dir = try std.fs.cwd().openDir(root_dir_path, .{ .iterate = true });
defer root_dir.close();
- const parent_fd = try self.addDir(gpa, tree_kind, root_dir_path);
+ const parent_fd = try self.addDir(gpa, dir_index, root_dir_path);
// tracker for fds associated with dir paths
// helps to track children within a recursive walk
@@ -111,7 +103,7 @@ fn addTree(
else => continue,
.directory => {
const dir_path = try std.fs.path.join(gpa, &.{ root_dir_path, entry.path });
- const dir_fd = try self.addDir(gpa, tree_kind, dir_path);
+ const dir_fd = try self.addDir(gpa, dir_index, dir_path);
const p_dir = std.fs.path.dirname(dir_path).?;
const p_fd = lookup.get(p_dir).?;
@@ -126,7 +118,7 @@ fn addTree(
fn addDir(
self: *LinuxWatcher,
gpa: std.mem.Allocator,
- tree_kind: TreeKind,
+ dir_index: u32,
dir_path: []const u8,
) !std.posix.fd_t {
const mask = Mask.all(&.{
@@ -144,7 +136,7 @@ fn addDir(
try self.watch_fds.put(gpa, watch_fd, .{
.dir_path = dir_path,
.name = name_copy,
- .kind = tree_kind,
+ .dir_index = dir_index,
});
log.debug("added {s} -> {}", .{ dir_path, watch_fd });
return watch_fd;
@@ -203,14 +195,14 @@ fn moveDirEnd(
gpa.free(watch_entry.name);
const name_copy = try gpa.dupe(u8, name);
watch_entry.name = name_copy;
- watch_entry.kind = parent.kind;
+ watch_entry.dir_index = parent.dir_index;
try self.updateDirPath(gpa, moved_fd, parent.dir_path);
try self.addChild(gpa, to_fd, moved_fd);
return moved_fd;
} else { // unknown cookie - move from the outside
const dir_path = try std.fs.path.join(gpa, &.{ parent.dir_path, name });
- const moved_fd = try self.addTree(gpa, parent.kind, dir_path);
+ const moved_fd = try self.addTree(gpa, parent.dir_index, dir_path);
try self.addChild(gpa, to_fd, moved_fd);
return moved_fd;
}
@@ -277,8 +269,9 @@ fn dropWatch(
pub fn listen(
self: *LinuxWatcher,
gpa: std.mem.Allocator,
- reloader: *Reloader,
-) !void {
+ context: anytype,
+ callback: fn (@TypeOf(context), changed_handle: usize) void,
+) (std.posix.INotifyAddWatchError || std.fs.File.ReadError || std.fs.Dir.OpenError || std.mem.Allocator.Error)!void {
const Event = std.os.linux.inotify_event;
const event_size = @sizeOf(Event);
while (true) {
@@ -317,17 +310,10 @@ pub fn listen(
log.debug("ISDIR CREATE {s}", .{dir_path});
- const new_fd = try self.addTree(gpa, parent.kind, dir_path);
+ const new_fd = try self.addTree(gpa, parent.dir_index, dir_path);
try self.addChild(gpa, event.wd, new_fd);
const data = self.watch_fds.get(new_fd).?;
- switch (data.kind) {
- .input => {
- reloader.onInputChange(data.dir_path, "");
- },
- .output => {
- reloader.onOutputChange(data.dir_path, "");
- },
- }
+ callback(context, data.dir_index);
continue;
} else if (Mask.is(event.mask, .IN_MOVED_FROM)) {
log.debug("MOVING {s}/{s}", .{ parent.dir_path, event.getName().? });
@@ -337,30 +323,14 @@ pub fn listen(
log.debug("MOVED {s}/{s}", .{ parent.dir_path, event.getName().? });
const moved_fd = try self.moveDirEnd(gpa, event.wd, event.cookie, event.getName().?);
const moved = self.watch_fds.get(moved_fd).?;
- switch (moved.kind) {
- .input => {
- reloader.onInputChange(moved.dir_path, "");
- },
- .output => {
- reloader.onOutputChange(moved.dir_path, "");
- },
- }
+ callback(context, moved.dir_index);
continue;
}
} else {
if (Mask.is(event.mask, .IN_CLOSE_WRITE) or
Mask.is(event.mask, .IN_MOVED_TO))
{
- switch (parent.kind) {
- .input => {
- const name = event.getName() orelse continue;
- reloader.onInputChange(parent.dir_path, name);
- },
- .output => {
- const name = event.getName() orelse continue;
- reloader.onOutputChange(parent.dir_path, name);
- },
- }
+ callback(context, parent.dir_index);
}
}
}
diff --git a/src/watch/watcher/WindowsWatcher.zig b/src/watch/watcher/WindowsWatcher.zig
index 65df7af..ea1037a 100644
--- a/src/watch/watcher/WindowsWatcher.zig
+++ b/src/watch/watcher/WindowsWatcher.zig
@@ -41,9 +41,12 @@ pub fn deinit(watcher: *WindowsWatcher, gpa: std.mem.Allocator) void {
pub fn listen(
watcher: *WindowsWatcher,
+ gpa: std.mem.Allocator,
context: anytype,
callback: fn (@TypeOf(context), changed_handle: usize) void,
) error{ UnknownWaitStatus, NextChangeFailed, WaitAbandoned, Unexpected }!void {
+ _ = gpa;
+
wait_loop: while (true) {
const status = windows.WaitForMultipleObjectsEx(watcher.handles.items, false, windows.INFINITE, false) catch |err| switch (err) {
error.WaitTimeOut => unreachable,
From ad7a01da3178a49941c3cd552d52d2f4562b241a Mon Sep 17 00:00:00 2001
From: SuperAuguste <19855629+SuperAuguste@users.noreply.github.com>
Date: Tue, 30 Apr 2024 23:29:54 -0400
Subject: [PATCH 3/6] Start adding docs, improve build API
---
README.md | 7 +-----
build.zig | 27 ++++++++++++++++------
docs/introduction/README.md | 17 ++++++++++++++
docs/introduction/build.zig | 15 ++++++++++++
docs/introduction/build.zig.zon | 14 +++++++++++
docs/introduction/hello.zig | 10 ++++++++
samples/feature_test.zig | 2 --
src/cart/api.zig | 41 +++++++++++++++++++--------------
8 files changed, 101 insertions(+), 32 deletions(-)
create mode 100644 docs/introduction/README.md
create mode 100644 docs/introduction/build.zig
create mode 100644 docs/introduction/build.zig.zon
create mode 100644 docs/introduction/hello.zig
diff --git a/README.md b/README.md
index 6714f93..ec100a2 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,7 @@
## Getting Started
-Temporarily this repo targets the `zig-master` branch of
-[MicroZig](https://github.com/ZigEmbeddedGroup/microzig). In order to build this
-repo, you must have it checked out, and `microzig` next to `sycl-badge-2024` in
-your filesystem. Once this branch is merged it won't be as janky to build this
-firmware.
-
+Check out the [Introduction](docs/introduction/README.md)!
## Uploading firmware using a debugger
diff --git a/build.zig b/build.zig
index fe5ae08..4996d3b 100644
--- a/build.zig
+++ b/build.zig
@@ -46,13 +46,17 @@ pub fn build(b: *Build) void {
watch.linkFramework("CoreServices");
}
+ b.getInstallStep().dependOn(&b.addInstallArtifact(watch, .{
+ .dest_dir = .disabled,
+ }).step);
+
var dep: std.Build.Dependency = .{ .builder = b };
const feature_test_cart = add_cart(&dep, b, .{
.name = "feature_test",
.optimize = .ReleaseSmall,
.root_source_file = .{ .path = "samples/feature_test.zig" },
});
- feature_test_cart.install(b);
+ const watch_run_step = feature_test_cart.install_with_watcher(&dep, b);
const zeroman_cart = add_cart(&dep, b, .{
.name = "zeroman",
@@ -61,13 +65,9 @@ pub fn build(b: *Build) void {
});
add_zeroman_assets_step(b, zeroman_cart);
zeroman_cart.install(b);
- //
- // TODO: parameterize:
- const watch_run = b.addRunArtifact(watch);
- watch_run.addArgs(&.{ "serve", b.graph.zig_exe, "--input-dir", b.pathFromRoot("samples"), "--cart", b.pathFromRoot("zig-out/bin/feature_test.wasm") });
const watch_step = b.step("watch", "");
- watch_step.dependOn(&watch_run.step);
+ watch_step.dependOn(&watch_run_step.step);
const badge = mz.add_firmware(b, .{
.name = "badge",
@@ -128,12 +128,25 @@ pub const Cart = struct {
cart_lib: *Build.Step.Compile,
options: CartOptions,
- //watch_run_cmd: *std.Build.Step.Run,
pub fn install(c: *const Cart, b: *Build) void {
c.mz.install_firmware(b, c.fw, .{ .format = .{ .uf2 = .SAMD51 } });
b.installArtifact(c.wasm);
}
+
+ pub fn install_with_watcher(c: *const Cart, d: *Build.Dependency, b: *Build) *Build.Step.Run {
+ c.mz.install_firmware(b, c.fw, .{ .format = .{ .uf2 = .SAMD51 } });
+ const install_artifact_step = b.addInstallArtifact(c.wasm, .{});
+ b.getInstallStep().dependOn(&install_artifact_step.step);
+
+ const watch_run = b.addRunArtifact(d.artifact("watch"));
+ // watch_run.addArgs(&.{ "serve", b.graph.zig_exe, "--input-dir", b.pathFromRoot(std.fs.path.dirname(options.root_source_file) orelse ""), "--cart", b.pathFromRoot("zig-out/bin/feature_test.wasm") });
+ watch_run.addArgs(&.{ "serve", b.graph.zig_exe, "--input-dir" });
+ watch_run.addFileArg(c.options.root_source_file.dirname());
+ watch_run.addArgs(&.{ "--cart", b.getInstallPath(install_artifact_step.dest_dir.?, install_artifact_step.dest_sub_path) });
+
+ return watch_run;
+ }
};
pub const CartOptions = struct {
diff --git a/docs/introduction/README.md b/docs/introduction/README.md
new file mode 100644
index 0000000..6440990
--- /dev/null
+++ b/docs/introduction/README.md
@@ -0,0 +1,17 @@
+# Introduction
+
+This guide will help you get up and running with the badge and fittingly help introduce you to other attendees.
+
+## Running
+
+## On the simulator
+
+The simulator is ideal for fast iteration as it supports live reloading.
+
+Run `zig build watch` and head to https://badgesim.microzig.tech/.
+
+## On hardware
+
+Once you're happy with what you've made, you'll need to flash it onto your badge!
+
+TODO
diff --git a/docs/introduction/build.zig b/docs/introduction/build.zig
new file mode 100644
index 0000000..501cb90
--- /dev/null
+++ b/docs/introduction/build.zig
@@ -0,0 +1,15 @@
+const std = @import("std");
+const badge = @import("sycl-badge");
+
+pub fn build(b: *std.Build) void {
+ const dep = b.dependency("sycl-badge", .{});
+ const feature_test_cart = badge.add_cart(dep, b, .{
+ .name = "hello",
+ .optimize = .ReleaseSmall,
+ .root_source_file = .{ .path = "hello.zig" },
+ });
+ const watch_run_step = feature_test_cart.install_with_watcher(dep, b);
+
+ const watch_step = b.step("watch", "");
+ watch_step.dependOn(&watch_run_step.step);
+}
diff --git a/docs/introduction/build.zig.zon b/docs/introduction/build.zig.zon
new file mode 100644
index 0000000..63fb3fa
--- /dev/null
+++ b/docs/introduction/build.zig.zon
@@ -0,0 +1,14 @@
+.{
+ .name = "sycl-badge-introduction",
+ .version = "0.0.0",
+ .paths = .{
+ "hello.zig",
+ "build.zig",
+ },
+
+ .dependencies = .{
+ .@"sycl-badge" = .{
+ .path = "../..",
+ },
+ },
+}
diff --git a/docs/introduction/hello.zig b/docs/introduction/hello.zig
new file mode 100644
index 0000000..b6b49c0
--- /dev/null
+++ b/docs/introduction/hello.zig
@@ -0,0 +1,10 @@
+const cart = @import("cart-api");
+
+export fn update() void {
+ // Set background to a nice gray
+ @memset(cart.framebuffer, cart.DisplayColor{
+ .red = 10,
+ .green = 20,
+ .blue = 10,
+ });
+}
diff --git a/samples/feature_test.zig b/samples/feature_test.zig
index 0d56b3c..8822b03 100644
--- a/samples/feature_test.zig
+++ b/samples/feature_test.zig
@@ -82,8 +82,6 @@ export fn update() void {
};
}
- // TODO: blit, blitSub
-
cart.blit(.{
.sprite = &.{
.{ .red = 31, .green = 0, .blue = 0 },
diff --git a/src/cart/api.zig b/src/cart/api.zig
index 4f1b778..85632fc 100644
--- a/src/cart/api.zig
+++ b/src/cart/api.zig
@@ -22,14 +22,22 @@ const base = if (builtin.target.isWasm()) 0 else 0x20000000;
pub const NeopixelColor = packed struct(u24) { blue: u8, green: u8, red: u8 };
/// RGB565, high color
-pub const DisplayColor = packed struct(u16) { blue: u5, green: u6, red: u5 };
-const OptionalDisplayColor = enum(i32) {
- none = -1,
- _,
-
- inline fn from(color: ?DisplayColor) OptionalDisplayColor {
- return if (color) |c| @enumFromInt(@as(u16, @bitCast(c))) else .none;
- }
+pub const DisplayColor = packed struct(u16) {
+ /// 0-31
+ blue: u5,
+ /// 0-63
+ green: u6,
+ /// 0-31
+ red: u5,
+
+ const Optional = enum(i32) {
+ none = -1,
+ _,
+
+ inline fn from(color: ?DisplayColor) Optional {
+ return if (color) |c| @enumFromInt(@as(u16, @bitCast(c))) else .none;
+ }
+ };
};
pub const Controls = packed struct {
@@ -71,9 +79,9 @@ const platform_specific = if (builtin.target.isWasm())
struct {
extern fn blit(sprite: [*]const DisplayColor, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: BlitOptions.Flags) void;
extern fn line(color: DisplayColor, x1: i32, y1: i32, x2: i32, y2: i32) void;
- extern fn oval(stroke_color: OptionalDisplayColor, fill_color: OptionalDisplayColor, x: i32, y: i32, width: u32, height: u32) void;
- extern fn rect(stroke_color: OptionalDisplayColor, fill_color: OptionalDisplayColor, x: i32, y: i32, width: u32, height: u32) void;
- extern fn text(text_color: DisplayColor, background_color: OptionalDisplayColor, str_ptr: [*]const u8, str_len: usize, x: i32, y: i32) void;
+ extern fn oval(stroke_color: DisplayColor.Optional, fill_color: DisplayColor.Optional, x: i32, y: i32, width: u32, height: u32) void;
+ extern fn rect(stroke_color: DisplayColor.Optional, fill_color: DisplayColor.Optional, x: i32, y: i32, width: u32, height: u32) void;
+ extern fn text(text_color: DisplayColor, background_color: DisplayColor.Optional, str_ptr: [*]const u8, str_len: usize, x: i32, y: i32) void;
extern fn vline(color: DisplayColor, x: i32, y: i32, len: u32) void;
extern fn hline(color: DisplayColor, x: i32, y: i32, len: u32) void;
extern fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneOptions.Flags) void;
@@ -130,7 +138,6 @@ pub inline fn blit(options: BlitOptions) void {
options.flags,
);
} else {
- @compileError("TODO");
// const rest: extern struct {
// width: u32,
// height: u32,
@@ -188,8 +195,8 @@ pub const OvalOptions = struct {
pub inline fn oval(options: OvalOptions) void {
if (comptime builtin.target.isWasm()) {
platform_specific.oval(
- OptionalDisplayColor.from(options.stroke_color),
- OptionalDisplayColor.from(options.fill_color),
+ DisplayColor.Optional.from(options.stroke_color),
+ DisplayColor.Optional.from(options.fill_color),
options.x,
options.y,
options.width,
@@ -220,8 +227,8 @@ pub const RectOptions = struct {
pub inline fn rect(options: RectOptions) void {
if (comptime builtin.target.isWasm()) {
platform_specific.rect(
- OptionalDisplayColor.from(options.stroke_color),
- OptionalDisplayColor.from(options.fill_color),
+ DisplayColor.Optional.from(options.stroke_color),
+ DisplayColor.Optional.from(options.fill_color),
options.x,
options.y,
options.width,
@@ -252,7 +259,7 @@ pub inline fn text(options: TextOptions) void {
if (comptime builtin.target.isWasm()) {
platform_specific.text(
options.text_color,
- OptionalDisplayColor.from(options.background_color),
+ DisplayColor.Optional.from(options.background_color),
options.str.ptr,
options.str.len,
options.x,
From e8dcaa9244d6b81bd254e4a07bad99901ee8b6ad Mon Sep 17 00:00:00 2001
From: Marcus Ramse
Date: Fri, 3 May 2024 20:50:50 +0000
Subject: [PATCH 4/6] QoL api.zig changes
---
docs/introduction/hello.zig | 6 ++--
samples/feature_test.zig | 42 +++++++++++++--------------
samples/zeroman/build/convert_gfx.zig | 2 +-
src/cart/api.zig | 37 +++++++++++------------
4 files changed, 44 insertions(+), 43 deletions(-)
diff --git a/docs/introduction/hello.zig b/docs/introduction/hello.zig
index b6b49c0..b87b3ea 100644
--- a/docs/introduction/hello.zig
+++ b/docs/introduction/hello.zig
@@ -3,8 +3,8 @@ const cart = @import("cart-api");
export fn update() void {
// Set background to a nice gray
@memset(cart.framebuffer, cart.DisplayColor{
- .red = 10,
- .green = 20,
- .blue = 10,
+ .r = 10,
+ .g = 20,
+ .b = 10,
});
}
diff --git a/samples/feature_test.zig b/samples/feature_test.zig
index 8822b03..c389e3d 100644
--- a/samples/feature_test.zig
+++ b/samples/feature_test.zig
@@ -67,27 +67,27 @@ export fn update() void {
for (0..cart.screen_height) |y| {
for (0..cart.screen_width) |x| {
cart.framebuffer[y * cart.screen_width + x] = .{
- .red = @intFromFloat(@as(f32, @floatFromInt(x)) / cart.screen_width * 31),
- .green = green_565,
- .blue = @intFromFloat(@as(f32, @floatFromInt(y)) / cart.screen_height * 31),
+ .r = @intFromFloat(@as(f32, @floatFromInt(x)) / cart.screen_width * 31),
+ .g = green_565,
+ .b = @intFromFloat(@as(f32, @floatFromInt(y)) / cart.screen_height * 31),
};
}
}
for (cart.neopixels, 0..) |*np, i| {
np.* = .{
- .red = @intFromFloat(@as(f32, @floatFromInt(i)) / 5 * 255),
- .green = @intFromFloat(@as(f32, @floatFromInt(cart.light_level.*)) / std.math.maxInt(u12) * 255),
- .blue = @intFromFloat(@as(f32, @floatFromInt(i)) / 5 * 255),
+ .r = @intFromFloat(@as(f32, @floatFromInt(i)) / 5 * 255),
+ .g = @intFromFloat(@as(f32, @floatFromInt(cart.light_level.*)) / std.math.maxInt(u12) * 255),
+ .b = @intFromFloat(@as(f32, @floatFromInt(i)) / 5 * 255),
};
}
cart.blit(.{
.sprite = &.{
- .{ .red = 31, .green = 0, .blue = 0 },
- .{ .red = 0, .green = 0, .blue = 31 },
- .{ .red = 31, .green = 0, .blue = 0 },
- .{ .red = 0, .green = 0, .blue = 31 },
+ .{ .r = 31, .g = 0, .b = 0 },
+ .{ .r = 0, .g = 0, .b = 31 },
+ .{ .r = 31, .g = 0, .b = 0 },
+ .{ .r = 0, .g = 0, .b = 31 },
},
.x = 40,
.y = 40,
@@ -101,21 +101,21 @@ export fn update() void {
.y1 = 50,
.x2 = 70,
.y2 = 70,
- .color = .{ .red = 0, .green = 63, .blue = 0 },
+ .color = .{ .r = 0, .g = 63, .b = 0 },
});
cart.hline(.{
.x = 30,
.y = 30,
.len = 20,
- .color = .{ .red = 31, .green = 0, .blue = 0 },
+ .color = .{ .r = 31, .g = 0, .b = 0 },
});
cart.vline(.{
.x = 30,
.y = 30,
.len = 20,
- .color = .{ .red = 31, .green = 0, .blue = 0 },
+ .color = .{ .r = 31, .g = 0, .b = 0 },
});
cart.oval(.{
@@ -123,8 +123,8 @@ export fn update() void {
.y = 80,
.width = 10,
.height = 10,
- .stroke_color = .{ .red = 0, .green = 0, .blue = 31 },
- .fill_color = .{ .red = 31, .green = 0, .blue = 31 },
+ .stroke_color = .{ .r = 0, .g = 0, .b = 31 },
+ .fill_color = .{ .r = 31, .g = 0, .b = 31 },
});
cart.rect(.{
@@ -132,23 +132,23 @@ export fn update() void {
.y = 100,
.width = 10,
.height = 10,
- .stroke_color = .{ .red = 31, .green = 31, .blue = 31 },
- .fill_color = .{ .red = 0, .green = 63, .blue = 31 },
+ .stroke_color = .{ .r = 31, .g = 31, .b = 31 },
+ .fill_color = .{ .r = 0, .g = 63, .b = 31 },
});
cart.text(.{
.str = fbs.getWritten(),
.x = 0,
.y = 0,
- .text_color = .{ .red = 0, .green = 0, .blue = 0 },
- .background_color = .{ .red = 31, .green = 63, .blue = 31 },
+ .text_color = .{ .r = 0, .g = 0, .b = 0 },
+ .background_color = .{ .r = 31, .g = 63, .b = 31 },
});
cart.text(.{
.str = "\x80\x81\x82\x83\x84\x85\x86\x87\x88",
.x = 0,
.y = 120,
- .text_color = .{ .red = 0, .green = 0, .blue = 0 },
- .background_color = .{ .red = 31, .green = 63, .blue = 31 },
+ .text_color = .{ .r = 0, .g = 0, .b = 0 },
+ .background_color = .{ .r = 31, .g = 63, .b = 31 },
});
}
diff --git a/samples/zeroman/build/convert_gfx.zig b/samples/zeroman/build/convert_gfx.zig
index c4042de..f69551f 100644
--- a/samples/zeroman/build/convert_gfx.zig
+++ b/samples/zeroman/build/convert_gfx.zig
@@ -80,7 +80,7 @@ fn convert(args: ConvertFile, writer: std.fs.File.Writer) !void {
try writer.writeAll(" pub const colors = [_]DisplayColor{\n");
for (colors.items) |c| {
- try writer.print(" .{{ .red = {}, .green = {}, .blue = {} }},\n", .{ c.r, c.g, c.b });
+ try writer.print(" .{{ .r = {}, .g = {}, .b = {} }},\n", .{ c.r, c.g, c.b });
}
try writer.writeAll(" };\n");
diff --git a/src/cart/api.zig b/src/cart/api.zig
index 85632fc..31ebacf 100644
--- a/src/cart/api.zig
+++ b/src/cart/api.zig
@@ -19,16 +19,16 @@ pub const screen_height: u32 = 128;
const base = if (builtin.target.isWasm()) 0 else 0x20000000;
/// RGB888, true color
-pub const NeopixelColor = packed struct(u24) { blue: u8, green: u8, red: u8 };
+pub const NeopixelColor = packed struct(u24) { b: u8, g: u8, r: u8 };
/// RGB565, high color
pub const DisplayColor = packed struct(u16) {
/// 0-31
- blue: u5,
+ b: u5,
/// 0-63
- green: u6,
+ g: u6,
/// 0-31
- red: u5,
+ r: u5,
const Optional = enum(i32) {
none = -1,
@@ -40,7 +40,7 @@ pub const DisplayColor = packed struct(u16) {
};
};
-pub const Controls = packed struct {
+pub const Controls = packed struct(u9) {
/// START button
start: bool,
/// SELECT button
@@ -81,7 +81,7 @@ const platform_specific = if (builtin.target.isWasm())
extern fn line(color: DisplayColor, x1: i32, y1: i32, x2: i32, y2: i32) void;
extern fn oval(stroke_color: DisplayColor.Optional, fill_color: DisplayColor.Optional, x: i32, y: i32, width: u32, height: u32) void;
extern fn rect(stroke_color: DisplayColor.Optional, fill_color: DisplayColor.Optional, x: i32, y: i32, width: u32, height: u32) void;
- extern fn text(text_color: DisplayColor, background_color: DisplayColor.Optional, str_ptr: [*]const u8, str_len: usize, x: i32, y: i32) void;
+ extern fn text(text_color: DisplayColor.Optional, background_color: DisplayColor.Optional, str_ptr: [*]const u8, str_len: usize, x: i32, y: i32) void;
extern fn vline(color: DisplayColor, x: i32, y: i32, len: u32) void;
extern fn hline(color: DisplayColor, x: i32, y: i32, len: u32) void;
extern fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneOptions.Flags) void;
@@ -115,12 +115,12 @@ pub const BlitOptions = struct {
width: u32,
height: u32,
/// x within the sprite atlas.
- src_x: i32 = 0,
+ src_x: u32 = 0,
/// y within the sprite atlas.
- src_y: i32 = 0,
+ src_y: u32 = 0,
/// Width of the entire sprite atlas.
stride: ?u32 = null,
- flags: Flags,
+ flags: Flags = .{},
};
/// Copies pixels to the framebuffer.
@@ -187,8 +187,8 @@ pub const OvalOptions = struct {
y: i32,
width: u32,
height: u32,
- stroke_color: ?DisplayColor,
- fill_color: ?DisplayColor,
+ stroke_color: ?DisplayColor = null,
+ fill_color: ?DisplayColor = null,
};
/// Draws an oval (or circle).
@@ -219,8 +219,8 @@ pub const RectOptions = struct {
y: i32,
width: u32,
height: u32,
- stroke_color: ?DisplayColor,
- fill_color: ?DisplayColor,
+ stroke_color: ?DisplayColor = null,
+ fill_color: ?DisplayColor = null,
};
/// Draws a rectangle.
@@ -250,15 +250,15 @@ pub const TextOptions = struct {
str: []const u8,
x: i32,
y: i32,
- text_color: DisplayColor,
- background_color: ?DisplayColor,
+ text_color: ?DisplayColor = null,
+ background_color: ?DisplayColor = null,
};
/// Draws text using the built-in system font.
pub inline fn text(options: TextOptions) void {
if (comptime builtin.target.isWasm()) {
platform_specific.text(
- options.text_color,
+ DisplayColor.Optional.from(options.text_color),
DisplayColor.Optional.from(options.background_color),
options.str.ptr,
options.str.len,
@@ -353,8 +353,9 @@ pub const ToneOptions = struct {
};
channel: Channel,
- duty_cycle: DutyCycle,
- panning: Panning,
+ /// `duty_cycle` is only used when `channel` is set to `pulse1` or `pulse2`
+ duty_cycle: DutyCycle = .@"1/8",
+ panning: Panning = .stereo,
padding: u26 = undefined,
};
From a4f52bc3f31b1ce8092b1d2de695dd5bfd1721db Mon Sep 17 00:00:00 2001
From: Marcus Ramse
Date: Fri, 3 May 2024 22:26:45 +0000
Subject: [PATCH 5/6] sim: less tall bluescreen errors
---
simulator/src/runtime.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/simulator/src/runtime.ts b/simulator/src/runtime.ts
index a1b9a92..4588db8 100644
--- a/simulator/src/runtime.ts
+++ b/simulator/src/runtime.ts
@@ -246,7 +246,7 @@ export class Runtime {
const headerX = (160 - (8 * title.length)) / 2;
const headerY = 20;
const messageX = 9;
- const messageY = 60;
+ const messageY = 52;
this.framebuffer.fillScreen(blue);
@@ -272,15 +272,15 @@ function errorToBlueScreenText(err: Error) {
} else if (err.message.match(/out of bounds/)) {
message = "The cartridge has\nattempted a memory\naccess that is\nout of bounds.";
}
- return message + "\n\n\n\n\nHit R to reboot.";
+ return message + "\n\n\n\nHit R to reboot.";
} else if (err instanceof WebAssembly.LinkError) {
- return "The cartridge has\ntried to import\na missing function.\n\n\n\nSee console for\nmore details.";
+ return "The cartridge has\ntried to import\na missing function.\n\n\nSee console for\nmore details.";
} else if (err instanceof WebAssembly.CompileError) {
- return "The cartridge is\ncorrupted.\n\n\n\nSee console for\nmore details.";
+ return "The cartridge is\ncorrupted.\n\n\nSee console for\nmore details.";
} else if (err instanceof Wasm4Error) {
return err.wasm4Message;
}
- return "Unknown error.\n\n\n\nSee console for\nmore details.";
+ return "Unknown error.\n\n\nSee console for\nmore details.";
}
class Wasm4Error extends Error {
From 55113566bf0abb6e45a4f61d17a5d7b9a35ed6ac Mon Sep 17 00:00:00 2001
From: SuperAuguste <19855629+SuperAuguste@users.noreply.github.com>
Date: Fri, 3 May 2024 20:06:02 -0400
Subject: [PATCH 6/6] Make reloaders compile hopefully
---
src/watch/Reloader.zig | 2 +-
src/watch/watcher/LinuxWatcher.zig | 2 +-
src/watch/watcher/MacosWatcher.zig | 154 +++++++++++++++------------
src/watch/watcher/WindowsWatcher.zig | 2 +-
4 files changed, 87 insertions(+), 73 deletions(-)
diff --git a/src/watch/Reloader.zig b/src/watch/Reloader.zig
index c753366..2603fb1 100644
--- a/src/watch/Reloader.zig
+++ b/src/watch/Reloader.zig
@@ -37,7 +37,7 @@ pub fn init(
}
pub fn listen(reloader: *Reloader) !void {
- try reloader.watcher.listen(reloader.gpa, reloader, onChange);
+ try reloader.watcher.listen(reloader.gpa, reloader, &onChange);
}
pub fn onChange(reloader: *Reloader, dir_that_changed: usize) void {
diff --git a/src/watch/watcher/LinuxWatcher.zig b/src/watch/watcher/LinuxWatcher.zig
index e4bb522..90c3ed1 100644
--- a/src/watch/watcher/LinuxWatcher.zig
+++ b/src/watch/watcher/LinuxWatcher.zig
@@ -270,7 +270,7 @@ pub fn listen(
self: *LinuxWatcher,
gpa: std.mem.Allocator,
context: anytype,
- callback: fn (@TypeOf(context), changed_handle: usize) void,
+ callback: *const fn (@TypeOf(context), changed_handle: usize) void,
) (std.posix.INotifyAddWatchError || std.fs.File.ReadError || std.fs.Dir.OpenError || std.mem.Allocator.Error)!void {
const Event = std.os.linux.inotify_event;
const event_size = @sizeOf(Event);
diff --git a/src/watch/watcher/MacosWatcher.zig b/src/watch/watcher/MacosWatcher.zig
index ac8e7ec..352019e 100644
--- a/src/watch/watcher/MacosWatcher.zig
+++ b/src/watch/watcher/MacosWatcher.zig
@@ -8,22 +8,41 @@ const c = @cImport({
const log = std.log.scoped(.watcher);
-out_dir_path: []const u8,
-in_dir_paths: []const []const u8,
+paths: []const []const u8,
+macos_paths: []const c.CFStringRef,
+paths_to_watch: c.CFArrayRef,
+
+pub fn init(gpa: std.mem.Allocator, paths: []const []const u8) !MacosWatcher {
+ const macos_paths = try gpa.alloc(c.CFStringRef, paths.len);
+
+ for (paths, macos_paths) |str, *ref| {
+ ref.* = c.CFStringCreateWithCString(
+ null,
+ str.ptr,
+ c.kCFStringEncodingUTF8,
+ );
+ }
+
+ const paths_to_watch: c.CFArrayRef = c.CFArrayCreate(
+ null,
+ @ptrCast(macos_paths.ptr),
+ @intCast(macos_paths.len),
+ null,
+ );
-pub fn init(
- gpa: std.mem.Allocator,
- out_dir_path: []const u8,
- in_dir_paths: []const []const u8,
-) !MacosWatcher {
- _ = gpa;
return .{
- .out_dir_path = out_dir_path,
- .in_dir_paths = in_dir_paths,
+ .paths = paths,
+ .macos_paths = macos_paths,
+ .paths_to_watch = paths_to_watch,
};
}
-pub fn callback(
+pub fn deinit(watcher: *MacosWatcher, gpa: std.mem.Allocator) void {
+ gpa.free(watcher.macos_paths);
+ c.CFRelease(watcher.paths_to_watch);
+}
+
+fn eventStreamCallback(comptime ContextType: type) fn (
streamRef: c.ConstFSEventStreamRef,
clientCallBackInfo: ?*anyopaque,
numEvents: usize,
@@ -31,74 +50,71 @@ pub fn callback(
eventFlags: ?[*]const c.FSEventStreamEventFlags,
eventIds: ?[*]const c.FSEventStreamEventId,
) callconv(.C) void {
- _ = eventIds;
- _ = eventFlags;
- _ = streamRef;
- const ctx: *Context = @alignCast(@ptrCast(clientCallBackInfo));
-
- const paths: [*][*:0]u8 = @alignCast(@ptrCast(eventPaths));
- for (paths[0..numEvents]) |p| {
- const path = std.mem.span(p);
- log.debug("Changed: {s}\n", .{path});
-
- const basename = std.fs.path.basename(path);
- var base_path = path[0 .. path.len - basename.len];
- if (std.mem.endsWith(u8, base_path, "/"))
- base_path = base_path[0 .. base_path.len - 1];
-
- const is_out = std.mem.startsWith(u8, path, ctx.out_dir_path);
- if (is_out) {
- ctx.reloader.onOutputChange(base_path, basename);
- } else {
- ctx.reloader.onInputChange(base_path, basename);
+ return struct {
+ fn call(
+ streamRef: c.ConstFSEventStreamRef,
+ clientCallBackInfo: ?*anyopaque,
+ numEvents: usize,
+ eventPaths: ?*anyopaque,
+ eventFlags: ?[*]const c.FSEventStreamEventFlags,
+ eventIds: ?[*]const c.FSEventStreamEventId,
+ ) callconv(.C) void {
+ _ = eventIds;
+ _ = eventFlags;
+ _ = streamRef;
+ const ctx: *Context(ContextType) = @alignCast(@ptrCast(clientCallBackInfo));
+
+ const watcher = ctx.watcher;
+ const callback = ctx.callback;
+
+ const paths: [*][*:0]u8 = @alignCast(@ptrCast(eventPaths));
+ for (paths[0..numEvents]) |p| {
+ const path = std.mem.span(p);
+ log.debug("Changed: {s}\n", .{path});
+
+ const basename = std.fs.path.basename(path);
+ var base_path = path[0 .. path.len - basename.len];
+ if (std.mem.endsWith(u8, base_path, "/"))
+ base_path = base_path[0 .. base_path.len - 1];
+
+ for (watcher.paths, 0..) |target_path, idx| {
+ if (std.mem.startsWith(u8, path, target_path)) {
+ callback(ctx.context, idx);
+ break;
+ }
+ }
+ }
}
- }
+ }.call;
+}
+
+pub fn Context(comptime ContextType: type) type {
+ return struct {
+ watcher: *const MacosWatcher,
+ context: ContextType,
+ callback: *const fn (ContextType, changed_handle: usize) void,
+ };
}
-const Context = struct {
- reloader: *Reloader,
- out_dir_path: []const u8,
-};
pub fn listen(
- self: *MacosWatcher,
+ watcher: *MacosWatcher,
gpa: std.mem.Allocator,
- reloader: *Reloader,
+ context: anytype,
+ callback: *const fn (@TypeOf(context), changed_handle: usize) void,
) !void {
- var macos_paths = try gpa.alloc(c.CFStringRef, self.in_dir_paths.len + 1);
- defer gpa.free(macos_paths);
-
- macos_paths[0] = c.CFStringCreateWithCString(
- null,
- self.out_dir_path.ptr,
- c.kCFStringEncodingUTF8,
- );
-
- for (self.in_dir_paths, macos_paths[1..]) |str, *ref| {
- ref.* = c.CFStringCreateWithCString(
- null,
- str.ptr,
- c.kCFStringEncodingUTF8,
- );
- }
-
- const paths_to_watch: c.CFArrayRef = c.CFArrayCreate(
- null,
- @ptrCast(macos_paths.ptr),
- @intCast(macos_paths.len),
- null,
- );
-
- var ctx: Context = .{
- .reloader = reloader,
- .out_dir_path = self.out_dir_path,
+ _ = gpa; // autofix
+ var stream_context_context = Context(@TypeOf(context)){
+ .watcher = watcher,
+ .callback = callback,
+ .context = context,
};
- var stream_context: c.FSEventStreamContext = .{ .info = &ctx };
+ var stream_context: c.FSEventStreamContext = .{ .info = &stream_context_context };
const stream: c.FSEventStreamRef = c.FSEventStreamCreate(
null,
- &callback,
+ &eventStreamCallback(@TypeOf(callback)),
&stream_context,
- paths_to_watch,
+ watcher.paths_to_watch,
c.kFSEventStreamEventIdSinceNow,
0.05,
c.kFSEventStreamCreateFlagFileEvents,
@@ -119,6 +135,4 @@ pub fn listen(
c.FSEventStreamStop(stream);
c.FSEventStreamInvalidate(stream);
c.FSEventStreamRelease(stream);
-
- c.CFRelease(paths_to_watch);
}
diff --git a/src/watch/watcher/WindowsWatcher.zig b/src/watch/watcher/WindowsWatcher.zig
index ea1037a..c92e421 100644
--- a/src/watch/watcher/WindowsWatcher.zig
+++ b/src/watch/watcher/WindowsWatcher.zig
@@ -43,7 +43,7 @@ pub fn listen(
watcher: *WindowsWatcher,
gpa: std.mem.Allocator,
context: anytype,
- callback: fn (@TypeOf(context), changed_handle: usize) void,
+ callback: *const fn (@TypeOf(context), changed_handle: usize) void,
) error{ UnknownWaitStatus, NextChangeFailed, WaitAbandoned, Unexpected }!void {
_ = gpa;