diff --git a/crates/haku2/src/canvas.zig b/crates/haku2/src/canvas.zig new file mode 100644 index 0000000..cfd32cb --- /dev/null +++ b/crates/haku2/src/canvas.zig @@ -0,0 +1,48 @@ +const value = @import("value.zig"); +const Value = value.Value; + +/// Canvases are opaque handles passed from the host to the VM. +/// The host creates and manages the canvas, hence no `new` function. +pub const Canvas = opaque { + fn wrap(status: bool) !void { + if (!status) return error.Draw; + } + + pub fn begin(c: *Canvas) !void { + try wrap(__haku2_canvas_begin(c)); + } + + pub fn line(c: *Canvas, start: value.Vec2, end: value.Vec2) !void { + const x1, const y1 = start; + const x2, const y2 = end; + try wrap(__haku2_canvas_line(c, x1, y1, x2, y2)); + } + + pub fn rect(c: *Canvas, top_left: value.Vec2, size: value.Vec2) !void { + const x, const y = top_left; + const width, const height = size; + try wrap(__haku2_canvas_rectangle(c, x, y, width, height)); + } + + pub fn circle(c: *Canvas, center: value.Vec2, r: f32) !void { + const x, const y = center; + try wrap(__haku2_canvas_circle(c, x, y, r)); + } + + pub fn fill(c: *Canvas, color: value.Rgba8) !void { + const r, const g, const b, const a = color; + try wrap(__haku2_canvas_fill(c, r, g, b, a)); + } + + pub fn stroke(c: *Canvas, color: value.Rgba8, thickness: f32) !void { + const r, const g, const b, const a = color; + try wrap(__haku2_canvas_stroke(c, r, g, b, a, thickness)); + } +}; + +extern fn __haku2_canvas_begin(c: *Canvas) bool; +extern fn __haku2_canvas_line(c: *Canvas, x1: f32, y1: f32, x2: f32, y2: f32) bool; +extern fn __haku2_canvas_rectangle(c: *Canvas, x: f32, y: f32, width: f32, height: f32) bool; +extern fn __haku2_canvas_circle(c: *Canvas, x: f32, y: f32, r: f32) bool; +extern fn __haku2_canvas_fill(c: *Canvas, r: u8, g: u8, b: u8, a: u8) bool; +extern fn __haku2_canvas_stroke(c: *Canvas, r: u8, g: u8, b: u8, a: u8, thickness: f32) bool; diff --git a/crates/haku2/src/haku2.zig b/crates/haku2/src/haku2.zig index f201fbd..9c5363a 100644 --- a/crates/haku2/src/haku2.zig +++ b/crates/haku2/src/haku2.zig @@ -1,21 +1,24 @@ const std = @import("std"); const mem = std.mem; +const builtin = @import("builtin"); const bytecode = @import("bytecode.zig"); +const Canvas = @import("canvas.zig").Canvas; +const render = @import("render.zig"); const Scratch = @import("scratch.zig"); const value = @import("value.zig"); const Vm = @import("vm.zig"); -const hostAllocator = @import("allocator.zig").hostAllocator; +const allocator = if (builtin.cpu.arch == .wasm32) std.heap.wasm_allocator else @import("allocator.zig").hostAllocator; // Scratch export fn haku2_scratch_new(max: usize) ?*Scratch { - return Scratch.create(hostAllocator, max) catch return null; + return Scratch.create(allocator, max) catch return null; } export fn haku2_scratch_destroy(scratch: *Scratch) void { - scratch.destroy(hostAllocator); + scratch.destroy(allocator); } export fn haku2_scratch_reset(scratch: *Scratch) void { @@ -25,11 +28,11 @@ export fn haku2_scratch_reset(scratch: *Scratch) void { // Limits export fn haku2_limits_new() ?*Vm.Limits { - return hostAllocator.create(Vm.Limits) catch null; + return allocator.create(Vm.Limits) catch null; } export fn haku2_limits_destroy(limits: *Vm.Limits) void { - hostAllocator.destroy(limits); + allocator.destroy(limits); } export fn haku2_limits_set_stack_capacity(limits: *Vm.Limits, new: usize) void { @@ -53,27 +56,31 @@ export fn haku2_defs_parse( tags_len: usize, ) ?*bytecode.Defs { return bytecode.Defs.parse( - hostAllocator, + allocator, defs_string[0..defs_len], tags_string[0..tags_len], ) catch null; } export fn haku2_defs_destroy(defs: *bytecode.Defs) void { - defs.destroy(hostAllocator); + defs.destroy(allocator); } // VM export fn haku2_vm_new(s: *Scratch, defs: *const bytecode.Defs, limits: *const Vm.Limits) ?*Vm { - const vm = hostAllocator.create(Vm) catch return null; - errdefer hostAllocator.destroy(vm); + const vm = allocator.create(Vm) catch return null; + errdefer allocator.destroy(vm); vm.* = Vm.init(s.allocator(), defs, limits) catch return null; return vm; } +export fn haku2_vm_destroy(vm: *Vm) void { + allocator.destroy(vm); +} + export fn haku2_vm_run_main( vm: *Vm, scratch: *Scratch, @@ -95,6 +102,46 @@ export fn haku2_vm_run_main( return true; } -export fn haku2_vm_destroy(vm: *Vm) void { - hostAllocator.destroy(vm); +export fn haku2_vm_is_dotter(vm: *const Vm) bool { + if (vm.stack.len == 0) return false; + const top = vm.stack[vm.stack_top]; + return top == .ref and top.ref.* == .reticle and top.ref.reticle == .dotter; +} + +export fn haku2_vm_run_dotter( + vm: *Vm, + scratch: *Scratch, + from_x: f32, + from_y: f32, + to_x: f32, + to_y: f32, + num: f32, +) bool { + vm.runDotter( + scratch.allocator(), + .{ from_x, from_y, 0, 0 }, + .{ to_x, to_y, 0, 0 }, + num, + ) catch return false; + return true; +} + +export fn haku2_vm_exception_len(vm: *const Vm) usize { + if (vm.exception) |exn| { + return exn.len; + } else { + return 0; + } +} + +export fn haku2_vm_exception_render(vm: *const Vm, buffer: [*]u8) void { + const exn = vm.exception.?; + _ = exn.format(buffer[0..exn.len], &exn.args); +} + +// Renderer + +export fn haku2_render(vm: *Vm, canvas: *Canvas, max_depth: usize) bool { + render.render(vm, canvas, max_depth) catch return false; + return true; } diff --git a/crates/haku2/src/lib.rs b/crates/haku2/src/lib.rs index 489559c..f246983 100644 --- a/crates/haku2/src/lib.rs +++ b/crates/haku2/src/lib.rs @@ -1,6 +1,9 @@ use std::{ alloc::{self, Layout}, - ptr, + error::Error, + fmt::{self, Display}, + marker::{PhantomData, PhantomPinned}, + ptr::{self, NonNull}, }; #[unsafe(no_mangle)] @@ -35,3 +38,415 @@ unsafe extern "C" fn __haku2_dealloc(ptr: *mut u8, size: usize, align: usize) { } } } + +#[repr(C)] +struct ScratchC { + _data: (), + _marker: PhantomData<(*mut u8, PhantomPinned)>, +} + +#[repr(C)] +struct LimitsC { + _data: (), + _marker: PhantomData<(*mut u8, PhantomPinned)>, +} + +#[repr(C)] +struct DefsC { + _data: (), + _marker: PhantomData<(*mut u8, PhantomPinned)>, +} + +#[repr(C)] +struct VmC { + _data: (), + _marker: PhantomData<(*mut u8, PhantomPinned)>, +} + +extern "C" { + fn haku2_scratch_new(max: usize) -> *mut ScratchC; + fn haku2_scratch_destroy(scratch: *mut ScratchC); + fn haku2_scratch_reset(scratch: *mut ScratchC); + + fn haku2_limits_new() -> *mut LimitsC; + fn haku2_limits_destroy(limits: *mut LimitsC); + fn haku2_limits_set_stack_capacity(limits: *mut LimitsC, new: usize); + fn haku2_limits_set_call_stack_capacity(limits: *mut LimitsC, new: usize); + fn haku2_limits_set_fuel(limits: *mut LimitsC, new: u32); + + fn haku2_defs_parse( + defs_string: *const u8, + defs_len: usize, + tags_string: *const u8, + tags_len: usize, + ) -> *mut DefsC; + fn haku2_defs_destroy(defs: *mut DefsC); + + fn haku2_vm_new(s: *mut ScratchC, defs: *const DefsC, limits: *const LimitsC) -> *mut VmC; + fn haku2_vm_destroy(vm: *mut VmC); + fn haku2_vm_run_main( + vm: *mut VmC, + scratch: *mut ScratchC, + code: *const u8, + code_len: usize, + local_count: u8, + ) -> bool; + fn haku2_vm_is_dotter(vm: *const VmC) -> bool; + fn haku2_vm_run_dotter( + vm: *mut VmC, + scratch: *mut ScratchC, + from_x: f32, + from_y: f32, + to_x: f32, + to_y: f32, + num: f32, + ) -> bool; + fn haku2_vm_exception_len(vm: *const VmC) -> usize; + fn haku2_vm_exception_render(vm: *const VmC, buffer: *mut u8); + + // improper_ctypes is emitted for `*mut CanvasC`, which is an opaque {} on the Zig side and + // therefore FFI-safe. + #[expect(improper_ctypes)] + fn haku2_render(vm: *mut VmC, canvas: *mut CanvasC, max_depth: usize) -> bool; +} + +#[derive(Debug)] +pub struct Scratch { + raw: NonNull, +} + +impl Scratch { + pub fn new(max: usize) -> Scratch { + Scratch { + // SAFETY: haku2_scratch_new does not have any safety invariants. + raw: NonNull::new(unsafe { haku2_scratch_new(max) }).expect("out of memory"), + } + } + + pub fn reset(&mut self) { + // SAFETY: The pointer passed is non-null. + unsafe { + haku2_scratch_reset(self.raw.as_ptr()); + } + } +} + +impl Drop for Scratch { + fn drop(&mut self) { + // SAFETY: The pointer passed is non-null. + unsafe { + haku2_scratch_destroy(self.raw.as_ptr()); + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct LimitsSpec { + pub stack_capacity: usize, + pub call_stack_capacity: usize, + pub fuel: u32, +} + +#[derive(Debug)] +pub struct Limits { + raw: NonNull, +} + +// SAFETY: Limits's backing storage is only modified on creation. +// Changing the limits requires creating a new instance. +unsafe impl Send for Limits {} +unsafe impl Sync for Limits {} + +impl Limits { + pub fn new(spec: LimitsSpec) -> Self { + // SAFETY: haku2_limits_new has no safety invariants. + let limits = NonNull::new(unsafe { haku2_limits_new() }).expect("out of memory"); + + // SAFETY: The following functions are called on a valid pointer. + unsafe { + haku2_limits_set_stack_capacity(limits.as_ptr(), spec.stack_capacity); + haku2_limits_set_call_stack_capacity(limits.as_ptr(), spec.call_stack_capacity); + haku2_limits_set_fuel(limits.as_ptr(), spec.fuel); + } + + Self { raw: limits } + } +} + +impl Drop for Limits { + fn drop(&mut self) { + // SAFETY: The pointer passed is non-null. + unsafe { + haku2_limits_destroy(self.raw.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct Defs { + raw: NonNull, +} + +// SAFETY: Defs' backing storage is not modified after creation. +unsafe impl Send for Defs {} +unsafe impl Sync for Defs {} + +impl Defs { + pub fn parse(defs: &str, tags: &str) -> Self { + Self { + raw: NonNull::new(unsafe { + haku2_defs_parse(defs.as_ptr(), defs.len(), tags.as_ptr(), tags.len()) + }) + .expect("out of memory"), + } + } +} + +impl Drop for Defs { + fn drop(&mut self) { + // SAFETY: The pointer passed is non-null. + unsafe { + haku2_defs_destroy(self.raw.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct Vm { + scratch: Scratch, + raw: NonNull, +} + +#[derive(Debug)] +pub enum Cont<'vm> { + None, + Dotter(ContDotter<'vm>), +} + +#[derive(Debug)] +pub struct ContDotter<'vm> { + vm: &'vm mut Vm, +} + +#[derive(Debug, Clone, Copy)] +pub struct Dotter { + pub from: (f32, f32), + pub to: (f32, f32), + pub num: f32, +} + +impl Vm { + pub fn new(scratch: Scratch, defs: &Defs, limits: &Limits) -> Self { + Self { + // SAFETY: + // - Ownership of s is passed to the VM, so the VM cannot outlive the scratch space. + // - The VM never gives you any references back, so this is safe to do. + // - The other arguments are only borrowed immutably for construction. + raw: NonNull::new(unsafe { + haku2_vm_new(scratch.raw.as_ptr(), defs.raw.as_ptr(), limits.raw.as_ptr()) + }) + .expect("out of memory"), + scratch, + } + } + + /// Begin running code. This makes the VM enter a "trampoline" state: after this call, you may + /// proceed to call `cont` as many times as it returns a value other than [`Cont::None`]. + /// + /// Calling `begin` again during this process will work correctly, and result in another + /// continuation being stack on top of the old one---at the expense of a stack slot. + /// + /// # Safety + /// + /// The bytecode passed in must be valid, because bytecode validation is done on a best-effort + /// basis. Bytecode retrieved out of the compiler is guaranteed to be safe. + pub unsafe fn begin(&mut self, code: &[u8], local_count: u8) -> Result<(), Exception> { + let ok = unsafe { + haku2_vm_run_main( + self.raw.as_ptr(), + self.scratch.raw.as_ptr(), + code.as_ptr(), + code.len(), + local_count, + ) + }; + if ok { + Ok(()) + } else { + Err(self.exception().expect("missing exception after !ok")) + } + } + + fn is_dotter(&self) -> bool { + // SAFETY: The pointer is valid. + unsafe { haku2_vm_is_dotter(self.raw.as_ptr()) } + } + + /// Returns how the VM should continue executing after the previous execution. + pub fn cont(&mut self) -> Cont<'_> { + match () { + _ if self.is_dotter() => Cont::Dotter(ContDotter { vm: self }), + _ => Cont::None, + } + } + + /// Renders the current scribble on top of the stack. + /// If the value on top is not a scribble, throws an exception (indicated by the return type.) + /// + /// The rendering is performed by calling into the [`Canvas`] trait. + pub fn render(&mut self, canvas: &mut dyn Canvas, max_depth: usize) -> Result<(), Exception> { + let mut wrapped = CanvasC { inner: canvas }; + let ok = unsafe { haku2_render(self.raw.as_ptr(), &mut wrapped, max_depth) }; + if ok { + Ok(()) + } else { + Err(self.exception().expect("missing exception after !ok")) + } + } + + /// Render the current exception out to a string. + /// Returns `None` if there's no exception. + pub fn exception(&self) -> Option { + // SAFETY: The pointer passed to this function is valid. + let len = unsafe { haku2_vm_exception_len(self.raw.as_ptr()) }; + if len == 0 { + return None; + } + + let mut buffer = vec![0; len]; + // SAFETY: The length of the buffer is as indicated by haku2_vm_exception_len. + unsafe { + haku2_vm_exception_render(self.raw.as_ptr(), buffer.as_mut_ptr()); + } + Some(Exception { + message: String::from_utf8_lossy(&buffer).into_owned(), + }) + } +} + +impl ContDotter<'_> { + pub fn run(self, dotter: &Dotter) -> Result<(), Exception> { + let Dotter { + from: (from_x, from_y), + to: (to_x, to_y), + num, + } = *dotter; + + let ok = unsafe { + haku2_vm_run_dotter( + self.vm.raw.as_ptr(), + self.vm.scratch.raw.as_ptr(), + from_x, + from_y, + to_x, + to_y, + num, + ) + }; + if ok { + Ok(()) + } else { + Err(self.vm.exception().expect("missing exception after !ok")) + } + } +} + +impl Drop for Vm { + fn drop(&mut self) { + // SAFETY: The pointer passed is non-null. + unsafe { + haku2_vm_destroy(self.raw.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct Exception { + message: String, +} + +impl Display for Exception { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl Error for Exception {} + +/// Marker for the VM to indicate that the rendering did not go down correctly. +/// If this is encountered, it throws an exception and aborts rendering. +#[derive(Debug)] +pub struct RenderError; + +pub trait Canvas { + fn begin(&mut self) -> Result<(), RenderError>; + fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) -> Result<(), RenderError>; + fn rectangle(&mut self, x: f32, y: f32, width: f32, height: f32) -> Result<(), RenderError>; + fn circle(&mut self, x: f32, y: f32, r: f32) -> Result<(), RenderError>; + fn fill(&mut self, r: u8, g: u8, b: u8, a: u8) -> Result<(), RenderError>; + fn stroke(&mut self, r: u8, g: u8, b: u8, a: u8, thickness: f32) -> Result<(), RenderError>; +} + +// SAFETY NOTE: I'm not sure the ownership model for this is quite correct. +// Given how the &mut's ownership flows through the Zig side of the code, it _should_ be fine, +// but I'm not an unsafe code expert to say this is the case for sure. + +#[repr(C)] +struct CanvasC<'a> { + inner: &'a mut dyn Canvas, +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn __haku2_canvas_begin(c: *mut CanvasC) -> bool { + let c = &mut *c; + c.inner.begin().is_ok() +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn __haku2_canvas_line( + c: *mut CanvasC, + x1: f32, + y1: f32, + x2: f32, + y2: f32, +) -> bool { + let c = &mut *c; + c.inner.line(x1, y1, x2, y2).is_ok() +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn __haku2_canvas_rectangle( + c: *mut CanvasC, + x: f32, + y: f32, + width: f32, + height: f32, +) -> bool { + let c = &mut *c; + c.inner.rectangle(x, y, width, height).is_ok() +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn __haku2_canvas_circle(c: *mut CanvasC, x: f32, y: f32, r: f32) -> bool { + let c = &mut *c; + c.inner.circle(x, y, r).is_ok() +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn __haku2_canvas_fill(c: *mut CanvasC, r: u8, g: u8, b: u8, a: u8) -> bool { + let c = &mut *c; + c.inner.fill(r, g, b, a).is_ok() +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn __haku2_canvas_stroke( + c: *mut CanvasC, + r: u8, + g: u8, + b: u8, + a: u8, + thickness: f32, +) -> bool { + let c = &mut *c; + c.inner.stroke(r, g, b, a, thickness).is_ok() +} diff --git a/crates/haku2/src/render.zig b/crates/haku2/src/render.zig new file mode 100644 index 0000000..72ce279 --- /dev/null +++ b/crates/haku2/src/render.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +const Canvas = @import("canvas.zig").Canvas; +const value = @import("value.zig"); +const Value = value.Value; +const Vm = @import("vm.zig"); + +fn notAScribble(vm: *Vm, val: Value) Vm.Error { + return vm.throw( + "the brush returned a {s}, which cannot be drawn. return a scribble (e.g. fill, stroke, list) instead", + .{val.typeName()}, + ); +} + +fn renderRec(vm: *Vm, canvas: *Canvas, val: Value, depth: usize, max_depth: usize) !void { + if (depth > max_depth) { + return vm.throw( + "the brush returned a scribble that's nested too deep ({} levels). try generating lists that aren't as deep using the (map (range min max) f) idiom, or flatten your lists using the flatten function", + .{max_depth}, + ); + } + if (val != .ref) return notAScribble(vm, val); + + switch (val.ref.*) { + .scribble => { + try canvas.begin(); + + switch (val.ref.scribble.shape) { + .point => |point| try canvas.line(point, point), + .line => |line| try canvas.line(line.start, line.end), + .rect => |rect| try canvas.rect(rect.top_left, rect.size), + .circle => |circle| try canvas.circle(circle.center, circle.radius), + } + + switch (val.ref.scribble.action) { + .stroke => |stroke| try canvas.stroke(value.rgbaTo8(stroke.color), stroke.thickness), + .fill => |fill| try canvas.fill(value.rgbaTo8(fill.color)), + } + }, + + .list => { + for (val.ref.list) |nested| { + try vm.consumeFuel(&vm.fuel, 1); + try renderRec(vm, canvas, nested, depth + 1, max_depth); + } + }, + + .shape => { + return vm.throw("the brush returned a bare shape, which cannot be drawn. try wrapping your shape in a fill or a stroke: (fill #000 )", .{}); + }, + + else => return notAScribble(vm, val), + } +} + +pub fn render(vm: *Vm, canvas: *Canvas, max_depth: usize) !void { + const val = try vm.pop(); + try renderRec(vm, canvas, val, 0, max_depth); +} diff --git a/crates/haku2/src/system.zig b/crates/haku2/src/system.zig index 3df802f..925afdb 100644 --- a/crates/haku2/src/system.zig +++ b/crates/haku2/src/system.zig @@ -3,10 +3,50 @@ const mem = std.mem; const meta = std.meta; const math = std.math; +const bytecode = @import("bytecode.zig"); +const Opcode = bytecode.Opcode; const value = @import("value.zig"); const Value = value.Value; const Vm = @import("vm.zig"); +fn recordBytecodeSize(fields: []const value.TagId) usize { + var size: usize = 0; + + size += 1; // Opcode.field + size += 1; // count: u8 + size += 2 * fields.len; // tags: [count]u16 + size += 1; // Opcode.return + + return size; +} + +fn recordBytecode(comptime fields: []const value.TagId) [recordBytecodeSize(fields)]u8 { + if (fields.len > 255) @compileError("too many fields"); + + var code = [_]u8{undefined} ** recordBytecodeSize(fields); + var cursor: usize = 0; + + code[cursor] = @intFromEnum(Opcode.field); + cursor += 1; + code[cursor] = @as(u8, @truncate(fields.len)); + cursor += 1; + + for (fields) |field| { + const tag_id = mem.toBytes(field); + code[cursor] = tag_id[0]; + code[cursor + 1] = tag_id[1]; + cursor += 2; + } + + code[cursor] = @intFromEnum(Opcode.ret); + cursor += 1; + + return code; +} + +const record_dotter_bytecode = recordBytecode(&.{ .From, .To, .Num }); +pub const record_dotter: bytecode.Chunk = .{ .bytecode = &record_dotter_bytecode }; + pub const Context = struct { vm: *Vm, allocator: mem.Allocator, @@ -692,18 +732,22 @@ fn circle(center: Vec4, radius: f32) value.Ref { } fn stroke(thickness: f32, color: Rgba, shape: *const value.Shape) value.Ref { - return .{ .scribble = .{ .stroke = .{ - .thickness = thickness, - .color = color.value, + return .{ .scribble = .{ .shape = shape.*, - } } }; + .action = .{ .stroke = .{ + .thickness = thickness, + .color = color.value, + } }, + } }; } fn fill(color: Rgba, shape: *const value.Shape) value.Ref { - return .{ .scribble = .{ .fill = .{ - .color = color.value, + return .{ .scribble = .{ .shape = shape.*, - } } }; + .action = .{ .fill = .{ + .color = color.value, + } }, + } }; } fn withDotter(cont: *const value.Closure, vm: *Vm) Vm.Error!value.Ref { diff --git a/crates/haku2/src/value.zig b/crates/haku2/src/value.zig index ec700d6..51a6e76 100644 --- a/crates/haku2/src/value.zig +++ b/crates/haku2/src/value.zig @@ -1,6 +1,7 @@ const std = @import("std"); const meta = std.meta; const mem = std.mem; +const math = std.math; const bytecode = @import("bytecode.zig"); @@ -88,7 +89,12 @@ pub const Rgba8 = @Vector(4, u8); pub const Rgba = @Vector(4, f32); pub fn rgbaFrom8(rgba: Rgba8) Rgba { - return @as(Rgba, @floatFromInt(rgba)) / @as(Rgba, @splat(255.0)); + return @as(Rgba, @floatFromInt(rgba)) / @as(Rgba, @splat(255)); +} + +pub fn rgbaTo8(rgba: Rgba) Rgba8 { + const clamped = math.clamp(rgba, @as(Rgba, @splat(0)), @as(Rgba, @splat(1))); + return @as(Rgba8, @intFromFloat(clamped * @as(Rgba, @splat(255)))); } pub const Ref = union(enum) { @@ -131,19 +137,22 @@ pub const Shape = union(enum) { }; }; -pub const Scribble = union(enum) { - stroke: Stroke, - fill: Fill, +pub const Scribble = struct { + shape: Shape, + action: Action, - pub const Stroke = struct { - thickness: f32, - color: Rgba, - shape: Shape, - }; + pub const Action = union(enum) { + stroke: Stroke, + fill: Fill, - pub const Fill = struct { - color: Rgba, - shape: Shape, + pub const Stroke = struct { + thickness: f32, + color: Rgba, + }; + + pub const Fill = struct { + color: Rgba, + }; }; }; diff --git a/crates/haku2/src/vm.zig b/crates/haku2/src/vm.zig index 96eb3cd..42556fa 100644 --- a/crates/haku2/src/vm.zig +++ b/crates/haku2/src/vm.zig @@ -1,8 +1,10 @@ const std = @import("std"); const mem = std.mem; +const debug = std.debug; const testAllocator = std.testing.allocator; const bytecode = @import("bytecode.zig"); +const Canvas = @import("canvas.zig"); const system = @import("system.zig"); const value = @import("value.zig"); const Value = value.Value; @@ -15,7 +17,6 @@ call_stack: []CallFrame, call_stack_top: u32 = 0, defs: []Value, fuel: u32, -exception_buffer: [1024]u8 = [_]u8{0} ** 1024, // buffer for exception message exception: ?Exception = null, pub const Limits = struct { @@ -31,7 +32,9 @@ pub const CallFrame = struct { }; pub const Exception = struct { - message: []const u8, + len: usize, + format: *const fn (buf: []u8, args: *align(64) const anyopaque) []u8, + args: [40]u8 align(64), // increase the size if we ever throw a larger exception }; /// All errors coming from inside the VM get turned into a single Exception type, which signals @@ -51,11 +54,28 @@ pub fn init(a: mem.Allocator, defs: *const bytecode.Defs, limits: *const Limits) } pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error { - const message = std.fmt.bufPrint(vm.exception_buffer[0..], fmt, args) catch { - vm.exception = .{ .message = "[exception message is too long; format string: " ++ fmt ++ "]" }; - return error.Exception; + const Args = @TypeOf(args); + const max_args_size = @sizeOf(@TypeOf(vm.exception.?.args)); + if (@sizeOf(Args) > max_args_size) { + @compileError(std.fmt.comptimePrint( + "format arguments are too large; size={}, max={}", + .{ @sizeOf(Args), max_args_size }, + )); + } + + const Formatter = struct { + fn format(buf: []u8, erased_args: *align(64) const anyopaque) []u8 { + return std.fmt.bufPrint(buf, fmt, @as(*const Args, @ptrCast(erased_args)).*) catch unreachable; + } }; - vm.exception = .{ .message = message }; + + var exn = Exception{ + .len = @truncate(std.fmt.count(fmt, args)), + .format = Formatter.format, + .args = undefined, + }; + @memcpy(exn.args[0..@sizeOf(Args)], mem.asBytes(&args)); + return error.Exception; } @@ -452,7 +472,43 @@ pub fn run( try vm.consumeFuel(&fuel, 1); // NOTE: Not a validateBytecode call because this is zero-cost on the happy path, // so we don't need to disable it on release builds. - return vm.throw("corrupted bytecode (invalid opcode {})", .{invalid_opcode}); + return vm.throw("corrupted bytecode: invalid opcode {}", .{invalid_opcode}); }, } } + +pub const Dotter = struct { + from: value.Vec4, + to: value.Vec4, + num: f32, +}; + +/// NOTE: Assumes the value at the top is a dotter reticle. +pub fn runDotter( + vm: *Vm, + allocator: mem.Allocator, + from: value.Vec4, + to: value.Vec4, + num: f32, +) Error!void { + const reticle = try vm.pop(); + const draw = reticle.ref.reticle.dotter.draw; // parameter count checked on construction + + const data = allocator.dupe(Value, &[_]Value{ + .{ .vec4 = from }, + .{ .vec4 = to }, + .{ .number = num }, + }) catch return vm.outOfMemory(); + const ref = allocator.create(value.Ref) catch return vm.outOfMemory(); + ref.* = value.Ref{ .closure = .{ + .chunk = &system.record_dotter, + .start = 0, + .param_count = 1, + .local_count = 0, + .captures = data, + } }; + + const bottom = vm.stack_top; + try vm.push(.{ .ref = ref }); + try vm.run(allocator, draw, bottom); +}