diff --git a/crates/haku/src/render.rs b/crates/haku/src/render.rs new file mode 100644 index 0000000..b31fa7d --- /dev/null +++ b/crates/haku/src/render.rs @@ -0,0 +1,173 @@ +use alloc::vec::Vec; +use tiny_skia::{ + BlendMode, Color, FillRule, LineCap, Paint, Path, PathBuilder, Pixmap, Shader, + Stroke as SStroke, Transform, +}; + +use crate::{ + value::{Fill, Ref, Rgba, Scribble, Shape, Stroke, Value}, + vm::{Exception, Vm}, +}; + +pub use tiny_skia; + +pub struct RendererLimits { + pub pixmap_stack_capacity: usize, + pub transform_stack_capacity: usize, +} + +pub enum RenderTarget<'a> { + Borrowed(&'a mut Pixmap), + Owned(Pixmap), +} + +pub struct Renderer<'a> { + pixmap_stack: Vec>, + transform_stack: Vec, +} + +impl<'a> Renderer<'a> { + pub fn new(pixmap: &'a mut Pixmap, limits: &RendererLimits) -> Self { + assert!(limits.pixmap_stack_capacity > 0); + assert!(limits.transform_stack_capacity > 0); + + let mut blend_stack = Vec::with_capacity(limits.pixmap_stack_capacity); + blend_stack.push(RenderTarget::Borrowed(pixmap)); + + let mut transform_stack = Vec::with_capacity(limits.transform_stack_capacity); + transform_stack.push(Transform::identity()); + + Self { + pixmap_stack: blend_stack, + transform_stack, + } + } + + fn create_exception(vm: &Vm, _at: Value, message: &'static str) -> Exception { + vm.create_exception(message) + } + + fn transform(&self) -> Transform { + self.transform_stack.last().copied().unwrap() + } + + fn transform_mut(&mut self) -> &mut Transform { + self.transform_stack.last_mut().unwrap() + } + + pub fn translate(&mut self, x: f32, y: f32) { + let translated = self.transform().post_translate(x, y); + *self.transform_mut() = translated; + } + + fn pixmap_mut(&mut self) -> &mut Pixmap { + match self.pixmap_stack.last_mut().unwrap() { + RenderTarget::Borrowed(pixmap) => pixmap, + RenderTarget::Owned(pixmap) => pixmap, + } + } + + pub fn render(&mut self, vm: &Vm, value: Value) -> Result<(), Exception> { + static NOT_A_SCRIBBLE: &str = "cannot draw something that is not a scribble"; + let (_id, scribble) = vm + .get_ref_value(value) + .ok_or_else(|| Self::create_exception(vm, value, NOT_A_SCRIBBLE))?; + + match &scribble { + Ref::List(list) => { + for element in &list.elements { + self.render(vm, *element)?; + } + } + Ref::Scribble(scribble) => match scribble { + Scribble::Stroke(stroke) => self.render_stroke(vm, value, stroke)?, + Scribble::Fill(fill) => self.render_fill(vm, value, fill)?, + }, + _ => return Err(Self::create_exception(vm, value, NOT_A_SCRIBBLE))?, + } + + Ok(()) + } + + fn shape_to_path(shape: &Shape) -> Option { + let mut pb = PathBuilder::new(); + match shape { + Shape::Point(vec) => { + pb.move_to(vec.x, vec.y); + pb.line_to(vec.x, vec.y); + } + Shape::Line(start, end) => { + pb.move_to(start.x, start.y); + pb.line_to(end.x, end.y); + } + Shape::Rect(position, size) => { + if let Some(rect) = + tiny_skia::Rect::from_xywh(position.x, position.y, size.x, size.y) + { + pb.push_rect(rect); + } + } + Shape::Circle(position, radius) => { + pb.push_circle(position.x, position.y, *radius); + } + } + pb.finish() + } + + fn render_stroke(&mut self, _vm: &Vm, _value: Value, stroke: &Stroke) -> Result<(), Exception> { + let paint = Paint { + shader: Shader::SolidColor(tiny_skia_color(stroke.color)), + ..default_paint() + }; + let transform = self.transform(); + if let Some(path) = Self::shape_to_path(&stroke.shape) { + self.pixmap_mut().stroke_path( + &path, + &paint, + &SStroke { + width: stroke.thickness, + line_cap: LineCap::Round, + ..Default::default() + }, + transform, + None, + ); + } + + Ok(()) + } + + fn render_fill(&mut self, _vm: &Vm, _value: Value, fill: &Fill) -> Result<(), Exception> { + let paint = Paint { + shader: Shader::SolidColor(tiny_skia_color(fill.color)), + ..default_paint() + }; + + let transform = self.transform(); + if let Some(path) = Self::shape_to_path(&fill.shape) { + self.pixmap_mut() + .fill_path(&path, &paint, FillRule::EvenOdd, 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, + } +} + +fn tiny_skia_color(color: Rgba) -> Color { + Color::from_rgba( + color.r.clamp(0.0, 1.0), + color.g.clamp(0.0, 1.0), + color.b.clamp(0.0, 1.0), + color.a.clamp(0.0, 1.0), + ) + .unwrap() +} diff --git a/crates/rkgk/src/api/wall.rs b/crates/rkgk/src/api/wall.rs index 16f88b2..ab13378 100644 --- a/crates/rkgk/src/api/wall.rs +++ b/crates/rkgk/src/api/wall.rs @@ -25,7 +25,7 @@ use tokio::{ select, sync::{mpsc, oneshot}, }; -use tracing::{debug, error, info, info_span, instrument}; +use tracing::{error, info, info_span, instrument}; use crate::{ login::{self, database::LoginStatus}, @@ -370,7 +370,6 @@ impl SessionLoop { top_left, bottom_right, } => { - debug!(?top_left, ?bottom_right, "Request::Viewport"); self.viewport_chunks = ChunkIterator::new(top_left, bottom_right); self.send_chunks(ws).await?; } @@ -392,12 +391,11 @@ impl SessionLoop { // Number of chunks iterated is limited per packet, so as not to let the client // stall the server by sending in a huge viewport. - debug!(?self.viewport_chunks, ?self.sent_chunks); for _ in 0..9000 { if let Some(position) = self.viewport_chunks.next() { - let sent = !self.sent_chunks.insert(position); - if sent || !self.chunk_images.chunk_exists(position) { - debug!(?position, "skipping chunk"); + if !self.sent_chunks.insert(position) + || !self.chunk_images.chunk_exists(position) + { continue; } positions.push(position); @@ -406,8 +404,6 @@ impl SessionLoop { } } - debug!(num = positions.len(), "pending chunk images"); - self.pending_images .extend(self.chunk_images.encoded(positions).await.data); } @@ -433,15 +429,13 @@ impl SessionLoop { } } - if !chunk_infos.is_empty() { - ws.send(to_message(&Notify::Chunks { - chunks: chunk_infos, - has_more: !self.pending_images.is_empty() - || self.viewport_chunks.clone().next().is_some(), - })) - .await?; - ws.send(Message::Binary(packet)).await?; - } + ws.send(to_message(&Notify::Chunks { + chunks: chunk_infos, + has_more: !self.pending_images.is_empty() + || self.viewport_chunks.clone().next().is_some(), + })) + .await?; + ws.send(Message::Binary(packet)).await?; Ok(()) } diff --git a/crates/rkgk/src/wall/broker.rs b/crates/rkgk/src/wall/broker.rs index 46ad53d..96bfe8f 100644 --- a/crates/rkgk/src/wall/broker.rs +++ b/crates/rkgk/src/wall/broker.rs @@ -68,13 +68,7 @@ impl Broker { default_wall_settings: self.settings.default_wall_settings, })?); let wall = Arc::new(Wall::new(*db.wall_settings())); - let chunk_images = Arc::new(ChunkImages::new( - Arc::clone(&wall), - Arc::clone(&db), - // NOTE: Upon initial loading, this will stall until the query finishes. - // This is probably very sub-optimal on walls with a lot of chunks. - ChunkImages::populate(&db).await, - )); + let chunk_images = Arc::new(ChunkImages::new(Arc::clone(&wall), Arc::clone(&db))); let auto_save = Arc::new(AutoSave::new( Arc::clone(&wall), Arc::clone(&chunk_images), diff --git a/crates/rkgk/src/wall/chunk_images.rs b/crates/rkgk/src/wall/chunk_images.rs index 7b6f31d..0e73d9b 100644 --- a/crates/rkgk/src/wall/chunk_images.rs +++ b/crates/rkgk/src/wall/chunk_images.rs @@ -5,16 +5,10 @@ use eyre::Context; use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use tiny_skia::{IntSize, Pixmap}; use tokio::sync::{mpsc, oneshot, Mutex}; -use tracing::{debug, error, info, instrument}; +use tracing::{error, info, instrument}; use super::{database::ChunkDataPair, Chunk, ChunkPosition, Database, Wall}; -/// Initial population of `ChunkImages`'s cache. -/// Created as part of an async process before `new`. -pub struct Population { - chunks_in_db: DashSet, -} - /// Chunk image encoding, caching, and storage service. pub struct ChunkImages { wall: Arc, @@ -54,13 +48,13 @@ enum Command { } impl ChunkImages { - pub fn new(wall: Arc, db: Arc, population: Population) -> Self { + pub fn new(wall: Arc, db: Arc) -> Self { let (commands_tx, commands_rx) = mpsc::channel(32); let async_loop = Arc::new(ChunkImageLoop { wall: Arc::clone(&wall), db, - chunks_in_db: population.chunks_in_db, + chunks_in_db: DashSet::new(), }); tokio::spawn(Arc::clone(&async_loop).enter(commands_rx)); @@ -71,17 +65,6 @@ impl ChunkImages { } } - pub async fn populate(db: &Database) -> Population { - Population { - chunks_in_db: db - .get_all_chunks() - .await - .expect("could not list chunks in the database") - .into_iter() - .collect(), - } - } - pub async fn encoded(&self, chunks: Vec) -> EncodeResult { let (tx, rx) = oneshot::channel(); _ = self @@ -250,7 +233,14 @@ impl ChunkImageLoop { } async fn enter(self: Arc, mut commands_rx: mpsc::Receiver) { - debug!(num = ?self.chunks_in_db.len(), "chunks in database"); + let all_chunks = self + .db + .get_all_chunks() + .await + .expect("could not list chunks in the database"); + for position in all_chunks { + self.chunks_in_db.insert(position); + } while let Some(command) = commands_rx.recv().await { match command { diff --git a/docs/haku.dj b/docs/haku.dj index b371d88..aa93e43 100644 --- a/docs/haku.dj +++ b/docs/haku.dj @@ -394,7 +394,7 @@ If you'd like to reference a built-in function to e.g. pass it to a list-transfo ```haku add: \x, y -> x + y -sum: [1, 2, 3] |reduce 0 add +sum: [1, 2, 3] |reduce 0 sum sin': \x -> sin x sines: [0, pi*1/2, pi*2/2, pi*3/2] |map sin'