diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs index 3a4cb6f..29dff6a 100644 --- a/crates/haku-wasm/src/lib.rs +++ b/crates/haku-wasm/src/lib.rs @@ -7,19 +7,20 @@ use core::{alloc::Layout, slice}; use alloc::{boxed::Box, vec::Vec}; use haku::{ ast::Ast, - bytecode::{Chunk, Defs, DefsImage}, + bytecode::{Chunk, Defs, DefsImage, DefsLimits}, compiler::{compile_expr, ClosureSpec, CompileError, Compiler, Source}, diagnostic::Diagnostic, lexer::{lex, Lexer}, parser::{self, IntoAstError, Parser}, render::{ tiny_skia::{Pixmap, PremultipliedColorU8}, - Renderer, RendererLimits, + RendererLimits, }, source::SourceCode, system::{ChunkId, System, SystemImage}, token::Lexis, - value::{Closure, Ref, Value}, + trampoline::{Cont, Trampoline}, + value::{Closure, Ref, Vec2}, vm::{Exception, Vm, VmImage, VmLimits}, }; use log::{debug, info}; @@ -46,6 +47,7 @@ struct Limits { max_source_code_len: usize, max_chunks: usize, max_defs: usize, + max_tags: usize, max_tokens: usize, max_parser_events: usize, ast_capacity: usize, @@ -65,6 +67,7 @@ impl Default for Limits { max_source_code_len: 65536, max_chunks: 2, max_defs: 256, + max_tags: 256, max_tokens: 1024, max_parser_events: 1024, ast_capacity: 1024, @@ -110,6 +113,7 @@ macro_rules! limit_setter { limit_setter!(max_source_code_len); limit_setter!(max_chunks); limit_setter!(max_defs); +limit_setter!(max_tags); limit_setter!(max_tokens); limit_setter!(max_parser_events); limit_setter!(ast_capacity); @@ -133,10 +137,22 @@ struct Instance { vm: Vm, vm_image: VmImage, - value: Value, + trampoline: Option, exception: Option, } +impl Instance { + fn set_exception(&mut self, exn: Exception) { + debug!("setting exception = {exn:?}"); + self.exception = Some(exn); + } + + fn reset_exception(&mut self) { + debug!("resetting exception"); + self.exception = None; + } +} + #[no_mangle] unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance { let limits = *limits; @@ -144,7 +160,10 @@ unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance { let system = System::new(limits.max_chunks); - let defs = Defs::new(limits.max_defs); + let defs = Defs::new(&DefsLimits { + max_defs: limits.max_defs, + max_tags: limits.max_tags, + }); let vm = Vm::new( &defs, &VmLimits { @@ -168,7 +187,7 @@ unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance { defs_image, vm, vm_image, - value: Value::Nil, + trampoline: None, exception: None, }); @@ -191,13 +210,6 @@ unsafe extern "C" fn haku_reset(instance: *mut Instance) { instance.defs.restore_image(&instance.defs_image); } -#[no_mangle] -unsafe extern "C" fn haku_reset_vm(instance: *mut Instance) { - debug!("resetting instance VM: {instance:?}"); - let instance = &mut *instance; - instance.vm.restore_image(&instance.vm_image); -} - #[no_mangle] unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool { (*instance).exception.is_some() @@ -426,39 +438,35 @@ unsafe extern "C" fn haku_compile_brush( StatusCode::Ok } -struct PixmapLock { - pixmap: Pixmap, -} - #[no_mangle] -extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut PixmapLock { - let ptr = Box::leak(Box::new(PixmapLock { - pixmap: Pixmap::new(width, height).expect("invalid pixmap size"), - })) as *mut _; +extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut Pixmap { + let ptr = Box::leak(Box::new( + Pixmap::new(width, height).expect("invalid pixmap size"), + )) as *mut _; debug!("created pixmap with size {width}x{height}: {ptr:?}"); ptr } #[no_mangle] -unsafe extern "C" fn haku_pixmap_destroy(pixmap: *mut PixmapLock) { +unsafe extern "C" fn haku_pixmap_destroy(pixmap: *mut Pixmap) { debug!("destroying pixmap: {pixmap:?}"); drop(Box::from_raw(pixmap)) } #[no_mangle] -unsafe extern "C" fn haku_pixmap_data(pixmap: *mut PixmapLock) -> *mut u8 { - let pixmap = &mut (*pixmap).pixmap; +unsafe extern "C" fn haku_pixmap_data(pixmap: *mut Pixmap) -> *mut u8 { + let pixmap = &mut *pixmap; pixmap.pixels_mut().as_mut_ptr() as *mut u8 } #[no_mangle] -unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut PixmapLock) { - let pixmap = &mut (*pixmap).pixmap; +unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut Pixmap) { + let pixmap = &mut *pixmap; pixmap.pixels_mut().fill(PremultipliedColorU8::TRANSPARENT); } #[no_mangle] -unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brush) -> StatusCode { +unsafe extern "C" fn haku_begin_brush(instance: *mut Instance, brush: *const Brush) -> StatusCode { let instance = &mut *instance; let brush = &*brush; @@ -466,8 +474,10 @@ unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brus panic!("brush is not compiled and ready to be used"); }; - debug!("applying defs"); + instance.vm.restore_image(&instance.vm_image); instance.vm.apply_defs(&instance.defs); + instance.reset_exception(); + instance.trampoline = None; let Ok(closure_id) = instance .vm @@ -476,51 +486,91 @@ unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brus return StatusCode::OutOfRefSlots; }; - debug!("resetting exception"); - instance.exception = None; - instance.value = match instance.vm.run(&instance.system, closure_id) { + instance.reset_exception(); + let value = match instance.vm.run(&instance.system, closure_id, &[]) { Ok(value) => value, Err(exn) => { - debug!("setting exception {exn:?}"); - instance.exception = Some(exn); + instance.set_exception(exn); return StatusCode::EvalException; } }; + instance.trampoline = Some(Trampoline::new(value)); StatusCode::Ok } #[no_mangle] -unsafe extern "C" fn haku_render_value( +unsafe extern "C" fn haku_cont_kind(instance: *mut Instance) -> Cont { + let instance = &mut *instance; + instance.trampoline.as_ref().unwrap().cont(&instance.vm) +} + +fn wrap_exception( + instance: &mut Instance, + error_code: StatusCode, + f: impl FnOnce(&mut Instance) -> Result<(), Exception>, +) -> StatusCode { + match f(instance) { + Ok(_) => StatusCode::Ok, + Err(exn) => { + instance.set_exception(exn); + error_code + } + } +} + +#[no_mangle] +unsafe extern "C" fn haku_cont_scribble( instance: *mut Instance, - pixmap: *mut PixmapLock, + pixmap: *mut Pixmap, translation_x: f32, translation_y: f32, ) -> StatusCode { let instance = &mut *instance; - debug!("resetting exception"); - instance.exception = None; + instance.reset_exception(); - debug!("will render value: {:?}", instance.value); + debug!("cont_scribble: pixmap={pixmap:?} translation_x={translation_x:?} translation_y={translation_y:?} trampoline={:?}", instance.trampoline); - let pixmap_locked = &mut (*pixmap).pixmap; - - let mut renderer = Renderer::new( - pixmap_locked, - &RendererLimits { - pixmap_stack_capacity: instance.limits.pixmap_stack_capacity, - transform_stack_capacity: instance.limits.transform_stack_capacity, - }, - ); - renderer.translate(translation_x, translation_y); - match renderer.render(&instance.vm, instance.value) { - Ok(()) => (), - Err(exn) => { - instance.exception = Some(exn); - instance.vm.restore_image(&instance.vm_image); - return StatusCode::RenderException; - } - } - - StatusCode::Ok + wrap_exception(instance, StatusCode::RenderException, |instance| { + instance.trampoline.as_mut().unwrap().scribble( + &instance.vm, + &mut *pixmap, + Vec2 { + x: translation_x, + y: translation_y, + }, + &RendererLimits { + pixmap_stack_capacity: instance.limits.pixmap_stack_capacity, + transform_stack_capacity: instance.limits.transform_stack_capacity, + }, + ) + }) +} + +#[no_mangle] +unsafe extern "C" fn haku_cont_dotter( + instance: *mut Instance, + from_x: f32, + from_y: f32, + to_x: f32, + to_y: f32, + num: f32, +) -> StatusCode { + let instance = &mut *instance; + instance.reset_exception(); + + debug!( + "cont_dotter: from_x={from_x} from_y={from_y} to_x={to_x} to_y={to_y} trampoline={:?}", + instance.trampoline + ); + + wrap_exception(instance, StatusCode::RenderException, |instance| { + instance.trampoline.as_mut().unwrap().dotter( + &mut instance.vm, + &instance.system, + Vec2::new(from_x, from_y), + Vec2::new(to_x, to_y), + num, + ) + }) } diff --git a/crates/haku/src/bytecode.rs b/crates/haku/src/bytecode.rs index 8d63892..d6598c1 100644 --- a/crates/haku/src/bytecode.rs +++ b/crates/haku/src/bytecode.rs @@ -12,6 +12,7 @@ pub enum Opcode { Nil, False, True, + Tag, Number, // (float: f32) Rgba, // (r: u8, g: u8, b: u8, a: u8) @@ -36,6 +37,7 @@ pub enum Opcode { // Control flow. Jump, // (offset: u16) JumpIfNot, // (offset: u16) + Field, // (count: u8, tags: [u16; count]) // Function calls. Call, // (argc: u8) @@ -157,6 +159,12 @@ impl Chunk { } } +impl Offset { + pub fn to_u16(self) -> u16 { + self.0 + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ChunkSizeError; @@ -193,21 +201,48 @@ impl DefId { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct TagId(u16); + +impl TagId { + pub(crate) fn from_u16(x: u16) -> Self { + Self(x) + } + + pub fn to_u16(self) -> u16 { + self.0 + } +} + #[derive(Debug, Clone)] pub struct Defs { defs: Vec, + tags: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct DefsLimits { + pub max_defs: usize, + pub max_tags: usize, } #[derive(Debug, Clone, Copy)] pub struct DefsImage { defs: usize, + tags: usize, } impl Defs { - pub fn new(capacity: usize) -> Self { - assert!(capacity < u16::MAX as usize + 1); + pub fn new(limits: &DefsLimits) -> Self { + assert!(limits.max_defs < u16::MAX as usize + 1); + assert!(limits.max_tags < u16::MAX as usize + 1); + + let mut tags = Vec::with_capacity(limits.max_tags); + add_well_known_tags(&mut tags); + Self { - defs: Vec::with_capacity(capacity), + defs: Vec::with_capacity(limits.max_defs), + tags, } } @@ -219,14 +254,14 @@ impl Defs { self.len() != 0 } - pub fn get(&mut self, name: &str) -> Option { + pub fn get_def(&mut self, name: &str) -> Option { self.defs .iter() .position(|n| *n == name) .map(|index| DefId(index as u16)) } - pub fn add(&mut self, name: &str) -> Result { + pub fn add_def(&mut self, name: &str) -> Result { if self.defs.iter().any(|n| n == name) { Err(DefError::Exists) } else { @@ -239,9 +274,27 @@ impl Defs { } } + fn add_tag(tags: &mut Vec, name: &str) -> Result { + if tags.len() >= tags.capacity() { + return Err(TagError::OutOfSpace); + } + let id = TagId(tags.len() as u16); + tags.push(name.to_owned()); + Ok(id) + } + + pub fn get_or_add_tag(&mut self, name: &str) -> Result { + if let Some(index) = self.tags.iter().position(|n| n == name) { + Ok(TagId(index as u16)) + } else { + Self::add_tag(&mut self.tags, name) + } + } + pub fn image(&self) -> DefsImage { DefsImage { defs: self.defs.len(), + tags: self.tags.len(), } } @@ -249,6 +302,9 @@ impl Defs { self.defs.resize_with(image.defs, || { panic!("image must be a subset of the current defs") }); + self.tags.resize_with(image.tags, || { + panic!("image must be a subset of the current defs") + }); } } @@ -266,3 +322,45 @@ impl Display for DefError { }) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TagError { + OutOfSpace, +} + +impl Display for TagError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + TagError::OutOfSpace => "too many tags", + }) + } +} + +macro_rules! well_known_tags { + ($($ident:tt = $value:tt),* $(,)?) => { + impl TagId { + $( + #[allow(non_upper_case_globals)] + pub const $ident: Self = Self($value); + )* + } + + fn add_well_known_tags(tags: &mut Vec) { + $( + let id = Defs::add_tag(tags, stringify!($ident)).unwrap(); + assert_eq!(id, TagId::from_u16($value)); + )* + } + } +} + +well_known_tags! { + // NOTE: The numbers must be sorted from 0 to N, due to limitations of Rust's macro system. + // https://github.com/rust-lang/rust/issues/83527 + + Nil = 0, + + From = 1, + To = 2, + Num = 3, +} diff --git a/crates/haku/src/compiler.rs b/crates/haku/src/compiler.rs index 823aa50..50eb6ac 100644 --- a/crates/haku/src/compiler.rs +++ b/crates/haku/src/compiler.rs @@ -7,7 +7,9 @@ use alloc::vec::Vec; use crate::{ ast::{Ast, NodeId, NodeKind}, - bytecode::{Chunk, DefError, Defs, EmitError, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL}, + bytecode::{ + Chunk, DefError, Defs, EmitError, Opcode, TagError, TagId, CAPTURE_CAPTURE, CAPTURE_LOCAL, + }, diagnostic::Diagnostic, source::SourceCode, system::{System, SystemFnArity}, @@ -123,11 +125,6 @@ pub fn compile_expr<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) } } -fn unsupported(c: &mut Compiler, src: &Source, node_id: NodeId, message: &str) -> CompileResult { - c.emit(Diagnostic::error(src.ast.span(node_id), message)); - Ok(()) -} - fn compile_nil(c: &mut Compiler) -> CompileResult { c.chunk.emit_opcode(Opcode::Nil)?; @@ -183,7 +180,7 @@ fn compile_ident<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> c.chunk.emit_u8(index)?; } Ok(None) => { - if let Some(def_id) = c.defs.get(name) { + if let Some(def_id) = c.defs.get_def(name) { c.chunk.emit_opcode(Opcode::Def)?; c.chunk.emit_u16(def_id.to_u16())?; } else { @@ -202,7 +199,8 @@ fn compile_ident<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> } fn compile_tag(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult { - let tag = src.ast.span(node_id).slice(src.code); + let span = src.ast.span(node_id); + let tag = span.slice(src.code); match tag { "False" => { @@ -212,7 +210,17 @@ fn compile_tag(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult c.chunk.emit_opcode(Opcode::True)?; } _ => { - c.emit(Diagnostic::error(src.ast.span(node_id), "uppercased identifiers are reserved for future use; please start your identifiers with a lowercase letter instead")); + let tag_id = c.defs.get_or_add_tag(tag).unwrap_or_else(|error| { + match error { + TagError::OutOfSpace => { + c.emit(Diagnostic::error(span, "too many unique tags used")); + } + } + TagId::Nil + }); + + c.chunk.emit_opcode(Opcode::Tag)?; + c.chunk.emit_u16(tag_id.to_u16())?; } } @@ -598,7 +606,7 @@ fn def_prepass<'a>(c: &mut Compiler<'a>, src: &Source<'a>, toplevel: NodeId) -> if let (Some(ident), Some(op)) = (binary_walk.node(), binary_walk.get(NodeKind::Op)) { if src.ast.span(op).slice(src.code) == "=" { let name = src.ast.span(ident).slice(src.code); - match c.defs.add(name) { + match c.defs.add_def(name) { Ok(_) => (), Err(DefError::Exists) => c.emit(Diagnostic::error( src.ast.span(ident), @@ -651,7 +659,12 @@ fn compile_def<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> C return Ok(()); }; - if src.ast.kind(left) != NodeKind::Ident { + if src.ast.kind(left) == NodeKind::Tag { + c.emit(Diagnostic::error( + src.ast.span(left), + "defs may not be named with uppercase letters, because uppercase letters are used for tags", + )); + } else if src.ast.kind(left) != NodeKind::Ident { c.emit(Diagnostic::error( src.ast.span(left), "def name (identifier) expected", @@ -662,7 +675,7 @@ fn compile_def<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> C // NOTE: def_prepass collects all definitions beforehand. // In case a def ends up not existing, that means we ran out of space for defs - so emit a // zero def instead. - let def_id = c.defs.get(name).unwrap_or_default(); + let def_id = c.defs.get_def(name).unwrap_or_default(); compile_expr(c, src, right)?; c.chunk.emit_opcode(Opcode::SetDef)?; diff --git a/crates/haku/src/lib.rs b/crates/haku/src/lib.rs index 81e69f2..3b28e28 100644 --- a/crates/haku/src/lib.rs +++ b/crates/haku/src/lib.rs @@ -12,5 +12,6 @@ pub mod render; pub mod source; pub mod system; pub mod token; +pub mod trampoline; pub mod value; pub mod vm; diff --git a/crates/haku/src/render.rs b/crates/haku/src/render.rs index 623e0da..ed8aa74 100644 --- a/crates/haku/src/render.rs +++ b/crates/haku/src/render.rs @@ -127,7 +127,7 @@ impl<'a> Renderer<'a> { &paint, &SStroke { width: stroke.thickness, - line_cap: LineCap::Square, + line_cap: LineCap::Round, ..Default::default() }, transform, diff --git a/crates/haku/src/system.rs b/crates/haku/src/system.rs index d90cd7b..e0c2655 100644 --- a/crates/haku/src/system.rs +++ b/crates/haku/src/system.rs @@ -6,8 +6,8 @@ use core::{ use alloc::vec::Vec; use crate::{ - bytecode::Chunk, - value::Value, + bytecode::{Chunk, EmitError, Offset, Opcode, TagId}, + value::{BytecodeLoc, Closure, FunctionName, Value, Vec4}, vm::{Exception, FnArgs, Vm}, }; @@ -29,6 +29,7 @@ pub struct System { pub resolve_fn: fn(SystemFnArity, &str) -> Option, pub fns: [Option; 256], pub chunks: Vec, + structs_chunk_offsets: StructsChunkOffsets, } #[derive(Debug, Clone, Copy)] @@ -36,33 +37,28 @@ pub struct SystemImage { chunks: usize, } -macro_rules! def_fns { - ($($index:tt $arity:tt $name:tt => $fnref:expr),* $(,)?) => { - pub(crate) fn init_fns(system: &mut System) { - $( - debug_assert!(system.fns[$index].is_none()); - system.fns[$index] = Some($fnref); - )* - } - - pub(crate) fn resolve(arity: SystemFnArity, name: &str) -> Option { - match (arity, name){ - $((SystemFnArity::$arity, $name) => Some($index),)* - _ => None, - } - } - }; +#[derive(Debug, Clone)] +struct StructsChunkOffsets { + dotter: BytecodeLoc, } impl System { pub fn new(max_chunks: usize) -> Self { + assert!( + max_chunks > 1, + "the 0th chunk is allocated for internal purposes; therefore there must be more than one chunk to execute bytecode" + ); assert!(max_chunks < u32::MAX as usize); + let (structs_chunk, structs_chunk_offsets) = Self::structs_chunk().unwrap(); + let mut system = Self { resolve_fn: Self::resolve, fns: [None; 256], chunks: Vec::with_capacity(max_chunks), + structs_chunk_offsets, }; + system.chunks.push(structs_chunk); Self::init_fns(&mut system); system } @@ -92,6 +88,64 @@ impl System { panic!("image must be a subset of the current system") }); } + + // The structs chunk contains bytecode for _struct functions_. + // + // Struct functions are a way of encoding structures with arbitrary named data fields. + // They are called like regular functions, except they always expect a single tag-type + // argument. They're used where performance is not a primary concern---in structures that appear + // once or a couple times throughout the lifetime of a program, in which convenient, + // backwards-compatible field access is a priority. + // + // Each struct function has only two opcodes: `Field`, followed by a `Return`. + // The `Field` opcode is an efficient way of encoding an if chain made solely out of tag + // comparisons, returning a closure capture (or error if there's no field with the given name.) + // + // if (tag == A) capture_0 + // else if (tag == B) capture_1 + // else if (tag == C) capture_2 + // else error + // + // Closure captures are used here, because they're a convenient way of attaching indexed data + // to any function, even created outside the language itself. + // + // All of this results in a function that can be called like `d From` to obtain a piece of data + // stored inside of the `d` structure. + fn structs_chunk() -> Result<(Chunk, StructsChunkOffsets), EmitError> { + let mut chunk = Chunk::new(128).unwrap(); + + let dotter = chunk.offset(); + chunk.emit_opcode(Opcode::Field)?; + chunk.emit_u8(3)?; + chunk.emit_u16(TagId::From.to_u16())?; + chunk.emit_u16(TagId::To.to_u16())?; + chunk.emit_u16(TagId::Num.to_u16())?; + chunk.emit_opcode(Opcode::Return)?; + + fn loc(offset: Offset) -> BytecodeLoc { + BytecodeLoc { + chunk_id: ChunkId(0), + offset: offset.to_u16(), + } + } + + Ok(( + chunk, + StructsChunkOffsets { + dotter: loc(dotter), + }, + )) + } + + pub fn create_dotter(&self, from: Vec4, to: Vec4, num: f32) -> Closure { + Closure { + start: self.structs_chunk_offsets.dotter, + name: FunctionName::Anonymous, + param_count: 1, + local_count: 0, + captures: Vec::from_iter([Value::Vec4(from), Value::Vec4(to), Value::Number(num)]), + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -105,11 +159,29 @@ impl Display for ChunkError { impl Error for ChunkError {} +macro_rules! def_fns { + ($($index:tt $arity:tt $name:tt => $fnref:expr),* $(,)?) => { + pub(crate) fn init_fns(system: &mut System) { + $( + debug_assert!(system.fns[$index].is_none()); + system.fns[$index] = Some($fnref); + )* + } + + pub(crate) fn resolve(arity: SystemFnArity, name: &str) -> Option { + match (arity, name){ + $((SystemFnArity::$arity, $name) => Some($index),)* + _ => None, + } + } + }; +} + pub mod fns { use alloc::{format, vec::Vec}; use crate::{ - value::{Fill, List, Ref, Rgba, Scribble, Shape, Stroke, Value, Vec2, Vec4}, + value::{Fill, List, Ref, Reticle, Rgba, Scribble, Shape, Stroke, Value, Vec2, Vec4}, vm::{Exception, FnArgs, Vm}, }; @@ -181,8 +253,11 @@ pub mod fns { 0xc1 Nary "line" => line, 0xc2 Nary "rect" => rect, 0xc3 Nary "circle" => circle, + 0xe0 Nary "stroke" => stroke, 0xe1 Nary "fill" => fill, + + 0xf0 Nary "withDotter" => with_dotter, } } @@ -469,7 +544,13 @@ pub mod fns { fn to_shape(value: Value, vm: &Vm) -> Option { match value { - Value::Nil | Value::False | Value::True | Value::Number(_) | Value::Rgba(_) => None, + Value::Nil + | Value::False + | Value::True + | Value::Tag(_) + | Value::Number(_) + | Value::Rgba(_) => None, + Value::Ref(id) => { if let Ref::Shape(shape) = vm.get_ref(id) { Some(shape.clone()) @@ -477,6 +558,7 @@ pub mod fns { None } } + Value::Vec4(vec) => Some(Shape::Point(vec.into())), } } @@ -588,4 +670,22 @@ pub mod fns { Ok(Value::Nil) } } + + pub fn with_dotter(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception( + "`withDotter` expects a single argument (withDotter \\d -> [])", + )); + } + + let draw = args.get_closure(vm, 0, "argument to `withDotter` must be a closure")?; + if draw.param_count != 1 { + return Err(vm.create_exception("function passed to `withDotter` must take in a single parameter (withDotter \\d -> [])")); + } + + let id = vm.create_ref(Ref::Reticle(Reticle::Dotter { + draw: args.get(vm, 0), + }))?; + Ok(Value::Ref(id)) + } } diff --git a/crates/haku/src/trampoline.rs b/crates/haku/src/trampoline.rs new file mode 100644 index 0000000..27e8aca --- /dev/null +++ b/crates/haku/src/trampoline.rs @@ -0,0 +1,78 @@ +use tiny_skia::Pixmap; + +use crate::{ + render::{Renderer, RendererLimits}, + system::System, + value::{Ref, Reticle, Value, Vec2}, + vm::{Exception, Vm}, +}; + +#[derive(Debug, Clone, Copy)] +pub struct Trampoline { + pub value: Value, +} + +// NOTE: This must be kept in sync with haku.js. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Cont { + Scribble, + Dotter, +} + +impl Trampoline { + pub fn new(init: Value) -> Self { + Self { value: init } + } + + pub fn cont(&self, vm: &Vm) -> Cont { + let Some((_, refv)) = vm.get_ref_value(self.value) else { + return Cont::Scribble; + }; + + match refv { + Ref::Reticle(_) => Cont::Dotter, + _ => Cont::Scribble, + } + } + + pub fn scribble( + &mut self, + vm: &Vm, + pixmap: &mut Pixmap, + translation: Vec2, + limits: &RendererLimits, + ) -> Result<(), Exception> { + let mut renderer = Renderer::new(&mut *pixmap, limits); + renderer.translate(translation.x, translation.y); + renderer.render(vm, self.value) + } + + pub fn dotter( + &mut self, + vm: &mut Vm, + system: &System, + from: Vec2, + to: Vec2, + num: f32, + ) -> Result<(), Exception> { + let (_, vref) = vm.get_ref_value(self.value).expect("value must be a ref"); + let &Ref::Reticle(Reticle::Dotter { + draw: Value::Ref(draw_id), + }) = vref + else { + panic!("value must be a dotter reticle"); + }; + + let dotter = vm.create_ref(Ref::Closure(system.create_dotter( + from.into(), + to.into(), + num, + )))?; + + let value = vm.run(system, draw_id, &[Value::Ref(dotter)])?; + self.value = value; + + Ok(()) + } +} diff --git a/crates/haku/src/value.rs b/crates/haku/src/value.rs index b169e81..1e4a2fe 100644 --- a/crates/haku/src/value.rs +++ b/crates/haku/src/value.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use crate::{compiler::ClosureSpec, system::ChunkId}; +use crate::{bytecode::TagId, compiler::ClosureSpec, system::ChunkId}; // TODO: Probably needs some pretty hardcore space optimization. // Maybe when we have static typing. @@ -9,6 +9,7 @@ pub enum Value { Nil, False, True, + Tag(TagId), Number(f32), Vec4(Vec4), Rgba(Rgba), @@ -73,6 +74,12 @@ pub struct Vec2 { pub y: f32, } +impl Vec2 { + pub fn new(x: f32, y: f32) -> Self { + Self { x, y } + } +} + impl From for Vec2 { fn from(value: Vec4) -> Self { Self { @@ -90,6 +97,17 @@ pub struct Vec4 { pub w: f32, } +impl From for Vec4 { + fn from(value: Vec2) -> Self { + Self { + x: value.x, + y: value.y, + z: 0.0, + w: 0.0, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] #[repr(C)] pub struct Rgba { @@ -123,6 +141,7 @@ pub enum Ref { List(List), Shape(Shape), Scribble(Scribble), + Reticle(Reticle), } impl Ref { @@ -206,3 +225,8 @@ pub enum Scribble { Stroke(Stroke), Fill(Fill), } + +#[derive(Debug, Clone)] +pub enum Reticle { + Dotter { draw: Value }, +} diff --git a/crates/haku/src/vm.rs b/crates/haku/src/vm.rs index 70b9293..46f04c9 100644 --- a/crates/haku/src/vm.rs +++ b/crates/haku/src/vm.rs @@ -5,9 +5,10 @@ use core::{ }; use alloc::{string::String, vec::Vec}; +use log::debug; use crate::{ - bytecode::{self, Defs, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL}, + bytecode::{self, Defs, Opcode, TagId, CAPTURE_CAPTURE, CAPTURE_LOCAL}, system::{ChunkId, System}, value::{BytecodeLoc, Closure, FunctionName, List, Ref, RefId, Rgba, Value, Vec4}, }; @@ -178,7 +179,12 @@ impl Vm { .ok_or_else(|| self.create_exception("corrupted bytecode (call stack underflow)")) } - pub fn run(&mut self, system: &System, mut closure_id: RefId) -> Result { + pub fn run( + &mut self, + system: &System, + mut closure_id: RefId, + params: &[Value], + ) -> Result { let closure = self .get_ref(closure_id) .as_closure() @@ -191,7 +197,12 @@ impl Vm { let mut fuel = self.fuel; let init_bottom = bottom; - for _ in 0..closure.local_count { + let local_count = closure.local_count; + + for ¶m in params { + self.push(param)?; + } + for _ in 0..local_count { self.push(Value::Nil)?; } @@ -219,6 +230,11 @@ impl Vm { Opcode::False => self.push(Value::False)?, Opcode::True => self.push(Value::True)?, + Opcode::Tag => { + let i = chunk.read_u16(&mut pc)?; + self.push(Value::Tag(TagId::from_u16(i)))?; + } + Opcode::Number => { let x = chunk.read_f32(&mut pc)?; self.push(Value::Number(x))?; @@ -344,6 +360,34 @@ impl Vm { } } + Opcode::Field => { + let count = chunk.read_u8(&mut pc)? as usize; + + let field_tag = self.pop()?; + let Value::Tag(field_tag_id) = field_tag else { + return Err(self.create_exception("name of data field to look up must be a tag (starting with an uppercase letter)")); + }; + + let mut index = None; + for i in 0..count { + let tag_id = TagId::from_u16(chunk.read_u16(&mut pc)?); + if tag_id == field_tag_id { + index = Some(i); + } + } + + let Some(index) = index else { + return Err(self.create_exception( + "field with this name does not exist in the given data structure", + )); + }; + + let closure = self.get_ref(closure_id).as_closure().unwrap(); + self.push(closure.captures.get(index).copied().ok_or_else(|| { + self.create_exception("corrupted bytecode (field index out of bounds)") + })?)?; + } + Opcode::Call => { let argument_count = chunk.read_u8(&mut pc)? as usize; @@ -554,6 +598,23 @@ impl FnArgs { .to_rgba() .ok_or_else(|| vm.create_exception(message)) } + + #[inline(never)] + pub fn get_closure<'vm>( + &self, + vm: &'vm Vm, + index: usize, + message: &'static str, + ) -> Result<&'vm Closure, Exception> { + let value = self.get(vm, index); + let (_, any_ref) = vm + .get_ref_value(value) + .ok_or_else(|| vm.create_exception(message))?; + let Ref::Closure(closure) = any_ref else { + return Err(vm.create_exception(message)); + }; + Ok(closure) + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/haku/tests/language.rs b/crates/haku/tests/language.rs index 5b979bf..1aca4f5 100644 --- a/crates/haku/tests/language.rs +++ b/crates/haku/tests/language.rs @@ -2,7 +2,7 @@ use std::error::Error; use haku::{ ast::{dump::dump, Ast}, - bytecode::{Chunk, Defs}, + bytecode::{Chunk, Defs, DefsLimits}, compiler::{compile_expr, Compiler, Source}, lexer::{lex, Lexer}, parser::{self, Parser, ParserLimits}, @@ -14,7 +14,7 @@ use haku::{ }; fn eval(code: &str) -> Result> { - let mut system = System::new(1); + let mut system = System::new(2); let code = SourceCode::unlimited_len(code); @@ -32,7 +32,10 @@ fn eval(code: &str) -> Result> { system: &system, }; - let mut defs = Defs::new(256); + let mut defs = Defs::new(&DefsLimits { + max_defs: 256, + max_tags: 256, + }); let mut chunk = Chunk::new(65536).unwrap(); let mut compiler = Compiler::new(&mut defs, &mut chunk); compile_expr(&mut compiler, &src, root)?; @@ -70,7 +73,7 @@ fn eval(code: &str) -> Result> { println!("closure spec: {closure_spec:?}"); let closure = vm.create_ref(Ref::Closure(Closure::chunk(chunk_id, closure_spec)))?; - let result = vm.run(&system, closure)?; + let result = vm.run(&system, closure, &[])?; println!("used fuel: {}", limits.fuel - vm.remaining_fuel()); @@ -281,3 +284,11 @@ fn issue_78() { "#; assert_eq!(eval(code).unwrap(), Value::Ref(RefId::from_u32(2))) } + +#[test] +fn with_dotter_identity() { + let code = r#" + withDotter \d -> d + "#; + assert_eq!(eval(code).unwrap(), Value::Ref(RefId::from_u32(0))) +} diff --git a/crates/rkgk/src/api/wall.rs b/crates/rkgk/src/api/wall.rs index f5a8110..d3aeb72 100644 --- a/crates/rkgk/src/api/wall.rs +++ b/crates/rkgk/src/api/wall.rs @@ -12,7 +12,10 @@ use axum::{ }; use base64::Engine; use eyre::{bail, Context, OptionExt}; -use haku::value::Value; +use haku::{ + trampoline::{Cont, Trampoline}, + value::Value, +}; use schema::{ ChunkInfo, Error, LoginRequest, LoginResponse, Notify, Online, Request, Version, WallInfo, }; @@ -29,7 +32,8 @@ use crate::{ schema::Vec2, wall::{ self, auto_save::AutoSave, chunk_images::ChunkImages, chunk_iterator::ChunkIterator, - database::ChunkDataPair, ChunkPosition, JoinError, SessionHandle, UserInit, Wall, WallId, + database::ChunkDataPair, ChunkPosition, Interaction, JoinError, SessionHandle, UserInit, + Wall, WallId, }, }; @@ -220,12 +224,8 @@ struct SessionLoop { } enum RenderCommand { - SetBrush { - brush: String, - }, - - Plot { - points: Vec, + Interact { + interactions: Vec, done: oneshot::Sender<()>, }, } @@ -245,10 +245,17 @@ impl SessionLoop { // If this ends up dropping commands - it's your fault for trying to DoS my server! let (render_commands_tx, render_commands_rx) = mpsc::channel(1); - render_commands_tx - .send(RenderCommand::SetBrush { brush }) - .await - .unwrap(); + let thread_ready = { + let (done_tx, done_rx) = oneshot::channel(); + render_commands_tx + .send(RenderCommand::Interact { + interactions: vec![Interaction::SetBrush { brush }], + done: done_tx, + }) + .await + .unwrap(); + done_rx + }; // We spawn our own thread so as not to clog the tokio blocking thread pool with our // rendering shenanigans. @@ -265,6 +272,8 @@ impl SessionLoop { }) .context("could not spawn render thread")?; + thread_ready.await?; + Ok(Self { wall_id, wall, @@ -312,47 +321,82 @@ impl SessionLoop { | wall::EventKind::Leave | wall::EventKind::Cursor { .. } => (), - wall::EventKind::SetBrush { brush } => { - // SetBrush is not dropped because it is a very important event. - _ = self - .render_commands_tx - .send(RenderCommand::SetBrush { - brush: brush.clone(), - }) - .await; - } - wall::EventKind::Plot { points } => { - let chunks_to_modify: Vec<_> = - chunks_to_modify(&self.wall, points).into_iter().collect(); - match self.chunk_images.load(chunks_to_modify.clone()).await { - Ok(_) => { - // We drop commands if we take too long to render instead of lagging - // the WebSocket thread. - // Theoretically this will yield much better responsiveness, but it _will_ - // result in some visual glitches if we're getting bottlenecked. - let (done_tx, done_rx) = oneshot::channel(); - let send_result = - self.render_commands_tx.try_send(RenderCommand::Plot { - points: points.clone(), - done: done_tx, - }); + wall::EventKind::Interact { interactions } => { + let (done_tx, done_rx) = oneshot::channel(); - if send_result.is_err() { - info!( - ?points, - "render thread is overloaded, dropping request to draw points" - ); - } - - let auto_save = Arc::clone(&self.auto_save); - tokio::spawn(async move { - _ = done_rx.await; - auto_save.request(chunks_to_modify).await; + if interactions + .iter() + .any(|i| matches!(i, Interaction::SetBrush { .. })) + { + // SetBrush is an important event, so we wait for the render thread + // to unload. + _ = self + .render_commands_tx + .send(RenderCommand::Interact { + interactions: interactions.clone(), + done: done_tx, + }) + .await; + } else { + // If there is no SetBrush, there's no need to wait, so we fire events + // blindly. If the thread's not okay with that... well, whatever. + // That's your issue for making a really slow brush. + let send_result = + self.render_commands_tx.try_send(RenderCommand::Interact { + interactions: interactions.clone(), + done: done_tx, }); + if send_result.is_err() { + info!( + ?interactions, + "render thread is overloaded, dropping interaction request" + ); } - Err(err) => error!(?err, "while loading chunks for render command"), } - } + + // TODO: Auto save. This'll need us to compute which chunks will be affected + // by the interactions. + } // wall::EventKind::SetBrush { brush } => { + // // SetBrush is not dropped because it is a very important event. + // _ = self + // .render_commands_tx + // .send(RenderCommand::SetBrush { + // brush: brush.clone(), + // }) + // .await; + // } + // wall::EventKind::Plot { points } => { + // let chunks_to_modify: Vec<_> = + // chunks_to_modify(&self.wall, points).into_iter().collect(); + // match self.chunk_images.load(chunks_to_modify.clone()).await { + // Ok(_) => { + // // We drop commands if we take too long to render instead of lagging + // // the WebSocket thread. + // // Theoretically this will yield much better responsiveness, but it _will_ + // // result in some visual glitches if we're getting bottlenecked. + // let (done_tx, done_rx) = oneshot::channel(); + // let send_result = + // self.render_commands_tx.try_send(RenderCommand::Plot { + // points: points.clone(), + // done: done_tx, + // }); + + // if send_result.is_err() { + // info!( + // ?points, + // "render thread is overloaded, dropping request to draw points" + // ); + // } + + // let auto_save = Arc::clone(&self.auto_save); + // tokio::spawn(async move { + // _ = done_rx.await; + // auto_save.request(chunks_to_modify).await; + // }); + // } + // Err(err) => error!(?err, "while loading chunks for render command"), + // } + // } } self.wall.event(wall::Event { @@ -437,32 +481,97 @@ impl SessionLoop { fn render_thread(wall: Arc, limits: Limits, mut commands: mpsc::Receiver) { let mut haku = Haku::new(limits); + let mut trampoline = None; let mut brush_ok = false; + let mut current_render_area = RenderArea::default(); while let Some(command) = commands.blocking_recv() { - match command { - RenderCommand::SetBrush { brush } => { - brush_ok = haku.set_brush(&brush).is_ok(); + let RenderCommand::Interact { interactions, done } = command; + + let mut queue = VecDeque::from(interactions); + while let Some(interaction) = queue.pop_front() { + if let Some(render_area) = render_area(wall.settings(), &interaction) { + current_render_area = render_area; } - RenderCommand::Plot { points, done } => { - if brush_ok { - if let Ok(value) = haku.eval_brush() { - for point in points { - // Ignore the result. It's better if we render _something_ rather - // than nothing. - _ = draw_to_chunks(&wall, &haku, value, point); + match interaction { + Interaction::SetBrush { brush } => { + brush_ok = haku.set_brush(&brush).is_ok(); + } + + Interaction::Dotter { from, to, num } => { + if brush_ok { + if let Some(trampoline) = + jumpstart_trampoline(&mut haku, &mut trampoline) + { + let cont = haku.cont(trampoline); + if cont == Cont::Dotter { + _ = haku.dotter(trampoline, from, to, num); + } else { + error!("received Dotter interaction when a {cont:?} continuation was next"); + } + } else { + info!("failed to jumpstart trampoline for Dotter interaction"); } - haku.reset_vm(); } } - _ = done.send(()); + + Interaction::Scribble => { + // Regarding the take(): this is to self-synchronize in case of error. + // Once a scribble is rendered, we reset back to not having a trampoline, + // and further interactions must be sent to kickstart a new one. + // + // Regarding not jumpstarting a trampoline here: it would be useless, + // 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(); + } } } + + _ = done.send(()); } } } +#[derive(Debug, Clone, Copy, Default)] +struct RenderArea { + top_left: Vec2, + bottom_right: Vec2, +} + +fn render_area(wall_settings: &wall::Settings, interaction: &Interaction) -> Option { + match interaction { + Interaction::Dotter { from, to, .. } => { + let half_paint_area = wall_settings.paint_area as f32 / 2.0; + Some(RenderArea { + top_left: Vec2::new(from.x - half_paint_area, from.y - half_paint_area), + bottom_right: Vec2::new(to.x + half_paint_area, to.y + half_paint_area), + }) + } + Interaction::SetBrush { .. } | Interaction::Scribble => None, + } +} + +fn jumpstart_trampoline<'a>( + haku: &mut Haku, + trampoline: &'a mut Option, +) -> Option<&'a mut Trampoline> { + if trampoline.is_none() { + *trampoline = haku.eval_brush().ok().map(Trampoline::new); + } + trampoline.as_mut() +} + fn chunks_to_modify(wall: &Wall, points: &[Vec2]) -> HashSet { let mut chunks = HashSet::new(); for point in points { @@ -483,23 +592,23 @@ fn chunks_to_modify(wall: &Wall, points: &[Vec2]) -> HashSet { } #[instrument(skip(wall, haku, value))] -fn draw_to_chunks(wall: &Wall, haku: &Haku, value: Value, center: Vec2) -> eyre::Result<()> { +fn draw_to_chunks( + wall: &Wall, + haku: &Haku, + render_area: RenderArea, + value: Value, +) -> eyre::Result<()> { let settings = wall.settings(); let chunk_size = settings.chunk_size as f32; - let paint_area = settings.paint_area as f32; - let left = center.x - paint_area / 2.0; - let top = center.y - paint_area / 2.0; + let top_left_chunk = settings.chunk_at(render_area.top_left); + let bottom_right_chunk = settings.chunk_at_ceil(render_area.bottom_right); - let left_chunk = settings.chunk_at_1d(left); - let top_chunk = settings.chunk_at_1d(top); - let right_chunk = settings.chunk_at_1d_ceil(left + paint_area); - let bottom_chunk = settings.chunk_at_1d_ceil(top + paint_area); - for chunk_y in top_chunk..bottom_chunk { - for chunk_x in left_chunk..right_chunk { - let x = f32::floor(-chunk_x as f32 * chunk_size + center.x); - let y = f32::floor(-chunk_y as f32 * chunk_size + center.y); + for chunk_y in top_left_chunk.y..bottom_right_chunk.y { + for chunk_x in top_left_chunk.x..bottom_right_chunk.x { + let x = f32::floor(-chunk_x 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 mut chunk = chunk_ref.blocking_lock(); haku.render_value(&mut chunk.pixmap, value, Vec2 { x, y })?; diff --git a/crates/rkgk/src/haku.rs b/crates/rkgk/src/haku.rs index da62300..48ecd52 100644 --- a/crates/rkgk/src/haku.rs +++ b/crates/rkgk/src/haku.rs @@ -6,7 +6,7 @@ use eyre::{bail, Context, OptionExt}; use haku::{ ast::Ast, - bytecode::{Chunk, Defs, DefsImage}, + bytecode::{Chunk, Defs, DefsImage, DefsLimits}, compiler::{ClosureSpec, Compiler, Source}, lexer::{lex, Lexer}, parser::{self, Parser, ParserLimits}, @@ -14,6 +14,7 @@ use haku::{ source::SourceCode, system::{ChunkId, System, SystemImage}, token::Lexis, + trampoline::{Cont, Trampoline}, value::{Closure, Ref, Value}, vm::{Vm, VmImage, VmLimits}, }; @@ -30,6 +31,7 @@ pub struct Limits { pub max_source_code_len: u32, pub max_chunks: usize, pub max_defs: usize, + pub max_tags: usize, pub max_tokens: usize, pub max_parser_events: usize, pub ast_capacity: usize, @@ -59,7 +61,10 @@ pub struct Haku { impl Haku { pub fn new(limits: Limits) -> Self { let system = System::new(limits.max_chunks); - let defs = Defs::new(limits.max_defs); + let defs = Defs::new(&DefsLimits { + max_defs: limits.max_defs, + max_tags: limits.max_tags, + }); let vm = Vm::new( &defs, &VmLimits { @@ -158,7 +163,7 @@ impl Haku { let scribble = self .vm - .run(&self.system, closure_id) + .run(&self.system, closure_id, &[]) .context("an exception occurred while evaluating the scribble")?; Ok(scribble) @@ -187,4 +192,18 @@ impl Haku { pub fn reset_vm(&mut self) { self.vm.restore_image(&self.vm_image); } + + pub fn cont(&self, trampoline: &Trampoline) -> Cont { + trampoline.cont(&self.vm) + } + + 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)?) + } } diff --git a/crates/rkgk/src/schema.rs b/crates/rkgk/src/schema.rs index af22e75..5a38f43 100644 --- a/crates/rkgk/src/schema.rs +++ b/crates/rkgk/src/schema.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)] pub struct Vec2 { pub x: f32, pub y: f32, @@ -11,3 +11,9 @@ impl Vec2 { Self { x, y } } } + +impl From for haku::value::Vec2 { + fn from(value: Vec2) -> Self { + Self::new(value.x, value.y) + } +} diff --git a/crates/rkgk/src/wall.rs b/crates/rkgk/src/wall.rs index c04b1cd..5dca597 100644 --- a/crates/rkgk/src/wall.rs +++ b/crates/rkgk/src/wall.rs @@ -192,9 +192,19 @@ pub enum EventKind { Leave, Cursor { position: Vec2 }, + Interact { interactions: Vec }, +} +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum Interaction { SetBrush { brush: String }, - Plot { points: Vec }, + Dotter { from: Vec2, to: Vec2, num: f32 }, + Scribble, } #[derive(Debug, Clone, Serialize)] @@ -276,7 +286,7 @@ impl Wall { // Drawing events are handled by the owner session's thread to make drawing as // parallel as possible. - EventKind::SetBrush { .. } | EventKind::Plot { .. } => (), + EventKind::Interact { .. } => {} } } diff --git a/docs/rkgk.dj b/docs/rkgk.dj index 5f1e66c..288dd6f 100644 --- a/docs/rkgk.dj +++ b/docs/rkgk.dj @@ -27,7 +27,8 @@ In case you edited anything in the input box on the right, paste the following t -- Try playing around with the numbers, -- and see what happens! -stroke 8 #000 (vec 0 0) +withDotter \d -> + stroke 8 #000 (d To) ``` rakugaki is a drawing program for digital scribbles and other pieces of art. @@ -83,10 +84,11 @@ If you want to draw multiple scribbles, you can wrap them into a list, which we ```haku -- Draw two colorful dots instead of one! -[ - stroke 8 #F00 (vec 4 0) - stroke 8 #00F (vec (-4) 0)) -] +withDotter \d -> + [ + stroke 8 #F00 (vec ((vecX (d To)) + 4) (vecY (d To))) + stroke 8 #00F (vec ((vecX (d To)) - 4) (vecY (d To))) + ] ``` ::: aside @@ -103,25 +105,29 @@ And what's even crazier is that you can compose lists _further_---you can make a It'll draw the first inner list, which contains two scribbles, and then it'll draw the second inner list, which contains two scribbles. ```haku -[ +withDotter \d -> [ - stroke 8 #F00 (vec 4 (-4)) - stroke 8 #00F (vec (-4) (-4)) + [ + stroke 8 #F00 (vec ((vecX (d To)) + 4) (vecY (d To))) + stroke 8 #00F (vec ((vecX (d To)) - 4) (vecY (d To))) + ] + [ + stroke 8 #FF0 (vec (vecX (d To)) ((vecY (d To)) + 4)) + stroke 8 #0FF (vec (vecX (d To)) ((vecY (d To)) - 4)) + ] ] - [ - stroke 8 #FF0 (vec 4 4) - stroke 8 #0FF (vec (-4) 4) - ] -] ``` ::: aside -Another weird thing: when negating a number, you have to put it in parentheses. +I know this example is kind of horrendous to read right now. -This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing! -In this case, it will always choose the 2nd interpretation---vec minus four. -So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses. +This is because haku currently does not have vector math, which means we have to disassemble and reassemble vectors manually. + +{% Another weird thing: when negating a number, you have to put it in parentheses. %} +{% This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing! %} +{% In this case, it will always choose the 2nd interpretation---vec minus four. %} +{% So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses. %} ::: @@ -142,15 +148,24 @@ Anyways! Recall that super simple brush from before... ```haku -stroke 8 #000 (vec 0 0) +withDotter \d -> + stroke 8 #000 (d To) ``` -This reads as "a stroke that's 8 pixels wide, has the color `#000`, and is drawn at the point `(0, 0)` relative to the mouse cursor." +This reads as "given a dotter, output a stroke that's 8 pixels wide, has the color `#000`, and is drawn at the dotter's `To` coordinates." All these symbols are very meaningful to haku. If you reorder or remove any one of them, your brush isn't going to work! -- Reading from left to right, we start with `stroke`.\ +- Reading from left to right, we start with `withDotter`. + We can't draw without knowing _where_ to draw, and the `withDotter` incantation lets us ask the UI for that. + + - Then, `\d ->` lets us name the data we get back from the UI. + `d` ends up containing a few useful properties, but the most useful one for us is `To`, which contains the current mouse position (where *`To`* draw). + + - We'll get to what all these sigils mean to haku later! + +- On the next line we have a `stroke`. `stroke` is a _function_---a recipe for producing data!\ haku has [many such built-in recipes](/docs/system.html). `stroke` is one of them. @@ -192,7 +207,8 @@ Note how it's parenthesized though---recall that function arguments are separate And with all that, we let haku mix all the ingredients together, and get a black dot under the cursor. ```haku -stroke 8 #000 (vec 0 0) +withDotter \d -> + stroke 8 #000 (d To) ``` Nice! @@ -206,8 +222,16 @@ So to spice things up, haku has a few shapes you can choose from! Recall that 3rd argument to `stroke`. We can actually pass any arbitrary shape to it, and haku will outline it for us. -Right now haku supports two additional shapes: rectangles and circles. -You can try them out by playing with this brush! +The experience of drawing with that example brush is pretty crap, because it can draw dots that don't connect with each other at all. +Let's fix that by drawing a `line` instead! + +```haku +withDotter \d -> + stroke 8 #000 (line (d From) (d To)) +``` + +We replace the singular position `d To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points. +For the starting position we use a _different_ property of `d`, which is `From`---this is the _previous_ value of `To`, which allows us to draw a continuous line. ```haku [ diff --git a/rkgk.toml b/rkgk.toml index 9a56ffe..6b5959c 100644 --- a/rkgk.toml +++ b/rkgk.toml @@ -69,6 +69,11 @@ max_chunks = 2 # Maximum amount of defs across all source code chunks. max_defs = 256 +# Maximum amount of unique tags across all source code chunks. +# Note that tag uniqueness is determined by the tag's source code string, so two instances of `A` +# only occupy one slot. +max_tags = 256 + # Maximum amount of tokens a single chunk can have. max_tokens = 65536 diff --git a/static/brush-editor.js b/static/brush-editor.js index 07c115a..5404ec4 100644 --- a/static/brush-editor.js +++ b/static/brush-editor.js @@ -1,12 +1,12 @@ -import { CodeEditor, getLineStart } from "rkgk/code-editor.js"; -import { BrushPreview } from "rkgk/brush-preview.js"; +import { CodeEditor } from "rkgk/code-editor.js"; const defaultBrush = ` -- This is your brush. --- Try playing around with the numbers, +-- Try playing around with the numbers, -- and see what happens! -stroke 8 #000 (vec 0 0) +withDotter \d -> + stroke 8 #000 (d To) `.trim(); export class BrushEditor extends HTMLElement { diff --git a/static/brush-preview.js b/static/brush-preview.js index cfba2cb..6f550d2 100644 --- a/static/brush-preview.js +++ b/static/brush-preview.js @@ -21,32 +21,35 @@ export class BrushPreview extends HTMLElement { this.pixmap = new Pixmap(this.canvas.width, this.canvas.height); } - #renderBrushInner(haku) { - haku.resetVm(); + async #renderBrushInner(haku) { + this.pixmap.clear(); + let evalResult = await haku.evalBrush({ + runDotter: async () => { + return { + fromX: this.canvas.width / 2, + fromY: this.canvas.width / 2, + toX: this.canvas.width / 2, + toY: this.canvas.width / 2, + num: 0, + }; + }, - let evalResult = haku.evalBrush(); + runScribble: async (renderToPixmap) => { + return renderToPixmap(this.pixmap, 0, 0); + }, + }); if (evalResult.status != "ok") { return { status: "error", phase: "eval", result: evalResult }; } - this.pixmap.clear(); - let renderResult = haku.renderValue( - this.pixmap, - this.canvas.width / 2, - this.canvas.height / 2, - ); - if (renderResult.status != "ok") { - return { status: "error", phase: "render", result: renderResult }; - } - this.ctx.putImageData(this.pixmap.getImageData(), 0, 0); return { status: "ok" }; } - renderBrush(haku) { + async renderBrush(haku) { this.unsetErrorFlag(); - let result = this.#renderBrushInner(haku); + let result = await this.#renderBrushInner(haku); if (result.status == "error") { this.setErrorFlag(); } diff --git a/static/canvas-renderer.js b/static/canvas-renderer.js index 48b7551..8999cbc 100644 --- a/static/canvas-renderer.js +++ b/static/canvas-renderer.js @@ -19,7 +19,7 @@ class CanvasRenderer extends HTMLElement { this.#cursorReportingBehaviour(); this.#panningBehaviour(); this.#zoomingBehaviour(); - this.#paintingBehaviour(); + this.#interactionBehaviour(); this.addEventListener("contextmenu", (event) => event.preventDefault()); } @@ -388,18 +388,6 @@ class CanvasRenderer extends HTMLElement { // Behaviours - async #cursorReportingBehaviour() { - while (true) { - let event = await listen([this, "mousemove"]); - let [x, y] = this.viewport.toViewportSpace( - event.clientX - this.clientLeft, - event.offsetY - this.clientTop, - this.getWindowSize(), - ); - this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y })); - } - } - sendViewportUpdate() { this.dispatchEvent(new Event(".viewportUpdate")); } @@ -443,24 +431,28 @@ class CanvasRenderer extends HTMLElement { ); } - async #paintingBehaviour() { - const paint = (x, y) => { - let [wallX, wallY] = this.viewport.toViewportSpace(x, y, this.getWindowSize()); - this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY })); - }; + async #cursorReportingBehaviour() { + while (true) { + let event = await listen([this, "mousemove"]); + let [x, y] = this.viewport.toViewportSpace( + event.clientX - this.clientLeft, + event.offsetY - this.clientTop, + this.getWindowSize(), + ); + this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y })); + } + } + async #interactionBehaviour() { while (true) { let mouseDown = await listen([this, "mousedown"]); if (mouseDown.button == 0) { - paint(mouseDown.offsetX, mouseDown.offsetY); - while (true) { - let event = await listen([window, "mousemove"], [window, "mouseup"]); - if (event.type == "mousemove") { - paint(event.clientX - this.clientLeft, event.offsetY - this.clientTop); - } else if (event.type == "mouseup") { - break; - } - } + let [mouseX, mouseY] = this.viewport.toViewportSpace( + mouseDown.clientX - this.clientLeft, + mouseDown.clientY - this.clientTop, + this.getWindowSize(), + ); + notifyInteraction(this, "start", { mouseX, mouseY, num: 0 }); } } } @@ -468,6 +460,68 @@ class CanvasRenderer extends HTMLElement { customElements.define("rkgk-canvas-renderer", CanvasRenderer); +function notifyInteraction(canvasRenderer, kind, fields) { + canvasRenderer.dispatchEvent( + Object.assign(new InteractEvent(canvasRenderer), { interactionKind: kind, ...fields }), + ); +} + +class InteractEvent extends Event { + constructor(canvasRenderer) { + super(".interact"); + + this.canvasRenderer = canvasRenderer; + } + + continueAsDotter() { + (async () => { + let event = await listen( + [this.canvasRenderer, "mousemove"], + [this.canvasRenderer, "mouseup"], + ); + + if (event.type == "mousemove") { + let [mouseX, mouseY] = this.canvasRenderer.viewport.toViewportSpace( + event.clientX - this.canvasRenderer.clientLeft, + event.clientY - this.canvasRenderer.clientTop, + this.canvasRenderer.getWindowSize(), + ); + + notifyInteraction(this.canvasRenderer, "dotter", { + previousX: this.mouseX, + previousY: this.mouseY, + mouseX, + mouseY, + num: this.num + 1, + }); + } + + if (event.type == "mouseup" && event.button == 0) { + // Break the loop. + return; + } + })(); + + if (this.previousX != null && this.previousY != null) { + return { + fromX: this.previousX, + fromY: this.previousY, + toX: this.mouseX, + toY: this.mouseY, + num: this.num, + }; + } else { + return { + fromX: this.mouseX, + fromY: this.mouseY, + toX: this.mouseX, + toY: this.mouseY, + num: this.num, + }; + } + } +} + class Atlas { static getInitBuffer(chunkSize, nChunks) { let imageSize = chunkSize * nChunks; diff --git a/static/haku.js b/static/haku.js index 1392cbe..efede63 100644 --- a/static/haku.js +++ b/static/haku.js @@ -113,6 +113,12 @@ export class Pixmap { } } +// NOTE: This must be kept in sync with ContKind on the haku-wasm side. +export const ContKind = { + Scribble: 0, + Dotter: 1, +}; + export class Haku { #pInstance = 0; #pBrush = 0; @@ -206,17 +212,49 @@ export class Haku { } } - evalBrush() { - return this.#statusCodeToResultObject(w.haku_eval_brush(this.#pInstance, this.#pBrush)); + beginBrush() { + return this.#statusCodeToResultObject(w.haku_begin_brush(this.#pInstance, this.#pBrush)); } - renderValue(pixmap, translationX, translationY) { + expectedContKind() { + return w.haku_cont_kind(this.#pInstance); + } + + contScribble(pixmap, translationX, translationY) { return this.#statusCodeToResultObject( - w.haku_render_value(this.#pInstance, pixmap.ptr, translationX, translationY), + w.haku_cont_scribble(this.#pInstance, pixmap.ptr, translationX, translationY), ); } - resetVm() { - w.haku_reset_vm(this.#pInstance); + contDotter({ fromX, fromY, toX, toY, num }) { + return this.#statusCodeToResultObject( + w.haku_cont_dotter(this.#pInstance, fromX, fromY, toX, toY, num), + ); + } + + async evalBrush(options) { + let { runDotter, runScribble } = options; + + let result; + result = this.beginBrush(); + if (result.status != "ok") return result; + + while (this.expectedContKind() != ContKind.Invalid) { + switch (this.expectedContKind()) { + case ContKind.Scribble: + result = await runScribble((pixmap, translationX, translationY) => { + return this.contScribble(pixmap, translationX, translationY); + }); + return result; + + case ContKind.Dotter: + let dotter = await runDotter(); + result = this.contDotter(dotter); + if (result.status != "ok") return result; + break; + } + } + + return { status: "ok" }; } } diff --git a/static/index.js b/static/index.js index 9fa5ecb..5434f61 100644 --- a/static/index.js +++ b/static/index.js @@ -9,6 +9,7 @@ import { } from "rkgk/session.js"; import { debounce } from "rkgk/framework.js"; import { ReticleCursor } from "rkgk/reticle-renderer.js"; +import { selfController } from "rkgk/painter.js"; const updateInterval = 1000 / 60; @@ -175,14 +176,8 @@ function readUrl(urlString) { user.reticle.setCursor(x, y); } - if (wallEvent.kind.event == "setBrush") { - user.setBrush(wallEvent.kind.brush); - } - - if (wallEvent.kind.event == "plot") { - for (let { x, y } of wallEvent.kind.points) { - user.renderBrushToChunks(wall, x, y); - } + if (wallEvent.kind.event == "interact") { + user.simulate(wall, wallEvent.kind.interactions); } } }); @@ -230,30 +225,21 @@ function readUrl(urlString) { reportCursor(event.x, event.y); }); - let plotQueue = []; - async function flushPlotQueue() { - let points = plotQueue.splice(0, plotQueue.length); - if (points.length != 0) { - session.sendPlot(points); + let interactionQueue = []; + function flushInteractionQueue() { + if (interactionQueue.length != 0) { + session.sendInteraction(interactionQueue); + interactionQueue.splice(0); } } - setInterval(flushPlotQueue, updateInterval); + setInterval(flushInteractionQueue, updateInterval); - canvasRenderer.addEventListener(".paint", async (event) => { - plotQueue.push({ x: event.x, y: event.y }); - - if (currentUser.isBrushOk) { - brushEditor.resetErrors(); - - let result = currentUser.renderBrushToChunks(wall, event.x, event.y); - if (result.status == "error") { - brushEditor.renderHakuResult( - result.phase == "eval" ? "Evaluation" : "Rendering", - result.result, - ); - } - } + canvasRenderer.addEventListener(".interact", async (event) => { + let result = await currentUser.haku.evalBrush( + selfController(interactionQueue, wall, event), + ); + brushEditor.renderHakuResult(result.phase == "eval" ? "Evaluation" : "Rendering", result); }); canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render()); @@ -270,20 +256,23 @@ function readUrl(urlString) { return; } - let previewResult = brushPreview.renderBrush(currentUser.haku); - if (previewResult.status == "error") { - brushEditor.renderHakuResult( - previewResult.phase == "eval" ? "Evaluation" : "Rendering", - previewResult.result, - ); - } + brushPreview.renderBrush(currentUser.haku).then((previewResult) => { + if (previewResult.status == "error") { + brushEditor.renderHakuResult( + previewResult.phase == "eval" ? "Evaluation" : "Rendering", + previewResult.result, + ); + } + }); } compileBrush(); brushEditor.addEventListener(".codeChanged", async () => { - flushPlotQueue(); compileBrush(); - session.sendSetBrush(brushEditor.code); + interactionQueue.push({ + kind: "setBrush", + brush: brushEditor.code, + }); }); session.eventLoop(); diff --git a/static/online-users.js b/static/online-users.js index 016e572..16f39a0 100644 --- a/static/online-users.js +++ b/static/online-users.js @@ -1,5 +1,5 @@ -import { Haku } from "rkgk/haku.js"; -import { Painter } from "rkgk/painter.js"; +import { ContKind, Haku } from "rkgk/haku.js"; +import { renderToChunksInArea, dotterRenderArea } from "rkgk/painter.js"; export class User { nickname = ""; @@ -7,12 +7,11 @@ export class User { reticle = null; isBrushOk = false; + simulation = null; constructor(wallInfo, nickname) { this.nickname = nickname; - this.haku = new Haku(wallInfo.hakuLimits); - this.painter = new Painter(wallInfo.paintArea); } destroy() { @@ -38,6 +37,58 @@ export class User { return result; } + + simulate(wall, interactions) { + console.group("simulate"); + for (let interaction of interactions) { + if (interaction.kind == "setBrush") { + this.simulation = null; + this.setBrush(interaction.brush); + } + + if (this.isBrushOk) { + if (this.simulation == null) { + console.log("no simulation -- beginning brush"); + this.simulation = { renderArea: { left: 0, top: 0, right: 0, bottom: 0 } }; + this.haku.beginBrush(); + } + + if (interaction.kind == "dotter" && this.#expectContKind(ContKind.Dotter)) { + let dotter = { + fromX: interaction.from.x, + fromY: interaction.from.y, + toX: interaction.to.x, + toY: interaction.to.y, + num: interaction.num, + }; + this.haku.contDotter(dotter); + this.simulation.renderArea = dotterRenderArea(wall, dotter); + } + + if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) { + renderToChunksInArea( + wall, + this.simulation.renderArea, + (pixmap, translationX, translationY) => { + return this.haku.contScribble(pixmap, translationX, translationY); + }, + ); + console.log("ended simulation"); + this.simulation = null; + } + } + } + console.groupEnd(); + } + + #expectContKind(kind) { + if (this.haku.expectedContKind() == kind) { + return true; + } else { + console.error(`expected cont kind: ${kind}`); + return false; + } + } } export class OnlineUsers extends EventTarget { diff --git a/static/painter.js b/static/painter.js index ab5b252..5456586 100644 --- a/static/painter.js +++ b/static/painter.js @@ -1,43 +1,69 @@ -export class Painter { - constructor(paintArea) { - this.paintArea = paintArea; - } +import { listen } from "rkgk/framework.js"; - renderBrushToWall(haku, centerX, centerY, wall) { - haku.resetVm(); - - let evalResult = haku.evalBrush(); - if (evalResult.status != "ok") - return { status: "error", phase: "eval", result: evalResult }; - - let left = centerX - this.paintArea / 2; - let top = centerY - this.paintArea / 2; - - let leftChunk = Math.floor(left / wall.chunkSize); - let topChunk = Math.floor(top / wall.chunkSize); - let rightChunk = Math.ceil((left + this.paintArea) / wall.chunkSize); - let bottomChunk = Math.ceil((top + this.paintArea) / wall.chunkSize); - for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) { - for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) { - let x = Math.floor(-chunkX * wall.chunkSize + centerX); - let y = Math.floor(-chunkY * wall.chunkSize + centerY); - let chunk = wall.getOrCreateChunk(chunkX, chunkY); - chunk.markModified(); - - let renderResult = haku.renderValue(chunk.pixmap, x, y); - if (renderResult.status != "ok") { - return { status: "error", phase: "render", result: renderResult }; - } - } +function* chunksInRectangle(left, top, right, bottom, chunkSize) { + let leftChunk = Math.floor(left / chunkSize); + let topChunk = Math.floor(top / chunkSize); + let rightChunk = Math.ceil(right / chunkSize); + let bottomChunk = Math.ceil(bottom / chunkSize); + for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) { + for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) { + yield [chunkX, chunkY]; } - - for (let y = topChunk; y < bottomChunk; ++y) { - for (let x = leftChunk; x < rightChunk; ++x) { - let chunk = wall.getChunk(x, y); - chunk.syncFromPixmap(); - } - } - - return { status: "ok" }; } } + +export function renderToChunksInArea(wall, renderArea, renderToPixmap) { + for (let [chunkX, chunkY] of chunksInRectangle( + renderArea.left, + renderArea.top, + renderArea.right, + renderArea.bottom, + wall.chunkSize, + )) { + let chunk = wall.getOrCreateChunk(chunkX, chunkY); + let translationX = -chunkX * wall.chunkSize; + let translationY = -chunkY * wall.chunkSize; + let result = renderToPixmap(chunk.pixmap, translationX, translationY); + chunk.markModified(); + if (result.status != "ok") return result; + } + + return { status: "ok" }; +} + +export function dotterRenderArea(wall, dotter) { + let halfPaintArea = wall.paintArea / 2; + return { + left: dotter.toX - halfPaintArea, + top: dotter.toY - halfPaintArea, + right: dotter.toX + halfPaintArea, + bottom: dotter.toY + halfPaintArea, + }; +} + +export function selfController(interactionQueue, wall, event) { + let renderArea = null; + return { + async runScribble(renderToPixmap) { + interactionQueue.push({ kind: "scribble" }); + if (renderArea != null) { + return renderToChunksInArea(wall, renderArea, renderToPixmap); + } else { + console.debug("render area is empty, nothing will be rendered"); + } + return { status: "ok" }; + }, + + async runDotter() { + let dotter = await event.continueAsDotter(); + interactionQueue.push({ + kind: "dotter", + from: { x: dotter.fromX, y: dotter.fromY }, + to: { x: dotter.toX, y: dotter.toY }, + num: dotter.num, + }); + renderArea = dotterRenderArea(wall, dotter); + return dotter; + }, + }; +} diff --git a/static/session.js b/static/session.js index 9558b29..8c1beac 100644 --- a/static/session.js +++ b/static/session.js @@ -267,22 +267,13 @@ class Session extends EventTarget { }); } - sendPlot(points) { + sendInteraction(interactions) { + console.log(interactions); this.#sendJson({ request: "wall", wallEvent: { - event: "plot", - points, - }, - }); - } - - sendSetBrush(brush) { - this.#sendJson({ - request: "wall", - wallEvent: { - event: "setBrush", - brush, + event: "interact", + interactions, }, }); } diff --git a/static/signal.js b/static/signal.js new file mode 100644 index 0000000..e69de29 diff --git a/static/wall.js b/static/wall.js index 30d051f..fd34636 100644 --- a/static/wall.js +++ b/static/wall.js @@ -28,6 +28,7 @@ export class Wall { constructor(wallInfo) { this.chunkSize = wallInfo.chunkSize; + this.paintArea = wallInfo.paintArea; this.onlineUsers = new OnlineUsers(wallInfo); }