diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index d69a8a04e..a122f3801 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -29,6 +29,7 @@ [app.render-wasm.wasm :as wasm] [app.util.debug :as dbg] [app.util.http :as http] + [app.util.perf :as uperf] [app.util.webapi :as wapi] [beicon.v2.core :as rx] [promesa.core :as p] @@ -93,11 +94,9 @@ (rds/renderToStaticMarkup))) ;; This should never be called from the outside. -;; This function receives a "time" parameter that we're not using but maybe in the future could be useful (it is the time since -;; the window started rendering elements so it could be useful to measure time between frames). (defn- render - [_] - (h/call wasm/internal-module "_render") + [timestamp] + (h/call wasm/internal-module "_render" timestamp) (set! wasm/internal-frame-id nil)) @@ -613,7 +612,7 @@ (defn set-view-box [zoom vbox] (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) - (render nil)) + (render (uperf/now))) (defn clear-drawing-cache [] (h/call wasm/internal-module "_clear_drawing_cache")) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 2ac6bf9f7..609810c71 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -146,7 +146,7 @@ impl RenderState { } pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { - self.images.add(id, image_data) + self.images.add(id, image_data, &mut self.gpu_state.context) } pub fn has_image(&mut self, id: &Uuid) -> bool { @@ -193,9 +193,7 @@ impl RenderState { 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.cache_current_tile_texture((x, y)); self.surfaces .draw_cached_tile_surface(self.current_tile.unwrap(), rect); diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 0c44b7fa6..230464139 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -171,15 +171,6 @@ pub fn render(render_state: &mut RenderState) { ); } -#[cfg(target_arch = "wasm32")] -#[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); - - #[cfg(target_arch = "wasm32")] - run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')")) -} - #[cfg(target_arch = "wasm32")] #[allow(dead_code)] pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) { diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index 651f85ae4..138fc4a7f 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -1,5 +1,8 @@ +use crate::math::Rect as MathRect; use crate::uuid::Uuid; + use skia_safe as skia; +use skia_safe::gpu::{surfaces, Budgeted, DirectContext}; use std::collections::HashMap; pub type Image = skia::Image; @@ -15,11 +18,41 @@ impl ImageStore { } } - pub fn add(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { - let image_data = skia::Data::new_copy(image_data); + pub fn add( + &mut self, + id: Uuid, + image_data: &[u8], + context: &mut DirectContext, + ) -> Result<(), String> { + let image_data = unsafe { skia::Data::new_bytes(image_data) }; let image = Image::from_encoded(image_data).ok_or("Error decoding image data")?; - self.images.insert(id, image); + let width = image.width(); + let height = image.height(); + + let image_info = skia::ImageInfo::new_n32_premul((width, height), None); + let mut surface = surfaces::render_target( + context, + Budgeted::Yes, + &image_info, + None, + None, + None, + None, + false, + ) + .ok_or("Can't create GPU surface")?; + + let dest_rect = MathRect::from_xywh(0.0, 0.0, width as f32, height as f32); + + surface + .canvas() + .draw_image_rect(&image, None, dest_rect, &skia::Paint::default()); + + let gpu_image = surface.image_snapshot(); + + // This way we store the image as a texture + self.images.insert(id, gpu_image); Ok(()) } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 854a820ac..a1c2cb02c 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -1,14 +1,17 @@ use crate::shapes::Shape; use crate::view::Viewbox; -use skia_safe::{self as skia, Paint, RRect}; +use skia_safe::{self as skia, IRect, Paint, RRect}; use super::{gpu_state::GpuState, tiles::Tile}; use base64::{engine::general_purpose, Engine as _}; use std::collections::HashMap; -const POOL_CAPACITY_MINIMUM: i32 = 32; -const POOL_CAPACITY_THRESHOLD: i32 = 4; +const TEXTURES_CACHE_CAPACITY: usize = 512; +const TEXTURES_BATCH_DELETE: usize = 32; +// 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. +const TILE_SIZE_MULTIPLIER: i32 = 2; #[derive(Debug, PartialEq, Clone, Copy)] pub enum SurfaceId { @@ -37,7 +40,7 @@ pub struct Surfaces { // for drawing debug info. debug: skia::Surface, // for drawing tiles. - tiles: TileSurfaceCache, + tiles: TileTextureCache, sampling_options: skia::SamplingOptions, margins: skia::ISize, } @@ -50,13 +53,9 @@ impl Surfaces { 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, + tile_dims.width * TILE_SIZE_MULTIPLIER, + tile_dims.height * TILE_SIZE_MULTIPLIER, ); let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4); @@ -68,12 +67,7 @@ impl Surfaces { let shape_strokes = target.new_surface_with_dimensions(extra_tile_dims).unwrap(); let debug = target.new_surface_with_dimensions((width, height)).unwrap(); - let pool_capacity = - ((width / tile_dims.width) * (height / tile_dims.height) * POOL_CAPACITY_THRESHOLD) - .max(POOL_CAPACITY_MINIMUM); - - let pool = SurfacePool::with_capacity(&mut target, tile_dims, pool_capacity as usize); - let tiles = TileSurfaceCache::new(pool); + let tiles = TileTextureCache::new(); Surfaces { target, current, @@ -82,8 +76,8 @@ impl Surfaces { shape_fills, shape_strokes, debug, - sampling_options, tiles, + sampling_options, margins, } } @@ -92,16 +86,6 @@ impl Surfaces { self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)); } - 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(); @@ -238,26 +222,19 @@ impl Surfaces { self.tiles.visit(tile); } - pub fn cache_visited_amount(&self) -> usize { - self.tiles.visited_amount() - } - - pub fn cache_visited_capacity(&self) -> usize { - self.tiles.visited_capacity() - } - - 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 cache_current_tile_texture(&mut self, tile: Tile) { + let snapshot = self.current.image_snapshot(); + let rect = IRect::from_xywh( + self.margins.width, + self.margins.height, + snapshot.width() - TILE_SIZE_MULTIPLIER * self.margins.width, + snapshot.height() - TILE_SIZE_MULTIPLIER * self.margins.height, ); + + let mut context = self.current.direct_context(); + if let Some(snapshot) = snapshot.make_subset(&mut context, &rect) { + self.tiles.add(tile, snapshot); + } } pub fn has_cached_tile_surface(&mut self, tile: Tile) -> bool { @@ -269,127 +246,25 @@ impl Surfaces { } 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()), - ); + let image = self.tiles.get(tile).unwrap(); + self.target + .canvas() + .draw_image_rect(&image, None, rect, &skia::Paint::default()); } pub fn remove_cached_tiles(&mut self) { - self.tiles.clear_grid(); + self.tiles.clear(); } } -pub struct SurfaceRef { - pub index: usize, - pub in_use: bool, - pub surface: skia::Surface, -} - -impl Clone for SurfaceRef { - fn clone(&self) -> Self { - Self { - index: self.index, - in_use: self.in_use, - surface: self.surface.clone(), - } - } -} - -pub struct SurfacePool { - pub surfaces: Vec, - pub index: usize, -} - -#[allow(dead_code)] -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()) - } - - Self { - index: 0, - surfaces: surfaces - .into_iter() - .enumerate() - .map(|(index, surface)| SurfaceRef { - index, - in_use: false, - surface: surface, - }) - .collect(), - } - } - - pub fn clear(&mut self) { - for surface in self.surfaces.iter_mut() { - surface.in_use = false; - } - } - - pub fn capacity(&self) -> usize { - self.surfaces.len() - } - - pub fn available(&self) -> usize { - let mut available: usize = 0; - for surface_ref in self.surfaces.iter() { - if surface_ref.in_use == false { - available += 1; - } - } - available - } - - pub fn deallocate(&mut self, surface_ref_to_deallocate: &SurfaceRef) { - let surface_ref = self - .surfaces - .get_mut(surface_ref_to_deallocate.index) - .unwrap(); - - // This could happen when the "clear" method of the pool is called. - if surface_ref.in_use == false { - return; - } - surface_ref.in_use = false; - self.index = surface_ref_to_deallocate.index; - } - - pub fn allocate(&mut self) -> Option { - let start = self.index; - let len = self.surfaces.len(); - loop { - if let Some(surface_ref) = self.surfaces.get_mut(self.index) { - if !surface_ref.in_use { - surface_ref.in_use = true; - return Some(surface_ref.clone()); - } - } - self.index = (self.index + 1) % len; - if self.index == start { - return None; - } - } - } -} - -pub struct TileSurfaceCache { - pool: SurfacePool, - grid: HashMap, +pub struct TileTextureCache { + grid: HashMap, visited: HashMap, } -#[allow(dead_code)] -impl TileSurfaceCache { - pub fn new(pool: SurfacePool) -> Self { +impl TileTextureCache { + pub fn new() -> Self { Self { - pool, grid: HashMap::new(), visited: HashMap::new(), } @@ -405,66 +280,40 @@ impl TileSurfaceCache { } } - fn try_get_or_create(&mut self, tile: Tile) -> Result { - // TODO: I don't know yet how to improve this but I don't like it. I think - // there should be a better solution. - let mut marked = vec![]; - for (tile, surface_ref) in self.grid.iter_mut() { - let exists_as_visited = self.visited.contains_key(tile); - if !exists_as_visited { - marked.push(tile.clone()); - self.pool.deallocate(surface_ref); - continue; - } - - let is_visited = self.visited.get(tile).unwrap(); - if !*is_visited { - marked.push(tile.clone()); - self.pool.deallocate(surface_ref); - } + pub fn add(&mut self, 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) { + Some(tile.clone()) + } else { + None + } + }) + .take(TEXTURES_BATCH_DELETE) + .collect(); + self.remove_list(marked); } - - self.remove_list(marked); - - if let Some(surface_ref) = self.pool.allocate() { - self.grid.insert(tile, surface_ref.clone()); - return Ok(surface_ref.surface.clone()); - } - return Err("Not enough surfaces".into()); + self.grid.insert(tile, image); } - pub fn get_or_create(&mut self, tile: Tile) -> Result { - if let Some(surface_ref) = self.pool.allocate() { - self.grid.insert(tile, surface_ref.clone()); - return Ok(surface_ref.surface.clone()); - } - self.try_get_or_create(tile) - } - - pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Surface, String> { - Ok(&mut self.grid.get_mut(&tile).unwrap().surface) + pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Image, String> { + let image = self.grid.get_mut(&tile).unwrap(); + Ok(image) } pub fn remove(&mut self, tile: Tile) -> bool { if !self.grid.contains_key(&tile) { return false; } - let surface_ref_to_deallocate = self.grid.remove(&tile); - self.pool.deallocate(&surface_ref_to_deallocate.unwrap()); + self.grid.remove(&tile); true } - pub fn clear_grid(&mut self) { + pub fn clear(&mut self) { self.grid.clear(); - self.pool.clear(); - } - - pub fn visited_amount(&self) -> usize { - self.visited.len() - } - - pub fn visited_capacity(&self) -> usize { - self.visited.capacity() } pub fn clear_visited(&mut self) {