🔧 Use LRU cache for tile surfaces

This commit is contained in:
Elena Torro 2025-03-26 09:58:53 +01:00
parent 60bc88a075
commit 7aeefb5b2e
4 changed files with 77 additions and 149 deletions

34
render-wasm/Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "adler2" name = "adler2"
@ -8,6 +8,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.20" version = "0.7.20"
@ -823,6 +834,15 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.0" version = "0.15.0"
@ -901,7 +921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.15.0",
] ]
[[package]] [[package]]
@ -1036,6 +1056,15 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ea2d928b485416e8908cff2d97d621db22b27f7b3b6729e438bcf42c671ba91"
dependencies = [
"hashbrown 0.11.2",
]
[[package]] [[package]]
name = "mac-notification-sys" name = "mac-notification-sys"
version = "0.6.2" version = "0.6.2"
@ -1511,6 +1540,7 @@ dependencies = [
"cargo-watch", "cargo-watch",
"gl", "gl",
"indexmap", "indexmap",
"lru",
"skia-safe", "skia-safe",
"uuid", "uuid",
] ]

View File

@ -11,6 +11,7 @@ name = "render_wasm"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
lru = "0.6"
base64 = "0.22.1" base64 = "0.22.1"
gl = "0.14.0" gl = "0.14.0"
indexmap = "2.7.1" indexmap = "2.7.1"

View File

@ -462,12 +462,10 @@ impl RenderState {
ey + interest_delta ey + interest_delta
*/ */
self.pending_tiles = vec![]; self.pending_tiles = vec![];
self.surfaces.cache_clear_visited();
for y in sy..=ey { for y in sy..=ey {
for x in sx..=ex { for x in sx..=ex {
let tile = (x, y); let tile = (x, y);
self.pending_tiles.push(tile); self.pending_tiles.push(tile);
self.surfaces.cache_visit(tile);
} }
} }
self.current_tile = None; self.current_tile = None;

View File

@ -1,11 +1,11 @@
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, Paint, RRect, Surface};
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 lru::LruCache;
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId { pub enum SurfaceId {
@ -22,22 +22,22 @@ pub enum SurfaceId {
pub struct Surfaces { pub struct Surfaces {
// is the final destination surface, the one that it is represented in the canvas element. // is the final destination surface, the one that it is represented in the canvas element.
target: skia::Surface, target: Surface,
// keeps the current render // keeps the current render
current: skia::Surface, current: Surface,
// keeps the current shape's fills // keeps the current shape's fills
shape_fills: skia::Surface, shape_fills: Surface,
// keeps the current shape's strokes // keeps the current shape's strokes
shape_strokes: skia::Surface, shape_strokes: Surface,
// used for rendering shadows // used for rendering shadows
shadow: skia::Surface, shadow: Surface,
// used for new shadow rendering // used for new shadow rendering
drop_shadows: skia::Surface, drop_shadows: Surface,
inner_shadows: skia::Surface, inner_shadows: Surface,
// for drawing the things that are over shadows. // for drawing the things that are over shadows.
overlay: skia::Surface, overlay: Surface,
// for drawing debug info. // for drawing debug info.
debug: skia::Surface, debug: Surface,
// for drawing tiles. // for drawing tiles.
tiles: TileSurfaceCache, tiles: TileSurfaceCache,
sampling_options: skia::SamplingOptions, sampling_options: skia::SamplingOptions,
@ -74,8 +74,11 @@ impl Surfaces {
const POOL_CAPACITY_THRESHOLD: i32 = 4; const POOL_CAPACITY_THRESHOLD: i32 = 4;
let pool_capacity = let pool_capacity =
(width / tile_dims.width) * (height / tile_dims.height) * POOL_CAPACITY_THRESHOLD; (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(
let tiles = TileSurfaceCache::new(pool); pool_capacity as usize,
target.new_surface_with_dimensions(tile_dims).unwrap(),
tile_dims,
);
Surfaces { Surfaces {
target, target,
current, current,
@ -145,7 +148,7 @@ impl Surfaces {
.draw(self.canvas(to), (0.0, 0.0), sampling_options, paint); .draw(self.canvas(to), (0.0, 0.0), sampling_options, paint);
} }
pub fn apply_mut(&mut self, ids: &[SurfaceId], mut f: impl FnMut(&mut skia::Surface) -> ()) { pub fn apply_mut(&mut self, ids: &[SurfaceId], mut f: impl FnMut(&mut Surface) -> ()) {
for id in ids { for id in ids {
let surface = self.get_mut(*id); let surface = self.get_mut(*id);
f(surface); f(surface);
@ -172,7 +175,7 @@ impl Surfaces {
); );
} }
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface { fn get_mut(&mut self, id: SurfaceId) -> &mut Surface {
match id { match id {
SurfaceId::Target => &mut self.target, SurfaceId::Target => &mut self.target,
SurfaceId::Current => &mut self.current, SurfaceId::Current => &mut self.current,
@ -186,7 +189,7 @@ impl Surfaces {
} }
} }
fn reset_from_target(&mut self, target: skia::Surface) { fn reset_from_target(&mut self, target: Surface) {
let dim = (target.width(), target.height()); let dim = (target.width(), target.height());
self.target = target; self.target = target;
self.debug = self.target.new_surface_with_dimensions(dim).unwrap(); self.debug = self.target.new_surface_with_dimensions(dim).unwrap();
@ -238,18 +241,10 @@ impl Surfaces {
.reset_matrix(); .reset_matrix();
} }
pub fn cache_clear_visited(&mut self) {
self.tiles.clear_visited();
}
pub fn cache_visit(&mut self, tile: Tile) {
self.tiles.visit(tile);
}
pub fn cache_tile_surface(&mut self, tile: Tile, id: SurfaceId, color: skia::Color) { pub fn cache_tile_surface(&mut self, tile: Tile, id: SurfaceId, color: skia::Color) {
let sampling_options = self.sampling_options; let sampling_options = self.sampling_options;
let mut tile_surface = self.tiles.get_or_create(tile).unwrap();
let margins = self.margins; let margins = self.margins;
let mut tile_surface = self.tiles.get_or_create(tile);
let surface = self.get_mut(id); let surface = self.get_mut(id);
tile_surface.canvas().clear(color); tile_surface.canvas().clear(color);
surface.draw( surface.draw(
@ -280,148 +275,52 @@ impl Surfaces {
} }
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 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,
}
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 deallocate(&mut self, surface_ref_to_deallocate: &SurfaceRef) {
let surface_ref = self
.surfaces
.get_mut(surface_ref_to_deallocate.index)
.unwrap();
surface_ref.in_use = false;
}
pub fn allocate(&mut self) -> Option<SurfaceRef> {
let start = self.index;
let len = self.surfaces.len();
loop {
self.index = (self.index + 1) % len;
if self.index == start {
return None;
}
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());
}
}
}
} }
} }
pub struct TileSurfaceCache { pub struct TileSurfaceCache {
pool: SurfacePool, surface: Surface,
grid: HashMap<Tile, SurfaceRef>, dims: skia::ISize,
visited: HashMap<Tile, bool>, lru_cache: LruCache<Tile, Surface>,
} }
impl TileSurfaceCache { impl TileSurfaceCache {
pub fn new(pool: SurfacePool) -> Self { pub fn new(pool_capacity: usize, surface: Surface, dims: skia::ISize) -> Self {
let lru_cache = LruCache::new(pool_capacity);
Self { Self {
pool, surface,
grid: HashMap::new(), dims,
visited: HashMap::new(), lru_cache,
} }
} }
pub fn has(&mut self, tile: Tile) -> bool { pub fn has(&mut self, tile: Tile) -> bool {
return self.grid.contains_key(&tile); self.lru_cache.contains(&tile)
} }
pub fn get_or_create(&mut self, tile: Tile) -> Result<skia::Surface, String> { pub fn get_or_create(&mut self, tile: Tile) -> Surface {
if let Some(surface_ref) = self.pool.allocate() { if let Some(surface) = self.lru_cache.get(&tile) {
self.grid.insert(tile, surface_ref.clone()); return surface.clone();
Ok(surface_ref.surface.clone())
} else {
// TODO: I don't know yet how to improve this but I don't like it. I think
// there should be a better solution.
for (tile, surface_ref) in self.grid.iter() {
if !self.visited.contains_key(tile) {
continue;
}
if !self.visited.get(tile).unwrap() {
self.pool.deallocate(surface_ref);
}
}
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());
} }
let new_surface = self.surface.new_surface_with_dimensions(self.dims).unwrap();
self.lru_cache.put(tile, new_surface.clone());
new_surface
} }
pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Surface, String> { pub fn get(&mut self, tile: Tile) -> Result<&mut Surface, String> {
Ok(&mut self.grid.get_mut(&tile).unwrap().surface) self.lru_cache
.get_mut(&tile)
.ok_or_else(|| "Tile not found".to_string())
} }
pub fn remove(&mut self, tile: Tile) -> bool { pub fn remove(&mut self, tile: Tile) -> bool {
if !self.grid.contains_key(&tile) { self.lru_cache.pop(&tile);
return false;
}
self.grid.remove(&tile);
true true
} }
pub fn clear_grid(&mut self) { pub fn clear(&mut self) {
self.grid.clear(); self.lru_cache.clear();
self.pool.clear();
}
pub fn clear_visited(&mut self) {
self.visited.clear();
}
pub fn visit(&mut self, tile: Tile) {
self.visited.insert(tile, true);
} }
} }