diff --git a/crates/haku/src/render.rs b/crates/haku/src/render.rs deleted file mode 100644 index b31fa7d..0000000 --- a/crates/haku/src/render.rs +++ /dev/null @@ -1,173 +0,0 @@ -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 ab13378..16f88b2 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::{error, info, info_span, instrument}; +use tracing::{debug, error, info, info_span, instrument}; use crate::{ login::{self, database::LoginStatus}, @@ -370,6 +370,7 @@ 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?; } @@ -391,11 +392,12 @@ 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() { - if !self.sent_chunks.insert(position) - || !self.chunk_images.chunk_exists(position) - { + let sent = !self.sent_chunks.insert(position); + if sent || !self.chunk_images.chunk_exists(position) { + debug!(?position, "skipping chunk"); continue; } positions.push(position); @@ -404,6 +406,8 @@ impl SessionLoop { } } + debug!(num = positions.len(), "pending chunk images"); + self.pending_images .extend(self.chunk_images.encoded(positions).await.data); } @@ -429,13 +433,15 @@ impl SessionLoop { } } - 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?; + 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?; + } Ok(()) } diff --git a/crates/rkgk/src/wall/broker.rs b/crates/rkgk/src/wall/broker.rs index 96bfe8f..46ad53d 100644 --- a/crates/rkgk/src/wall/broker.rs +++ b/crates/rkgk/src/wall/broker.rs @@ -68,7 +68,13 @@ 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))); + 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 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 0e73d9b..7b6f31d 100644 --- a/crates/rkgk/src/wall/chunk_images.rs +++ b/crates/rkgk/src/wall/chunk_images.rs @@ -5,10 +5,16 @@ use eyre::Context; use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use tiny_skia::{IntSize, Pixmap}; use tokio::sync::{mpsc, oneshot, Mutex}; -use tracing::{error, info, instrument}; +use tracing::{debug, 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, @@ -48,13 +54,13 @@ enum Command { } impl ChunkImages { - pub fn new(wall: Arc, db: Arc) -> Self { + pub fn new(wall: Arc, db: Arc, population: Population) -> Self { let (commands_tx, commands_rx) = mpsc::channel(32); let async_loop = Arc::new(ChunkImageLoop { wall: Arc::clone(&wall), db, - chunks_in_db: DashSet::new(), + chunks_in_db: population.chunks_in_db, }); tokio::spawn(Arc::clone(&async_loop).enter(commands_rx)); @@ -65,6 +71,17 @@ 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 @@ -233,14 +250,7 @@ impl ChunkImageLoop { } async fn enter(self: Arc, mut commands_rx: mpsc::Receiver) { - 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); - } + debug!(num = ?self.chunks_in_db.len(), "chunks in database"); while let Some(command) = commands_rx.recv().await { match command { diff --git a/docs/haku.dj b/docs/haku.dj index aa93e43..b371d88 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 sum +sum: [1, 2, 3] |reduce 0 add sin': \x -> sin x sines: [0, pi*1/2, pi*2/2, pi*3/2] |map sin'