diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index d3534eb22..602ea625f 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,5 +1,3 @@ -use skia_safe as skia; - #[cfg(target_arch = "wasm32")] mod emscripten; mod math; @@ -9,19 +7,21 @@ mod performance; mod render; mod shapes; mod state; +mod tiles; mod utils; mod uuid; mod view; mod wapi; mod wasm; -use crate::mem::SerializableResult; -use crate::shapes::{BoolType, ConstraintH, ConstraintV, StructureEntry, TransformEntry, Type}; -use crate::utils::uuid_from_u32_quartet; -use crate::uuid::Uuid; use indexmap::IndexSet; use math::{Bounds, Matrix}; +use mem::SerializableResult; +use shapes::{BoolType, ConstraintH, ConstraintV, StructureEntry, TransformEntry, Type}; +use skia_safe as skia; use state::State; +use utils::uuid_from_u32_quartet; +use uuid::Uuid; pub(crate) static mut STATE: Option> = None; diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index bcc4bc9f0..40112717d 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,13 +1,3 @@ -use skia_safe::{self as skia, image, Matrix, RRect, Rect}; - -use crate::uuid::Uuid; -use std::borrow::Cow; -use std::collections::{HashMap, HashSet}; - -use crate::performance; -use crate::view::Viewbox; -use crate::wapi; - mod blend; mod debug; mod fills; @@ -19,24 +9,65 @@ mod shadows; mod strokes; mod surfaces; mod text; -mod tiles; -use crate::shapes::{modified_children_ids, Corners, Fill, Shape, StructureEntry, Type}; +use skia_safe::{self as skia, image, Matrix, RRect, Rect}; +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; + use gpu_state::GpuState; use options::RenderOptions; use surfaces::{SurfaceId, Surfaces}; +use crate::performance; +use crate::shapes::{modified_children_ids, Corners, Fill, Shape, StructureEntry, Type}; +use crate::tiles::{self, TileRect, TileViewbox, TileWithDistance}; +use crate::uuid::Uuid; +use crate::view::Viewbox; +use crate::wapi; + pub use blend::BlendMode; pub use fonts::*; pub use images::*; // This is the extra are used for tile rendering. const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1; +const VIEWPORT_DEFAULT_CAPACITY: usize = 24 * 12; const MAX_BLOCKING_TIME_MS: i32 = 32; const NODE_BATCH_THRESHOLD: i32 = 10; -struct NodeRenderState { - id: Uuid, +pub struct PendingTiles { + pub list: Vec, +} + +impl PendingTiles { + pub fn new_empty() -> Self { + Self { + list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), + } + } + + pub fn update(&mut self, tile_viewbox: &TileViewbox) { + self.list.clear(); + for y in tile_viewbox.interest_rect.1..=tile_viewbox.interest_rect.3 { + for x in tile_viewbox.interest_rect.0..=tile_viewbox.interest_rect.2 { + let tile = tiles::Tile(x, y); + let distance = tiles::manhattan_distance(tile, tile_viewbox.center); + self.list.push((x, y, distance)); + } + } + } + + pub fn pop(&mut self) -> Option { + self.list.pop() + } + + pub fn sort(&mut self) { + self.list.sort_by(|a, b| b.2.cmp(&a.2)); + } +} + +pub struct NodeRenderState { + pub id: Uuid, // We use this bool to keep that we've traversed all the children inside this node. visited_children: bool, // This is used to clip the content of frames. @@ -95,13 +126,14 @@ pub(crate) struct RenderState { pub current_tile: Option, pub sampling_options: skia::SamplingOptions, pub render_area: Rect, + pub tile_viewbox: tiles::TileViewbox, pub tiles: tiles::TileHashMap, - pub pending_tiles: Vec, + pub pending_tiles: PendingTiles, } pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { // First we retrieve the extended area of the viewport that we could render. - let (isx, isy, iex, iey) = tiles::get_tiles_for_viewbox_with_interest( + let TileRect(isx, isy, iex, iey) = tiles::get_tiles_for_viewbox_with_interest( viewbox, VIEWPORT_INTEREST_AREA_THRESHOLD, scale, @@ -136,6 +168,7 @@ impl RenderState { // This is used multiple times everywhere so instead of creating new instances every // time we reuse this one. + let viewbox = Viewbox::new(width as f32, height as f32); let tiles = tiles::TileHashMap::new(); RenderState { @@ -143,7 +176,7 @@ impl RenderState { options: RenderOptions::default(), surfaces, fonts, - viewbox: Viewbox::new(width as f32, height as f32), + viewbox, cached_viewbox: Viewbox::new(0., 0.), cached_target_snapshot: None, images: ImageStore::new(), @@ -155,7 +188,12 @@ impl RenderState { sampling_options, render_area: Rect::new_empty(), tiles, - pending_tiles: vec![], + tile_viewbox: tiles::TileViewbox::new_with_interest( + viewbox, + VIEWPORT_INTEREST_AREA_THRESHOLD, + 1.0, + ), + pending_tiles: PendingTiles::new_empty(), } } @@ -199,6 +237,7 @@ impl RenderState { self.surfaces .resize(&mut self.gpu_state, dpr_width, dpr_height); self.viewbox.set_wh(width as f32, height as f32); + self.tile_viewbox.update(self.viewbox, self.get_scale()); } pub fn flush_and_submit(&mut self) { @@ -211,11 +250,12 @@ impl RenderState { } pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) { - let x = self.current_tile.unwrap().0; - let y = self.current_tile.unwrap().1; - let tile_rect = self.get_current_aligned_tile_bounds(); - self.surfaces.cache_current_tile_texture((x, y), tile_rect); + self.surfaces.cache_current_tile_texture( + &self.tile_viewbox, + &self.current_tile.unwrap(), + &tile_rect, + ); self.surfaces .draw_cached_tile_surface(self.current_tile.unwrap(), rect); @@ -470,11 +510,12 @@ impl RenderState { navigate_zoom * self.options.dpr(), )); - let (start_tile_x, start_tile_y, _, _) = tiles::get_tiles_for_viewbox_with_interest( - self.cached_viewbox, - VIEWPORT_INTEREST_AREA_THRESHOLD, - scale, - ); + let TileRect(start_tile_x, start_tile_y, _, _) = + tiles::get_tiles_for_viewbox_with_interest( + self.cached_viewbox, + VIEWPORT_INTEREST_AREA_THRESHOLD, + scale, + ); let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom; let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom; @@ -522,13 +563,6 @@ impl RenderState { }, ); - // First we retrieve the extended area of the viewport that we could render. - let (isx, isy, iex, iey) = tiles::get_tiles_for_viewbox_with_interest( - self.viewbox, - VIEWPORT_INTEREST_AREA_THRESHOLD, - scale, - ); - let viewbox_cache_size = get_cache_size(self.viewbox, scale); let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox, scale); if viewbox_cache_size != cached_viewbox_cache_size { @@ -539,32 +573,15 @@ impl RenderState { ); } - // Then we get the real amount of tiles rendered for the current viewbox. - let (sx, sy, ex, ey) = tiles::get_tiles_for_viewbox(self.viewbox, self.options.dpr()); - debug::render_debug_tiles_for_viewbox(self, isx, isy, iex, iey); - let tile_center = ((iex - isx) / 2, (iey - isy) / 2); + debug::render_debug_tiles_for_viewbox(self); performance::begin_measure!("tile_cache"); - self.pending_tiles = vec![]; - self.surfaces.cache_clear_visited(); - for y in isy..=iey { - for x in isx..=iex { - let tile = (x, y); - let distance = tiles::manhattan_distance(tile, tile_center); - self.pending_tiles.push((x, y, distance)); - // We only need to mark! as visited the visible - // tiles, the ones that are outside the viewport - // should not be rendered. - if x >= sx && x <= ex && y >= sy && y <= ey { - self.surfaces.cache_visit(tile); - } - } - } + self.pending_tiles.update(&self.tile_viewbox); performance::end_measure!("tile_cache"); self.pending_nodes = vec![]; // reorder by distance to the center. - self.pending_tiles.sort_by(|a, b| b.2.cmp(&a.2)); + self.pending_tiles.sort(); self.current_tile = None; self.render_in_progress = true; self.apply_drawing_to_render_canvas(None); @@ -654,8 +671,7 @@ impl RenderState { } pub fn get_current_tile_bounds(&mut self) -> Rect { - // TODO: check if we need to add dpr to something else here - let (tile_x, tile_y) = self.current_tile.unwrap(); + let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap(); let scale = self.get_scale(); let offset_x = self.viewbox.area.left * scale; let offset_y = self.viewbox.area.top * scale; @@ -676,7 +692,7 @@ impl RenderState { // with the global tile grid, which is useful for rendering tiles in a /// consistent and predictable layout. pub fn get_current_aligned_tile_bounds(&mut self) -> Rect { - let (tile_x, tile_y) = self.current_tile.unwrap(); + let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap(); let scale = self.get_scale(); let start_tile_x = (self.viewbox.area.left * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; @@ -877,7 +893,7 @@ impl RenderState { // let's check if there are more pending nodes if let Some(next_tile_with_distance) = self.pending_tiles.pop() { let (x, y, _) = next_tile_with_distance; - let next_tile = (x, y); + let next_tile = tiles::Tile(x, y); self.update_render_context(next_tile); if !self.surfaces.has_cached_tile_surface(next_tile) { @@ -921,15 +937,15 @@ impl RenderState { Ok(()) } - pub fn get_tiles_for_shape(&mut self, shape: &Shape) -> (i32, i32, i32, i32) { - let tile_size = tiles::get_tile_size(self.get_scale()); + pub fn get_tiles_for_shape(&mut self, shape: &Shape) -> TileRect { + let tile_size = tiles::get_tile_size(self.viewbox.zoom); tiles::get_tiles_for_rect(shape.extrect(), tile_size) } pub fn update_tile_for(&mut self, shape: &Shape) { - let (rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape); - let new_tiles: HashSet<(i32, i32)> = (rsx..=rex) - .flat_map(|x| (rsy..=rey).map(move |y| (x, y))) + let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape); + let new_tiles: HashSet = (rsx..=rex) + .flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile(x, y))) .collect(); // Update tiles where the shape was diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index cf9b2b33d..7bb0828d1 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -75,13 +75,8 @@ pub fn render_debug_shape(render_state: &mut RenderState, element: &Shape, inter .draw_rect(rect, &paint); } -pub fn render_debug_tiles_for_viewbox( - render_state: &mut RenderState, - sx: i32, - sy: i32, - ex: i32, - ey: i32, -) { +pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) { + let tiles::TileRect(sx, sy, ex, ey) = render_state.tile_viewbox.interest_rect; let canvas = render_state.surfaces.canvas(SurfaceId::Debug); let mut paint = skia::Paint::default(); paint.set_style(skia::PaintStyle::Stroke); @@ -102,7 +97,9 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) { paint.set_color(skia::Color::from_rgb(255, 0, 127)); paint.set_stroke_width(1.); - let (sx, sy, ex, ey) = tiles::get_tiles_for_viewbox(render_state.viewbox, scale); + let tile_size = tiles::get_tile_size(scale); + let tiles::TileRect(sx, sy, ex, ey) = + tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size); let str_rect = format!("{} {} {} {}", sx, sy, ex, ey); let debug_font = render_state.fonts.debug_font(); @@ -135,11 +132,12 @@ pub fn render_debug_tiles(render_state: &mut RenderState) { paint.set_color(skia::Color::from_rgb(127, 0, 255)); paint.set_stroke_width(1.); - let (sx, sy, ex, ey) = tiles::get_tiles_for_viewbox(render_state.viewbox, scale); let tile_size = tiles::get_tile_size(scale); + let tiles::TileRect(sx, sy, ex, ey) = + tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size); for y in sy..=ey { for x in sx..=ex { - let tile = (x, y); + let tile = tiles::Tile(x, y); let shape_count = render_state.tiles.get_shapes_at(tile).iter().len(); if shape_count == 0 { continue; diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 5e4204ec0..d1c1b513f 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -1,7 +1,7 @@ use crate::shapes::Shape; use skia_safe::{self as skia, IRect, Paint, RRect}; -use super::{gpu_state::GpuState, tiles::Tile, tiles::TILE_SIZE}; +use super::{gpu_state::GpuState, tiles::Tile, tiles::TileViewbox, tiles::TILE_SIZE}; use base64::{engine::general_purpose, Engine as _}; use std::collections::HashMap; @@ -241,15 +241,12 @@ impl Surfaces { .reset_matrix(); } - pub fn cache_clear_visited(&mut self) { - self.tiles.clear_visited(); - } - - pub fn cache_visit(&mut self, tile: Tile) { - self.tiles.visit(tile); - } - - pub fn cache_current_tile_texture(&mut self, tile: Tile, tile_rect: skia::Rect) { + pub fn cache_current_tile_texture( + &mut self, + tile_viewbox: &TileViewbox, + tile: &Tile, + tile_rect: &skia::Rect, + ) { let rect = IRect::from_xywh( self.margins.width, self.margins.height, @@ -258,7 +255,7 @@ impl Surfaces { ); if let Some(snapshot) = self.current.image_snapshot_with_bounds(rect) { - self.tiles.add(tile, snapshot.clone()); + self.tiles.add(tile_viewbox, tile, snapshot.clone()); self.cache.canvas().draw_image_rect( snapshot.clone(), None, @@ -290,14 +287,12 @@ impl Surfaces { pub struct TileTextureCache { grid: HashMap, - visited: HashMap, } impl TileTextureCache { pub fn new() -> Self { Self { grid: HashMap::new(), - visited: HashMap::new(), } } @@ -311,13 +306,13 @@ impl TileTextureCache { } } - pub fn add(&mut self, tile: Tile, image: skia::Image) { + pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile, image: skia::Image) { if self.grid.len() > TEXTURES_CACHE_CAPACITY { let marked: Vec<_> = self .grid .iter_mut() .filter_map(|(tile, _)| { - if !self.visited.contains_key(tile) { + if !tile_viewbox.is_visible(tile) { Some(*tile) } else { None @@ -327,7 +322,7 @@ impl TileTextureCache { .collect(); self.remove_list(marked); } - self.grid.insert(tile, image); + self.grid.insert(*tile, image); } pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Image, String> { @@ -346,12 +341,4 @@ impl TileTextureCache { pub fn clear(&mut self) { self.grid.clear(); } - - pub fn clear_visited(&mut self) { - self.visited.clear(); - } - - pub fn visit(&mut self, tile: Tile) { - self.visited.insert(tile, true); - } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index dce678f73..4392c4d56 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -5,6 +5,7 @@ use skia_safe as skia; use crate::render::RenderState; use crate::shapes::Shape; use crate::shapes::StructureEntry; +use crate::tiles; use crate::uuid::Uuid; /// A pool allocator for `Shape` objects that attempts to minimize memory reallocations. @@ -129,10 +130,10 @@ impl<'a> State<'a> { pub fn delete_shape(&mut self, id: Uuid) { // We don't really do a self.shapes.remove so that redo/undo keep working if let Some(shape) = self.shapes.get(&id) { - let (rsx, rsy, rex, rey) = self.render_state.get_tiles_for_shape(shape); + let tiles::TileRect(rsx, rsy, rex, rey) = self.render_state.get_tiles_for_shape(shape); for x in rsx..=rex { for y in rsy..=rey { - let tile = (x, y); + let tile = tiles::Tile(x, y); self.render_state.surfaces.remove_cached_tile_surface(tile); self.render_state.tiles.remove_shape_at(tile, id); } diff --git a/render-wasm/src/render/tiles.rs b/render-wasm/src/tiles.rs similarity index 56% rename from render-wasm/src/render/tiles.rs rename to render-wasm/src/tiles.rs index 3bac5bb3b..d24c1e07f 100644 --- a/render-wasm/src/render/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -4,13 +4,52 @@ use indexmap::IndexSet; use skia_safe as skia; use std::collections::{HashMap, HashSet}; -pub type Tile = (i32, i32); +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct Tile(pub i32, pub i32); + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct TileRect(pub i32, pub i32, pub i32, pub i32); + +impl TileRect { + pub fn contains(&self, tile: &Tile) -> bool { + tile.0 >= self.0 && tile.1 >= self.1 && tile.0 <= self.2 && tile.1 <= self.3 + } +} + +pub struct TileViewbox { + pub visible_rect: TileRect, + pub interest_rect: TileRect, + pub interest: i32, + pub center: Tile, +} + +impl TileViewbox { + pub fn new_with_interest(viewbox: Viewbox, interest: i32, scale: f32) -> Self { + Self { + visible_rect: get_tiles_for_viewbox(viewbox, scale), + interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest, scale), + interest, + center: get_tile_center_for_viewbox(viewbox, scale), + } + } + + pub fn update(&mut self, viewbox: Viewbox, scale: f32) { + self.visible_rect = get_tiles_for_viewbox(viewbox, scale); + self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest, scale); + self.center = get_tile_center_for_viewbox(viewbox, scale); + } + + pub fn is_visible(&self, tile: &Tile) -> bool { + self.visible_rect.contains(tile) + } +} + pub type TileWithDistance = (i32, i32, i32); pub const TILE_SIZE: f32 = 512.; // @see https://en.wikipedia.org/wiki/Taxicab_geometry -pub fn manhattan_distance(a: (i32, i32), b: (i32, i32)) -> i32 { +pub fn manhattan_distance(a: Tile, b: Tile) -> i32 { (a.0 - b.0).abs() + (a.1 - b.1).abs() } @@ -18,17 +57,17 @@ pub fn get_tile_dimensions() -> skia::ISize { (TILE_SIZE as i32, TILE_SIZE as i32).into() } -pub fn get_tiles_for_rect(rect: skia::Rect, tile_size: f32) -> (i32, i32, i32, i32) { +pub fn get_tiles_for_rect(rect: skia::Rect, tile_size: f32) -> TileRect { // start let sx = (rect.left / tile_size).floor() as i32; let sy = (rect.top / tile_size).floor() as i32; // end let ex = (rect.right / tile_size).floor() as i32; let ey = (rect.bottom / tile_size).floor() as i32; - (sx, sy, ex, ey) + TileRect(sx, sy, ex, ey) } -pub fn get_tiles_for_viewbox(viewbox: Viewbox, scale: f32) -> (i32, i32, i32, i32) { +pub fn get_tiles_for_viewbox(viewbox: Viewbox, scale: f32) -> TileRect { let tile_size = get_tile_size(scale); get_tiles_for_rect(viewbox.area, tile_size) } @@ -37,12 +76,17 @@ pub fn get_tiles_for_viewbox_with_interest( viewbox: Viewbox, interest: i32, scale: f32, -) -> (i32, i32, i32, i32) { - let (sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale); - (sx - interest, sy - interest, ex + interest, ey + interest) +) -> TileRect { + let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale); + TileRect(sx - interest, sy - interest, ex + interest, ey + interest) } -pub fn get_tile_pos((x, y): Tile, scale: f32) -> (f32, f32) { +pub fn get_tile_center_for_viewbox(viewbox: Viewbox, scale: f32) -> Tile { + let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale); + Tile((ex - sx) / 2, (ey - sy) / 2) +} + +pub fn get_tile_pos(Tile(x, y): Tile, scale: f32) -> (f32, f32) { ( x as f32 * get_tile_size(scale), y as f32 * get_tile_size(scale),