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.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"))

View file

@ -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);

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")]
#[allow(dead_code)]
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 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(())
}

View file

@ -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<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>,
pub struct TileTextureCache {
grid: HashMap<Tile, skia::Image>,
visited: HashMap<Tile, bool>,
}
#[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<skia::Surface, String> {
// 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<skia::Surface, String> {
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) {