Compare commits

...

4 commits

5 changed files with 46 additions and 197 deletions

View file

@ -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<RenderTarget<'a>>,
transform_stack: Vec<Transform>,
}
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<Path> {
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()
}

View file

@ -25,7 +25,7 @@ use tokio::{
select, select,
sync::{mpsc, oneshot}, sync::{mpsc, oneshot},
}; };
use tracing::{error, info, info_span, instrument}; use tracing::{debug, error, info, info_span, instrument};
use crate::{ use crate::{
login::{self, database::LoginStatus}, login::{self, database::LoginStatus},
@ -370,6 +370,7 @@ impl SessionLoop {
top_left, top_left,
bottom_right, bottom_right,
} => { } => {
debug!(?top_left, ?bottom_right, "Request::Viewport");
self.viewport_chunks = ChunkIterator::new(top_left, bottom_right); self.viewport_chunks = ChunkIterator::new(top_left, bottom_right);
self.send_chunks(ws).await?; 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 // Number of chunks iterated is limited per packet, so as not to let the client
// stall the server by sending in a huge viewport. // stall the server by sending in a huge viewport.
debug!(?self.viewport_chunks, ?self.sent_chunks);
for _ in 0..9000 { for _ in 0..9000 {
if let Some(position) = self.viewport_chunks.next() { if let Some(position) = self.viewport_chunks.next() {
if !self.sent_chunks.insert(position) let sent = !self.sent_chunks.insert(position);
|| !self.chunk_images.chunk_exists(position) if sent || !self.chunk_images.chunk_exists(position) {
{ debug!(?position, "skipping chunk");
continue; continue;
} }
positions.push(position); positions.push(position);
@ -404,6 +406,8 @@ impl SessionLoop {
} }
} }
debug!(num = positions.len(), "pending chunk images");
self.pending_images self.pending_images
.extend(self.chunk_images.encoded(positions).await.data); .extend(self.chunk_images.encoded(positions).await.data);
} }
@ -429,13 +433,15 @@ impl SessionLoop {
} }
} }
ws.send(to_message(&Notify::Chunks { if !chunk_infos.is_empty() {
chunks: chunk_infos, ws.send(to_message(&Notify::Chunks {
has_more: !self.pending_images.is_empty() chunks: chunk_infos,
|| self.viewport_chunks.clone().next().is_some(), has_more: !self.pending_images.is_empty()
})) || self.viewport_chunks.clone().next().is_some(),
.await?; }))
ws.send(Message::Binary(packet)).await?; .await?;
ws.send(Message::Binary(packet)).await?;
}
Ok(()) Ok(())
} }

View file

@ -68,7 +68,13 @@ impl Broker {
default_wall_settings: self.settings.default_wall_settings, default_wall_settings: self.settings.default_wall_settings,
})?); })?);
let wall = Arc::new(Wall::new(*db.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( let auto_save = Arc::new(AutoSave::new(
Arc::clone(&wall), Arc::clone(&wall),
Arc::clone(&chunk_images), Arc::clone(&chunk_images),

View file

@ -5,10 +5,16 @@ use eyre::Context;
use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
use tiny_skia::{IntSize, Pixmap}; use tiny_skia::{IntSize, Pixmap};
use tokio::sync::{mpsc, oneshot, Mutex}; 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}; 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<ChunkPosition>,
}
/// Chunk image encoding, caching, and storage service. /// Chunk image encoding, caching, and storage service.
pub struct ChunkImages { pub struct ChunkImages {
wall: Arc<Wall>, wall: Arc<Wall>,
@ -48,13 +54,13 @@ enum Command {
} }
impl ChunkImages { impl ChunkImages {
pub fn new(wall: Arc<Wall>, db: Arc<Database>) -> Self { pub fn new(wall: Arc<Wall>, db: Arc<Database>, population: Population) -> Self {
let (commands_tx, commands_rx) = mpsc::channel(32); let (commands_tx, commands_rx) = mpsc::channel(32);
let async_loop = Arc::new(ChunkImageLoop { let async_loop = Arc::new(ChunkImageLoop {
wall: Arc::clone(&wall), wall: Arc::clone(&wall),
db, db,
chunks_in_db: DashSet::new(), chunks_in_db: population.chunks_in_db,
}); });
tokio::spawn(Arc::clone(&async_loop).enter(commands_rx)); 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<ChunkPosition>) -> EncodeResult { pub async fn encoded(&self, chunks: Vec<ChunkPosition>) -> EncodeResult {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
_ = self _ = self
@ -233,14 +250,7 @@ impl ChunkImageLoop {
} }
async fn enter(self: Arc<Self>, mut commands_rx: mpsc::Receiver<Command>) { async fn enter(self: Arc<Self>, mut commands_rx: mpsc::Receiver<Command>) {
let all_chunks = self debug!(num = ?self.chunks_in_db.len(), "chunks in database");
.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 { while let Some(command) = commands_rx.recv().await {
match command { match command {

View file

@ -394,7 +394,7 @@ If you'd like to reference a built-in function to e.g. pass it to a list-transfo
```haku ```haku
add: \x, y -> x + y add: \x, y -> x + y
sum: [1, 2, 3] |reduce 0 sum sum: [1, 2, 3] |reduce 0 add
sin': \x -> sin x sin': \x -> sin x
sines: [0, pi*1/2, pi*2/2, pi*3/2] |map sin' sines: [0, pi*1/2, pi*2/2, pi*3/2] |map sin'