Compare commits
No commits in common. "8e467d5447d5fdcd19e7589d7b3a56fde5b78414" and "914da923f7b5e5aee0d7a703a676a201a3e594b7" have entirely different histories.
8e467d5447
...
914da923f7
5 changed files with 197 additions and 46 deletions
173
crates/haku/src/render.rs
Normal file
173
crates/haku/src/render.rs
Normal file
|
|
@ -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<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()
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ use tokio::{
|
||||||
select,
|
select,
|
||||||
sync::{mpsc, oneshot},
|
sync::{mpsc, oneshot},
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info, info_span, instrument};
|
use tracing::{error, info, info_span, instrument};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
login::{self, database::LoginStatus},
|
login::{self, database::LoginStatus},
|
||||||
|
|
@ -370,7 +370,6 @@ 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?;
|
||||||
}
|
}
|
||||||
|
|
@ -392,12 +391,11 @@ 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() {
|
||||||
let sent = !self.sent_chunks.insert(position);
|
if !self.sent_chunks.insert(position)
|
||||||
if sent || !self.chunk_images.chunk_exists(position) {
|
|| !self.chunk_images.chunk_exists(position)
|
||||||
debug!(?position, "skipping chunk");
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
positions.push(position);
|
positions.push(position);
|
||||||
|
|
@ -406,8 +404,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +429,6 @@ impl SessionLoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !chunk_infos.is_empty() {
|
|
||||||
ws.send(to_message(&Notify::Chunks {
|
ws.send(to_message(&Notify::Chunks {
|
||||||
chunks: chunk_infos,
|
chunks: chunk_infos,
|
||||||
has_more: !self.pending_images.is_empty()
|
has_more: !self.pending_images.is_empty()
|
||||||
|
|
@ -441,7 +436,6 @@ impl SessionLoop {
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
ws.send(Message::Binary(packet)).await?;
|
ws.send(Message::Binary(packet)).await?;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,13 +68,7 @@ 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(
|
let chunk_images = Arc::new(ChunkImages::new(Arc::clone(&wall), Arc::clone(&db)));
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,10 @@ 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::{debug, error, info, instrument};
|
use tracing::{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>,
|
||||||
|
|
@ -54,13 +48,13 @@ enum Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChunkImages {
|
impl ChunkImages {
|
||||||
pub fn new(wall: Arc<Wall>, db: Arc<Database>, population: Population) -> Self {
|
pub fn new(wall: Arc<Wall>, db: Arc<Database>) -> 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: population.chunks_in_db,
|
chunks_in_db: DashSet::new(),
|
||||||
});
|
});
|
||||||
tokio::spawn(Arc::clone(&async_loop).enter(commands_rx));
|
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<ChunkPosition>) -> EncodeResult {
|
pub async fn encoded(&self, chunks: Vec<ChunkPosition>) -> EncodeResult {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
_ = self
|
_ = self
|
||||||
|
|
@ -250,7 +233,14 @@ 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>) {
|
||||||
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 {
|
while let Some(command) = commands_rx.recv().await {
|
||||||
match command {
|
match command {
|
||||||
|
|
|
||||||
|
|
@ -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 add
|
sum: [1, 2, 3] |reduce 0 sum
|
||||||
|
|
||||||
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'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue