This commit is contained in:
Aitor Moreno 2026-06-24 16:30:36 +02:00
parent cf8ff3828b
commit 2ed7e55c31
5 changed files with 144 additions and 64 deletions

View File

@ -67,17 +67,11 @@ pub enum RenderFlag {
}
pub struct TileRenderState {
pub current_tile: Option<tiles::Tile>,
/// True if the current tile had shapes assigned to it when we
/// started rendering it. Lets us distinguish a genuinely empty
/// tile (skip composite, just clear) from a tile whose walker
/// finished its work in a previous PAF and is now being resumed
/// (must composite to present the work). Reset when current_tile
/// changes.
pub current_tile_had_shapes: bool,
pub tile_viewbox: tiles::TileViewbox,
pub current: Option<tiles::Tile>,
pub current_had_shapes: bool,
pub viewbox: tiles::TileViewbox,
pub tiles: tiles::TileHashMap,
pub pending_tiles: PendingTiles,
pub pending: PendingTiles,
}
pub(crate) struct RenderState {
@ -96,6 +90,8 @@ pub(crate) struct RenderState {
// render_area expanded by surface margins — used for visibility checks so that
// shapes in the margin zone are rendered (needed for background blur sampling).
pub render_area_with_margins: Rect,
// This is an structure that keeps all the necessary state
// for tile rendering.
pub tile: TileRenderState,
// nested_fills maintains a stack of group fills that apply to nested shapes
// without their own fill definitions. This is necessary because in SVG, a group's `fill`
@ -164,14 +160,14 @@ impl RenderState {
render_area: Rect::new_empty(),
render_area_with_margins: Rect::new_empty(),
tile: TileRenderState {
current_tile: None,
current_tile_had_shapes: false,
current: None,
current_had_shapes: false,
tiles,
tile_viewbox: tiles::TileViewbox::new_with_interest(
viewbox: tiles::TileViewbox::new_with_interest(
&viewbox,
options.dpr_viewport_interest_area_threshold,
),
pending_tiles: PendingTiles::new(),
pending: PendingTiles::new(),
},
nested_fills: vec![],
nested_blurs: vec![],
@ -267,7 +263,7 @@ impl RenderState {
// Only when this function returns true (it means the value
// was properly changed) the rest of the functions is called.
if self.options.set_dpr(dpr) {
self.tile.tile_viewbox
self.tile.viewbox
.set_interest(self.options.dpr_viewport_interest_area_threshold);
self.resize(
self.viewbox.width().floor() as i32,
@ -291,7 +287,7 @@ impl RenderState {
// The TileViewbox stores its own copy of `interest` (set at
// construction). Without propagating, options change wouldn't
// affect pending_tiles generation.
self.tile.tile_viewbox
self.tile.viewbox
.set_interest(self.options.dpr_viewport_interest_area_threshold);
}
}
@ -321,7 +317,7 @@ impl RenderState {
let dpr_height = (height as f32 * self.options.dpr).floor() as i32;
self.surfaces.resize(dpr_width, dpr_height)?;
self.viewbox.set_wh(width as f32, height as f32);
self.tile.tile_viewbox.update(&self.viewbox);
self.tile.viewbox.update(&self.viewbox);
Ok(())
}
@ -387,6 +383,7 @@ impl RenderState {
}
pub fn reset_canvas(&mut self) {
println!("reset_canvas");
self.surfaces.reset(self.background_color);
self.surfaces.clear_backbuffer(self.background_color);
self.surfaces.clear_target(self.background_color);
@ -566,6 +563,18 @@ impl RenderState {
| innershadows_surface_id as u32
| text_drop_shadows_surface_id as u32;
// NOTE: We don't need to perform all these operations
// unless the shape is rotated.
let mut matrix = shape.get_transform();
// Apply the additional transformation matrix if exists
if let Some(offset) = offset {
matrix.pre_translate(offset);
} else {
// NOTE: I think this could be updated.
// println!("No offset");
}
// Only save canvas state if we have clipping or transforms
// For simple shapes without clipping, skip expensive save/restore
let needs_save =
@ -594,6 +603,7 @@ impl RenderState {
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
});
// TODO: I'm not sure if this is necessary at all.
let can_render_directly = apply_to_current_surface
&& clip_bounds.is_none()
&& offset.is_none()
@ -609,13 +619,48 @@ impl RenderState {
Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_)
)
&& !(shape.fills.is_empty() && has_nested_fills)
&& !shape
.svg_attrs
.as_ref()
.is_some_and(|attrs| attrs.fill_none)
&& target_surface != SurfaceId::Export;
// println!("can_render_directly {}\n
// \tapply_to_current_surface {}\n
// \tclip_bounds.is_none {}\n
// \toffset.is_none {}\n
// \tparent_shadows {}\n
// \t!shape.needs_layer {}\n
// \tshape.blur.is_none {}\n
// \tshape.background_blur.is_none {}\n
// \thas_inherited_blur {}\n
// \tshape.shadows.is_empty {}\n
// \tshape.transform.is_identity {}\n
// \tmatches!(shape.shape_type, Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_)) {}\n
// \t!(shape.fills.is_empty() && has_nested_fills) {}\n
// \t!shape.svg_attrs.as_ref().is_some_and(|attrs| attrs.fill_none) {}\n
// \ttarget_surface != SurfaceId::Export {}\n",
// can_render_directly,
// apply_to_current_surface,
// clip_bounds.is_none(),
// offset.is_none(),
// parent_shadows.is_none(),
// !shape.needs_layer(),
// shape.blur.is_none(),
// shape.background_blur.is_none(),
// !has_inherited_blur,
// shape.shadows.is_empty(),
// shape.transform.is_identity(),
// matches!(
// shape.shape_type,
// Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_)
// ),
// !(shape.fills.is_empty() && has_nested_fills),
// !shape
// .svg_attrs
// .as_ref()
// .is_some_and(|attrs| attrs.fill_none),
// target_surface != SurfaceId::Export
// );
if can_render_directly {
println!("can_render_directly");
let scale = self.get_scale_fast();
let translation = self
.surfaces
@ -628,7 +673,20 @@ impl RenderState {
canvas.translate(translation);
});
fills::render(self, shape, &shape.fills, antialias, target_surface, None)?;
if !shape
.svg_attrs
.as_ref()
.is_some_and(|attrs| attrs.fill_none) {
fills::render(
self,
shape,
&shape.fills,
antialias,
target_surface,
None
)?;
}
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
strokes::render(
@ -685,15 +743,15 @@ impl RenderState {
// This renders a red line around clipped
// shapes (frames).
if self.options.is_debug_visible() {
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
paint.set_stroke_width(4.);
self.surfaces
.canvas(fills_surface_id)
.draw_rect(bounds, &paint);
}
// if self.options.is_debug_visible() {
// let mut paint = skia::Paint::default();
// paint.set_style(skia::PaintStyle::Stroke);
// paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
// paint.set_stroke_width(4.);
// self.surfaces
// .canvas(fills_surface_id)
// .draw_rect(bounds, &paint);
// }
// Uncomment to debug the render_position_data
// if let Type::Text(text_content) = &shape.shape_type {
@ -752,16 +810,6 @@ impl RenderState {
None
};
let center = shape.center();
let mut matrix = shape.transform;
matrix.post_translate(center);
matrix.pre_translate(-center);
// Apply the additional transformation matrix if exists
if let Some(offset) = offset {
matrix.pre_translate(offset);
}
match &shape.shape_type {
Type::SVGRaw(sr) => {
if let Some(svg_transform) = shape.svg_transform() {
@ -1073,8 +1121,7 @@ impl RenderState {
let shape = &shape;
if shape.fills.is_empty()
&& !matches!(shape.shape_type, Type::Group(_))
&& !matches!(shape.shape_type, Type::Frame(_))
&& !matches!(shape.shape_type, Type::Group(_) | Type::Frame(_))
&& !shape
.svg_attrs
.as_ref()
@ -1165,7 +1212,7 @@ impl RenderState {
}
pub fn update_render_context(&mut self, tile: tiles::Tile) {
self.tile.current_tile = Some(tile);
self.tile.current = Some(tile);
let scale = self.get_scale();
self.render_area = tiles::get_tile_rect(tile, scale);
let margins = self.surfaces.margins();
@ -1214,18 +1261,27 @@ impl RenderState {
Ok(())
}
fn populate_pending_nodes(&mut self) {
}
/// Clears all the necessary vecs and hashmaps.
/// Also garbage collects surfaces.
fn clear(&mut self, tree: ShapesPoolRef) {
#[cfg(feature = "stats")]
self.stats.clear();
// We clear the backbuffer and the target.
self.surfaces.clear_backbuffer(self.background_color);
self.surfaces.clear_target(self.background_color);
// We clear the pending nodes and
// we generate the pending nodes.
self.pending_nodes.clear();
self.pending_nodes.reserve(tree.len());
self.populate_pending_nodes();
// Clear nested state stacks to avoid residual fills/blurs from previous renders
// being incorrectly applied to new frames
self.nested_fills.clear();
@ -1234,7 +1290,7 @@ impl RenderState {
self.nested_shadows.clear();
// reorder by distance to the center.
self.tile.current_tile = None;
self.tile.current = None;
self.empty_grid_frame_ids.clear();
if self.show_grid.is_some() {
@ -1268,7 +1324,7 @@ impl RenderState {
}
}
self.tile.tile_viewbox.update(&self.viewbox);
self.tile.viewbox.update(&self.viewbox);
self.rebuild_tile_index();
if self.viewbox.is_updated(ViewboxUpdated::Zoom as u32) {
@ -1327,8 +1383,8 @@ impl RenderState {
performance::begin_measure!("tile_cache");
let only_visible = self.options.is_interactive_transform();
self.tile.pending_tiles
.update(&self.tile.tile_viewbox, &self.surfaces, only_visible);
self.tile.pending
.update(&self.tile.viewbox, &self.surfaces, only_visible);
performance::end_measure!("tile_cache");
performance::end_timed_log!("tile_cache_update", _tile_start);
@ -1372,7 +1428,7 @@ impl RenderState {
// if !self.options.is_interactive_transform() && matches!(frame_type, FrameType::Full) {
self.surfaces.draw_tile_atlas_to_backbuffer(
&self.viewbox,
&self.tile.tile_viewbox
&self.tile.viewbox
);
// }
@ -1406,7 +1462,7 @@ impl RenderState {
if !self.viewer_masked_pass() {
self.surfaces.draw_tile_atlas_to_backbuffer(
&self.viewbox,
&self.tile.tile_viewbox
&self.tile.viewbox
);
}
@ -1438,8 +1494,8 @@ impl RenderState {
// popping in sequentially. Only yield once all visible work is
// done and we are processing the interest-area pre-render.
if self.options.is_interactive_transform() {
if let Some(tile) = self.tile.current_tile {
if self.tile.tile_viewbox.is_visible(&tile) {
if let Some(tile) = self.tile.current {
if self.tile.viewbox.is_visible(&tile) {
return false;
}
}
@ -1556,7 +1612,7 @@ impl RenderState {
/// consistent and predictable layout.
pub fn get_current_aligned_tile_bounds(&mut self) -> Result<Rect> {
Ok(self.get_aligned_tile_bounds(
self.tile.current_tile
self.tile.current
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
))
}
@ -1910,7 +1966,7 @@ impl RenderState {
};
while !should_stop {
if let Some(current_tile) = self.tile.current_tile {
if let Some(current_tile) = self.tile.current {
// println!("current_tile {:?}", current_tile);
// NOTE: For now we don't need to cover the case where the tile
// is not cached because everything will be handled from draw_atlas.
@ -1932,12 +1988,12 @@ impl RenderState {
// the tile has unfinished work from a previous PAF
// (`current_tile_had_shapes` was set when we populated pending_nodes
// for this tile).
if !is_empty || self.tile.current_tile_had_shapes {
if !is_empty || self.tile.current_had_shapes {
let tile_rect = self.get_current_aligned_tile_bounds()?;
let current_tile = *self
.tile
.current_tile
.current
.as_ref()
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
@ -1966,13 +2022,13 @@ impl RenderState {
// If we finish processing every node rendering is complete
// let's check if there are more pending nodes
if let Some(next_tile) = self.tile.pending_tiles.pop() {
if let Some(next_tile) = self.tile.pending.pop() {
self.update_render_context(next_tile);
// Reset for the new tile. We'll flip it to true if the
// tile has shapes, so a later "is_empty=true" reflects
// a resumed-from-yield case rather than a genuinely
// empty tile.
self.tile.current_tile_had_shapes = false;
self.tile.current_had_shapes = false;
let viewer_masked_pass = self.viewer_masked_pass();
@ -2017,7 +2073,7 @@ impl RenderState {
}
if !valid_ids.is_empty() {
self.tile.current_tile_had_shapes = true;
self.tile.current_had_shapes = true;
}
self.pending_nodes
@ -2059,7 +2115,7 @@ impl RenderState {
let extrect = shape.extrect(tree, scale);
let tile_size = tiles::get_tile_size(scale);
let shape_tiles = tiles::get_tiles_for_rect(extrect, tile_size);
let interest_rect = &self.tile.tile_viewbox.interest_rect;
let interest_rect = &self.tile.viewbox.interest_rect;
// Calculate the intersection of shape_tiles with interest_rect
// This returns only the tiles that are both in the shape and in the interest area
let intersection_x1 = shape_tiles.x1().max(interest_rect.x1());

View File

@ -69,7 +69,7 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
}
pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
let tiles::TileRect(sx, sy, ex, ey) = render_state.tile.tile_viewbox.interest_rect;
let tiles::TileRect(sx, sy, ex, ey) = render_state.tile.viewbox.interest_rect;
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
let mut paint = skia::Paint::default();
paint.set_color(skia::Color::RED);

View File

@ -19,7 +19,7 @@ pub fn render_shape_pixels(
let saved_export_context = render_state.export_context;
let saved_render_area = render_state.render_area;
let saved_render_area_with_margins = render_state.render_area_with_margins;
let saved_current_tile = render_state.tile.current_tile;
let saved_current_tile = render_state.tile.current;
let saved_pending_nodes = std::mem::take(&mut render_state.pending_nodes);
let saved_nested_fills = std::mem::take(&mut render_state.nested_fills);
let saved_nested_blurs = std::mem::take(&mut render_state.nested_blurs);
@ -77,7 +77,7 @@ pub fn render_shape_pixels(
render_state.export_context = saved_export_context;
render_state.render_area = saved_render_area;
render_state.render_area_with_margins = saved_render_area_with_margins;
render_state.tile.current_tile = saved_current_tile;
render_state.tile.current = saved_current_tile;
render_state.pending_nodes = saved_pending_nodes;
render_state.nested_fills = saved_nested_fills;
render_state.nested_blurs = saved_nested_blurs;
@ -86,7 +86,7 @@ pub fn render_shape_pixels(
render_state.preview_mode = saved_preview_mode;
let workspace_scale = render_state.get_scale();
if let Some(tile) = render_state.tile.current_tile {
if let Some(tile) = render_state.tile.current {
render_state.update_render_context(tile);
} else if !render_state.render_area.is_empty() {
render_state

View File

@ -66,13 +66,17 @@ pub struct Surfaces {
export: skia::Surface,
// Persistent viewport-sized surface used to keep the last presented frame.
backbuffer: skia::Surface,
// Atlas used to keep tiles.
tile_atlas: skia::Surface,
tile_atlas_image: Option<skia::Image>,
tiles: TileTextureCache,
sampling_options: skia::SamplingOptions,
atlas_sampling_options: skia::SamplingOptions,
pub margins: skia::ISize,
// Tracks which surfaces have content (dirty flag bitmask)
dirty_surfaces: u32,
extra_tile_dims: skia::ISize,

View File

@ -179,6 +179,7 @@ pub struct Shape {
pub children: Vec<Uuid>,
pub selrect: math::Rect,
pub transform: Matrix,
transform_centered: Option<Matrix>,
pub rotation: f32,
pub constraint_h: Option<ConstraintH>,
pub constraint_v: Option<ConstraintV>,
@ -281,6 +282,7 @@ impl Shape {
children: Vec::new(),
selrect: math::Rect::new_empty(),
transform: Matrix::default(),
transform_centered: None,
rotation: 0.,
constraint_h: None,
constraint_v: None,
@ -417,11 +419,29 @@ impl Shape {
self.invalidate_extrect();
}
pub fn is_rotated(&self) -> bool {
self.rotation != 0.0
}
pub fn set_transform(&mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) {
self.transform = Matrix::new_all(a, c, e, b, d, f, 0.0, 0.0, 1.0);
if self.transform_centered.is_none() && self.is_rotated() {
let center= self.center();
let mut matrix = self.transform;
matrix.post_translate(center);
matrix.pre_translate(-center);
self.transform_centered = Some(matrix);
}
self.invalidate_extrect();
}
pub fn get_transform(&self) -> Matrix {
let Some(transform) = self.transform_centered else {
return self.transform;
};
transform
}
pub fn set_opacity(&mut self, opacity: f32) {
self.opacity = opacity;
}