mirror of
https://github.com/penpot/penpot.git
synced 2026-02-12 22:53:02 +00:00
🔧 Improve surface rendering performance
This commit is contained in:
@@ -264,7 +264,6 @@ pub(crate) struct RenderState {
|
||||
pub fonts: FontStore,
|
||||
pub viewbox: Viewbox,
|
||||
pub cached_viewbox: Viewbox,
|
||||
pub cached_target_snapshot: Option<skia::Image>,
|
||||
pub images: ImageStore,
|
||||
pub background_color: skia::Color,
|
||||
// Identifier of the current requestAnimationFrame call, if any.
|
||||
@@ -345,7 +344,6 @@ impl RenderState {
|
||||
fonts,
|
||||
viewbox,
|
||||
cached_viewbox: Viewbox::new(0., 0.),
|
||||
cached_target_snapshot: None,
|
||||
images: ImageStore::new(gpu_state.context.clone()),
|
||||
background_color: skia::Color::TRANSPARENT,
|
||||
render_request_id: None,
|
||||
@@ -1094,15 +1092,12 @@ impl RenderState {
|
||||
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();
|
||||
|
||||
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
||||
if self.cached_viewbox.area.width() > 0.0 {
|
||||
// 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,
|
||||
@@ -1111,15 +1106,24 @@ impl RenderState {
|
||||
);
|
||||
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();
|
||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||
let bg_color = self.background_color;
|
||||
|
||||
canvas.translate((
|
||||
(start_tile_x as f32 * tiles::TILE_SIZE) - offset_x,
|
||||
(start_tile_y as f32 * tiles::TILE_SIZE) - offset_y,
|
||||
));
|
||||
// Setup canvas transform
|
||||
{
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
||||
canvas.save();
|
||||
canvas.scale((navigate_zoom, navigate_zoom));
|
||||
canvas.translate((translate_x, translate_y));
|
||||
canvas.clear(bg_color);
|
||||
}
|
||||
|
||||
canvas.clear(self.background_color);
|
||||
canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default()));
|
||||
canvas.restore();
|
||||
// Draw directly from cache surface, avoiding snapshot overhead
|
||||
self.surfaces.draw_cache_to_target();
|
||||
|
||||
// Restore canvas state
|
||||
self.surfaces.canvas(SurfaceId::Target).restore();
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
@@ -1587,7 +1591,7 @@ impl RenderState {
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((image, filter_scale)) = filter_result {
|
||||
if let Some((mut surface, filter_scale)) = filter_result {
|
||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((scale, scale));
|
||||
@@ -1597,34 +1601,26 @@ impl RenderState {
|
||||
|
||||
// If we scaled down in the filter surface, we need to scale back up
|
||||
if filter_scale < 1.0 {
|
||||
let scaled_width = bounds.width() * filter_scale;
|
||||
let scaled_height = bounds.height() * filter_scale;
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
||||
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale));
|
||||
drop_canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
skia::Rect::from_xywh(
|
||||
bounds.left * filter_scale,
|
||||
bounds.top * filter_scale,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
),
|
||||
drop_canvas.translate((bounds.left * filter_scale, bounds.top * filter_scale));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
self.sampling_options,
|
||||
&drop_paint,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
} else {
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
||||
drop_canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
bounds,
|
||||
drop_canvas.save();
|
||||
drop_canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
self.sampling_options,
|
||||
&drop_paint,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
}
|
||||
drop_canvas.restore();
|
||||
}
|
||||
@@ -2097,11 +2093,9 @@ impl RenderState {
|
||||
|
||||
self.surfaces.gc();
|
||||
|
||||
// Cache target surface in a texture
|
||||
// Mark cache as valid for render_from_cache
|
||||
self.cached_viewbox = self.viewbox;
|
||||
|
||||
self.cached_target_snapshot = Some(self.surfaces.snapshot(SurfaceId::Cache));
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
}
|
||||
|
||||
@@ -40,41 +40,21 @@ pub fn render_with_filter_surface<F>(
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
|
||||
// If we scaled down, we need to scale the source rect and adjust the destination
|
||||
if scale < 1.0 {
|
||||
// The image was rendered at a smaller scale, so we need to scale it back up
|
||||
let scaled_width = bounds.width() * scale;
|
||||
let scaled_height = bounds.height() * scale;
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
||||
|
||||
canvas.save();
|
||||
canvas.scale((1.0 / scale, 1.0 / scale));
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
skia::Rect::from_xywh(
|
||||
bounds.left * scale,
|
||||
bounds.top * scale,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
),
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
canvas.translate((bounds.left * scale, bounds.top * scale));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
canvas.restore();
|
||||
} else {
|
||||
// No scaling needed, draw normally
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
bounds,
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
canvas.save();
|
||||
canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
canvas.restore();
|
||||
}
|
||||
true
|
||||
} else {
|
||||
@@ -93,7 +73,7 @@ pub fn render_into_filter_surface<F>(
|
||||
render_state: &mut RenderState,
|
||||
bounds: Rect,
|
||||
draw_fn: F,
|
||||
) -> Option<(skia::Image, f32)>
|
||||
) -> Option<(skia::Surface, f32)>
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
@@ -129,5 +109,6 @@ where
|
||||
|
||||
render_state.surfaces.canvas(filter_id).restore();
|
||||
|
||||
Some((render_state.surfaces.snapshot(filter_id), scale))
|
||||
let filter_surface = render_state.surfaces.surface_clone(filter_id);
|
||||
Some((filter_surface, scale))
|
||||
}
|
||||
|
||||
@@ -175,6 +175,10 @@ impl Surfaces {
|
||||
self.get_mut(id).canvas()
|
||||
}
|
||||
|
||||
pub fn surface_clone(&self, id: SurfaceId) -> skia::Surface {
|
||||
self.get(id).clone()
|
||||
}
|
||||
|
||||
/// Marks a surface as having content (dirty)
|
||||
pub fn mark_dirty(&mut self, id: SurfaceId) {
|
||||
self.dirty_surfaces |= id as u32;
|
||||
@@ -211,6 +215,18 @@ impl Surfaces {
|
||||
);
|
||||
}
|
||||
|
||||
/// Draws the cache surface directly to the target canvas.
|
||||
/// This avoids creating an intermediate snapshot, reducing GPU stalls.
|
||||
pub fn draw_cache_to_target(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.cache.clone().draw(
|
||||
self.target.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
||||
performance::begin_measure!("apply_mut::flags");
|
||||
if ids & SurfaceId::Target as u32 != 0 {
|
||||
@@ -305,6 +321,22 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, id: SurfaceId) -> &skia::Surface {
|
||||
match id {
|
||||
SurfaceId::Target => &self.target,
|
||||
SurfaceId::Filter => &self.filter,
|
||||
SurfaceId::Cache => &self.cache,
|
||||
SurfaceId::Current => &self.current,
|
||||
SurfaceId::DropShadows => &self.drop_shadows,
|
||||
SurfaceId::InnerShadows => &self.inner_shadows,
|
||||
SurfaceId::TextDropShadows => &self.text_drop_shadows,
|
||||
SurfaceId::Fills => &self.shape_fills,
|
||||
SurfaceId::Strokes => &self.shape_strokes,
|
||||
SurfaceId::Debug => &self.debug,
|
||||
SurfaceId::UI => &self.ui,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_from_target(&mut self, target: skia::Surface) {
|
||||
let dim = (target.width(), target.height());
|
||||
self.target = target;
|
||||
@@ -386,14 +418,22 @@ impl Surfaces {
|
||||
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||
);
|
||||
|
||||
if let Some(snapshot) = self.current.image_snapshot_with_bounds(rect) {
|
||||
self.tiles.add(tile_viewbox, tile, snapshot.clone());
|
||||
let snapshot = self.current.image_snapshot();
|
||||
let mut direct_context = self.current.direct_context();
|
||||
let tile_image_opt = snapshot
|
||||
.make_subset(direct_context.as_mut(), rect)
|
||||
.or_else(|| self.current.image_snapshot_with_bounds(rect));
|
||||
|
||||
if let Some(tile_image) = tile_image_opt {
|
||||
// Draw to cache first (takes reference), then move to tile cache
|
||||
self.cache.canvas().draw_image_rect(
|
||||
snapshot.clone(),
|
||||
&tile_image,
|
||||
None,
|
||||
tile_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
|
||||
self.tiles.add(tile_viewbox, tile, tile_image);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,16 +449,57 @@ impl Surfaces {
|
||||
}
|
||||
|
||||
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||
let image = self.tiles.get(tile).unwrap();
|
||||
if let Some(image) = self.tiles.get(tile) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
|
||||
self.target.canvas().draw_rect(rect, &paint);
|
||||
|
||||
self.target
|
||||
.canvas()
|
||||
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the current tile directly to the target and cache surfaces without
|
||||
/// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't
|
||||
/// populate the tile texture cache (suitable for one-shot renders like tests).
|
||||
pub fn draw_current_tile_direct(&mut self, tile_rect: &skia::Rect, color: skia::Color) {
|
||||
let sampling_options = self.sampling_options;
|
||||
let src_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,
|
||||
);
|
||||
let src_rect_f = skia::Rect::from(src_rect);
|
||||
|
||||
// Draw background
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
self.target.canvas().draw_rect(tile_rect, &paint);
|
||||
|
||||
self.target.canvas().draw_rect(rect, &paint);
|
||||
// Draw current surface directly to target (no snapshot)
|
||||
self.current.clone().draw(
|
||||
self.target.canvas(),
|
||||
(
|
||||
tile_rect.left - src_rect_f.left,
|
||||
tile_rect.top - src_rect_f.top,
|
||||
),
|
||||
sampling_options,
|
||||
None,
|
||||
);
|
||||
|
||||
self.target
|
||||
.canvas()
|
||||
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
||||
// Also draw to cache for render_from_cache
|
||||
self.current.clone().draw(
|
||||
self.cache.canvas(),
|
||||
(
|
||||
tile_rect.left - src_rect_f.left,
|
||||
tile_rect.top - src_rect_f.top,
|
||||
),
|
||||
sampling_options,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
||||
@@ -491,9 +572,11 @@ impl TileTextureCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Image, String> {
|
||||
let image = self.grid.get_mut(&tile).unwrap();
|
||||
Ok(image)
|
||||
pub fn get(&mut self, tile: Tile) -> Option<&mut skia::Image> {
|
||||
if self.removed.contains(&tile) {
|
||||
return None;
|
||||
}
|
||||
self.grid.get_mut(&tile)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, tile: Tile) {
|
||||
|
||||
Reference in New Issue
Block a user