Compare commits

...

1 Commits

Author SHA1 Message Date
Elena Torro
a4b19be10d wip 2026-01-20 13:53:49 +01:00
4 changed files with 190 additions and 84 deletions

View File

@@ -276,13 +276,10 @@ pub extern "C" fn set_view_end() {
performance::end_measure!("set_view_end::rebuild_tiles");
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
} else {
// During pan, we only clear the tile index without
// invalidating cached textures, which is more efficient.
let _clear_start = performance::begin_timed_log!("clear_tile_index");
performance::begin_measure!("set_view_end::clear_tile_index");
state.clear_tile_index();
performance::end_measure!("set_view_end::clear_tile_index");
performance::end_timed_log!("clear_tile_index", _clear_start);
// During pan, don't clear anything - tiles remain valid since
// tile coordinates are world-based and don't change with pan.
// The renderer will use cached tiles where available and only
// render new tiles that come into view.
}
performance::end_measure!("set_view_end");
performance::end_timed_log!("set_view_end", _end_start);

View File

@@ -514,7 +514,7 @@ impl RenderState {
self.surfaces.canvas(surface_id).restore();
}
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect, present_to_target: bool) {
let tile_rect = self.get_current_aligned_tile_bounds();
self.surfaces.cache_current_tile_texture(
&self.tile_viewbox,
@@ -522,11 +522,95 @@ impl RenderState {
&tile_rect,
);
self.surfaces.draw_cached_tile_surface(
self.current_tile.unwrap(),
rect,
self.background_color,
);
if present_to_target {
self.surfaces.draw_cached_tile_surface(
self.current_tile.unwrap(),
rect,
self.background_color,
);
}
}
fn present_cached_tiles_to_target(&mut self) {
// Clear target before presenting buffered tiles.
self.surfaces
.canvas(SurfaceId::Target)
.clear(self.background_color);
self.present_cached_tiles_to_target_no_clear();
}
/// Presents cached tiles to the target surface without clearing first.
/// Used when combining with a snapshot placeholder background.
fn present_cached_tiles_to_target_no_clear(&mut self) {
let scale = self.get_scale();
let offset_x = self.viewbox.area.left * scale;
let offset_y = self.viewbox.area.top * scale;
let start_tile_x = (offset_x / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
let start_tile_y = (offset_y / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
let delta_x = start_tile_x - offset_x;
let delta_y = start_tile_y - offset_y;
let tiles::TileRect(start_x, start_y, end_x, end_y) =
tiles::get_tiles_for_viewbox(self.viewbox, scale);
for ty in start_y..=end_y {
for tx in start_x..=end_x {
let tile = tiles::Tile::from(tx, ty);
let has_tile = self.surfaces.has_cached_tile_surface(tile);
if has_tile {
let aligned = self.get_aligned_tile_bounds(tile);
let rect = Rect::from_xywh(
aligned.left + delta_x,
aligned.top + delta_y,
aligned.width(),
aligned.height(),
);
self.surfaces
.draw_cached_tile_surface(tile, rect, self.background_color);
}
}
}
}
/// Draws the cached target snapshot transformed to the current view.
/// Returns `true` when the snapshot was available and rendered.
fn draw_cached_snapshot_placeholder(&mut self) -> bool {
let Some(snapshot) = &self.cached_target_snapshot else {
return false;
};
let cached_scale = self.get_cached_scale();
if cached_scale <= f32::EPSILON {
return false;
}
let canvas = self.surfaces.canvas(SurfaceId::Target);
canvas.save();
// Scale and translate the target according to the cached data
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
canvas.scale((navigate_zoom, navigate_zoom));
let TileRect(start_tile_x, start_tile_y, _, _) =
tiles::get_tiles_for_viewbox_with_interest(
self.cached_viewbox,
VIEWPORT_INTEREST_AREA_THRESHOLD,
cached_scale,
);
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr();
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
canvas.translate((
(start_tile_x as f32 * tiles::TILE_SIZE) - offset_x,
(start_tile_y as f32 * tiles::TILE_SIZE) - offset_y,
));
canvas.clear(self.background_color);
canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default()));
canvas.restore();
true
}
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
@@ -1086,43 +1170,25 @@ impl RenderState {
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
let _start = performance::begin_timed_log!("render_from_cache");
performance::begin_measure!("render_from_cache");
let scale = self.get_cached_scale();
if let Some(snapshot) = &self.cached_target_snapshot {
let canvas = self.surfaces.canvas(SurfaceId::Target);
canvas.save();
// Scale and translate the target according to the cached data
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
canvas.scale((navigate_zoom, navigate_zoom));
let TileRect(start_tile_x, start_tile_y, _, _) =
tiles::get_tiles_for_viewbox_with_interest(
self.cached_viewbox,
VIEWPORT_INTEREST_AREA_THRESHOLD,
scale,
);
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr();
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
canvas.translate((
(start_tile_x as f32 * tiles::TILE_SIZE) - offset_x,
(start_tile_y as f32 * tiles::TILE_SIZE) - offset_y,
));
canvas.clear(self.background_color);
canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default()));
canvas.restore();
if self.options.is_debug_visible() {
debug::render(self);
}
ui::render(self, shapes);
debug::render_wasm_label(self);
self.flush_and_submit();
if self.zoom_changed() {
// For zoom: use only the scaled snapshot (original behavior from staging-render).
// Mixing with tiles causes flickering due to coordinate misalignment.
self.draw_cached_snapshot_placeholder();
} else {
// For panning: present cached tiles directly.
self.present_cached_tiles_to_target();
}
if self.options.is_debug_visible() {
debug::render(self);
}
ui::render(self, shapes);
debug::render_wasm_label(self);
self.flush_and_submit();
performance::end_measure!("render_from_cache");
performance::end_timed_log!("render_from_cache", _start);
}
@@ -1152,7 +1218,7 @@ impl RenderState {
s.canvas().scale((scale, scale));
});
let viewbox_cache_size = get_cache_size(self.viewbox, scale);
let viewbox_cache_size = get_cache_size(self.viewbox, scale);
let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox, scale);
// Only resize cache if the new size is larger than the cached size
// This avoids unnecessary surface recreations when the cache size decreases
@@ -1965,7 +2031,8 @@ impl RenderState {
timestamp: i32,
allow_stop: bool,
) -> Result<(), String> {
let mut should_stop = false;
let mut should_stop = false;
let present_incrementally = false;
let root_ids = {
if let Some(shape_id) = base_object {
vec![*shape_id]
@@ -1981,22 +2048,24 @@ impl RenderState {
if let Some(current_tile) = self.current_tile {
if self.surfaces.has_cached_tile_surface(current_tile) {
performance::begin_measure!("render_shape_tree::cached");
let tile_rect = self.get_current_tile_bounds();
self.surfaces.draw_cached_tile_surface(
current_tile,
tile_rect,
self.background_color,
);
performance::end_measure!("render_shape_tree::cached");
if self.options.is_debug_visible() {
debug::render_workspace_current_tile(
self,
"Cached".to_string(),
if present_incrementally {
let tile_rect = self.get_current_tile_bounds();
self.surfaces.draw_cached_tile_surface(
current_tile,
tile_rect,
self.background_color,
);
if self.options.is_debug_visible() {
debug::render_workspace_current_tile(
self,
"Cached".to_string(),
current_tile,
tile_rect,
);
}
}
performance::end_measure!("render_shape_tree::cached");
} else {
performance::begin_measure!("render_shape_tree::uncached");
let (is_empty, early_return) =
@@ -2008,7 +2077,7 @@ impl RenderState {
performance::end_measure!("render_shape_tree::uncached");
let tile_rect = self.get_current_tile_bounds();
if !is_empty {
self.apply_render_to_final_canvas(tile_rect);
self.apply_render_to_final_canvas(tile_rect, present_incrementally);
if self.options.is_debug_visible() {
debug::render_workspace_current_tile(
@@ -2063,6 +2132,10 @@ impl RenderState {
}
}
if !present_incrementally {
self.present_cached_tiles_to_target();
}
self.render_in_progress = false;
self.surfaces.gc();
@@ -2172,17 +2245,6 @@ impl RenderState {
performance::end_measure!("rebuild_tiles_shallow");
}
/// Clears the tile index without invalidating cached tile textures.
/// This is useful when tile positions don't change (e.g., during pan operations)
/// but the tile index needs to be synchronized. The cached tile textures remain
/// valid since they don't depend on the current view position, only on zoom level.
/// This is much more efficient than clearing the entire cache surface.
pub fn clear_tile_index(&mut self) {
performance::begin_measure!("clear_tile_index");
self.surfaces.clear_tiles();
performance::end_measure!("clear_tile_index");
}
pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
performance::begin_measure!("rebuild_tiles");

View File

@@ -397,6 +397,30 @@ impl Surfaces {
}
}
/// Cache a tile from an existing snapshot (used in round-robin rendering)
pub fn cache_tile_from_snapshot(
&mut self,
tile_viewbox: &TileViewbox,
tile: &Tile,
snapshot: &skia::Image,
tile_rect: &skia::Rect,
) {
// Crop the snapshot to tile bounds (removing margins)
let rect = IRect::from_xywh(
self.margins.width,
self.margins.height,
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
);
if let Some(cropped) = snapshot.make_subset(None, rect) {
self.tiles.add(tile_viewbox, tile, cropped.clone());
self.cache
.canvas()
.draw_image_rect(cropped, None, tile_rect, &skia::Paint::default());
}
}
pub fn has_cached_tile_surface(&self, tile: Tile) -> bool {
self.tiles.has(tile)
}
@@ -409,19 +433,36 @@ impl Surfaces {
self.tiles.remove(tile);
}
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, _color: skia::Color) {
let image = self.tiles.get(tile).unwrap();
let mut paint = skia::Paint::default();
paint.set_color(color);
self.target.canvas().draw_rect(rect, &paint);
// Draw the cached tile image directly without background rect
// The tile image already contains the full tile content including background
self.target
.canvas()
.draw_image_rect(&image, None, rect, &skia::Paint::default());
}
/// Draws the current tile surface directly to the target without caching.
/// Used for incremental rendering to show progress as shapes are rendered.
pub fn draw_current_to_target(&mut self, tile_rect: &skia::Rect) {
let rect = IRect::from_xywh(
self.margins.width,
self.margins.height,
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
);
if let Some(snapshot) = self.current.image_snapshot_with_bounds(rect) {
self.target.canvas().draw_image_rect(
snapshot,
None,
tile_rect,
&skia::Paint::default(),
);
}
}
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
self.tiles.clear();
self.cache.canvas().clear(color);
@@ -430,6 +471,16 @@ impl Surfaces {
pub fn gc(&mut self) {
self.tiles.gc();
}
/// Takes a snapshot of the Current surface for later restoration.
pub fn snapshot_current(&mut self) -> skia::Image {
self.current.image_snapshot()
}
/// Restores a snapshot to the Current surface by drawing it at origin.
pub fn restore_snapshot_to_current(&mut self, snapshot: &skia::Image) {
self.current.canvas().draw_image(snapshot, (0.0, 0.0), None);
}
}
pub struct TileTextureCache {

View File

@@ -197,10 +197,6 @@ impl<'a> State<'a> {
self.render_state.rebuild_tiles_shallow(&self.shapes);
}
pub fn clear_tile_index(&mut self) {
self.render_state.clear_tile_index();
}
pub fn rebuild_tiles(&mut self) {
self.render_state.rebuild_tiles_from(&self.shapes, None);
}