From 2c42d4fb06711afcb2cce8fefa84522fe05f107e Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Tue, 30 Jun 2026 09:45:29 +0200 Subject: [PATCH] WIP --- render-wasm/Cargo.toml | 2 +- render-wasm/src/globals.rs | 26 +- render-wasm/src/main.rs | 18 +- render-wasm/src/render.rs | 624 +++++++++++++++++++------- render-wasm/src/render/debug.rs | 4 +- render-wasm/src/render/drop_shadow.rs | 3 +- render-wasm/src/render/enter_exit.rs | 193 -------- render-wasm/src/render/export.rs | 98 ---- render-wasm/src/render/surfaces.rs | 50 +-- render-wasm/src/shapes.rs | 11 +- render-wasm/src/shapes/modifiers.rs | 10 +- render-wasm/src/state.rs | 12 +- render-wasm/src/tiles.rs | 174 ++++++- render-wasm/src/wasm/fills/image.rs | 4 +- render-wasm/src/wasm/ui.rs | 2 +- 15 files changed, 686 insertions(+), 545 deletions(-) delete mode 100644 render-wasm/src/render/enter_exit.rs delete mode 100644 render-wasm/src/render/export.rs diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index febfeaae69..9548d7b583 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -9,7 +9,7 @@ description = "Wasm-based canvas renderer for Penpot" build = "build.rs" [features] -default = [] +default = ["profile"] stats = [] profile = ["profile-macros", "profile-raf"] diff --git a/render-wasm/src/globals.rs b/render-wasm/src/globals.rs index 64e7d8c94f..5f65348ef3 100644 --- a/render-wasm/src/globals.rs +++ b/render-wasm/src/globals.rs @@ -5,12 +5,13 @@ use crate::emscripten::init_gl; use crate::mem; use crate::render::{gpu_state::GpuState, RenderState}; -use crate::state::{State, TextEditorState, UIState}; +use crate::state::{DesignState, TextEditorState, UIState}; +use crate::tiles::TileRenderState; -static mut DESIGN_STATE: *mut State = std::ptr::null_mut(); +static mut DESIGN_STATE: *mut DesignState = std::ptr::null_mut(); /// Design State. -pub(crate) fn get_design_state() -> &'static mut State { +pub(crate) fn get_design_state() -> &'static mut DesignState { unsafe { debug_assert!(!DESIGN_STATE.is_null(), "Design State is null"); &mut *DESIGN_STATE @@ -39,6 +40,16 @@ pub(crate) fn get_render_state() -> &'static mut RenderState { } } +static mut TILE_RENDER_STATE: *mut TileRenderState = std::ptr::null_mut(); + +#[inline(always)] +pub(crate) fn get_tile_render_state() -> &'static mut TileRenderState { + unsafe { + debug_assert!(!TILE_RENDER_STATE.is_null(), "Tile Render State is null"); + &mut *TILE_RENDER_STATE + } +} + /// Text Editor State static mut TEXT_EDITOR_STATE: *mut TextEditorState = std::ptr::null_mut(); @@ -113,10 +124,16 @@ fn render_init(width: i32, height: i32) { } } +fn tile_render_init() { + unsafe { + TILE_RENDER_STATE = Box::into_raw(Box::new(TileRenderState::new())); + } +} + /// Initializes DesignState. fn design_init() { unsafe { - let design_state = State::new(); + let design_state = DesignState::new(); DESIGN_STATE = Box::into_raw(Box::new(design_state)); } } @@ -144,6 +161,7 @@ pub extern "C" fn init(width: i32, height: i32) -> Result<()> { init_gl!(); gpu_init(); render_init(width, height); + tile_render_init(); text_editor_init(); design_init(); ui_init(); diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 871f6f520c..e674a96a21 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -20,7 +20,10 @@ use std::collections::HashMap; #[allow(unused_imports)] use crate::error::{Error, Result}; -use crate::{render::{FrameType, RenderFlag}, shapes::Frame}; +use crate::{ + render::{FrameType, RenderFlag}, + shapes::Frame, +}; use globals::{get_design_state, get_gpu_state, get_render_state}; @@ -115,22 +118,17 @@ pub extern "C" fn render2(timestamp: i32, flags: u8) -> Result { // si el render es sync o no y que esto no permita let allow_stop = true; render_state - .continue_render_loop( - timestamp, - allow_stop - ) + .continue_render_loop(timestamp, allow_stop) .map_err(|_| Error::RecoverableError("Error rendering".to_string()))? } else { let sync_render = false; - render_state.start_render_loop( - timestamp, - sync_render - ).map_err(|_| Error::RecoverableError("Error rendering".to_string()))? + render_state + .start_render_loop(timestamp, sync_render) + .map_err(|_| Error::RecoverableError("Error rendering".to_string()))? }; render_state.end_render_loop(&frame_type); return Ok(frame_type); }); - } /* diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index c629ad2c69..25fe81768c 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,8 +1,6 @@ mod background_blur; mod debug; pub mod drop_shadow; -pub mod enter_exit; -pub mod export; mod fills; pub mod filters; pub mod focus_mode; @@ -27,7 +25,7 @@ pub mod walk; use skia_safe::{self as skia, Matrix, RRect, Rect}; use std::borrow::Cow; -use std::collections::{HashSet}; +use std::collections::{HashMap, HashSet}; use options::RenderOptions; pub use surfaces::{SurfaceId, Surfaces}; @@ -37,12 +35,15 @@ pub(crate) use walk::{get_simplified_children, sort_z_index}; pub use walk::{ClipStack, NodeRenderState, RenderStats}; use crate::error::{Error, Result}; -use crate::globals::get_design_state; +use crate::globals::{get_design_state, get_tile_render_state}; use crate::shapes::{ - Blur, BlurType, Fill, Frame, Layout, Shadow, Shape, Stroke, StrokeKind, TextContent, Type, all_with_ancestors, + all_with_ancestors, radius_to_sigma, Blur, BlurType, Fill, Frame, Layout, Shadow, Shape, + Stroke, StrokeKind, TextContent, Type, }; use crate::state::{RulerState, ShapesPoolMutRef, ShapesPoolRef}; -use crate::tiles::{self, PendingTiles, TileRect}; +use crate::tiles::{ + self, PendingTiles, Tile, TileDisplayItem, TileDisplayPhase, TileHashMap, TileRect, TileViewbox, +}; use crate::uuid::Uuid; use crate::view::{Viewbox, ViewboxUpdated}; use crate::wapi; @@ -66,12 +67,9 @@ pub enum RenderFlag { Full = 2, } -pub struct TileRenderState { - pub current: Option, - pub current_had_shapes: bool, - pub viewbox: tiles::TileViewbox, - pub tiles: tiles::TileHashMap, - pub pending: PendingTiles, +pub struct RenderResult { + is_empty: bool, + early_return: bool, } pub(crate) struct RenderState { @@ -90,9 +88,6 @@ 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` // can affect its child elements if they don't specify one themselves. If the planned @@ -143,7 +138,6 @@ impl RenderState { // time we reuse this one. let viewbox = Viewbox::new(width as f32, height as f32); - let tiles = tiles::TileHashMap::new(); let options = RenderOptions::default(); Ok(Self { @@ -159,16 +153,6 @@ impl RenderState { sampling_options, render_area: Rect::new_empty(), render_area_with_margins: Rect::new_empty(), - tile: TileRenderState { - current: None, - current_had_shapes: false, - tiles, - viewbox: tiles::TileViewbox::new_with_interest( - &viewbox, - options.dpr_viewport_interest_area_threshold, - ), - pending: PendingTiles::new(), - }, nested_fills: vec![], nested_blurs: vec![], cached_layer_blur: None, @@ -263,7 +247,9 @@ 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.viewbox + let tile_render_state = get_tile_render_state(); + tile_render_state + .viewbox .set_interest(self.options.dpr_viewport_interest_area_threshold); self.resize( self.viewbox.width().floor() as i32, @@ -284,10 +270,12 @@ impl RenderState { // Only when this function returns true (it means the value // was changed properly) the tile_viewbox.set_interest is called. if self.options.set_viewport_interest_area_threshold(value) { + let tile_render_state = get_tile_render_state(); // The TileViewbox stores its own copy of `interest` (set at // construction). Without propagating, options change wouldn't // affect pending_tiles generation. - self.tile.viewbox + tile_render_state + .viewbox .set_interest(self.options.dpr_viewport_interest_area_threshold); } } @@ -317,7 +305,8 @@ 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.viewbox.update(&self.viewbox); + let tile_render_state = get_tile_render_state(); + tile_render_state.viewbox.update(&self.viewbox); Ok(()) } @@ -329,15 +318,16 @@ impl RenderState { /// Copy the clean (no UI overlay) Backbuffer to Target, draw UI/debug overlays /// on top of Target, then present. Backbuffer is left clean so it can be reused /// as-is across interactive-transform frames without stale overlay pixels. - pub fn present_frame(&mut self, tree: ShapesPoolRef) { + pub fn present_frame(&mut self) { + let design_state = get_design_state(); + let tree = &design_state.shapes; // Viewer masked passes render a partial scene onto a transparent backbuffer. // SrcOver would keep pass-1 pixels wherever the backbuffer stays transparent. if self.viewer_masked_pass() { self.surfaces.clear_target(skia::Color::TRANSPARENT); self.surfaces.copy_backbuffer_to_target(); } else { - self.surfaces - .copy_backbuffer_to_target(); + self.surfaces.copy_backbuffer_to_target(); } if self.options.is_debug_visible() { @@ -557,7 +547,7 @@ impl RenderState { ) -> Result<()> { #[cfg(feature = "stats")] self.stats.count(shape.id); - println!("render_shape"); + // println!("render_shape"); let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 @@ -591,7 +581,7 @@ impl RenderState { // gestures + pan/zoom). AA edge sampling is per-pixel and adds // up across many shapes; reverts to full quality on commit. let antialias = true; - // && shape.should_use_antialias(self.get_scale_fast(), self.options.antialias_threshold); + // && shape.should_use_antialias(self.viewbox.get_scale(), self.options.antialias_threshold); let skip_effects = false; let has_nested_fills = self @@ -661,7 +651,7 @@ impl RenderState { // ); if can_render_directly { - let scale = self.get_scale_fast(); + let scale = self.viewbox.get_scale(); let translation = self .surfaces .get_render_context_translation(self.render_area, scale); @@ -676,15 +666,9 @@ impl RenderState { if !shape .svg_attrs .as_ref() - .is_some_and(|attrs| attrs.fill_none) { - fills::render( - self, - shape, - &shape.fills, - antialias, - target_surface, - None - )?; + .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. @@ -717,7 +701,7 @@ impl RenderState { // set clipping if let Some(clips) = clip_bounds.as_ref() { - let scale = self.get_scale_fast(); + let scale = self.viewbox.get_scale(); for (mut bounds, corners, transform, _inverse_transform) in clips.iter() { self.surfaces.apply_mut(surface_ids, |s| { s.canvas().concat(transform); @@ -821,7 +805,6 @@ impl RenderState { .concat(&matrix); if let Some(svg) = shape.svg.as_ref() { - println!("Never passes"); svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id)); } else { let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone()); @@ -858,7 +841,7 @@ impl RenderState { let count_inner_strokes = shape.count_visible_inner_strokes(); // Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge. let text_fill_inset = - (count_inner_strokes > 0).then(|| 1.0 / self.get_scale_fast()); + (count_inner_strokes > 0).then(|| 1.0 / self.viewbox.get_scale()); let text_stroke_blur_outset = Stroke::max_bounds_width(shape.visible_strokes(), false); let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None); @@ -1213,7 +1196,9 @@ impl RenderState { } pub fn update_render_context(&mut self, tile: tiles::Tile) { - self.tile.current = Some(tile); + let tile_render_state = get_tile_render_state(); + tile_render_state.current = Some(tile); + let scale = self.get_scale(); self.render_area = tiles::get_tile_rect(tile, scale); let margins = self.surfaces.margins(); @@ -1262,8 +1247,21 @@ impl RenderState { Ok(()) } - fn populate_pending_nodes(&mut self) { + // NOTA: Esto sólo debería llamarse cuando el árbol cambia + // o una shape se cambia de lugar. + fn populate_tile_pending_nodes(&mut self) -> Result<()> { + println!("populate_tile_pending_nodes"); + performance::begin_measure!("populate_tile_pending_nodes"); + let tile_render_state = get_tile_render_state(); + // Clears tile pending nodes. + tile_render_state.display_list.compute_from( + Uuid::nil() + ); + + // println!("display_list {:?}", tile_render_state.display_list); + performance::end_measure!("populate_tile_pending_nodes"); + Ok(()) } /// Clears all the necessary vecs and hashmaps. @@ -1281,8 +1279,6 @@ impl RenderState { 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(); @@ -1291,7 +1287,11 @@ impl RenderState { self.nested_shadows.clear(); // reorder by distance to the center. - self.tile.current = None; + let tile_render_state = get_tile_render_state(); + tile_render_state.current = None; + + // WIP: Generamos la lista de nodos pendientes por cada tile. + self.populate_tile_pending_nodes(); self.empty_grid_frame_ids.clear(); if self.show_grid.is_some() { @@ -1325,7 +1325,8 @@ impl RenderState { } } - self.tile.viewbox.update(&self.viewbox); + let tile_render_state = get_tile_render_state(); + tile_render_state.viewbox.update(&self.viewbox); self.rebuild_tile_index(); if self.viewbox.is_updated(ViewboxUpdated::Zoom as u32) { @@ -1338,17 +1339,13 @@ impl RenderState { match frame_type { FrameType::Partial => { self.viewbox.update_handled(); - }, - FrameType::Full => {}, + } + FrameType::Full => {} _ => {} }; } - pub fn start_render_loop( - &mut self, - timestamp: i32, - sync_render: bool, - ) -> Result { + pub fn start_render_loop(&mut self, timestamp: i32, sync_render: bool) -> Result { let design_state = get_design_state(); let tree = &design_state.shapes; self.clear(tree); @@ -1384,8 +1381,10 @@ impl RenderState { performance::begin_measure!("tile_cache"); let only_visible = self.options.is_interactive_transform(); - self.tile.pending - .update(&self.tile.viewbox, &self.surfaces, only_visible); + let tile_render_state = get_tile_render_state(); + tile_render_state + .pending + .update(&tile_render_state.viewbox, &self.surfaces, only_visible); performance::end_measure!("tile_cache"); performance::end_timed_log!("tile_cache_update", _tile_start); @@ -1408,29 +1407,21 @@ impl RenderState { Ok(frame_type) } - pub fn continue_render_loop( - &mut self, - timestamp: i32, - allow_stop: bool, - ) -> Result { - let design_state = get_design_state(); - let tree = &design_state.shapes; + pub fn continue_render_loop(&mut self, timestamp: i32, allow_stop: bool) -> Result { performance::begin_measure!("continue_render_loop"); + let tile_render_state = get_tile_render_state(); // println!("continue_render_loop {:p} {:p}", // std::ptr::addr_of!(self.base_object), // std::ptr::addr_of!(tree), // ); - let frame_type = - self.render_shape_tree_tiled(tree, timestamp, allow_stop)?; + let frame_type = self.render_shape_tree_tiled(timestamp, allow_stop)?; // `draw_atlas` needs a snapshot of the tile atlas. Partial frames are not // presented (only flushed), so defer composition to the final frame and // avoid re-snapshotting up to 4096² on every rAF during async tile work. // if !self.options.is_interactive_transform() && matches!(frame_type, FrameType::Full) { - self.surfaces.draw_tile_atlas_to_backbuffer( - &self.viewbox, - &self.tile.viewbox - ); + self.surfaces + .draw_tile_atlas_to_backbuffer(&self.viewbox, &tile_render_state.viewbox); // } match frame_type { @@ -1438,10 +1429,10 @@ impl RenderState { panic!("FrameType::None"); } FrameType::Partial => { - self.present_frame(tree); + self.present_frame(); } FrameType::Full => { - self.present_frame(tree); + self.present_frame(); wapi::notify_tiles_render_complete!(); performance::end_measure!("render"); } @@ -1450,26 +1441,25 @@ impl RenderState { Ok(frame_type) } - pub fn render_shape_tree_sync( - &mut self, - timestamp: i32, - ) -> Result { - let design_state = get_design_state(); - let tree = &design_state.shapes; - self.render_shape_tree_tiled(tree, timestamp, false)?; + pub fn render_shape_tree(&mut self, timestamp: i32) -> Result { + // TODO: Make a version of the render_shape_tree without tiles. + Ok(FrameType::Full) + } + + pub fn render_shape_tree_sync(&mut self, timestamp: i32) -> Result { + self.render_shape_tree_tiled(timestamp, false)?; // Same composition as `continue_render_loop` for full frames: snapshot only the // drawable tile rect into the atlas (no blur-margin overlap), then blit once. if !self.viewer_masked_pass() { - self.surfaces.draw_tile_atlas_to_backbuffer( - &self.viewbox, - &self.tile.viewbox - ); + let tile_render_state = get_tile_render_state(); + self.surfaces + .draw_tile_atlas_to_backbuffer(&self.viewbox, &tile_render_state.viewbox); } let saved_preview_mode = self.preview_mode; self.preview_mode = true; - self.present_frame(tree); + self.present_frame(); self.preview_mode = saved_preview_mode; Ok(FrameType::Full) } @@ -1481,7 +1471,87 @@ impl RenderState { scale: f32, timestamp: i32, ) -> Result<(Vec, i32, i32)> { - export::render_shape_pixels(self, id, tree, scale, timestamp) + let target_surface = SurfaceId::Export; + let tile_render_state = get_tile_render_state(); + + let saved_focus_mode = self.focus_mode.clone(); + let saved_export_context = self.export_context; + let saved_render_area = self.render_area; + let saved_render_area_with_margins = self.render_area_with_margins; + let saved_current_tile = tile_render_state.current; + let saved_pending_nodes = std::mem::take(&mut self.pending_nodes); + let saved_nested_fills = std::mem::take(&mut self.nested_fills); + let saved_nested_blurs = std::mem::take(&mut self.nested_blurs); + let saved_nested_shadows = std::mem::take(&mut self.nested_shadows); + let saved_ignore_nested_blurs = self.ignore_nested_blurs; + let saved_preview_mode = self.preview_mode; + + self.focus_mode.clear(); + + self.surfaces + .canvas(target_surface) + .clear(skia::Color::TRANSPARENT); + + if tree.len() != 0 { + let Some(shape) = tree.get(id) else { + return Ok((Vec::new(), 0, 0)); + }; + let mut extrect = shape.extrect(tree, scale); + self.export_context = Some((extrect, scale)); + let margins = self.surfaces.margins; + extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale)); + + self.surfaces.resize_export_surface(scale, extrect); + self.render_area = extrect; + self.render_area_with_margins = extrect; + self.surfaces.update_render_context(extrect, scale); + + self.pending_nodes.push(NodeRenderState { + id: *id, + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: false, + flattened: false, + }); + self.render_shape_tree_tile(timestamp, false, true)?; + } + + self.export_context = None; + + self.surfaces.flush_and_submit(target_surface); + + let image = self.surfaces.snapshot(target_surface); + let data = image + .encode( + Some(&mut get_gpu_state().context), + skia::EncodedImageFormat::PNG, + 100, + ) + .expect("PNG encode failed"); + let skia::ISize { width, height } = image.dimensions(); + + self.focus_mode = saved_focus_mode; + self.export_context = saved_export_context; + self.render_area = saved_render_area; + self.render_area_with_margins = saved_render_area_with_margins; + tile_render_state.current = saved_current_tile; + self.pending_nodes = saved_pending_nodes; + self.nested_fills = saved_nested_fills; + self.nested_blurs = saved_nested_blurs; + self.nested_shadows = saved_nested_shadows; + self.ignore_nested_blurs = saved_ignore_nested_blurs; + self.preview_mode = saved_preview_mode; + + let workspace_scale = self.get_scale(); + if let Some(tile) = tile_render_state.current { + self.update_render_context(tile); + } else if !self.render_area.is_empty() { + self.surfaces + .update_render_context(self.render_area, workspace_scale); + } + + Ok((data.as_bytes().to_vec(), width, height)) } #[inline] @@ -1495,8 +1565,9 @@ 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 { - if self.tile.viewbox.is_visible(&tile) { + let tile_render_state = get_tile_render_state(); + if let Some(tile) = tile_render_state.current { + if tile_render_state.viewbox.is_visible(&tile) { return false; } } @@ -1545,6 +1616,7 @@ impl RenderState { } } + #[inline(always)] pub fn render_shape_enter( &mut self, element: &Shape, @@ -1552,10 +1624,74 @@ impl RenderState { clip_bounds: Option<&ClipStack>, target_surface: SurfaceId, ) { - enter_exit::render_shape_enter(self, element, mask, clip_bounds, target_surface) + if let Type::Group(group) = element.shape_type { + let fills = &element.fills; + let shadows = &element.shadows; + self.nested_fills.push(fills.to_vec()); + self.nested_shadows.push(shadows.to_vec()); + + if group.masked { + let mask_group_blur = element.masked_group_layer_blur().is_some(); + if mask_group_blur { + self.surfaces.canvas(target_surface).save(); + if let Some(clips) = clip_bounds { + let scale = self.get_scale(); + let antialias = + element.should_use_antialias(scale, self.options.antialias_threshold); + self.clip_target_surface_to_stack(clips, target_surface, scale, antialias); + } + } + + let mut paint = skia::Paint::default(); + if let Some(blur) = element.masked_group_layer_blur() { + let scale = self.get_scale(); + let sigma = radius_to_sigma(blur.value * scale); + if let Some(filter) = + skia::image_filters::blur((sigma, sigma), None, None, None) + { + paint.set_image_filter(filter); + } + } + + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + self.surfaces.canvas(target_surface).save_layer(&layer_rec); + } + } + + if let Type::Frame(_) = element.shape_type { + self.nested_fills.push(Vec::new()); + } + + if mask { + let mut mask_paint = skia::Paint::default(); + mask_paint.set_blend_mode(skia::BlendMode::DstIn); + let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint); + self.surfaces.canvas(target_surface).save_layer(&mask_rec); + } + + let needs_layer = element.needs_layer(); + + if needs_layer { + let mut paint = skia::Paint::default(); + paint.set_blend_mode(element.blend_mode().into()); + paint.set_alpha_f(element.opacity()); + + if let Some(frame_blur) = layer_blur::frame_clip_layer_blur(element) { + let scale = self.get_scale(); + let sigma = radius_to_sigma(frame_blur.value * scale); + if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) { + paint.set_image_filter(filter); + } + } + + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + self.surfaces.canvas(target_surface).save_layer(&layer_rec); + } + + self.focus_mode.enter(&element.id); } - #[inline] + #[inline(always)] pub fn render_shape_exit( &mut self, element: &Shape, @@ -1563,7 +1699,95 @@ impl RenderState { clip_bounds: Option, target_surface: SurfaceId, ) -> Result<()> { - enter_exit::render_shape_exit(self, element, visited_mask, clip_bounds, target_surface) + if visited_mask { + if let Type::Group(group) = element.shape_type { + if group.masked { + self.surfaces.canvas(target_surface).restore(); + } + } + } else if let Type::Group(group) = element.shape_type { + if group.masked { + self.pending_nodes.push(NodeRenderState { + id: element.id, + visited_children: true, + clip_bounds: None, + visited_mask: true, + mask: false, + flattened: false, + }); + if let Some(&mask_id) = element.mask_id() { + /* + self.pending_nodes.push(NodeRenderState { + id: mask_id, + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: true, + flattened: false, + }); + */ + } + } + } + + match element.shape_type { + Type::Frame(_) | Type::Group(_) => { + self.nested_fills.pop(); + self.nested_blurs.pop(); + self.cached_layer_blur = None; + self.nested_shadows.pop(); + } + _ => {} + } + + let needs_exit_strokes = self.focus_mode.is_active() + && (element.clip() + || (matches!(element.shape_type, Type::Frame(_)) && element.has_inner_stroke())); + + if needs_exit_strokes { + let mut element_strokes: Cow = Cow::Borrowed(element); + element_strokes.to_mut().clear_fills(); + element_strokes.to_mut().clear_shadows(); + element_strokes.to_mut().clip_content = false; + + if !element.clip() { + let is_open = element.is_open(); + element_strokes + .to_mut() + .strokes + .retain(|s| s.render_kind(is_open) == StrokeKind::Inner); + } + + if layer_blur::frame_clip_layer_blur(element).is_some() { + element_strokes.to_mut().set_blur(None); + } + self.render_shape( + &element_strokes, + clip_bounds, + SurfaceId::Fills, + SurfaceId::Strokes, + SurfaceId::InnerShadows, + SurfaceId::TextDropShadows, + true, + None, + None, + None, + target_surface, + )?; + } + + let needs_layer = element.needs_layer(); + + if needs_layer { + self.surfaces.canvas(target_surface).restore(); + } + + if visited_mask && element.masked_group_layer_blur().is_some() { + self.surfaces.canvas(target_surface).restore(); + } + + self.focus_mode.exit(&element.id); + Ok(()) } pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect { @@ -1612,8 +1836,10 @@ impl RenderState { // with the global tile grid, which is useful for rendering tiles in a /// consistent and predictable layout. pub fn get_current_aligned_tile_bounds(&mut self) -> Result { + let tile_render_state = get_tile_render_state(); Ok(self.get_aligned_tile_bounds( - self.tile.current + tile_render_state + .current .ok_or(Error::CriticalError("Current tile not found".to_string()))?, )) } @@ -1648,7 +1874,92 @@ impl RenderState { timestamp: i32, allow_stop: bool, export: bool, - ) -> Result<(bool, bool)> { + ) -> Result { + // Redirects writings. + let mut target_surface = SurfaceId::Current; + if export { + target_surface = SurfaceId::Export; + } + + let design_state = get_design_state(); + let tree = &design_state.shapes; + + let tile_render_state = get_tile_render_state(); + + let Some(current_tile) = tile_render_state.current else { + return Ok(RenderResult { + is_empty: true, + early_return: false, + }); + }; + + let Some(display_list) = tile_render_state.display_list.get(current_tile) else { + return Ok(RenderResult { + is_empty: true, + early_return: false, + }); + }; + + if display_list.is_empty() { + return Ok(RenderResult { + is_empty: true, + early_return: false, + }); + } + + let len = display_list.len(); + for index in 0..len { + let display_item = &display_list[index]; + let Some(shape) = tree.get(&display_item.id) else { + continue; + }; + + match display_item.phase { + TileDisplayPhase::Enter => { + let clip_bounds = None; + self.render_shape_enter(shape, false, clip_bounds, target_surface); + + self.render_shape( + shape, + None, + SurfaceId::Fills, + SurfaceId::Strokes, + SurfaceId::InnerShadows, + SurfaceId::TextDropShadows, + true, + None, + None, + None, + target_surface, + ); + + /* + let mut paint = skia::Paint::default(); + paint.set_color(skia::Color::BLACK); + self.surfaces + .canvas(SurfaceId::Backbuffer) + .draw_rect(shape.selrect, &paint); + */ + } + TileDisplayPhase::Exit => { + let clip_bounds = None; + self.render_shape_exit(shape, false, clip_bounds, target_surface); + } + } + } + Ok(RenderResult { + is_empty: false, + early_return: false, + }) + } + + /* + pub fn render_shape_tree_tile( + &mut self, + timestamp: i32, + allow_stop: bool, + export: bool, + ) -> Result { let mut is_empty = true; let mut target_surface = SurfaceId::Current; @@ -1673,10 +1984,10 @@ impl RenderState { // Skip it for this pass; a subsequent render will pick it up once present. continue; }; - let scale = self.get_scale_fast(); + let scale = self.viewbox.get_scale(); let mut extrect: Option = None; - let Some(current_tile) = self.tile.current else { + let Some(current_tile) = tile_render_state.current else { println!("There's no current_tile"); continue; }; @@ -1687,12 +1998,12 @@ impl RenderState { // } // println!("element_tile_rect {:?}", element_tile_rect); - // if !self.tile.tiles.has_shape_at(current_tile, node_id) { + // if !tile_render_state.tiles.has_shape_at(current_tile, node_id) { // continue; // } // If the shape is not in the tile set, then we add them. - // if self.tile.tiles.get_tiles_of(node_id).is_none() { + // if tile_render_state.tiles.get_tiles_of(node_id).is_none() { // self.add_shape_tiles(element, tree); // } @@ -1957,10 +2268,10 @@ impl RenderState { Ok((is_empty, false)) } + */ pub fn render_shape_tree_tiled( &mut self, - tree: ShapesPoolRef, timestamp: i32, allow_stop: bool, ) -> Result { @@ -1970,6 +2281,10 @@ impl RenderState { // ); let mut should_stop = false; + let tile_render_state = get_tile_render_state(); + let design_state = get_design_state(); + let tree = &design_state.shapes; + self.viewer_render_root = self.base_object; let root_ids: Vec = { @@ -1984,7 +2299,7 @@ impl RenderState { }; while !should_stop { - if let Some(current_tile) = self.tile.current { + if let Some(current_tile) = tile_render_state.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. @@ -1993,8 +2308,10 @@ impl RenderState { if self.viewer_masked_pass() || !self.surfaces.has_cached_tile_surface(current_tile) { performance::begin_measure!("render_shape_tree::uncached"); - let (is_empty, early_return) = self - .render_shape_tree_tile(timestamp, allow_stop, false)?; + let RenderResult { + is_empty, + early_return, + } = self.render_shape_tree_tile(timestamp, allow_stop, false)?; if early_return { self.viewer_render_root = None; @@ -2006,8 +2323,8 @@ 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_had_shapes { - let tile_rect = self.get_current_aligned_tile_bounds()?; + if !is_empty || tile_render_state.current_had_shapes { + // let tile_rect = self.get_current_aligned_tile_bounds()?; // NOTE: Unnecessary code // let current_tile = *self @@ -2016,21 +2333,10 @@ impl RenderState { // .as_ref() // .ok_or(Error::CriticalError("Current tile not found".to_string()))?; - self.surfaces.draw_current_tile_into_tile_atlas( - ¤t_tile, - ); - - if self.options.is_debug_visible() { - debug::render_workspace_current_tile( - self, - "".to_string(), - current_tile, - tile_rect, - ); - } + self.surfaces + .draw_current_tile_into_tile_atlas(¤t_tile); } - } else if self.tile.tiles.is_empty_at(current_tile) { - // println!("self.tile.tiles.is_empty_at({:?}", current_tile); + } else if tile_render_state.tiles.is_empty_at(current_tile) { self.surfaces.remove_cached_tile_surface(current_tile); } } @@ -2041,17 +2347,17 @@ 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.pop() { + if let Some(next_tile) = tile_render_state.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_had_shapes = false; + tile_render_state.current_had_shapes = false; let viewer_masked_pass = self.viewer_masked_pass(); - let Some(ids) = self.tile.tiles.get_shapes_at(next_tile) else { + let Some(ids) = tile_render_state.tiles.get_shapes_at(next_tile) else { // If the tile is empty we do not need to render it. continue; }; @@ -2063,6 +2369,7 @@ impl RenderState { continue; } + /* // Check if any shape on this tile has a background blur. // If so, we need ALL root shapes rendered (not just those // assigned to this tile) because the blur snapshots Current @@ -2092,7 +2399,7 @@ impl RenderState { } if !valid_ids.is_empty() { - self.tile.current_had_shapes = true; + tile_render_state.current_had_shapes = true; } self.pending_nodes @@ -2104,11 +2411,12 @@ impl RenderState { mask: false, flattened: false, })); + */ } else { + println!("Pending tiles empty"); // If there are no more pending tiles, stop. should_stop = true; } - } self.viewer_render_root = None; @@ -2116,10 +2424,6 @@ impl RenderState { Ok(FrameType::Full) } - pub fn render_shape_tree() { - - } - /* * Given a shape returns the TileRect with the range of tiles that the shape is in. * This is always limited to the interest area to optimize performance and prevent @@ -2133,12 +2437,13 @@ impl RenderState { */ pub fn get_tiles_for_shape(&mut self, shape: &Shape) -> TileRect { let design_state = get_design_state(); + let tile_render_state = get_tile_render_state(); let tree = &design_state.shapes; let scale = self.get_scale(); 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.viewbox.interest_rect; + let interest_rect = &tile_render_state.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()); @@ -2167,15 +2472,12 @@ impl RenderState { * Given a shape, check the indexes and update it's location in the tile set * returns the tiles that have changed in the process. */ - pub fn update_shape_tiles( - &mut self, - shape: &Shape, - ) -> HashSet { + pub fn update_shape_tiles(&mut self, shape: &Shape) -> HashSet { let tile_rect = self.get_tiles_for_shape(shape); + let tile_render_state = get_tile_render_state(); // Collect old tiles to avoid borrow conflict with remove_shape_at - let old_tiles: Vec<_> = self - .tile + let old_tiles: Vec<_> = tile_render_state .tiles .get_tiles_of(shape.id) .map_or(Vec::new(), |t| t.iter().copied().collect()); @@ -2184,13 +2486,13 @@ impl RenderState { // First, remove the shape from all tiles where it was previously located for tile in old_tiles { - self.tile.tiles.remove_shape_at(tile, shape.id); + tile_render_state.tiles.remove_shape_at(tile, shape.id); result.insert(tile); } // Then, add the shape to the new tiles for tile in tile_rect.iter(true) { - self.tile.tiles.add_shape_at(tile, shape.id); + tile_render_state.tiles.add_shape_at(tile, shape.id); result.insert(tile); } @@ -2213,13 +2515,10 @@ impl RenderState { * Tile cache invalidation only happens when shapes actually move or change, * which is handled by rebuild_touched_tiles, not during pan/zoom. */ - pub fn update_shape_tiles_incremental( - &mut self, - shape: &Shape, - ) -> Vec { + pub fn update_shape_tiles_incremental(&mut self, shape: &Shape) -> Vec { + let tile_render_state = get_tile_render_state(); let tile_rect = self.get_tiles_for_shape(shape); - let old_tiles: HashSet = self - .tile + let old_tiles: HashSet = tile_render_state .tiles .get_tiles_of(shape.id) .map_or(HashSet::new(), |tiles| tiles.iter().copied().collect()); @@ -2233,12 +2532,12 @@ impl RenderState { // Update the index: remove from old tiles for tile in &removed { - self.tile.tiles.remove_shape_at(*tile, shape.id); + tile_render_state.tiles.remove_shape_at(*tile, shape.id); } // Update the index: add to new tiles for tile in &added { - self.tile.tiles.add_shape_at(*tile, shape.id); + tile_render_state.tiles.add_shape_at(*tile, shape.id); } // Don't invalidate cache for pan/zoom - the tile content hasn't changed, @@ -2254,9 +2553,10 @@ impl RenderState { */ pub fn add_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec { performance::begin_measure!("add_shape_tiles"); + let tile_render_state = get_tile_render_state(); let tiles: Vec = self.get_tiles_for_shape(shape).iter(true).collect(); for tile in tiles.iter() { - self.tile.tiles.add_shape_at(*tile, shape.id); + tile_render_state.tiles.add_shape_at(*tile, shape.id); } performance::end_measure!("add_shape_tiles"); tiles @@ -2273,7 +2573,7 @@ impl RenderState { let design_state = get_design_state(); let tree = &design_state.shapes; - let zoom_changed = self.zoom_changed(); + let zoom_changed = self.viewbox.is_zoom_changed(); performance::begin_measure!("rebuild_tile_index"); let mut nodes = Vec::::with_capacity(64); nodes.push(Uuid::nil()); @@ -2304,7 +2604,7 @@ impl RenderState { // Zoom changes world tile size: a partial cache update would mix scales in the // mosaic and glitch. Same zoom as last finished render (typical pan): drop only // tile textures and keep the cache canvas for render_from_cache. - if !self.zoom_changed() { + if !self.viewbox.is_zoom_changed() { self.surfaces.invalidate_tile_cache(); } @@ -2314,7 +2614,8 @@ impl RenderState { pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) { performance::begin_measure!("rebuild_tiles"); - self.tile.tiles.invalidate(); + let tile_render_state = get_tile_render_state(); + tile_render_state.tiles.invalidate(); let mut all_tiles = HashSet::::new(); let mut nodes = { @@ -2384,10 +2685,7 @@ impl RenderState { /// /// This is useful when you have a pre-computed set of shape IDs that need to be refreshed, /// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection). - pub fn update_tiles_shapes( - &mut self, - shape_ids: &[Uuid], - ) -> Result<()> { + pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid]) -> Result<()> { performance::begin_measure!("invalidate_and_update_tiles"); let design_state = get_design_state(); let tree = &design_state.shapes; @@ -2410,10 +2708,7 @@ impl RenderState { /// Additionally, it processes all ancestors of modified shapes to ensure their /// extended rectangles are properly recalculated and their tiles are updated. /// This is crucial for frames and groups that contain transformed children. - pub fn rebuild_modifier_tiles( - &mut self, - ids: &[Uuid], - ) -> Result<()> { + pub fn rebuild_modifier_tiles(&mut self, ids: &[Uuid]) -> Result<()> { // During interactive transform, skip ancestor invalidation: walking up to the // parent frame evicts every tile the frame covers, including dense tiles with // many siblings. Ancestor extrect caches are already invalidated by @@ -2438,17 +2733,6 @@ impl RenderState { self.viewbox.get_scale() } - /// Hot-path variant that skips the export_context check. - /// Use in render_shape / walk loops where export is never active. - #[inline] - pub fn get_scale_fast(&self) -> f32 { - self.viewbox.get_scale() - } - - pub fn zoom_changed(&self) -> bool { - self.viewbox.is_zoom_changed() - } - pub fn mark_touched(&mut self, uuid: Uuid) { self.touched_ids.insert(uuid); } diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index d3f08b904a..c08a055097 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -6,6 +6,7 @@ use macros::wasm_error; #[cfg(target_arch = "wasm32")] use crate::get_render_state; +use crate::globals::get_tile_render_state; use skia_safe::{self as skia, Rect}; @@ -69,7 +70,8 @@ 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.viewbox.interest_rect; + let tile_render_state = get_tile_render_state(); + let tiles::TileRect(sx, sy, ex, ey) = tile_render_state.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/drop_shadow.rs b/render-wasm/src/render/drop_shadow.rs index ff335a8739..41719206bd 100644 --- a/render-wasm/src/render/drop_shadow.rs +++ b/render-wasm/src/render/drop_shadow.rs @@ -335,7 +335,8 @@ pub fn render_element_drop_shadows_and_composite( } if let Some(clips) = clip_bounds.as_ref() { - let antialias = element.should_use_antialias(scale, render_state.options.antialias_threshold); + let antialias = + element.should_use_antialias(scale, render_state.options.antialias_threshold); render_state.surfaces.canvas(target_surface).save(); render_state.clip_target_surface_to_stack(clips, target_surface, scale, antialias); render_state diff --git a/render-wasm/src/render/enter_exit.rs b/render-wasm/src/render/enter_exit.rs deleted file mode 100644 index 1f6c484fe4..0000000000 --- a/render-wasm/src/render/enter_exit.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::borrow::Cow; - -use skia_safe as skia; - -use crate::error::Result; -use crate::shapes::{radius_to_sigma, Shape, StrokeKind, Type}; - -use super::layer_blur; -use super::{ClipStack, NodeRenderState, RenderState, SurfaceId}; - -pub fn render_shape_enter( - render_state: &mut RenderState, - element: &Shape, - mask: bool, - clip_bounds: Option<&ClipStack>, - target_surface: SurfaceId, -) { - if let Type::Group(group) = element.shape_type { - let fills = &element.fills; - let shadows = &element.shadows; - render_state.nested_fills.push(fills.to_vec()); - render_state.nested_shadows.push(shadows.to_vec()); - - if group.masked { - let mask_group_blur = element.masked_group_layer_blur().is_some(); - if mask_group_blur { - render_state.surfaces.canvas(target_surface).save(); - if let Some(clips) = clip_bounds { - let scale = render_state.get_scale(); - let antialias = element - .should_use_antialias(scale, render_state.options.antialias_threshold); - render_state.clip_target_surface_to_stack( - clips, - target_surface, - scale, - antialias, - ); - } - } - - let mut paint = skia::Paint::default(); - if let Some(blur) = element.masked_group_layer_blur() { - let scale = render_state.get_scale(); - let sigma = radius_to_sigma(blur.value * scale); - if let Some(filter) = - skia::image_filters::blur((sigma, sigma), None, None, None) - { - paint.set_image_filter(filter); - } - } - - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); - render_state - .surfaces - .canvas(target_surface) - .save_layer(&layer_rec); - } - } - - if let Type::Frame(_) = element.shape_type { - render_state.nested_fills.push(Vec::new()); - } - - if mask { - let mut mask_paint = skia::Paint::default(); - mask_paint.set_blend_mode(skia::BlendMode::DstIn); - let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint); - render_state - .surfaces - .canvas(target_surface) - .save_layer(&mask_rec); - } - - let needs_layer = element.needs_layer(); - - if needs_layer { - let mut paint = skia::Paint::default(); - paint.set_blend_mode(element.blend_mode().into()); - paint.set_alpha_f(element.opacity()); - - if let Some(frame_blur) = layer_blur::frame_clip_layer_blur(element) { - let scale = render_state.get_scale(); - let sigma = radius_to_sigma(frame_blur.value * scale); - if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) { - paint.set_image_filter(filter); - } - } - - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); - render_state - .surfaces - .canvas(target_surface) - .save_layer(&layer_rec); - } - - render_state.focus_mode.enter(&element.id); -} - -pub fn render_shape_exit( - render_state: &mut RenderState, - element: &Shape, - visited_mask: bool, - clip_bounds: Option, - target_surface: SurfaceId, -) -> Result<()> { - if visited_mask { - if let Type::Group(group) = element.shape_type { - if group.masked { - render_state.surfaces.canvas(target_surface).restore(); - } - } - } else if let Type::Group(group) = element.shape_type { - if group.masked { - render_state.pending_nodes.push(NodeRenderState { - id: element.id, - visited_children: true, - clip_bounds: None, - visited_mask: true, - mask: false, - flattened: false, - }); - if let Some(&mask_id) = element.mask_id() { - render_state.pending_nodes.push(NodeRenderState { - id: mask_id, - visited_children: false, - clip_bounds: None, - visited_mask: false, - mask: true, - flattened: false, - }); - } - } - } - - match element.shape_type { - Type::Frame(_) | Type::Group(_) => { - render_state.nested_fills.pop(); - render_state.nested_blurs.pop(); - render_state.cached_layer_blur = None; - render_state.nested_shadows.pop(); - } - _ => {} - } - - let needs_exit_strokes = render_state.focus_mode.is_active() - && (element.clip() - || (matches!(element.shape_type, Type::Frame(_)) && element.has_inner_stroke())); - - if needs_exit_strokes { - let mut element_strokes: Cow = Cow::Borrowed(element); - element_strokes.to_mut().clear_fills(); - element_strokes.to_mut().clear_shadows(); - element_strokes.to_mut().clip_content = false; - - if !element.clip() { - let is_open = element.is_open(); - element_strokes - .to_mut() - .strokes - .retain(|s| s.render_kind(is_open) == StrokeKind::Inner); - } - - if layer_blur::frame_clip_layer_blur(element).is_some() { - element_strokes.to_mut().set_blur(None); - } - render_state.render_shape( - &element_strokes, - clip_bounds, - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::InnerShadows, - SurfaceId::TextDropShadows, - true, - None, - None, - None, - target_surface, - )?; - } - - let needs_layer = element.needs_layer(); - - if needs_layer { - render_state.surfaces.canvas(target_surface).restore(); - } - - if visited_mask && element.masked_group_layer_blur().is_some() { - render_state.surfaces.canvas(target_surface).restore(); - } - - render_state.focus_mode.exit(&element.id); - Ok(()) -} diff --git a/render-wasm/src/render/export.rs b/render-wasm/src/render/export.rs deleted file mode 100644 index d6954e3f37..0000000000 --- a/render-wasm/src/render/export.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::error::Result; -use crate::get_gpu_state; -use crate::state::ShapesPoolRef; -use crate::uuid::Uuid; -use skia_safe as skia; - -use super::{NodeRenderState, RenderState, SurfaceId}; - -pub fn render_shape_pixels( - render_state: &mut RenderState, - id: &Uuid, - tree: ShapesPoolRef, - scale: f32, - timestamp: i32, -) -> Result<(Vec, i32, i32)> { - let target_surface = SurfaceId::Export; - - let saved_focus_mode = render_state.focus_mode.clone(); - 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; - 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); - let saved_nested_shadows = std::mem::take(&mut render_state.nested_shadows); - let saved_ignore_nested_blurs = render_state.ignore_nested_blurs; - let saved_preview_mode = render_state.preview_mode; - - render_state.focus_mode.clear(); - - render_state - .surfaces - .canvas(target_surface) - .clear(skia::Color::TRANSPARENT); - - if tree.len() != 0 { - let Some(shape) = tree.get(id) else { - return Ok((Vec::new(), 0, 0)); - }; - let mut extrect = shape.extrect(tree, scale); - render_state.export_context = Some((extrect, scale)); - let margins = render_state.surfaces.margins; - extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale)); - - render_state.surfaces.resize_export_surface(scale, extrect); - render_state.render_area = extrect; - render_state.render_area_with_margins = extrect; - render_state.surfaces.update_render_context(extrect, scale); - - render_state.pending_nodes.push(NodeRenderState { - id: *id, - visited_children: false, - clip_bounds: None, - visited_mask: false, - mask: false, - flattened: false, - }); - render_state.render_shape_tree_tile( timestamp, false, true)?; - } - - render_state.export_context = None; - - render_state.surfaces.flush_and_submit(target_surface); - - let image = render_state.surfaces.snapshot(target_surface); - let data = image - .encode( - Some(&mut get_gpu_state().context), - skia::EncodedImageFormat::PNG, - 100, - ) - .expect("PNG encode failed"); - let skia::ISize { width, height } = image.dimensions(); - - render_state.focus_mode = saved_focus_mode; - 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 = saved_current_tile; - render_state.pending_nodes = saved_pending_nodes; - render_state.nested_fills = saved_nested_fills; - render_state.nested_blurs = saved_nested_blurs; - render_state.nested_shadows = saved_nested_shadows; - render_state.ignore_nested_blurs = saved_ignore_nested_blurs; - render_state.preview_mode = saved_preview_mode; - - let workspace_scale = render_state.get_scale(); - if let Some(tile) = render_state.tile.current { - render_state.update_render_context(tile); - } else if !render_state.render_area.is_empty() { - render_state - .surfaces - .update_render_context(render_state.render_area, workspace_scale); - } - - Ok((data.as_bytes().to_vec(), width, height)) -} diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index c1c7f7a6cb..4395cdafe1 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -162,11 +162,7 @@ impl Surfaces { self.dpr = dpr; } - pub fn draw_tile_atlas_to_backbuffer( - &mut self, - viewbox: &Viewbox, - tile_viewbox: &TileViewbox, - ) { + pub fn draw_tile_atlas_to_backbuffer(&mut self, viewbox: &Viewbox, tile_viewbox: &TileViewbox) { self.tiles.update(viewbox, tile_viewbox); if self.tiles.needs_snapshot() || self.tile_atlas_image.is_none() { self.tile_atlas_image = Some(self.tile_atlas.image_snapshot()); @@ -218,11 +214,7 @@ impl Surfaces { Ok(general_purpose::STANDARD.encode(encoded_image.as_bytes())) } - pub fn base64_snapshot_rect( - &mut self, - id: SurfaceId, - irect: IRect, - ) -> Result> { + pub fn base64_snapshot_rect(&mut self, id: SurfaceId, irect: IRect) -> Result> { let surface = self.get_mut(id); if let Some(image) = surface.image_snapshot_with_bounds(irect) { let mut context = surface.direct_context(); @@ -356,11 +348,7 @@ impl Surfaces { f(self.get_mut(surface_id)); } - pub fn get_render_context_translation( - &mut self, - render_area: Rect, - scale: f32, - ) -> (f32, f32) { + pub fn get_render_context_translation(&mut self, render_area: Rect, scale: f32) -> (f32, f32) { ( -render_area.left() + self.margins.width as f32 / scale, -render_area.top() + self.margins.height as f32 / scale, @@ -438,12 +426,8 @@ impl Surfaces { pub fn copy_backbuffer_to_target(&mut self) { let sampling_options = self.sampling_options; - self.backbuffer.draw( - self.target.canvas(), - (0.0, 0.0), - sampling_options, - None, - ); + self.backbuffer + .draw(self.target.canvas(), (0.0, 0.0), sampling_options, None); } pub fn clear_target(&mut self, color: skia::Color) { @@ -652,10 +636,7 @@ impl Surfaces { self.clear_all_dirty(); } - pub fn draw_current_tile_into_tile_atlas( - &mut self, - tile: &Tile, - ) { + pub fn draw_current_tile_into_tile_atlas(&mut self, tile: &Tile) { let rect = TILE_DRAWABLE_RECT; let tile_image_opt = self.current.image_snapshot_with_bounds(rect); @@ -688,7 +669,6 @@ impl Surfaces { /// so that `render_from_cache` can still show a scaled preview of the old /// content while new tiles are being rendered. pub fn invalidate_tile_cache(&mut self) { - println!("invalidate_tile_cache"); self.tiles.clear(); self.tile_atlas_image = None; } @@ -761,9 +741,7 @@ impl TileAtlasTextureProvider { let bottom = top + tile_size as f32; rects.push(Rect::new(left, top, right, bottom)); } - Self { - rects, - } + Self { rects } } pub fn available(&self) -> usize { @@ -776,7 +754,10 @@ impl TileAtlasTextureProvider { pub fn deallocate(&mut self, rect: Rect) -> bool { println!("Deallocating {:?}", rect); - debug_assert!(!self.rects.contains(&rect), "Deallocating an already deallocated rect"); + debug_assert!( + !self.rects.contains(&rect), + "Deallocating an already deallocated rect" + ); self.rects.push(rect); true } @@ -831,10 +812,8 @@ impl TileTextureCache { } if self.textures.len() != tile_viewbox.visible_rect.len() as usize { - self.textures.resize( - tile_viewbox.visible_rect.len() as usize, - Rect::new_empty(), - ); + self.textures + .resize(tile_viewbox.visible_rect.len() as usize, Rect::new_empty()); } for texture in self.textures.iter_mut() { @@ -892,15 +871,12 @@ impl TileTextureCache { } pub fn remove(&mut self, tile: Tile) { - println!("remove {:?}", tile); self.is_updated = true; self.removed.insert(tile); } pub fn clear(&mut self) { - println!("clear"); for k in self.grid.keys() { - println!("{:?}", k); self.removed.insert(*k); } self.is_updated = true; diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 26fff74e93..d2d3993539 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -53,7 +53,7 @@ pub use svgraw::*; pub use text::*; pub use transform::*; -use crate::math::{self, Bounds, Matrix, Point, IRect}; +use crate::math::{self, Bounds, IRect, Matrix, Point}; use crate::state::ShapesPoolRef; @@ -202,7 +202,6 @@ pub struct Shape { pub extrect_cache: RefCell>, pub svg_transform: Option, pub ignore_constraints: bool, - pub tile_rect: TileRect, deleted: bool, } @@ -306,7 +305,6 @@ impl Shape { extrect_cache: RefCell::new(None), svg_transform: None, ignore_constraints: false, - tile_rect: TileRect::new_empty(), deleted: false, } } @@ -406,7 +404,6 @@ impl Shape { text.update_layout(self.selrect); text.set_xywh(left, top, self.selrect.width(), self.selrect.height()); } - self.tile_rect.set_from_tile_bounds(left, top, right, bottom, tiles::TILE_SIZE); } pub fn set_masked(&mut self, masked: bool) { @@ -431,7 +428,7 @@ impl Shape { 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 center = self.center(); let mut matrix = self.transform; matrix.post_translate(center); matrix.pre_translate(-center); @@ -447,10 +444,6 @@ impl Shape { transform } - pub fn get_tile_rect(&self, viewbox: &Viewbox) -> TileRect { - TileRect::from_scaled(&self.tile_rect, viewbox.get_scale()) - } - pub fn set_opacity(&mut self, opacity: f32) { self.opacity = opacity; } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 109fc605b2..3c4ef1ac27 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -15,7 +15,7 @@ use crate::shapes::{ ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry, TransformEntrySource, Type, }; -use crate::state::{ShapesPoolRef, State}; +use crate::state::{DesignState, ShapesPoolRef}; use crate::uuid::Uuid; #[allow(clippy::too_many_arguments)] @@ -176,7 +176,7 @@ fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) { fn propagate_transform( entry: TransformEntry, pixel_precision: bool, - state: &State, + state: &DesignState, entries: &mut VecDeque, bounds: &mut HashMap, modifiers: &mut HashMap, @@ -324,7 +324,7 @@ fn propagate_transform( #[allow(clippy::too_many_arguments)] fn propagate_reflow( id: &Uuid, - state: &State, + state: &DesignState, entries: &mut VecDeque, bounds: &mut HashMap, layout_reflows: &mut HashSet, @@ -380,7 +380,7 @@ fn propagate_reflow( fn reflow_shape( id: &Uuid, - state: &State, + state: &DesignState, reflown: &mut HashSet, entries: &mut VecDeque, bounds: &mut HashMap, @@ -409,7 +409,7 @@ fn reflow_shape( } pub fn propagate_modifiers( - state: &State, + state: &DesignState, modifiers: &[TransformEntry], pixel_precision: bool, ) -> Result> { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 92fb8170a3..4bc7fcd8c9 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -11,6 +11,7 @@ pub use text_editor::*; pub use ui::UIState; use crate::error::{Error, Result}; +use crate::globals::get_tile_render_state; use crate::render::FrameType; use crate::shapes::{grid_layout::grid_cell_data, Shape}; use crate::uuid::Uuid; @@ -21,7 +22,7 @@ use crate::{get_render_state, tiles}; /// It is created by [init] and passed to the other exported functions. /// Note that rust-skia data structures are not thread safe, so a state /// must not be shared between different Web Workers. -pub(crate) struct State { +pub(crate) struct DesignState { pub current_id: Option, pub current_browser: u8, pub shapes: ShapesPool, @@ -30,7 +31,7 @@ pub(crate) struct State { pub loading: bool, } -impl State { +impl DesignState { pub fn new() -> Self { Self { current_id: None, @@ -171,16 +172,17 @@ impl State { // // Instead, remove the shape from *all* tiles where it was indexed, and // drop cached tiles for those entries. - let indexed_tiles: Vec = render_state - .tile + let tile_render_state = get_tile_render_state(); + let indexed_tiles: Vec = tile_render_state .tiles .get_tiles_of(shape.id) .map(|t| t.iter().copied().collect()) .unwrap_or_default(); + let tile_render_state = get_tile_render_state(); for tile in indexed_tiles { render_state.remove_cached_tile(tile); - render_state.tile.tiles.remove_shape_at(tile, shape.id); + tile_render_state.tiles.remove_shape_at(tile, shape.id); } if let Some(shape_to_delete) = self.shapes.get(&id) { diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 20dda84d83..41800b5845 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -1,14 +1,134 @@ -use crate::render::Surfaces; +use crate::globals::{get_design_state, get_render_state, get_tile_render_state}; +use crate::shapes::Shape; use crate::uuid::Uuid; use crate::view::Viewbox; +use crate::{render::Surfaces, state::ShapesPoolRef}; use skia_safe as skia; use std::collections::{HashMap, HashSet}; + +#[derive(Debug)] +#[repr(u8)] +pub enum TileDisplayPhase { + Enter = 0, + Exit = 1, +} + +#[derive(Debug)] +pub struct TileDisplayItem { + pub id: Uuid, + pub phase: TileDisplayPhase, +} + +impl TileDisplayItem { + pub fn enter(id: Uuid) -> Self { + Self { + id, + phase: TileDisplayPhase::Enter, + } + } + + pub fn exit(id: Uuid) -> Self { + Self { + id, + phase: TileDisplayPhase::Exit, + } + } +} + +#[derive(Debug)] +pub struct TileDisplayList { + output: HashMap>, +} + +impl TileDisplayList { + pub fn new() -> Self { + Self { + output: HashMap::new() + } + } + + pub fn compute_from(&mut self, root_id: Uuid) { + self.output.clear(); + self.dfs(root_id); + } + + pub fn get(&self, tile: Tile) -> Option<&Vec> { + self.output.get(&tile) + } + + // Recursive helper that pushes to the output vector + fn dfs( + &mut self, + id: Uuid, + ) { + let design_state = get_design_state(); + let render_state = get_render_state(); + let tile_render_state= get_tile_render_state(); + + let shapes = &design_state.shapes; + if id.is_nil() { + let shape = shapes.get(&id).unwrap(); + for shape_id in shape.children_ids(false) { + self.dfs(shape_id); + } + return; + } + + let shape = shapes.get(&id).unwrap(); + let tile_rect = TileRect::from_shape_and_viewbox(shape, render_state.viewbox); + let intersection_rect = tile_render_state.viewbox.interest_rect.intersection(&tile_rect); + if intersection_rect.is_degenerate() { + return; + } + + for tile in intersection_rect.iter(true) { + let _ = self.output.entry(tile).or_insert_with(Vec::new); + self.output.get_mut(&tile).unwrap().push(TileDisplayItem::enter(shape.id)); + } + + for shape_id in shape.children_ids(false) { + self.dfs(shape_id); + } + + for tile in intersection_rect.iter(true) { + self.output.get_mut(&tile).unwrap().push(TileDisplayItem::exit(shape.id)); + } + } +} + +#[derive(Debug)] +pub struct TileRenderState { + pub current: Option, + pub current_had_shapes: bool, + pub viewbox: TileViewbox, + pub tiles: TileHashMap, + pub pending: PendingTiles, + pub display_list: TileDisplayList, +} + +impl TileRenderState { + pub fn new() -> Self { + Self { + current: None, + current_had_shapes: false, + tiles: TileHashMap::new(), + viewbox: TileViewbox::new(), + pending: PendingTiles::new(), + display_list: TileDisplayList::new(), + } + } +} + #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] pub struct Tile(pub i32, pub i32); impl Tile { pub fn from(x: i32, y: i32) -> Self { - Tile(x, y) + Self(x, y) + } + + pub fn new_empty() -> Self { + Self(0, 0) } #[inline(always)] @@ -32,6 +152,14 @@ impl Tile { } } +fn itrunc(x: f32) -> f32 { + if x < 0.0 { + x.floor() + } else { + x.ceil() + } +} + #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] pub struct TileRect(pub i32, pub i32, pub i32, pub i32); @@ -42,11 +170,30 @@ impl TileRect { } pub fn from_scaled(other: &TileRect, scale: f32) -> Self { + Self( + itrunc(other.0 as f32 * scale) as i32, + itrunc(other.1 as f32 * scale) as i32, + itrunc(other.2 as f32 * scale) as i32, + itrunc(other.3 as f32 * scale) as i32, + ) + } + + pub fn from_shape(shape: &Shape, scale: f32) -> Self { + let tile_rect = get_tiles_for_rect(shape.selrect, TILE_SIZE); + Self::from_scaled(&tile_rect, scale) + } + + pub fn from_shape_and_viewbox(shape: &Shape, viewbox: Viewbox) -> Self { + Self::from_shape(shape, viewbox.get_scale()) + } + + #[inline(always)] + pub fn intersection(&self, other: &Self) -> Self { Self ( - (other.0 as f32 * scale).trunc() as i32, - (other.1 as f32 * scale).trunc() as i32, - (other.2 as f32 * scale).trunc() as i32, - (other.3 as f32 * scale).trunc() as i32, + self.left().max(other.left()), + self.top().max(other.top()), + self.right().min(other.right()), + self.bottom().min(other.bottom()), ) } @@ -181,7 +328,7 @@ impl Iterator for TileRectIter { } } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct TileViewbox { pub visible_rect: TileRect, pub interest_rect: TileRect, @@ -190,6 +337,15 @@ pub struct TileViewbox { } impl TileViewbox { + pub fn new() -> Self { + Self { + visible_rect: TileRect::new_empty(), + interest_rect: TileRect::new_empty(), + interest: 0, + center: Tile::new_empty(), + } + } + pub fn new_with_interest(viewbox: &Viewbox, interest: i32) -> Self { Self { visible_rect: get_tiles_for_viewbox(viewbox), @@ -265,6 +421,7 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect { } // This structure is useful to keep all the shape uuids by shape id. +#[derive(Debug, Clone)] pub struct TileHashMap { grid: HashMap>, index: HashMap>, @@ -330,7 +487,7 @@ const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = VIEWPORT_DEFAULT_CAPACITY; /// Cached spiral of tile offsets for a given grid size. /// /// Offsets are centered at (0,0) and must be translated by the desired origin/center tile. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct TileSpiral { offsets: Vec, columns: usize, @@ -417,6 +574,7 @@ impl TileSpiral { // This structure keeps the list of tiles that are in the pending list, the // ones that are going to be rendered. +#[derive(Debug, Clone)] pub struct PendingTiles { pub list: Vec, pub spiral: TileSpiral, diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index 65a4697181..6b526fcfba 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -2,13 +2,13 @@ use crate::error::{Error, Result}; use crate::get_render_state; use crate::mem; use crate::shapes::Fill; -use crate::state::State; +use crate::state::DesignState; use crate::uuid::Uuid; use crate::with_state; use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet}; use macros::wasm_error; -fn touch_shapes_with_image(state: &mut State, image_id: Uuid) { +fn touch_shapes_with_image(state: &mut DesignState, image_id: Uuid) { let ids: Vec = state .shapes .iter() diff --git a/render-wasm/src/wasm/ui.rs b/render-wasm/src/wasm/ui.rs index e71fd20388..abe9be0345 100644 --- a/render-wasm/src/wasm/ui.rs +++ b/render-wasm/src/wasm/ui.rs @@ -108,7 +108,7 @@ pub extern "C" fn set_guides() -> Result<()> { // Guides are drawn on the UI overlay composited onto `Target`. Refresh the // presented frame immediately so removed guides do not linger as stale pixels. with_state!(state, { - get_render_state().present_frame(&state.shapes); + get_render_state().present_frame(); }); Ok(())