Merge pull request #6313 from penpot/superalex-improve-images-performance-wasm

🎉 Improve images performance
This commit is contained in:
Aitor Moreno 2025-04-22 11:36:24 +02:00 committed by GitHub
commit 484772e3b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 95 additions and 225 deletions

View file

@ -29,6 +29,7 @@
[app.render-wasm.wasm :as wasm] [app.render-wasm.wasm :as wasm]
[app.util.debug :as dbg] [app.util.debug :as dbg]
[app.util.http :as http] [app.util.http :as http]
[app.util.perf :as uperf]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[promesa.core :as p] [promesa.core :as p]
@ -93,11 +94,9 @@
(rds/renderToStaticMarkup))) (rds/renderToStaticMarkup)))
;; This should never be called from the outside. ;; 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 (defn- render
[_] [timestamp]
(h/call wasm/internal-module "_render") (h/call wasm/internal-module "_render" timestamp)
(set! wasm/internal-frame-id nil)) (set! wasm/internal-frame-id nil))
@ -613,7 +612,7 @@
(defn set-view-box (defn set-view-box
[zoom vbox] [zoom vbox]
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(render nil)) (render (uperf/now)))
(defn clear-drawing-cache [] (defn clear-drawing-cache []
(h/call wasm/internal-module "_clear_drawing_cache")) (h/call wasm/internal-module "_clear_drawing_cache"))

View file

@ -146,7 +146,7 @@ impl RenderState {
} }
pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { 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 { pub fn has_image(&mut self, id: &Uuid) -> bool {
@ -193,9 +193,7 @@ impl RenderState {
let x = self.current_tile.unwrap().0; let x = self.current_tile.unwrap().0;
let y = self.current_tile.unwrap().1; let y = self.current_tile.unwrap().1;
// This caches the current surface into the corresponding tile. self.surfaces.cache_current_tile_texture((x, y));
self.surfaces
.cache_tile_surface((x, y), SurfaceId::Current, self.background_color);
self.surfaces self.surfaces
.draw_cached_tile_surface(self.current_tile.unwrap(), rect); .draw_cached_tile_surface(self.current_tile.unwrap(), rect);

View file

@ -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")] #[cfg(target_arch = "wasm32")]
#[allow(dead_code)] #[allow(dead_code)]
pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) { pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {

View file

@ -1,5 +1,8 @@
use crate::math::Rect as MathRect;
use crate::uuid::Uuid; use crate::uuid::Uuid;
use skia_safe as skia; use skia_safe as skia;
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
use std::collections::HashMap; use std::collections::HashMap;
pub type Image = skia::Image; pub type Image = skia::Image;
@ -15,11 +18,41 @@ impl ImageStore {
} }
} }
pub fn add(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { pub fn add(
let image_data = skia::Data::new_copy(image_data); &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")?; 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(()) Ok(())
} }

View file

@ -1,14 +1,17 @@
use crate::shapes::Shape; use crate::shapes::Shape;
use crate::view::Viewbox; 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 super::{gpu_state::GpuState, tiles::Tile};
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use std::collections::HashMap; use std::collections::HashMap;
const POOL_CAPACITY_MINIMUM: i32 = 32; const TEXTURES_CACHE_CAPACITY: usize = 512;
const POOL_CAPACITY_THRESHOLD: i32 = 4; 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)] #[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId { pub enum SurfaceId {
@ -37,7 +40,7 @@ pub struct Surfaces {
// for drawing debug info. // for drawing debug info.
debug: skia::Surface, debug: skia::Surface,
// for drawing tiles. // for drawing tiles.
tiles: TileSurfaceCache, tiles: TileTextureCache,
sampling_options: skia::SamplingOptions, sampling_options: skia::SamplingOptions,
margins: skia::ISize, margins: skia::ISize,
} }
@ -50,13 +53,9 @@ impl Surfaces {
sampling_options: skia::SamplingOptions, sampling_options: skia::SamplingOptions,
tile_dims: skia::ISize, tile_dims: skia::ISize,
) -> Self { ) -> 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( let extra_tile_dims = skia::ISize::new(
tile_dims.width * extra_tile_size, tile_dims.width * TILE_SIZE_MULTIPLIER,
tile_dims.height * extra_tile_size, tile_dims.height * TILE_SIZE_MULTIPLIER,
); );
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4); 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 shape_strokes = target.new_surface_with_dimensions(extra_tile_dims).unwrap();
let debug = target.new_surface_with_dimensions((width, height)).unwrap(); let debug = target.new_surface_with_dimensions((width, height)).unwrap();
let pool_capacity = let tiles = TileTextureCache::new();
((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);
Surfaces { Surfaces {
target, target,
current, current,
@ -82,8 +76,8 @@ impl Surfaces {
shape_fills, shape_fills,
shape_strokes, shape_strokes,
debug, debug,
sampling_options,
tiles, tiles,
sampling_options,
margins, margins,
} }
} }
@ -92,16 +86,6 @@ impl Surfaces {
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)); 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 { pub fn base64_snapshot(&mut self, id: SurfaceId) -> String {
let surface = self.get_mut(id); let surface = self.get_mut(id);
let image = surface.image_snapshot(); let image = surface.image_snapshot();
@ -238,26 +222,19 @@ impl Surfaces {
self.tiles.visit(tile); self.tiles.visit(tile);
} }
pub fn cache_visited_amount(&self) -> usize { pub fn cache_current_tile_texture(&mut self, tile: Tile) {
self.tiles.visited_amount() let snapshot = self.current.image_snapshot();
} let rect = IRect::from_xywh(
self.margins.width,
pub fn cache_visited_capacity(&self) -> usize { self.margins.height,
self.tiles.visited_capacity() snapshot.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
} snapshot.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
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()),
); );
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 { 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) { pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect) {
let sampling_options = self.sampling_options; let image = self.tiles.get(tile).unwrap();
let tile_surface = self.tiles.get(tile).unwrap(); self.target
tile_surface.draw( .canvas()
self.target.canvas(), .draw_image_rect(&image, None, rect, &skia::Paint::default());
(rect.x(), rect.y()),
sampling_options,
Some(&skia::Paint::default()),
);
} }
pub fn remove_cached_tiles(&mut self) { pub fn remove_cached_tiles(&mut self) {
self.tiles.clear_grid(); self.tiles.clear();
} }
} }
pub struct SurfaceRef { pub struct TileTextureCache {
pub index: usize, grid: HashMap<Tile, skia::Image>,
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<SurfaceRef>,
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<SurfaceRef> {
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<Tile, SurfaceRef>,
visited: HashMap<Tile, bool>, visited: HashMap<Tile, bool>,
} }
#[allow(dead_code)] impl TileTextureCache {
impl TileSurfaceCache { pub fn new() -> Self {
pub fn new(pool: SurfacePool) -> Self {
Self { Self {
pool,
grid: HashMap::new(), grid: HashMap::new(),
visited: HashMap::new(), visited: HashMap::new(),
} }
@ -405,66 +280,40 @@ impl TileSurfaceCache {
} }
} }
fn try_get_or_create(&mut self, tile: Tile) -> Result<skia::Surface, String> { pub fn add(&mut self, tile: Tile, image: skia::Image) {
// TODO: I don't know yet how to improve this but I don't like it. I think if self.grid.len() > TEXTURES_CACHE_CAPACITY {
// there should be a better solution. let marked: Vec<_> = self
let mut marked = vec![]; .grid
for (tile, surface_ref) in self.grid.iter_mut() { .iter_mut()
let exists_as_visited = self.visited.contains_key(tile); .filter_map(|(tile, _)| {
if !exists_as_visited { if !self.visited.contains_key(tile) {
marked.push(tile.clone()); Some(tile.clone())
self.pool.deallocate(surface_ref); } else {
continue; None
} }
})
let is_visited = self.visited.get(tile).unwrap(); .take(TEXTURES_BATCH_DELETE)
if !*is_visited { .collect();
marked.push(tile.clone()); self.remove_list(marked);
self.pool.deallocate(surface_ref);
}
} }
self.grid.insert(tile, image);
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());
} }
pub fn get_or_create(&mut self, tile: Tile) -> Result<skia::Surface, String> { pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Image, String> {
if let Some(surface_ref) = self.pool.allocate() { let image = self.grid.get_mut(&tile).unwrap();
self.grid.insert(tile, surface_ref.clone()); Ok(image)
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 remove(&mut self, tile: Tile) -> bool { pub fn remove(&mut self, tile: Tile) -> bool {
if !self.grid.contains_key(&tile) { if !self.grid.contains_key(&tile) {
return false; return false;
} }
let surface_ref_to_deallocate = self.grid.remove(&tile); self.grid.remove(&tile);
self.pool.deallocate(&surface_ref_to_deallocate.unwrap());
true true
} }
pub fn clear_grid(&mut self) { pub fn clear(&mut self) {
self.grid.clear(); 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) { pub fn clear_visited(&mut self) {