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;