From 084816fb9f2beaad49fa96da3c885f7ac9d251a4 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Mon, 17 Feb 2025 12:54:51 +0100 Subject: [PATCH] :tada: Tile rendering system --- frontend/src/app/render_wasm/api.cljs | 3 +- render-wasm/Cargo.lock | 6 +- render-wasm/Cargo.toml | 2 + render-wasm/_build_env | 8 +- render-wasm/src/emscripten.rs | 45 +++ render-wasm/src/main.rs | 69 +--- render-wasm/src/math.rs | 8 + render-wasm/src/render.rs | 559 ++++++++++++++------------ render-wasm/src/render/cache.rs | 18 - render-wasm/src/render/debug.rs | 207 ++++++++-- render-wasm/src/render/fonts.rs | 28 +- render-wasm/src/render/strokes.rs | 11 +- render-wasm/src/render/surfaces.rs | 235 ++++++++++- render-wasm/src/render/tiles.rs | 95 +++++ render-wasm/src/shapes.rs | 35 +- render-wasm/src/state.rs | 22 +- render-wasm/src/view.rs | 13 - 17 files changed, 956 insertions(+), 408 deletions(-) create mode 100644 render-wasm/src/emscripten.rs delete mode 100644 render-wasm/src/render/cache.rs create mode 100644 render-wasm/src/render/tiles.rs diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 1656e547f..a4cba7557 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1006,7 +1006,8 @@ #js {:antialias false :depth true :stencil true - :alpha true}) + :alpha true + "preserveDrawingBuffer" true}) (defn resize-viewbox [width height] diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index 72fe0a9f2..0416a6697 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -896,9 +896,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -1507,8 +1507,10 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" name = "render" version = "0.1.0" dependencies = [ + "base64", "cargo-watch", "gl", + "indexmap", "skia-safe", "uuid", ] diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index fc5c3d61b..835423948 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -11,7 +11,9 @@ name = "render_wasm" path = "src/main.rs" [dependencies] +base64 = "0.22.1" gl = "0.14.0" +indexmap = "2.7.1" skia-safe = { version = "0.81.0", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]} uuid = { version = "1.11.0", features = ["v4"] } diff --git a/render-wasm/_build_env b/render-wasm/_build_env index eccf3a400..10f2e79af 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -7,7 +7,6 @@ else fi EMCC_CFLAGS="--no-entry \ - -Os \ -sASSERTIONS=1 \ -sALLOW_TABLE_GROWTH=1 \ -sALLOW_MEMORY_GROWTH=1 \ @@ -28,8 +27,13 @@ _CARGO_PARAMS="--target=wasm32-unknown-emscripten"; if [ "$_BUILD_MODE" = "release" ]; then _CARGO_PARAMS="--release $_CARGO_PARAMS" + EMCC_CFLAGS="-Os $EMCC_FLAGS" else - EMCC_CFLAGS="$EMCC_CFLAGS -sMALLOC=emmalloc-debug" + # TODO: Extra parameters that could be good to look into: + # -gseparate-dwarf + # -gsplit-dwarf + # -gsource-map + EMCC_CFLAGS="-g $EMCC_CFLAGS -sMALLOC=emmalloc-debug" fi export EMCC_CFLAGS; diff --git a/render-wasm/src/emscripten.rs b/render-wasm/src/emscripten.rs new file mode 100644 index 000000000..43f0d45a9 --- /dev/null +++ b/render-wasm/src/emscripten.rs @@ -0,0 +1,45 @@ +#[macro_export] +macro_rules! run_script { + ($s:expr) => {{ + extern "C" { + pub fn emscripten_run_script(script: *const i8); + } + + match std::ffi::CString::new($s) { + Ok(cstr) => unsafe { emscripten_run_script(cstr.as_ptr()) }, + Err(e) => panic!("Failed to create CString: {}", e), + } + }}; +} + +#[macro_export] +macro_rules! run_script_int { + ($s:expr) => {{ + extern "C" { + pub fn emscripten_run_script_int(script: *const i8) -> i32; + } + + match std::ffi::CString::new($s) { + Ok(cstr) => unsafe { emscripten_run_script_int(cstr.as_ptr()) }, + Err(e) => panic!("Failed to create CString: {}", e), + } + }}; +} + +#[macro_export] +macro_rules! init_gl { + () => {{ + extern "C" { + fn emscripten_GetProcAddress( + name: *const ::std::os::raw::c_char, + ) -> *const ::std::os::raw::c_void; + } + + unsafe { + gl::load_with(|addr| { + let addr = std::ffi::CString::new(addr).unwrap(); + emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _ + }); + } + }}; +} diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 9b53011fb..7f862843a 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,6 +1,7 @@ use skia_safe as skia; mod debug; +mod emscripten; mod math; mod mem; mod render; @@ -18,12 +19,6 @@ use state::State; pub(crate) static mut STATE: Option> = None; -extern "C" { - fn emscripten_GetProcAddress( - name: *const ::std::os::raw::c_char, - ) -> *const ::std::os::raw::c_void; -} - #[macro_export] macro_rules! with_state { ($state:ident, $block:block) => { @@ -50,15 +45,6 @@ macro_rules! with_current_shape { }; } -fn init_gl() { - unsafe { - gl::load_with(|addr| { - let addr = std::ffi::CString::new(addr).unwrap(); - emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _ - }); - } -} - /// This is called from JS after the WebGL context has been created. #[no_mangle] pub extern "C" fn init(width: i32, height: i32) { @@ -77,8 +63,7 @@ pub extern "C" fn clean_up() { #[no_mangle] pub extern "C" fn clear_cache() { with_state!(state, { - let render_state = state.render_state(); - render_state.clear_cache(); + state.rebuild_tiles(); }); } @@ -106,13 +91,6 @@ pub extern "C" fn render(timestamp: i32) { }); } -#[no_mangle] -pub extern "C" fn render_from_cache() { - with_state!(state, { - state.render_from_cache(); - }); -} - #[no_mangle] pub extern "C" fn process_animation_frame(timestamp: i32) { with_state!(state, { @@ -140,22 +118,13 @@ pub extern "C" fn resize_viewbox(width: i32, height: i32) { pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) { with_state!(state, { let render_state = state.render_state(); - render_state.invalidate_cache_if_needed(); + let zoom_changed = zoom != render_state.viewbox.zoom; render_state.viewbox.set_all(zoom, x, y); - }); -} - -#[no_mangle] -pub extern "C" fn set_view_zoom(zoom: f32) { - with_state!(state, { - state.render_state().viewbox.set_zoom(zoom); - }); -} - -#[no_mangle] -pub extern "C" fn set_view_xy(x: f32, y: f32) { - with_state!(state, { - state.render_state().viewbox.set_pan_xy(x, y); + if zoom_changed { + with_state!(state, { + state.rebuild_tiles(); + }); + } }); } @@ -169,11 +138,12 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { #[no_mangle] pub unsafe extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) { - let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); - let id = uuid_from_u32_quartet(a, b, c, d); - if let Some(shape) = state.current_shape() { - shape.set_parent(id); - } + with_state!(state, { + let id = uuid_from_u32_quartet(a, b, c, d); + with_current_shape!(state, |shape: &mut Shape| { + shape.set_parent(id); + }); + }); } #[no_mangle] @@ -199,8 +169,8 @@ pub unsafe extern "C" fn set_shape_type(shape_type: u8) { #[no_mangle] pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { - with_current_shape!(state, |shape: &mut Shape| { - shape.set_selrect(left, top, right, bottom); + with_state!(state, { + state.set_selrect_for_current_shape(left, top, right, bottom); }); } @@ -621,7 +591,10 @@ pub extern "C" fn set_modifiers() { for entry in entries { state.modifiers.insert(entry.id, entry.transform); } - state.render_state().clear_cache(); + // TODO: Do a more specific rebuild of tiles. For + // example: using only the selected shapes to rebuild + // the tiles affected by the selected shapes. + state.rebuild_tiles(); }); } @@ -752,5 +725,5 @@ pub extern "C" fn add_grid_track() {} pub extern "C" fn set_grid_cell() {} fn main() { - init_gl(); + init_gl!(); } diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index ada204936..3900c09fc 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -280,6 +280,14 @@ impl Bounds { let m = self.transform_matrix().unwrap_or(Matrix::default()); m.scale_y() < 0.0 } + + pub fn to_rect(&self) -> Rect { + let minx = self.nw.x.min(self.ne.x).min(self.sw.x).min(self.se.x); + let miny = self.nw.y.min(self.ne.y).min(self.sw.y).min(self.se.y); + let maxx = self.nw.x.max(self.ne.x).max(self.sw.x).max(self.se.x); + let maxy = self.nw.y.max(self.ne.y).max(self.sw.y).max(self.se.y); + Rect::from_ltrb(minx, miny, maxx, maxy) + } } #[derive(Debug, Clone, PartialEq)] diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 318df808e..b2a82844f 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,11 +1,12 @@ -use skia_safe::{self as skia, Contains, Matrix, RRect, Rect}; +use skia_safe::{self as skia, Matrix, RRect, Rect}; + use std::collections::HashMap; use uuid::Uuid; use crate::view::Viewbox; +use crate::{run_script, run_script_int}; mod blend; -mod cache; mod debug; mod fills; mod fonts; @@ -16,9 +17,9 @@ mod shadows; mod strokes; mod surfaces; mod text; +mod tiles; use crate::shapes::{Corners, Shape, Type}; -use cache::CachedSurfaceImage; use gpu_state::GpuState; use options::RenderOptions; use surfaces::{SurfaceId, Surfaces}; @@ -30,14 +31,8 @@ pub use images::*; const MAX_BLOCKING_TIME_MS: i32 = 32; const NODE_BATCH_THRESHOLD: i32 = 10; -extern "C" { - fn emscripten_run_script(script: *const i8); - fn emscripten_run_script_int(script: *const i8) -> i32; -} - fn get_time() -> i32 { - let script = std::ffi::CString::new("performance.now()").unwrap(); - unsafe { emscripten_run_script_int(script.as_ptr()) } + run_script_int!("performance.now()") } pub struct NodeRenderState { @@ -56,7 +51,7 @@ impl NodeRenderState { pub fn get_children_clip_bounds( &self, element: &Shape, - modifiers: &HashMap, + modifiers: Option<&Matrix>, ) -> Option<(Rect, Option, Matrix)> { if self.id.is_nil() || !element.clip() { return self.clip_bounds; @@ -67,7 +62,7 @@ impl NodeRenderState { transform.post_translate(bounds.center()); transform.pre_translate(-bounds.center()); - if let Some(modifier) = modifiers.get(&element.id) { + if let Some(modifier) = modifiers { transform.post_concat(modifier); } @@ -85,8 +80,7 @@ pub(crate) struct RenderState { gpu_state: GpuState, pub options: RenderOptions, pub surfaces: Surfaces, - fonts: FontStore, - pub cached_surface_image: Option, + pub fonts: FontStore, pub viewbox: Viewbox, pub images: ImageStore, pub background_color: skia::Color, @@ -96,39 +90,49 @@ pub(crate) struct RenderState { pub render_in_progress: bool, // Stack of nodes pending to be rendered. pub pending_nodes: Vec, - pub render_complete: bool, + pub current_tile: Option, pub sampling_options: skia::SamplingOptions, + pub render_area: Rect, + pub tiles: tiles::TileHashMap, + pub pending_tiles: Vec, } impl RenderState { pub fn new(width: i32, height: i32) -> RenderState { // This needs to be done once per WebGL context. let mut gpu_state = GpuState::new(); - let sampling_options = skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest); - let surfaces = Surfaces::new(&mut gpu_state, (width, height), sampling_options); let fonts = FontStore::new(); + let surfaces = Surfaces::new( + &mut gpu_state, + (width, height), + sampling_options, + tiles::get_tile_dimensions(), + ); // This is used multiple times everywhere so instead of creating new instances every // time we reuse this one. + + let tiles = tiles::TileHashMap::new(); + RenderState { gpu_state, - surfaces, - cached_surface_image: None, - // font_provider, - // font_collection, - fonts, options: RenderOptions::default(), + surfaces, + fonts, viewbox: Viewbox::new(width as f32, height as f32), images: ImageStore::new(), background_color: skia::Color::TRANSPARENT, render_request_id: None, render_in_progress: false, pending_nodes: vec![], - render_complete: true, + current_tile: None, sampling_options, + render_area: Rect::new_empty(), + tiles, + pending_tiles: vec![], } } @@ -164,7 +168,6 @@ impl RenderState { pub fn set_background_color(&mut self, color: skia::Color) { self.background_color = color; - let _ = self.render_from_cache(); } pub fn resize(&mut self, width: i32, height: i32) { @@ -182,42 +185,31 @@ impl RenderState { } pub fn reset_canvas(&mut self) { - self.surfaces.canvas(SurfaceId::Fills).restore_to_count(1); - self.surfaces - .canvas(SurfaceId::DropShadows) - .restore_to_count(1); - self.surfaces.canvas(SurfaceId::Strokes).restore_to_count(1); - self.surfaces.canvas(SurfaceId::Current).restore_to_count(1); - - self.surfaces.apply_mut( - &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::Current, - SurfaceId::DropShadows, - SurfaceId::Shadow, - SurfaceId::Overlay, - ], - |s| { - s.canvas().clear(self.background_color).reset_matrix(); - }, - ); - - self.surfaces - .canvas(SurfaceId::Debug) - .clear(skia::Color::TRANSPARENT) - .reset_matrix(); + self.surfaces.reset(self.background_color); } - pub fn apply_render_to_final_canvas(&mut self) { - self.surfaces.draw_into( - SurfaceId::Current, - SurfaceId::Target, - Some(&skia::Paint::default()), - ); + 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; + + // This caches the current surface into the corresponding tile. + self.surfaces + .cache_tile_surface((x, y), SurfaceId::Current, self.background_color); + + self.surfaces + .draw_cached_tile_surface(self.current_tile.unwrap(), rect); + + if self.options.is_debug_visible() { + debug::render_workspace_current_tile( + self, + "".to_string(), + self.current_tile.unwrap(), + rect, + ); + } } - pub fn apply_drawing_to_render_canvas(&mut self, shape: &Shape) { + pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) { self.surfaces .flush_and_submit(&mut self.gpu_state, SurfaceId::Fills); @@ -236,7 +228,11 @@ impl RenderState { Some(&skia::Paint::default()), ); - let render_overlay_below_strokes = shape.fills().len() > 0; + let mut render_overlay_below_strokes = false; + if let Some(shape) = shape { + render_overlay_below_strokes = shape.fills().len() > 0; + } + if render_overlay_below_strokes { self.surfaces .flush_and_submit(&mut self.gpu_state, SurfaceId::Overlay); @@ -284,12 +280,6 @@ impl RenderState { ); } - pub fn invalidate_cache_if_needed(&mut self) { - if let Some(ref mut cached_surface_image) = self.cached_surface_image { - cached_surface_image.invalidate_if_dirty(&self.viewbox); - } - } - pub fn render_shape( &mut self, shape: &mut Shape, @@ -303,12 +293,9 @@ impl RenderState { // set clipping if let Some((bounds, corners, transform)) = clip_bounds { - self.surfaces.apply_mut( - &[SurfaceId::Fills, SurfaceId::Strokes, SurfaceId::DropShadows], - |s| { - s.canvas().concat(&transform); - }, - ); + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().concat(&transform); + }); if let Some(corners) = corners { let rrect = RRect::new_rect_radii(bounds, &corners); @@ -321,6 +308,8 @@ impl RenderState { }); } + // This renders a red line around clipped + // shapes (frames). if self.options.is_debug_visible() { let mut paint = skia::Paint::default(); paint.set_style(skia::PaintStyle::Stroke); @@ -403,7 +392,7 @@ impl RenderState { } }; - self.apply_drawing_to_render_canvas(&shape); + self.apply_drawing_to_render_canvas(Some(&shape)); self.surfaces.apply_mut( &[SurfaceId::Fills, SurfaceId::Strokes, SurfaceId::DropShadows], |s| { @@ -412,6 +401,13 @@ impl RenderState { ); } + pub fn update_render_context(&mut self, tile: tiles::Tile) { + self.current_tile = Some(tile); + self.render_area = tiles::get_tile_rect(self.viewbox, tile); + self.surfaces + .update_render_context(self.render_area, self.viewbox); + } + pub fn start_render_loop( &mut self, tree: &mut HashMap, @@ -431,36 +427,40 @@ impl RenderState { self.viewbox.zoom * self.options.dpr(), self.viewbox.zoom * self.options.dpr(), )); - s.canvas() - .translate((self.viewbox.pan_x, self.viewbox.pan_y)); }, ); - self.pending_nodes = vec![NodeRenderState { - id: Uuid::nil(), - visited_children: false, - clip_bounds: None, - visited_mask: false, - mask: false, - }]; + let (sx, sy, ex, ey) = tiles::get_tiles_for_viewbox(self.viewbox); + debug::render_debug_tiles_for_viewbox(self, sx, sy, ex, ey); + /* + // TODO: Instead of rendering only the visible area + // we could apply an offset to the viewbox to render + // more tiles. + sx - interest_delta + sy - interest_delta + ex + interest_delta + ey + interest_delta + */ + self.pending_tiles = vec![]; + for y in sy..=ey { + for x in sx..=ex { + let tile = (x, y); + self.pending_tiles.push(tile); + } + } + self.current_tile = None; self.render_in_progress = true; + self.apply_drawing_to_render_canvas(None); self.process_animation_frame(tree, modifiers, timestamp)?; - self.render_complete = true; Ok(()) } pub fn request_animation_frame(&mut self) -> i32 { - let script = - std::ffi::CString::new("requestAnimationFrame(_process_animation_frame)").unwrap(); - unsafe { emscripten_run_script_int(script.as_ptr()) } + run_script_int!("requestAnimationFrame(_process_animation_frame)") } pub fn cancel_animation_frame(&mut self, frame_id: i32) { - let cancel_script = format!("cancelAnimationFrame({})", frame_id); - let c_cancel_script = std::ffi::CString::new(cancel_script).unwrap(); - unsafe { - emscripten_run_script(c_cancel_script.as_ptr()); - } + run_script!(format!("cancelAnimationFrame({})", frame_id)) } pub fn process_animation_frame( @@ -471,6 +471,8 @@ impl RenderState { ) -> Result<(), String> { if self.render_in_progress { self.render_shape_tree(tree, modifiers, timestamp)?; + self.flush(); + if self.render_in_progress { if let Some(frame_id) = self.render_request_id { self.cancel_animation_frame(frame_id); @@ -478,88 +480,9 @@ impl RenderState { self.render_request_id = Some(self.request_animation_frame()); } } - - // self.render_in_progress can have changed - if self.render_in_progress { - if self.cached_surface_image.is_some() { - self.render_from_cache()?; - } - return Ok(()); - } - - // Chech if cached_surface_image is not set or is invalid - if self - .cached_surface_image - .as_ref() - .is_none_or(|img| img.invalid) - { - self.cached_surface_image = Some(CachedSurfaceImage { - image: self.surfaces.snapshot(SurfaceId::Current), - viewbox: self.viewbox, - invalid: false, - has_all_shapes: self.render_complete, - }); - } - - if self.options.is_debug_visible() { - self.render_debug(); - } - - debug::render_wasm_label(self); - self.apply_render_to_final_canvas(); - self.flush(); Ok(()) } - pub fn clear_cache(&mut self) { - self.cached_surface_image = None; - } - - pub fn render_from_cache(&mut self) -> Result<(), String> { - let cached = self - .cached_surface_image - .as_ref() - .ok_or("Uninitialized cached surface image")?; - - let image = &cached.image; - let paint = skia::Paint::default(); - self.surfaces.canvas(SurfaceId::Target).save(); - self.surfaces.canvas(SurfaceId::Fills).save(); - self.surfaces.canvas(SurfaceId::Strokes).save(); - self.surfaces.canvas(SurfaceId::DropShadows).save(); - - let navigate_zoom = self.viewbox.zoom / cached.viewbox.zoom; - let navigate_x = cached.viewbox.zoom * (self.viewbox.pan_x - cached.viewbox.pan_x); - let navigate_y = cached.viewbox.zoom * (self.viewbox.pan_y - cached.viewbox.pan_y); - - self.surfaces - .canvas(SurfaceId::Target) - .scale((navigate_zoom, navigate_zoom)); - self.surfaces.canvas(SurfaceId::Target).translate(( - navigate_x * self.options.dpr(), - navigate_y * self.options.dpr(), - )); - self.surfaces - .canvas(SurfaceId::Target) - .clear(self.background_color); - self.surfaces - .canvas(SurfaceId::Target) - .draw_image(image, (0, 0), Some(&paint)); - - self.surfaces.canvas(SurfaceId::Target).restore(); - self.surfaces.canvas(SurfaceId::Fills).restore(); - self.surfaces.canvas(SurfaceId::Strokes).restore(); - self.surfaces.canvas(SurfaceId::DropShadows).restore(); - - self.flush(); - - Ok(()) - } - - fn render_debug(&mut self) { - debug::render(self); - } - pub fn render_shape_enter(&mut self, element: &mut Shape, mask: bool) { // Masked groups needs two rendering passes, the first one rendering // the content and the second one rendering the mask so we need to do @@ -621,6 +544,19 @@ impl RenderState { self.surfaces.canvas(SurfaceId::Current).restore(); } + pub fn get_current_tile_bounds(&mut self) -> Rect { + let (tile_x, tile_y) = self.current_tile.unwrap(); + let zoom = self.viewbox.zoom * self.options.dpr(); + let offset_x = self.viewbox.area.left * zoom; + let offset_y = self.viewbox.area.top * zoom; + Rect::from_xywh( + (tile_x as f32 * tiles::TILE_SIZE) - offset_x, + (tile_y as f32 * tiles::TILE_SIZE) - offset_y, + tiles::TILE_SIZE, + tiles::TILE_SIZE, + ) + } + pub fn render_shape_tree( &mut self, tree: &mut HashMap, @@ -631,108 +567,219 @@ impl RenderState { return Ok(()); } - let mut i = 0; - while let Some(node_render_state) = self.pending_nodes.pop() { - let NodeRenderState { - id: node_id, - visited_children, - clip_bounds, - visited_mask, - mask, - } = node_render_state; - let element = tree.get_mut(&node_id).ok_or( - "Error: Element with root_id {node_render_state.id} not found in the tree." - .to_string(), - )?; + let mut should_stop = false; + while !should_stop { + if let Some(current_tile) = self.current_tile { + if self.surfaces.has_cached_tile_surface(current_tile) { + let tile_rect = self.get_current_tile_bounds(); + self.surfaces + .draw_cached_tile_surface(current_tile, tile_rect); - let render_complete = self.viewbox.area.contains(element.selrect()); - if visited_children { - if !visited_mask { - match element.shape_type { - Type::Group(group) => { - // When we're dealing with masked groups we need to - // do a separate extra step to draw the mask (the last - // element of a masked group) and blend (using - // the blend mode 'destination-in') the content - // of the group and the mask. - if group.masked { - self.pending_nodes.push(NodeRenderState { - id: node_id, - visited_children: true, - clip_bounds: None, - visited_mask: true, - mask: false, - }); - if let Some(&mask_id) = element.mask_id() { - self.pending_nodes.push(NodeRenderState { - id: mask_id, - visited_children: false, - clip_bounds: None, - visited_mask: false, - mask: true, - }); + if self.options.is_debug_visible() { + debug::render_workspace_current_tile( + self, + "Cached".to_string(), + current_tile, + tile_rect, + ); + } + } else { + let mut i = 0; + let mut is_empty = true; + while let Some(node_render_state) = self.pending_nodes.pop() { + let NodeRenderState { + id: node_id, + visited_children, + clip_bounds, + visited_mask, + mask, + } = node_render_state; + is_empty = false; + let element = tree.get_mut(&node_id).ok_or( + "Error: Element with root_id {node_render_state.id} not found in the tree." + .to_string(), + )?; + + if visited_children { + if !visited_mask { + match element.shape_type { + Type::Group(group) => { + // When we're dealing with masked groups we need to + // do a separate extra step to draw the mask (the last + // element of a masked group) and blend (using + // the blend mode 'destination-in') the content + // of the group and the mask. + if group.masked { + self.pending_nodes.push(NodeRenderState { + id: node_id, + visited_children: true, + clip_bounds: None, + visited_mask: true, + mask: false, + }); + if let Some(&mask_id) = element.mask_id() { + self.pending_nodes.push(NodeRenderState { + id: mask_id, + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: true, + }); + } + } + } + _ => {} } } + self.render_shape_exit(element, visited_mask); + continue; } - _ => {} + + if !node_render_state.id.is_nil() { + // If we didn't visited_children this shape, then we need to do + let mut transformed_element = element.clone(); + if let Some(modifier) = modifiers.get(&node_id) { + transformed_element.apply_transform(modifier); + } + if !transformed_element.extrect().intersects(self.render_area) + || transformed_element.hidden() + { + debug::render_debug_shape(self, &transformed_element, false); + continue; + } else { + debug::render_debug_shape(self, &transformed_element, true); + } + } + + self.render_shape_enter(element, mask); + if !node_render_state.id.is_nil() { + self.render_shape(element, modifiers.get(&element.id), clip_bounds); + } else { + self.apply_drawing_to_render_canvas(Some(&element)); + } + + // Set the node as visited_children before processing children + self.pending_nodes.push(NodeRenderState { + id: node_id, + visited_children: true, + clip_bounds: None, + visited_mask: false, + mask: mask, + }); + + if element.is_recursive() { + // Fix this + let children_clip_bounds = node_render_state + .get_children_clip_bounds(element, modifiers.get(&element.id)); + for child_id in element.children_ids().iter().rev() { + self.pending_nodes.push(NodeRenderState { + id: *child_id, + visited_children: false, + clip_bounds: children_clip_bounds, + visited_mask: false, + mask: false, + }); + } + } + + // We try to avoid doing too many calls to get_time + if i % NODE_BATCH_THRESHOLD == 0 + && get_time() - timestamp > MAX_BLOCKING_TIME_MS + { + return Ok(()); + } + i += 1; + } + let tile_rect = self.get_current_tile_bounds(); + if !is_empty { + self.apply_render_to_final_canvas(tile_rect); + } else { + self.surfaces.apply_mut(&[SurfaceId::Target], |s| { + let mut paint = skia::Paint::default(); + paint.set_color(self.background_color); + s.canvas().draw_rect(tile_rect, &paint); + }); } } - self.render_shape_exit(element, visited_mask); - continue; } - // If we didn't visited_children this shape, then we need to do - if !node_render_state.id.is_nil() { - if !element.selrect().intersects(self.viewbox.area) || element.hidden() { - debug::render_debug_shape(self, element, false); - self.render_complete = render_complete; - continue; - } else { - debug::render_debug_shape(self, element, true); + self.surfaces + .canvas(SurfaceId::Current) + .clear(self.background_color); + + // If we finish processing every node rendering is complete + // let's check if there are more pending nodes + if let Some(next_tile) = self.pending_tiles.pop() { + self.update_render_context(next_tile); + if !self.surfaces.has_cached_tile_surface(next_tile) { + // If the tile is empty or it doesn't exists we don't do anything with it + if self.tiles.has_shapes_at(next_tile) { + // TODO: This should be more efficient, we should be able to know exactly what shapes tree + // are included for this tile + self.pending_nodes.push(NodeRenderState { + id: Uuid::nil(), + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: false, + }); + } } - } - - self.render_shape_enter(element, mask); - if !node_render_state.id.is_nil() { - self.render_shape(element, modifiers.get(&element.id), clip_bounds); } else { - self.apply_drawing_to_render_canvas(&element); + should_stop = true; } - - // Set the node as visited_children before processing children - self.pending_nodes.push(NodeRenderState { - id: node_id, - visited_children: true, - clip_bounds: None, - visited_mask: false, - mask: mask, - }); - - if element.is_recursive() { - let children_clip_bounds = - node_render_state.get_children_clip_bounds(element, &modifiers); - - for child_id in element.children_ids().iter().rev() { - self.pending_nodes.push(NodeRenderState { - id: *child_id, - visited_children: false, - clip_bounds: children_clip_bounds, - visited_mask: false, - mask: false, - }); - } - } - - // We try to avoid doing too many calls to get_time - if i % NODE_BATCH_THRESHOLD == 0 && get_time() - timestamp > MAX_BLOCKING_TIME_MS { - return Ok(()); - } - - i += 1; + } + self.render_in_progress = false; + if self.options.is_debug_visible() { + debug::render(self); } - // If we finish processing every node rendering is complete - self.render_in_progress = false; + debug::render_wasm_label(self); + self.flush(); + Ok(()) } + + pub fn update_tile_for(&mut self, shape: &Shape) { + let tile_size = tiles::get_tile_size(self.viewbox); + let (rsx, rsy, rex, rey) = tiles::get_tiles_for_rect(shape.extrect(), tile_size); + + // Update tiles where the shape was + if let Some(tiles) = self.tiles.get_tiles_of(shape.id) { + for tile in tiles.iter() { + self.surfaces.remove_cached_tile_surface(*tile); + } + } + + // Update tiles matching the actual selrect + for x in rsx..=rex { + for y in rsy..=rey { + let tile = (x, y); + self.tiles.add_shape_at(tile, shape.id); + self.surfaces.remove_cached_tile_surface(tile); + } + } + } + + pub fn rebuild_tiles( + &mut self, + tree: &mut HashMap, + modifiers: &HashMap, + ) { + self.tiles.invalidate(); + self.surfaces.remove_cached_tiles(); + let mut nodes = vec![Uuid::nil()]; + while let Some(shape_id) = nodes.pop() { + if let Some(shape) = tree.get(&shape_id) { + let mut shape = shape.clone(); + if let Some(modifier) = modifiers.get(&shape_id) { + shape.apply_transform(modifier); + } + self.update_tile_for(&shape); + for child_id in shape.children_ids().iter() { + nodes.push(*child_id); + } + } + } + } } diff --git a/render-wasm/src/render/cache.rs b/render-wasm/src/render/cache.rs deleted file mode 100644 index 04c0ec643..000000000 --- a/render-wasm/src/render/cache.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::{Image, Viewbox}; -use skia::Contains; -use skia_safe as skia; - -pub(crate) struct CachedSurfaceImage { - pub image: Image, - pub viewbox: Viewbox, - pub invalid: bool, - pub has_all_shapes: bool, -} - -impl CachedSurfaceImage { - pub fn invalidate_if_dirty(&mut self, viewbox: &Viewbox) { - if !self.has_all_shapes && !self.viewbox.area.contains(viewbox.area) { - self.invalid = true; - } - } -} diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 176f1aec6..0207b3383 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -1,71 +1,216 @@ use crate::shapes::Shape; -use skia_safe as skia; +use skia_safe::{self as skia, Rect}; -use super::{RenderState, SurfaceId}; +use super::{tiles, RenderState, SurfaceId}; + +use crate::run_script; + +const DEBUG_SCALE: f32 = 0.2; + +fn get_debug_rect(rect: Rect) -> Rect { + skia::Rect::from_xywh( + 100. + rect.x() * DEBUG_SCALE, + 100. + rect.y() * DEBUG_SCALE, + rect.width() * DEBUG_SCALE, + rect.height() * DEBUG_SCALE, + ) +} fn render_debug_view(render_state: &mut RenderState) { let mut paint = skia::Paint::default(); paint.set_style(skia::PaintStyle::Stroke); - paint.set_color(skia::Color::from_argb(255, 255, 0, 255)); + paint.set_color(skia::Color::from_rgb(255, 0, 255)); paint.set_stroke_width(1.); - let mut scaled_rect = render_state.viewbox.area.clone(); - let x = 100. + scaled_rect.x() * 0.2; - let y = 100. + scaled_rect.y() * 0.2; - let width = scaled_rect.width() * 0.2; - let height = scaled_rect.height() * 0.2; - scaled_rect.set_xywh(x, y, width, height); - + let rect = get_debug_rect(render_state.viewbox.area.clone()); render_state .surfaces .canvas(SurfaceId::Debug) - .draw_rect(scaled_rect, &paint); + .draw_rect(rect, &paint); } pub fn render_wasm_label(render_state: &mut RenderState) { - let font_provider = render_state.fonts().font_provider(); - let typeface = font_provider - .match_family_style("robotomono-regular", skia::FontStyle::default()) - .unwrap(); - - let canvas = render_state.surfaces.canvas(SurfaceId::Current); + let canvas = render_state.surfaces.canvas(SurfaceId::Debug); let skia::ISize { width, height } = canvas.base_layer_size(); - let p = skia::Point::new(width as f32 - 100.0, height as f32 - 25.0); let mut paint = skia::Paint::default(); paint.set_color(skia::Color::from_argb(100, 0, 0, 0)); - let font = skia::Font::new(typeface, 10.0); - canvas.draw_str("WASM RENDERER", p, &font, &paint); + let str = if render_state.options.is_debug_visible() { + "WASM RENDERER (DEBUG)" + } else { + "WASM RENDERER" + }; + let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None); + let p = skia::Point::new(width as f32 - 25.0 - scalar, height as f32 - 25.0); + + let debug_font = render_state.fonts.debug_font(); + canvas.draw_str(str, p, &debug_font, &paint); } pub fn render_debug_shape(render_state: &mut RenderState, element: &Shape, intersected: bool) { let mut paint = skia::Paint::default(); paint.set_style(skia::PaintStyle::Stroke); paint.set_color(if intersected { - skia::Color::from_argb(255, 255, 255, 0) + skia::Color::from_rgb(255, 255, 0) } else { - skia::Color::from_argb(255, 0, 255, 255) + skia::Color::from_rgb(0, 255, 255) }); paint.set_stroke_width(1.); - let mut scaled_rect = element.selrect(); - let x = 100. + scaled_rect.x() * 0.2; - let y = 100. + scaled_rect.y() * 0.2; - let width = scaled_rect.width() * 0.2; - let height = scaled_rect.height() * 0.2; - scaled_rect.set_xywh(x, y, width, height); - + let rect = get_debug_rect(element.extrect()); render_state .surfaces .canvas(SurfaceId::Debug) - .draw_rect(scaled_rect, &paint); + .draw_rect(rect, &paint); +} + +pub fn render_debug_tiles_for_viewbox( + render_state: &mut RenderState, + sx: i32, + sy: i32, + ex: i32, + ey: i32, +) { + let canvas = render_state.surfaces.canvas(SurfaceId::Debug); + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_rgb(255, 0, 127)); + paint.set_stroke_width(1.); + let str_rect = format!("{} {} {} {}", sx, sy, ex, ey); + + let debug_font = render_state.fonts.debug_font(); + canvas.draw_str( + str_rect, + skia::Point::new(100.0, 150.0), + &debug_font, + &paint, + ); +} + +// Renders the tiles in the viewbox +pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) { + let canvas = render_state.surfaces.canvas(SurfaceId::Debug); + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_rgb(255, 0, 127)); + paint.set_stroke_width(1.); + + let tile_size = tiles::get_tile_size(render_state.viewbox); + let (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(); + canvas.draw_str( + str_rect, + skia::Point::new(100.0, 100.0), + &debug_font, + &paint, + ); + + for y in sy..=ey { + for x in sx..=ex { + let rect = Rect::from_xywh( + x as f32 * tile_size, + y as f32 * tile_size, + tile_size, + tile_size, + ); + let debug_rect = get_debug_rect(rect); + let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.); + let str = format!("{}:{}", x, y); + let debug_font = render_state.fonts.debug_font(); + canvas.draw_str(str, p, &debug_font, &paint); + canvas.draw_rect(&debug_rect, &paint); + } + } +} + +pub fn render_debug_tiles(render_state: &mut RenderState) { + let canvas = render_state.surfaces.canvas(SurfaceId::Debug); + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_rgb(127, 0, 255)); + paint.set_stroke_width(1.); + + let tile_size = tiles::get_tile_size(render_state.viewbox); + let (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 shape_count = render_state.tiles.get_shapes_at(tile).iter().len(); + if shape_count == 0 { + continue; + } + + let rect = Rect::from_xywh( + x as f32 * tile_size, + y as f32 * tile_size, + tile_size, + tile_size, + ); + let debug_rect = get_debug_rect(rect); + let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.); + let str = format!("{}:{} {}", x, y, shape_count); + + let debug_font = render_state.fonts.debug_font(); + canvas.draw_str(str, p, &debug_font, &paint); + canvas.draw_rect(&debug_rect, &paint); + } + } } pub fn render(render_state: &mut RenderState) { render_debug_view(render_state); + render_debug_viewbox_tiles(render_state); + render_debug_tiles(render_state); render_state.surfaces.draw_into( SurfaceId::Debug, - SurfaceId::Current, + SurfaceId::Target, Some(&skia::Paint::default()), ); } + +#[allow(dead_code)] +pub fn console_debug_tile_surface(render_state: &mut RenderState, tile: tiles::Tile) { + let base64_image = render_state.surfaces.base64_snapshot_tile(tile); + run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')")) +} + +#[allow(dead_code)] +pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) { + let base64_image = render_state.surfaces.base64_snapshot(id); + run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')")) +} + +#[allow(dead_code)] +pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect: skia::Rect) { + let int_rect = skia::IRect::from_ltrb( + rect.left as i32, + rect.top as i32, + rect.right as i32, + rect.bottom as i32, + ); + let base64_image = render_state.surfaces.base64_snapshot_rect(id, int_rect); + if let Some(base64_image) = base64_image { + run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')")) + } +} + +pub fn render_workspace_current_tile( + render_state: &mut RenderState, + prefix: String, + tile: tiles::Tile, + rect: skia::Rect, +) { + let canvas = render_state.surfaces.canvas(SurfaceId::Target); + let mut p = skia::Paint::default(); + p.set_stroke_width(1.); + p.set_style(skia::PaintStyle::Stroke); + canvas.draw_rect(&rect, &p); + + let point = skia::Point::new(rect.x() + 10., rect.y() + 20.); + p.set_stroke_width(1.); + let str = format!("{prefix} {}:{}", tile.0, tile.1); + let debug_font = render_state.fonts.debug_font(); + canvas.draw_str(str, point, &debug_font, &p); +} diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 0dd881181..50d919216 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -1,4 +1,4 @@ -use skia_safe::{self as skia, textlayout, FontMgr}; +use skia_safe::{self as skia, textlayout, Font}; use crate::shapes::FontFamily; @@ -11,6 +11,7 @@ pub struct FontStore { // TODO: we should probably have just one of those font_provider: textlayout::TypefaceFontProvider, font_collection: textlayout::FontCollection, + debug_font: Font, } impl FontStore { @@ -30,12 +31,19 @@ impl FontStore { font_provider.register_typeface(emoji_font, DEFAULT_EMOJI_FONT); let mut font_collection = skia::textlayout::FontCollection::new(); - font_collection.set_default_font_manager(FontMgr::default(), None); - font_collection.set_dynamic_font_manager(FontMgr::from(font_provider.clone())); + font_collection.set_default_font_manager(Some(font_provider.clone().into()), None); + font_collection.set_dynamic_font_manager(Some(font_provider.clone().into())); + + let debug_typeface = font_provider + .match_family_style("robotomono-regular", skia::FontStyle::default()) + .unwrap(); + + let debug_font = skia::Font::new(debug_typeface, 10.0); Self { font_provider, font_collection, + debug_font, } } @@ -47,6 +55,10 @@ impl FontStore { &self.font_collection } + pub fn debug_font(&self) -> &Font { + &self.debug_font + } + pub fn add(&mut self, family: FontFamily, font_data: &[u8]) -> Result<(), String> { if self.has_family(&family) { return Ok(()); @@ -60,8 +72,6 @@ impl FontStore { self.font_provider .register_typeface(typeface, alias.as_str()); - self.refresh_font_collection(); - Ok(()) } @@ -69,12 +79,4 @@ impl FontStore { let serialized = format!("{}", family); self.font_provider.family_names().any(|x| x == serialized) } - - fn refresh_font_collection(&mut self) { - self.font_collection = skia::textlayout::FontCollection::new(); - self.font_collection - .set_default_font_manager(FontMgr::default(), None); - self.font_collection - .set_dynamic_font_manager(FontMgr::from(self.font_provider.clone())); - } } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 48665ccdd..b7c9ca064 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -69,8 +69,10 @@ fn draw_stroke_on_path( match stroke.render_kind(is_open) { // For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed) StrokeKind::InnerStroke => { + canvas.save(); // As we are using clear for surfaces we use save and restore here to still be able to clean the full surface canvas.clip_path(&skia_path, skia::ClipOp::Intersect, true); canvas.draw_path(&skia_path, &paint_stroke); + canvas.restore(); } // For center stroke we don't need to do anything extra StrokeKind::CenterStroke => { @@ -381,9 +383,9 @@ fn draw_image_stroke_in_container( let is_open = p.is_open(); let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, dpr_scale); canvas.draw_path(&path, &paint); - canvas.restore(); if stroke.render_kind(is_open) == StrokeKind::OuterStroke { - // Small extra inner stroke to overlap with the fill and avoid unnecesary artifacts + // Small extra inner stroke to overlap with the fill + // and avoid unnecesary artifacts. paint.set_stroke_width(1. / dpr_scale); canvas.draw_path(&path, &paint); } @@ -396,13 +398,16 @@ fn draw_image_stroke_in_container( svg_attrs, dpr_scale, ); + canvas.restore(); } } _ => unreachable!("This shape should not have strokes"), } - // Draw the image. We are using now the SrcIn blend mode, so the rendered piece of image will the area of the stroke over the image. + // Draw the image. We are using now the SrcIn blend mode, + // so the rendered piece of image will the area of the + // stroke over the image. let mut image_paint = skia::Paint::default(); image_paint.set_blend_mode(skia::BlendMode::SrcIn); image_paint.set_anti_alias(true); diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 8ff0db278..353cbc3e8 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -1,7 +1,12 @@ -use super::gpu_state::GpuState; use crate::shapes::Shape; +use crate::view::Viewbox; use skia_safe::{self as skia, Paint, RRect}; +use super::{gpu_state::GpuState, tiles::Tile}; + +use base64::{engine::general_purpose, Engine as _}; +use std::collections::HashMap; + #[derive(Debug, PartialEq, Clone, Copy)] pub enum SurfaceId { Target, @@ -31,8 +36,10 @@ pub struct Surfaces { overlay: skia::Surface, // for drawing debug info. debug: skia::Surface, - + // for drawing tiles. + tiles: TileSurfaceCache, sampling_options: skia::SamplingOptions, + margins: skia::ISize, } impl Surfaces { @@ -40,16 +47,32 @@ impl Surfaces { gpu_state: &mut GpuState, (width, height): (i32, i32), sampling_options: skia::SamplingOptions, + tile_dims: skia::ISize, ) -> Self { + // This is the amount of extra space we're going + // to give to all the surfaces to render shapes. + // If it's too big it could affect performance. + let extra_tile_size = 2; + let extra_tile_dims = skia::ISize::new( + tile_dims.width * extra_tile_size, + tile_dims.height * extra_tile_size, + ); + let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4); + let mut target = gpu_state.create_target_surface(width, height); - let current = target.new_surface_with_dimensions((width, height)).unwrap(); - let shadow = target.new_surface_with_dimensions((width, height)).unwrap(); - let drop_shadows = target.new_surface_with_dimensions((width, height)).unwrap(); - let overlay = target.new_surface_with_dimensions((width, height)).unwrap(); - let shape_fills = target.new_surface_with_dimensions((width, height)).unwrap(); - let shape_strokes = target.new_surface_with_dimensions((width, height)).unwrap(); + let current = target.new_surface_with_dimensions(extra_tile_dims).unwrap(); + let shadow = target.new_surface_with_dimensions(extra_tile_dims).unwrap(); + let drop_shadows = target.new_surface_with_dimensions(extra_tile_dims).unwrap(); + let overlay = target.new_surface_with_dimensions(extra_tile_dims).unwrap(); + let shape_fills = target.new_surface_with_dimensions(extra_tile_dims).unwrap(); + let shape_strokes = target.new_surface_with_dimensions(extra_tile_dims).unwrap(); let debug = target.new_surface_with_dimensions((width, height)).unwrap(); + const POOL_CAPACITY_THRESHOLD: i32 = 4; + let pool_capacity = + (width / tile_dims.width) * (height / tile_dims.height) * POOL_CAPACITY_THRESHOLD; + let pool = SurfacePool::with_capacity(&mut target, tile_dims, pool_capacity as usize); + let tiles = TileSurfaceCache::new(pool); Surfaces { target, current, @@ -60,6 +83,8 @@ impl Surfaces { shape_strokes, debug, sampling_options, + tiles, + margins, } } @@ -67,8 +92,36 @@ impl Surfaces { self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)); } - pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image { - self.get_mut(id).image_snapshot() + pub fn base64_snapshot_tile(&mut self, tile: Tile) -> String { + let surface = self.tiles.get(tile).unwrap(); + let image = surface.image_snapshot(); + let mut context = surface.direct_context(); + let encoded_image = image + .encode(context.as_mut(), skia::EncodedImageFormat::PNG, None) + .unwrap(); + general_purpose::STANDARD.encode(&encoded_image.as_bytes()) + } + + pub fn base64_snapshot(&mut self, id: SurfaceId) -> String { + let surface = self.get_mut(id); + let image = surface.image_snapshot(); + let mut context = surface.direct_context(); + let encoded_image = image + .encode(context.as_mut(), skia::EncodedImageFormat::PNG, None) + .unwrap(); + general_purpose::STANDARD.encode(&encoded_image.as_bytes()) + } + + pub fn base64_snapshot_rect(&mut self, id: SurfaceId, irect: skia::IRect) -> Option { + let surface = self.get_mut(id); + if let Some(image) = surface.image_snapshot_with_bounds(irect) { + let mut context = surface.direct_context(); + let encoded_image = image + .encode(context.as_mut(), skia::EncodedImageFormat::PNG, None) + .unwrap(); + return Some(general_purpose::STANDARD.encode(&encoded_image.as_bytes())); + } + None } pub fn canvas(&mut self, id: SurfaceId) -> &skia::Canvas { @@ -95,6 +148,21 @@ impl Surfaces { } } + pub fn update_render_context(&mut self, render_area: skia::Rect, viewbox: Viewbox) { + let translation = ( + -render_area.left() + self.margins.width as f32 / viewbox.zoom, + -render_area.top() + self.margins.height as f32 / viewbox.zoom, + ); + self.apply_mut( + &[SurfaceId::Fills, SurfaceId::Strokes, SurfaceId::DropShadows], + |s| { + s.canvas().restore(); + s.canvas().save(); + s.canvas().translate(translation); + }, + ); + } + fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface { match id { SurfaceId::Target => &mut self.target, @@ -111,12 +179,8 @@ impl Surfaces { fn reset_from_target(&mut self, target: skia::Surface) { let dim = (target.width(), target.height()); self.target = target; - self.current = self.target.new_surface_with_dimensions(dim).unwrap(); - self.overlay = self.target.new_surface_with_dimensions(dim).unwrap(); - self.shadow = self.target.new_surface_with_dimensions(dim).unwrap(); - self.drop_shadows = self.target.new_surface_with_dimensions(dim).unwrap(); - self.shape_fills = self.target.new_surface_with_dimensions(dim).unwrap(); self.debug = self.target.new_surface_with_dimensions(dim).unwrap(); + // The rest are tile size surfaces } pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { @@ -137,4 +201,145 @@ impl Surfaces { self.canvas(id).draw_path(&path, paint); } } + + pub fn reset(&mut self, color: skia::Color) { + self.canvas(SurfaceId::Fills).restore_to_count(1); + self.canvas(SurfaceId::DropShadows).restore_to_count(1); + self.canvas(SurfaceId::Strokes).restore_to_count(1); + self.canvas(SurfaceId::Current).restore_to_count(1); + self.apply_mut( + &[ + SurfaceId::Fills, + SurfaceId::Strokes, + SurfaceId::Current, + SurfaceId::DropShadows, + SurfaceId::Shadow, + SurfaceId::Overlay, + ], + |s| { + s.canvas().clear(color).reset_matrix(); + }, + ); + + self.canvas(SurfaceId::Debug) + .clear(skia::Color::TRANSPARENT) + .reset_matrix(); + } + + pub fn cache_tile_surface(&mut self, tile: Tile, id: SurfaceId, color: skia::Color) { + let sampling_options = self.sampling_options; + let mut tile_surface = self.tiles.get_or_create(tile).unwrap(); + let margins = self.margins; + let surface = self.get_mut(id); + tile_surface.canvas().clear(color); + surface.draw( + tile_surface.canvas(), + (-margins.width, -margins.height), + sampling_options, + Some(&skia::Paint::default()), + ); + } + + pub fn has_cached_tile_surface(&mut self, tile: Tile) -> bool { + self.tiles.has(tile) + } + + pub fn remove_cached_tile_surface(&mut self, tile: Tile) -> bool { + self.tiles.remove(tile) + } + + pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect) { + let sampling_options = self.sampling_options; + let tile_surface = self.tiles.get(tile).unwrap(); + tile_surface.draw( + self.target.canvas(), + (rect.x(), rect.y()), + sampling_options, + Some(&skia::Paint::default()), + ); + } + + pub fn remove_cached_tiles(&mut self) { + self.tiles.clear(); + } +} + +pub struct SurfaceRef { + pub surface: skia::Surface, +} + +pub struct SurfacePool { + pub surfaces: Vec, + pub index: usize, +} + +impl SurfacePool { + pub fn with_capacity(surface: &mut skia::Surface, dims: skia::ISize, capacity: usize) -> Self { + let mut surfaces = Vec::with_capacity(capacity); + for _ in 0..capacity { + surfaces.push(surface.new_surface_with_dimensions(dims).unwrap()) + } + + SurfacePool { + index: 0, + surfaces: surfaces + .into_iter() + .map(|surface| SurfaceRef { surface: surface }) + .collect(), + } + } + + pub fn allocate(&mut self) -> Result { + let start = self.index; + let len = self.surfaces.len(); + loop { + self.index = (self.index + 1) % len; + if self.index == start { + return Err("Not enough surfaces in the pool".into()); + } + if let Some(surface_ref) = self.surfaces.get(self.index) { + return Ok(surface_ref.surface.clone()); + } + } + } +} + +pub struct TileSurfaceCache { + pool: SurfacePool, + grid: HashMap, +} + +impl TileSurfaceCache { + pub fn new(pool: SurfacePool) -> Self { + TileSurfaceCache { + pool, + grid: HashMap::new(), + } + } + + pub fn has(&mut self, tile: Tile) -> bool { + return self.grid.contains_key(&tile); + } + + pub fn get_or_create(&mut self, tile: Tile) -> Result { + let surface = self.pool.allocate()?; + self.grid.insert(tile, surface.clone()); + Ok(surface) + } + + pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Surface, String> { + Ok(self.grid.get_mut(&tile).unwrap()) + } + + pub fn remove(&mut self, tile: Tile) -> bool { + if !self.grid.contains_key(&tile) { + return false; + } + self.grid.remove(&tile); + true + } + + pub fn clear(&mut self) { + self.grid.clear(); + } } diff --git a/render-wasm/src/render/tiles.rs b/render-wasm/src/render/tiles.rs new file mode 100644 index 000000000..b0befba35 --- /dev/null +++ b/render-wasm/src/render/tiles.rs @@ -0,0 +1,95 @@ +use skia_safe as skia; +use std::collections::{HashMap, HashSet}; +use uuid::Uuid; + +use crate::view::Viewbox; +use indexmap::IndexSet; + +pub type Tile = (i32, i32); + +pub const TILE_SIZE: f32 = 512.; + +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) { + // 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) +} + +pub fn get_tiles_for_viewbox(viewbox: Viewbox) -> (i32, i32, i32, i32) { + let tile_size = get_tile_size(viewbox); + get_tiles_for_rect(viewbox.area, tile_size) +} + +pub fn get_tile_pos(viewbox: Viewbox, (x, y): Tile) -> (f32, f32) { + ( + x as f32 * get_tile_size(viewbox), + y as f32 * get_tile_size(viewbox), + ) +} + +pub fn get_tile_size(viewbox: Viewbox) -> f32 { + // TODO: * self.options.dpr() too? + 1. / viewbox.zoom * TILE_SIZE +} + +pub fn get_tile_rect(viewbox: Viewbox, tile: Tile) -> skia::Rect { + let (tx, ty) = get_tile_pos(viewbox, tile); + let ts = get_tile_size(viewbox); + skia::Rect::from_xywh(tx, ty, ts, ts) +} + +// This structure is usseful to keep all the shape uuids by shape id. +pub struct TileHashMap { + grid: HashMap>, + index: HashMap>, +} + +impl TileHashMap { + pub fn new() -> Self { + TileHashMap { + grid: HashMap::new(), + index: HashMap::new(), + } + } + + pub fn has_shapes_at(&mut self, tile: Tile) -> bool { + return self.grid.contains_key(&tile); + } + + pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&IndexSet> { + return self.grid.get(&tile); + } + + pub fn get_tiles_of(&mut self, shape_id: Uuid) -> Option<&HashSet> { + self.index.get(&shape_id) + } + + pub fn add_shape_at(&mut self, tile: Tile, shape_id: Uuid) { + if !self.grid.contains_key(&tile) { + self.grid.insert(tile, IndexSet::new()); + } + + if !self.index.contains_key(&shape_id) { + self.index.insert(shape_id, HashSet::new()); + } + + let tile_set = self.grid.get_mut(&tile).unwrap(); + tile_set.insert(shape_id); + + let index_set = self.index.get_mut(&shape_id).unwrap(); + index_set.insert(tile); + } + + pub fn invalidate(&mut self) { + self.grid.clear(); + self.index.clear(); + } +} diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index ec58b4f0e..fc71c1a36 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -531,6 +531,33 @@ impl Shape { self.selrect } + pub fn extrect(&self) -> math::Rect { + let mut rect = self.bounds().to_rect(); + for shadow in self.shadows.iter() { + let (x, y) = shadow.offset; + let mut shadow_rect = rect.clone(); + shadow_rect.left += x; + shadow_rect.right += x; + shadow_rect.top += y; + shadow_rect.bottom += y; + + shadow_rect.left -= shadow.blur; + shadow_rect.top -= shadow.blur; + shadow_rect.right += shadow.blur; + shadow_rect.bottom += shadow.blur; + + rect.join(shadow_rect); + } + if self.blur.blur_type != blurs::BlurType::None { + rect.left -= self.blur.value; + rect.top -= self.blur.value; + rect.right += self.blur.value; + rect.bottom += self.blur.value; + } + + rect + } + pub fn center(&self) -> Point { self.selrect.center() } @@ -574,7 +601,10 @@ impl Shape { } pub fn is_recursive(&self) -> bool { - !matches!(self.shape_type, Type::SVGRaw(_)) + matches!( + self.shape_type, + Type::Frame(_) | Type::Group(_) | Type::Bool(_) + ) } pub fn add_shadow(&mut self, shadow: Shadow) { @@ -671,12 +701,13 @@ impl Shape { let width = bounds.width(); let height = bounds.height(); - self.selrect = math::Rect::from_xywh( + let new_selrect = math::Rect::from_xywh( center.x - width / 2.0, center.y - height / 2.0, width, height, ); + self.selrect = new_selrect; } pub fn apply_transform(&mut self, transform: &Matrix) { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 2f440ef78..6fdf6869c 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -50,10 +50,6 @@ impl<'a> State<'a> { Ok(()) } - pub fn render_from_cache(&mut self) { - let _ = self.render_state.render_from_cache(); - } - pub fn use_shape(&'a mut self, id: Uuid) { if !self.shapes.contains_key(&id) { let new_shape = Shape::new(id); @@ -70,4 +66,22 @@ impl<'a> State<'a> { pub fn set_background_color(&mut self, color: skia::Color) { self.render_state.set_background_color(color); } + + pub fn set_selrect_for_current_shape(&mut self, left: f32, top: f32, right: f32, bottom: f32) { + match self.current_shape.as_mut() { + Some(shape) => { + shape.set_selrect(left, top, right, bottom); + // We don't need to update the tile for the root shape. + if !shape.id.is_nil() { + self.render_state.update_tile_for(&shape); + } + } + None => panic!("Invalid current shape"), + } + } + + pub fn rebuild_tiles(&mut self) { + self.render_state + .rebuild_tiles(&mut self.shapes, &self.modifiers); + } } diff --git a/render-wasm/src/view.rs b/render-wasm/src/view.rs index 1c2351152..76585c078 100644 --- a/render-wasm/src/view.rs +++ b/render-wasm/src/view.rs @@ -45,19 +45,6 @@ impl Viewbox { ); } - pub fn set_zoom(&mut self, zoom: f32) { - self.zoom = zoom; - self.area - .set_wh(self.width / self.zoom, self.height / self.zoom); - } - - pub fn set_pan_xy(&mut self, pan_x: f32, pan_y: f32) { - self.pan_x = pan_x; - self.pan_y = pan_y; - self.area.left = -pan_x; - self.area.top = -pan_y; - } - pub fn set_wh(&mut self, width: f32, height: f32) { self.width = width; self.height = height;