From 2ed7e55c318a434f0a3864cb449b98f78221adb0 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Wed, 24 Jun 2026 16:30:36 +0200 Subject: [PATCH] WIP --- render-wasm/src/render.rs | 176 +++++++++++++++++++---------- render-wasm/src/render/debug.rs | 2 +- render-wasm/src/render/export.rs | 6 +- render-wasm/src/render/surfaces.rs | 4 + render-wasm/src/shapes.rs | 20 ++++ 5 files changed, 144 insertions(+), 64 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index afe006f09d..a48cdceb63 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -67,17 +67,11 @@ pub enum RenderFlag { } pub struct TileRenderState { - pub current_tile: Option, - /// 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, + 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 { 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()); diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index fe9bde42b0..d3f08b904a 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -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); diff --git a/render-wasm/src/render/export.rs b/render-wasm/src/render/export.rs index f487cf7894..c4a6ead1d7 100644 --- a/render-wasm/src/render/export.rs +++ b/render-wasm/src/render/export.rs @@ -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 diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index afefeaea87..c1c7f7a6cb 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -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, 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, diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 7fe2cdd515..017145b846 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -179,6 +179,7 @@ pub struct Shape { pub children: Vec, pub selrect: math::Rect, pub transform: Matrix, + transform_centered: Option, pub rotation: f32, pub constraint_h: Option, pub constraint_v: Option, @@ -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; }