haku2: make server use haku2 (and make it work!)

This commit is contained in:
りき萌 2025-06-13 20:11:52 +02:00
parent c5e2892def
commit 48d03699bd
16 changed files with 329 additions and 174 deletions

79
Cargo.lock generated
View file

@ -17,6 +17,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.11" version = "0.8.11"
@ -175,7 +181,7 @@ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.7.4",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@ -198,6 +204,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.6.0"
@ -353,6 +365,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.5" version = "0.8.5"
@ -484,6 +505,25 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide 0.8.4",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -955,6 +995,16 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.1" version = "1.0.1"
@ -1142,6 +1192,19 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.4",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.20"
@ -1225,7 +1288,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.6.0",
] ]
[[package]] [[package]]
@ -1298,6 +1361,7 @@ dependencies = [
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"tiny-skia",
"tokio", "tokio",
"toml", "toml",
"tower-http", "tower-http",
@ -1318,7 +1382,7 @@ version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.6.0",
"fallible-iterator", "fallible-iterator",
"fallible-streaming-iterator", "fallible-streaming-iterator",
"hashlink", "hashlink",
@ -1474,6 +1538,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -1575,6 +1645,7 @@ dependencies = [
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"log", "log",
"png",
"tiny-skia-path", "tiny-skia-path",
] ]
@ -1715,7 +1786,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.6.0",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",

View file

@ -493,7 +493,6 @@ unsafe extern "C" fn haku_begin_brush(instance: *mut Instance, brush: *const Bru
panic!("brush is not compiled and ready to be used"); panic!("brush is not compiled and ready to be used");
}; };
info!("old image: {:?}", instance.vm.image());
instance.vm.restore_image(&instance.vm_image); instance.vm.restore_image(&instance.vm_image);
instance.vm.apply_defs(&instance.defs); instance.vm.apply_defs(&instance.defs);
instance.reset_exception(); instance.reset_exception();

View file

@ -10,7 +10,7 @@ const value = @import("value.zig");
const Vm = @import("vm.zig"); const Vm = @import("vm.zig");
const log = @import("log.zig"); const log = @import("log.zig");
const vm_trace = true; const debug_logs = false;
pub const allocator = pub const allocator =
if (builtin.cpu.arch == .wasm32) if (builtin.cpu.arch == .wasm32)
@ -19,7 +19,7 @@ pub const allocator =
@import("allocator.zig").hostAllocator; @import("allocator.zig").hostAllocator;
pub const std_options: std.Options = .{ pub const std_options: std.Options = .{
.log_level = if (vm_trace) .debug else .info, .log_level = if (debug_logs) .debug else .info,
.logFn = log.logFn, .logFn = log.logFn,
}; };
@ -57,10 +57,6 @@ export fn haku2_limits_set_call_stack_capacity(limits: *Vm.Limits, new: usize) v
limits.call_stack_capacity = new; limits.call_stack_capacity = new;
} }
export fn haku2_limits_set_fuel(limits: *Vm.Limits, new: u32) void {
limits.fuel = new;
}
// Defs // Defs
export fn haku2_defs_parse( export fn haku2_defs_parse(
@ -95,6 +91,10 @@ export fn haku2_vm_destroy(vm: *Vm) void {
allocator.destroy(vm); allocator.destroy(vm);
} }
export fn haku2_vm_reset(vm: *Vm, fuel: u32) void {
vm.reset(fuel);
}
export fn haku2_vm_run_main( export fn haku2_vm_run_main(
vm: *Vm, vm: *Vm,
scratch: *Scratch, scratch: *Scratch,
@ -102,11 +102,15 @@ export fn haku2_vm_run_main(
code_len: usize, code_len: usize,
local_count: u8, local_count: u8,
) bool { ) bool {
const chunk = bytecode.Chunk{ const chunk = scratch.allocator().create(bytecode.Chunk) catch {
vm.outOfMemory() catch {};
return false;
};
chunk.* = bytecode.Chunk{
.bytecode = code[0..code_len], .bytecode = code[0..code_len],
}; };
const closure = value.Closure{ const closure = value.Closure{
.chunk = &chunk, .chunk = chunk,
.start = 0, .start = 0,
.param_count = 0, .param_count = 0,
.local_count = local_count, .local_count = local_count,

View file

@ -7,6 +7,8 @@ use std::{
slice, slice,
}; };
use log::{debug, trace};
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
unsafe extern "C" fn __haku2_alloc(size: usize, align: usize) -> *mut u8 { unsafe extern "C" fn __haku2_alloc(size: usize, align: usize) -> *mut u8 {
if let Ok(layout) = Layout::from_size_align(size, align) { if let Ok(layout) = Layout::from_size_align(size, align) {
@ -121,7 +123,6 @@ extern "C" {
fn haku2_limits_destroy(limits: *mut LimitsC); fn haku2_limits_destroy(limits: *mut LimitsC);
fn haku2_limits_set_stack_capacity(limits: *mut LimitsC, new: usize); 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_call_stack_capacity(limits: *mut LimitsC, new: usize);
fn haku2_limits_set_fuel(limits: *mut LimitsC, new: u32);
fn haku2_defs_parse( fn haku2_defs_parse(
defs_string: *const u8, defs_string: *const u8,
@ -133,6 +134,7 @@ extern "C" {
fn haku2_vm_new(s: *mut ScratchC, defs: *const DefsC, limits: *const LimitsC) -> *mut VmC; 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_destroy(vm: *mut VmC);
fn haku2_vm_reset(vm: *mut VmC, fuel: u32);
fn haku2_vm_run_main( fn haku2_vm_run_main(
vm: *mut VmC, vm: *mut VmC,
scratch: *mut ScratchC, scratch: *mut ScratchC,
@ -167,13 +169,14 @@ pub struct Scratch {
impl Scratch { impl Scratch {
pub fn new(max: usize) -> Scratch { pub fn new(max: usize) -> Scratch {
Scratch { // SAFETY: haku2_scratch_new does not have any safety invariants.
// SAFETY: haku2_scratch_new does not have any safety invariants. let raw = NonNull::new(unsafe { haku2_scratch_new(max) }).expect("out of memory");
raw: NonNull::new(unsafe { haku2_scratch_new(max) }).expect("out of memory"), trace!("Scratch::new -> {raw:?}");
} Scratch { raw }
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
trace!("Scratch::reset({:?})", self.raw);
// SAFETY: The pointer passed is non-null. // SAFETY: The pointer passed is non-null.
unsafe { unsafe {
haku2_scratch_reset(self.raw.as_ptr()); haku2_scratch_reset(self.raw.as_ptr());
@ -183,6 +186,7 @@ impl Scratch {
impl Drop for Scratch { impl Drop for Scratch {
fn drop(&mut self) { fn drop(&mut self) {
trace!("Scratch::drop({:?})", self.raw);
// SAFETY: The pointer passed is non-null. // SAFETY: The pointer passed is non-null.
unsafe { unsafe {
haku2_scratch_destroy(self.raw.as_ptr()); haku2_scratch_destroy(self.raw.as_ptr());
@ -194,7 +198,6 @@ impl Drop for Scratch {
pub struct LimitsSpec { pub struct LimitsSpec {
pub stack_capacity: usize, pub stack_capacity: usize,
pub call_stack_capacity: usize, pub call_stack_capacity: usize,
pub fuel: u32,
} }
#[derive(Debug)] #[derive(Debug)]
@ -216,7 +219,6 @@ impl Limits {
unsafe { unsafe {
haku2_limits_set_stack_capacity(limits.as_ptr(), spec.stack_capacity); 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_call_stack_capacity(limits.as_ptr(), spec.call_stack_capacity);
haku2_limits_set_fuel(limits.as_ptr(), spec.fuel);
} }
Self { raw: limits } Self { raw: limits }
@ -315,21 +317,21 @@ pub struct Dotter {
impl Vm { impl Vm {
pub fn new(scratch: Scratch, code: Code, limits: &Limits) -> Self { pub fn new(scratch: Scratch, code: Code, limits: &Limits) -> Self {
let raw = NonNull::new(unsafe {
haku2_vm_new(
scratch.raw.as_ptr(),
code.defs.raw.as_ptr(),
limits.raw.as_ptr(),
)
})
.expect("out of memory");
trace!("Vm::new({scratch:?}, {code:?}, {limits:?}) -> {raw:?}");
Self { Self {
// SAFETY: // SAFETY:
// - Ownership of scratch is passed to the VM, so the VM cannot outlive the scratch space. // - Ownership of scratch 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 VM never gives you any references back, so this is safe to do.
// - The other arguments are only borrowed immutably for construction. // - The other arguments are only borrowed immutably for construction.
inner: VmInner { inner: VmInner { raw },
raw: NonNull::new(unsafe {
haku2_vm_new(
scratch.raw.as_ptr(),
code.defs.raw.as_ptr(),
limits.raw.as_ptr(),
)
})
.expect("out of memory"),
},
scratch, scratch,
code, code,
} }
@ -340,8 +342,10 @@ impl Vm {
/// ///
/// Calling `begin` again during this process will work correctly, and result in another /// 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. /// continuation being stack on top of the old one---at the expense of a stack slot.
pub fn begin(&mut self) -> Result<(), Exception> { pub fn begin(&mut self, fuel: u32) -> Result<(), Exception> {
trace!("Vm::begin({self:?}, {fuel})");
let ok = unsafe { let ok = unsafe {
haku2_vm_reset(self.inner.raw.as_ptr(), fuel);
haku2_vm_run_main( haku2_vm_run_main(
self.inner.raw.as_ptr(), self.inner.raw.as_ptr(),
self.scratch.raw.as_ptr(), self.scratch.raw.as_ptr(),
@ -411,6 +415,7 @@ impl Vm {
/// Take the `Scratch` out of the VM for reuse in another one. /// Take the `Scratch` out of the VM for reuse in another one.
/// The scratch memory will be reset (no bytes will be consumed.) /// The scratch memory will be reset (no bytes will be consumed.)
pub fn into_scratch(self) -> Scratch { pub fn into_scratch(self) -> Scratch {
trace!("Vm::into_scratch({self:?})");
let Vm { let Vm {
mut scratch, mut scratch,
code: _, code: _,
@ -423,6 +428,8 @@ impl Vm {
impl ContDotter<'_> { impl ContDotter<'_> {
pub fn run(self, dotter: &Dotter) -> Result<(), Exception> { pub fn run(self, dotter: &Dotter) -> Result<(), Exception> {
trace!("ContDotter::run({self:?}, {dotter:?})");
let Dotter { let Dotter {
from: (from_x, from_y), from: (from_x, from_y),
to: (to_x, to_y), to: (to_x, to_y),
@ -455,6 +462,7 @@ struct VmInner {
impl Drop for VmInner { impl Drop for VmInner {
fn drop(&mut self) { fn drop(&mut self) {
trace!("VmInner::drop({:?})", self.raw);
// SAFETY: The pointer passed is non-null. // SAFETY: The pointer passed is non-null.
unsafe { unsafe {
haku2_vm_destroy(self.raw.as_ptr()); haku2_vm_destroy(self.raw.as_ptr());

View file

@ -2,6 +2,7 @@ const std = @import("std");
const mem = std.mem; const mem = std.mem;
const meta = std.meta; const meta = std.meta;
const math = std.math; const math = std.math;
const log = std.log.scoped(.system);
const bytecode = @import("bytecode.zig"); const bytecode = @import("bytecode.zig");
const Opcode = bytecode.Opcode; const Opcode = bytecode.Opcode;
@ -112,10 +113,13 @@ fn fromArgument(cx: Context, comptime T: type, i: usize) Vm.Error!T {
if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list"); if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list");
return val.ref.list; return val.ref.list;
}, },
*const value.Shape => { value.Shape => {
const val = cx.args[i]; const val = cx.args[i];
if (val != .ref or val.ref.* != .shape) return typeError(cx.vm, val, i, "shape"); if (toShape(val)) |shape| {
return &val.ref.shape; return shape;
} else {
return typeError(cx.vm, val, i, "shape");
}
}, },
*const value.Closure => { *const value.Closure => {
const val = cx.args[i]; const val = cx.args[i];
@ -756,9 +760,9 @@ fn circle(center: Vec4, radius: f32) value.Ref {
} } }; } } };
} }
fn stroke(thickness: f32, color: Rgba, shape: *const value.Shape) value.Ref { fn stroke(thickness: f32, color: Rgba, shape: value.Shape) value.Ref {
return .{ .scribble = .{ return .{ .scribble = .{
.shape = shape.*, .shape = shape,
.action = .{ .stroke = .{ .action = .{ .stroke = .{
.thickness = thickness, .thickness = thickness,
.color = color.value, .color = color.value,
@ -766,9 +770,9 @@ fn stroke(thickness: f32, color: Rgba, shape: *const value.Shape) value.Ref {
} }; } };
} }
fn fill(color: Rgba, shape: *const value.Shape) value.Ref { fn fill(color: Rgba, shape: value.Shape) value.Ref {
return .{ .scribble = .{ return .{ .scribble = .{
.shape = shape.*, .shape = shape,
.action = .{ .fill = .{ .action = .{ .fill = .{
.color = color.value, .color = color.value,
} }, } },
@ -776,6 +780,7 @@ fn fill(color: Rgba, shape: *const value.Shape) value.Ref {
} }
fn withDotter(cont: *const value.Closure, cx: Context) Vm.Error!value.Ref { fn withDotter(cont: *const value.Closure, cx: Context) Vm.Error!value.Ref {
log.debug("withDotter({})", .{cont});
if (cont.param_count != 1) { if (cont.param_count != 1) {
return cx.vm.throw("function passed to withDotter must have a single parameter (\\d -> _), but it has {}", .{cont.param_count}); return cx.vm.throw("function passed to withDotter must have a single parameter (\\d -> _), but it has {}", .{cont.param_count});
} }

View file

@ -68,6 +68,28 @@ pub const Value = union(enum) {
}, },
}; };
} }
pub fn format(value: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
switch (value) {
.nil => try std.fmt.formatBuf("Nil", options, writer),
.false => try std.fmt.formatBuf("False", options, writer),
.true => try std.fmt.formatBuf("True", options, writer),
inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}),
inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }),
.ref => |ref| switch (ref.*) {
.closure => |c| try std.fmt.format(writer, "function({})", .{c.param_count}),
.list => |l| {
try std.fmt.formatBuf("[", options, writer);
for (l, 0..) |elem, i| {
if (i != 0) try std.fmt.formatBuf(", ", options, writer);
try elem.format(fmt, options, writer);
}
try std.fmt.formatBuf("]", options, writer);
},
inline .shape, .scribble, .reticle => |x| try std.fmt.format(writer, "{}", .{x}),
},
}
}
}; };
pub const TagId = enum(u16) { pub const TagId = enum(u16) {

View file

@ -17,13 +17,12 @@ stack_top: u32 = 0,
call_stack: []CallFrame, call_stack: []CallFrame,
call_stack_top: u32 = 0, call_stack_top: u32 = 0,
defs: []Value, defs: []Value,
fuel: u32, fuel: u32 = 0, // NOTE: VM must be refueled via reset() before running code
exception: ?Exception = null, exception: ?Exception = null,
pub const Limits = struct { pub const Limits = struct {
stack_capacity: usize = 256, stack_capacity: usize = 256,
call_stack_capacity: usize = 256, call_stack_capacity: usize = 256,
fuel: u32 = 63336,
}; };
pub const CallFrame = struct { pub const CallFrame = struct {
@ -50,10 +49,16 @@ pub fn init(a: mem.Allocator, defs: *const bytecode.Defs, limits: *const Limits)
.stack = try a.alloc(Value, limits.stack_capacity), .stack = try a.alloc(Value, limits.stack_capacity),
.call_stack = try a.alloc(CallFrame, limits.call_stack_capacity), .call_stack = try a.alloc(CallFrame, limits.call_stack_capacity),
.defs = try a.alloc(Value, defs.num_defs), .defs = try a.alloc(Value, defs.num_defs),
.fuel = limits.fuel,
}; };
} }
pub fn reset(vm: *Vm, fuel: u32) void {
vm.stack_top = 0;
vm.call_stack_top = 0;
vm.fuel = fuel;
vm.exception = null;
}
pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error { pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error {
log.debug("throw: fmt={s}", .{fmt}); log.debug("throw: fmt={s}", .{fmt});

View file

@ -26,6 +26,7 @@ rayon = "1.10.0"
rusqlite = { version = "0.32.1", features = ["bundled"] } rusqlite = { version = "0.32.1", features = ["bundled"] }
serde = { version = "1.0.206", features = ["derive"] } serde = { version = "1.0.206", features = ["derive"] }
serde_json = "1.0.124" serde_json = "1.0.124"
tiny-skia = "0.11.4"
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.2", features = ["full"] }
toml = "0.8.19" toml = "0.8.19"
tower-http = { version = "0.5.2", features = ["fs"] } tower-http = { version = "0.5.2", features = ["fs"] }

View file

@ -12,10 +12,7 @@ use axum::{
}; };
use base64::Engine; use base64::Engine;
use eyre::{bail, Context, OptionExt}; use eyre::{bail, Context, OptionExt};
use haku::{ use haku::value::Value;
trampoline::{Cont, Trampoline},
value::Value,
};
use schema::{ use schema::{
ChunkInfo, Error, LoginRequest, LoginResponse, Notify, Online, Request, Version, WallInfo, ChunkInfo, Error, LoginRequest, LoginResponse, Notify, Online, Request, Version, WallInfo,
}; };
@ -32,8 +29,8 @@ use crate::{
schema::Vec2, schema::Vec2,
wall::{ wall::{
self, auto_save::AutoSave, chunk_images::ChunkImages, chunk_iterator::ChunkIterator, self, auto_save::AutoSave, chunk_images::ChunkImages, chunk_iterator::ChunkIterator,
database::ChunkDataPair, ChunkPosition, Interaction, JoinError, SessionHandle, UserInit, database::ChunkDataPair, render::ChunkCanvas, ChunkPosition, Interaction, JoinError,
Wall, WallId, SessionHandle, UserInit, Wall, WallId,
}, },
}; };
@ -265,7 +262,7 @@ impl SessionLoop {
let wall = Arc::clone(&wall); let wall = Arc::clone(&wall);
move || { move || {
let _span = let _span =
info_span!("render_thread", %wall_id, session_id = ?handle.session_id) info_span!("render_thread", ?wall_id, session_id = ?handle.session_id)
.entered(); .entered();
Self::render_thread(wall, limits, render_commands_rx) Self::render_thread(wall, limits, render_commands_rx)
} }
@ -287,7 +284,7 @@ impl SessionLoop {
}) })
} }
#[instrument(skip(self, ws), fields(wall_id = %self.wall_id, session_id = ?self.handle.session_id))] #[instrument(skip(self, ws), fields(wall_id = ?self.wall_id, session_id = ?self.handle.session_id))]
async fn event_loop(&mut self, ws: &mut WebSocket) -> eyre::Result<()> { async fn event_loop(&mut self, ws: &mut WebSocket) -> eyre::Result<()> {
loop { loop {
select! { select! {
@ -456,7 +453,6 @@ impl SessionLoop {
fn render_thread(wall: Arc<Wall>, limits: Limits, mut commands: mpsc::Receiver<RenderCommand>) { fn render_thread(wall: Arc<Wall>, limits: Limits, mut commands: mpsc::Receiver<RenderCommand>) {
let mut haku = Haku::new(limits); let mut haku = Haku::new(limits);
let mut trampoline = None;
let mut brush_ok = false; let mut brush_ok = false;
let mut current_render_area = RenderArea::default(); let mut current_render_area = RenderArea::default();
@ -488,37 +484,15 @@ impl SessionLoop {
}, },
other => error!("received Dotter interaction when a {other:?} continuation was next") other => error!("received Dotter interaction when a {other:?} continuation was next")
} }
// old v1 code
if let Some(tramp) = jumpstart_trampoline(&mut haku, &mut trampoline) {
let cont = haku.cont(tramp);
if cont == Cont::Dotter {
_ = haku.dotter(tramp, from, to, num);
} else {
error!("received Dotter interaction when a {cont:?} continuation was next");
}
} else {
info!("failed to jumpstart trampoline for Dotter interaction");
}
} }
} }
Interaction::Scribble => { Interaction::Scribble => {
// Regarding the take(): this is to self-synchronize in case of error. match haku.cont2() {
// Once a scribble is rendered, we reset back to not having a trampoline, haku2::Cont::None => {
// and further interactions must be sent to kickstart a new one. draw_to_chunks2(&wall, current_render_area, &mut haku);
// }
// Regarding not jumpstarting a trampoline here: it would be useless, _ => error!("tried to draw a scribble with an active continuation"),
// because a scribble is always the last thing in the chain, and it never
// modifies the render area. Therefore what would end up happening is we'd
// end up rendering to no chunks, therefore not doing anything.
// The server doesn't need to report anything to the user and so can save
// a bit of work by _not_ evaluating the brush initially here.
if let Some(trampoline) = trampoline.take() {
_ = draw_to_chunks(&wall, &haku, current_render_area, trampoline.value);
} else {
info!("tried to scribble without an active trampoline");
} }
current_render_area = RenderArea::default(); current_render_area = RenderArea::default();
@ -574,31 +548,16 @@ fn chunks_to_modify(
chunks chunks
} }
fn jumpstart_trampoline<'a>(
haku: &mut Haku,
trampoline: &'a mut Option<Trampoline>,
) -> Option<&'a mut Trampoline> {
if trampoline.is_none() {
*trampoline = haku.eval_brush().ok().map(Trampoline::new);
}
trampoline.as_mut()
}
fn jumpstart_trampoline2(haku: &mut Haku) { fn jumpstart_trampoline2(haku: &mut Haku) {
if !haku.has_cont2() { if !haku.has_cont2() {
if let Err(e) = haku.eval_brush2() { if let Err(e) = haku.eval_brush2() {
error!("eval_brush2: {e:?}"); error!("eval_brush2 exception: {e:?}");
} }
} }
} }
#[instrument(skip(wall, haku, value))] #[instrument(skip(wall, vm))]
fn draw_to_chunks( fn draw_to_chunks2(wall: &Wall, render_area: RenderArea, vm: &mut Haku) {
wall: &Wall,
haku: &Haku,
render_area: RenderArea,
value: Value,
) -> eyre::Result<()> {
let settings = wall.settings(); let settings = wall.settings();
let chunk_size = settings.chunk_size as f32; let chunk_size = settings.chunk_size as f32;
@ -612,9 +571,10 @@ fn draw_to_chunks(
let y = f32::floor(-chunk_y as f32 * chunk_size); let y = f32::floor(-chunk_y as f32 * chunk_size);
let chunk_ref = wall.get_or_create_chunk(ChunkPosition::new(chunk_x, chunk_y)); let chunk_ref = wall.get_or_create_chunk(ChunkPosition::new(chunk_x, chunk_y));
let mut chunk = chunk_ref.blocking_lock(); let mut chunk = chunk_ref.blocking_lock();
haku.render_value(&mut chunk.pixmap, value, Vec2 { x, y })?; let mut canvas = ChunkCanvas::new(&mut chunk).translated(x, y);
if let Err(e) = vm.render2(&mut canvas, 256) {
error!(chunk_x, chunk_y, "draw_to_chunks2 exception: {e:?}");
}
} }
} }
Ok(())
} }

View file

@ -52,8 +52,6 @@ pub struct Haku {
system_image: SystemImage, system_image: SystemImage,
defs: Defs, defs: Defs,
defs_image: DefsImage, defs_image: DefsImage,
vm: Vm,
vm_image: VmImage,
vm2: Option<haku2::Vm>, vm2: Option<haku2::Vm>,
@ -67,20 +65,9 @@ impl Haku {
max_defs: limits.max_defs, max_defs: limits.max_defs,
max_tags: limits.max_tags, max_tags: limits.max_tags,
}); });
let vm = Vm::new(
&defs,
&VmLimits {
stack_capacity: limits.stack_capacity,
call_stack_capacity: limits.call_stack_capacity,
ref_capacity: limits.ref_capacity,
fuel: limits.fuel,
memory: limits.memory,
},
);
let system_image = system.image(); let system_image = system.image();
let defs_image = defs.image(); let defs_image = defs.image();
let vm_image = vm.image();
Self { Self {
limits, limits,
@ -88,8 +75,6 @@ impl Haku {
system_image, system_image,
defs, defs,
defs_image, defs_image,
vm,
vm_image,
vm2: None, vm2: None,
brush: None, brush: None,
} }
@ -163,7 +148,6 @@ impl Haku {
let limits = haku2::Limits::new(haku2::LimitsSpec { let limits = haku2::Limits::new(haku2::LimitsSpec {
stack_capacity: self.limits.stack_capacity, stack_capacity: self.limits.stack_capacity,
call_stack_capacity: self.limits.call_stack_capacity, call_stack_capacity: self.limits.call_stack_capacity,
fuel: self.limits.fuel as u32,
}); });
self.vm2 = Some(haku2::Vm::new(scratch, code, &limits)) self.vm2 = Some(haku2::Vm::new(scratch, code, &limits))
} }
@ -173,60 +157,30 @@ impl Haku {
Ok(()) Ok(())
} }
#[instrument(skip(self), err(level = Level::INFO))]
pub fn eval_brush(&mut self) -> eyre::Result<Value> {
let (chunk_id, closure_spec) = self
.brush
.ok_or_eyre("brush is not compiled and ready to be used")?;
self.vm.restore_image(&self.vm_image);
self.vm.apply_defs(&self.defs);
let closure_id = self
.vm
.create_ref(Ref::Closure(Closure::chunk(chunk_id, closure_spec)))
.context("not enough ref slots to create initial closure")?;
let scribble = self
.vm
.run(&self.system, closure_id, &[])
.context("an exception occurred while evaluating the scribble")?;
Ok(scribble)
}
#[instrument(skip(self), err(level = Level::INFO))] #[instrument(skip(self), err(level = Level::INFO))]
pub fn eval_brush2(&mut self) -> eyre::Result<()> { pub fn eval_brush2(&mut self) -> eyre::Result<()> {
let vm = self let vm = self
.vm2 .vm2
.as_mut() .as_mut()
.ok_or_eyre("brush is not compiled and ready to be used")?; .ok_or_eyre("brush is not compiled and ready to be used")?;
vm.begin().context("an exception occurred during begin()")?; vm.begin(self.limits.fuel as u32)
.context("an exception occurred during begin()")?;
Ok(()) Ok(())
} }
#[instrument(skip(self, pixmap, value, translation), err(level = Level::INFO))] #[instrument(skip(self, canvas, max_depth), err(level = Level::INFO))]
pub fn render_value( pub fn render2(
&self, &mut self,
pixmap: &mut Pixmap, canvas: &mut dyn haku2::Canvas,
value: Value, max_depth: usize,
translation: Vec2,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let mut renderer = Renderer::new( let vm = self
pixmap, .vm2
&RendererLimits { .as_mut()
pixmap_stack_capacity: self.limits.pixmap_stack_capacity, .ok_or_eyre("VM is not ready for rendering")?;
transform_stack_capacity: self.limits.transform_stack_capacity, vm.render(canvas, max_depth)
}, .context("exception while rendering")?;
); Ok(())
renderer.translate(translation.x, translation.y);
let result = renderer.render(&self.vm, value);
result.context("an exception occurred while rendering the scribble")
}
pub fn cont(&self, trampoline: &Trampoline) -> Cont {
trampoline.cont(&self.vm)
} }
pub fn has_cont2(&mut self) -> bool { pub fn has_cont2(&mut self) -> bool {
@ -236,14 +190,4 @@ impl Haku {
pub fn cont2(&mut self) -> haku2::Cont<'_> { pub fn cont2(&mut self) -> haku2::Cont<'_> {
self.vm2.as_mut().expect("VM is not started").cont() self.vm2.as_mut().expect("VM is not started").cont()
} }
pub fn dotter(
&mut self,
trampoline: &mut Trampoline,
from: Vec2,
to: Vec2,
num: f32,
) -> eyre::Result<()> {
Ok(trampoline.dotter(&mut self.vm, &self.system, from.into(), to.into(), num)?)
}
} }

View file

@ -12,6 +12,15 @@ pub fn serialize(f: &mut fmt::Formatter<'_>, prefix: &str, bytes: &[u8; 32]) ->
Ok(()) Ok(())
} }
pub fn serialize_debug(f: &mut fmt::Formatter<'_>, bytes: &[u8; 32]) -> fmt::Result {
let mut buffer = [b'0'; 43];
base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode_slice(bytes, &mut buffer)
.unwrap();
f.write_str(std::str::from_utf8(&buffer[0..6]).unwrap())?;
Ok(())
}
#[derive(Debug)] #[derive(Debug)]
pub struct InvalidId; pub struct InvalidId;

View file

@ -5,7 +5,7 @@ use config::Config;
use eyre::Context; use eyre::Context;
use router::router; use router::router;
use tokio::{fs, net::TcpListener}; use tokio::{fs, net::TcpListener};
use tracing::info; use tracing::{info, level_filters::LevelFilter};
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter}; use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter};
mod api; mod api;
@ -95,8 +95,15 @@ async fn main() {
color_eyre::install().unwrap(); color_eyre::install().unwrap();
tracing_subscriber::registry() tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer()) .with(
.with(EnvFilter::from_default_env()) tracing_subscriber::fmt::layer()
.event_format(tracing_subscriber::fmt::format().without_time()),
)
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init(); .init();
tracing::debug!("debug logs are enabled"); tracing::debug!("debug logs are enabled");

View file

@ -22,11 +22,12 @@ pub mod broker;
pub mod chunk_images; pub mod chunk_images;
pub mod chunk_iterator; pub mod chunk_iterator;
pub mod database; pub mod database;
pub mod render;
pub use broker::Broker; pub use broker::Broker;
pub use database::Database; pub use database::Database;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct WallId([u8; 32]); pub struct WallId([u8; 32]);
impl WallId { impl WallId {
@ -37,6 +38,12 @@ impl WallId {
} }
} }
impl fmt::Debug for WallId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
id::serialize_debug(f, &self.0)
}
}
impl fmt::Display for WallId { impl fmt::Display for WallId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
id::serialize(f, "wall_", &self.0) id::serialize(f, "wall_", &self.0)

View file

@ -56,7 +56,7 @@ impl Broker {
WallId::new(&mut *rng) WallId::new(&mut *rng)
} }
#[instrument(skip(self), fields(%wall_id))] #[instrument(skip(self), fields(?wall_id))]
pub async fn open(&self, wall_id: WallId) -> eyre::Result<OpenWall> { pub async fn open(&self, wall_id: WallId) -> eyre::Result<OpenWall> {
let open_wall = self.open_walls.entry(wall_id); let open_wall = self.open_walls.entry(wall_id);

View file

@ -77,7 +77,7 @@ impl Database {
} }
} }
#[instrument(name = "wall::database::start", skip(settings), fields(wall_id = %settings.wall_id))] #[instrument(name = "wall::database::start", skip(settings), fields(wall_id = ?settings.wall_id))]
pub fn start(settings: Settings) -> eyre::Result<Database> { pub fn start(settings: Settings) -> eyre::Result<Database> {
let db = Connection::open(settings.path).context("cannot open wall database")?; let db = Connection::open(settings.path).context("cannot open wall database")?;

View file

@ -0,0 +1,113 @@
//! Implementation of a haku2 canvas based on tiny-skia.
use std::mem;
use tiny_skia::{
BlendMode, Color, FillRule, LineCap, Paint, PathBuilder, Shader, Stroke, Transform,
};
use super::Chunk;
pub struct ChunkCanvas<'c> {
chunk: &'c mut Chunk,
transform: Transform,
pb: PathBuilder,
}
impl<'c> ChunkCanvas<'c> {
pub fn new(chunk: &'c mut Chunk) -> Self {
Self {
chunk,
transform: Transform::identity(),
pb: PathBuilder::new(),
}
}
pub fn translated(mut self, x: f32, y: f32) -> Self {
self.transform = self.transform.post_translate(x, y);
self
}
}
impl haku2::Canvas for ChunkCanvas<'_> {
fn begin(&mut self) -> Result<(), haku2::RenderError> {
self.pb.clear();
Ok(())
}
fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) -> Result<(), haku2::RenderError> {
self.pb.move_to(x1, y1);
self.pb.line_to(x2, y2);
Ok(())
}
fn rectangle(
&mut self,
x: f32,
y: f32,
width: f32,
height: f32,
) -> Result<(), haku2::RenderError> {
if let Some(rect) = tiny_skia::Rect::from_xywh(x, y, width, height) {
self.pb.push_rect(rect);
}
Ok(())
}
fn circle(&mut self, x: f32, y: f32, r: f32) -> Result<(), haku2::RenderError> {
self.pb.push_circle(x, y, r);
Ok(())
}
fn fill(&mut self, r: u8, g: u8, b: u8, a: u8) -> Result<(), haku2::RenderError> {
let pb = mem::take(&mut self.pb);
if let Some(path) = pb.finish() {
let paint = Paint {
shader: Shader::SolidColor(Color::from_rgba8(r, g, b, a)),
..default_paint()
};
self.chunk
.pixmap
.fill_path(&path, &paint, FillRule::EvenOdd, self.transform, None);
}
Ok(())
}
fn stroke(
&mut self,
r: u8,
g: u8,
b: u8,
a: u8,
thickness: f32,
) -> Result<(), haku2::RenderError> {
let pb = mem::take(&mut self.pb);
if let Some(path) = pb.finish() {
let paint = Paint {
shader: Shader::SolidColor(Color::from_rgba8(r, g, b, a)),
..default_paint()
};
self.chunk.pixmap.stroke_path(
&path,
&paint,
&Stroke {
width: thickness,
line_cap: LineCap::Round,
..Default::default()
},
self.transform,
None,
);
}
Ok(())
}
}
fn default_paint() -> Paint<'static> {
Paint {
shader: Shader::SolidColor(Color::BLACK),
blend_mode: BlendMode::SourceOver,
anti_alias: false,
force_hq_pipeline: false,
}
}