diff --git a/render-wasm/_build_env b/render-wasm/_build_env index 7f9c2e2486..288a715366 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -50,7 +50,7 @@ export CARGO_PARAMS="${@:2}"; if [ "$BUILD_MODE" = "release" ]; then export CARGO_PARAMS="--release $CARGO_PARAMS" - export EMCC_CFLAGS="-O3 -sASSERTIONS=0 $EMCC_CFLAGS" + export EMCC_CFLAGS="-O3 -sASSERTIONS=0 --profiling $EMCC_CFLAGS" else # TODO: Extra parameters that could be good to look into: # -gseparate-dwarf @@ -80,6 +80,9 @@ function copy_artifacts { cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js $DEST/$BUILD_NAME.js; cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm $DEST/$BUILD_NAME.wasm; + if [ -f target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm.map ]; then + cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm.map $DEST/$BUILD_NAME.wasm.map; + fi sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$VERSION_TAG/g" $DEST/$BUILD_NAME.js; diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 61dffb7b36..b8121e8fef 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -18,7 +18,7 @@ use std::collections::HashMap; #[allow(unused_imports)] use crate::error::{Error, Result}; -use crate::render::QueueFrame; +use crate::render::RenderQueueFrame; use macros::wasm_error; use math::{Bounds, Matrix}; @@ -218,7 +218,7 @@ pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> { #[no_mangle] #[wasm_error] -pub extern "C" fn render(_: i32) -> Result { +pub extern "C" fn render(timestamp: i32) -> Result { with_state_mut!(state, { state.rebuild_touched_tiles(); // Drain the throttled modifier-tile invalidation accumulated @@ -233,8 +233,8 @@ pub extern "C" fn render(_: i32) -> Result { } } return state - .start_render_loop(performance::get_time()) - .map_err(|_| Error::RecoverableError("Error rendering".to_string())); + .start_render_loop(timestamp) + .map_err(|_| Error::RecoverableError("Error rendering".to_string())); }); } @@ -244,7 +244,7 @@ pub extern "C" fn render_sync() -> Result<()> { with_state_mut!(state, { state.rebuild_tiles(); state - .render_sync(performance::get_time()) + .render_sync() .map_err(|_| Error::RecoverableError("Error rendering".to_string()))?; }); Ok(()) @@ -270,7 +270,7 @@ pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<() state.rebuild_tiles_from(Some(&id)); state - .render_sync_shape(&id, performance::get_time()) + .render_sync_shape(&id) .map_err(|e| Error::RecoverableError(e.to_string()))?; }); Ok(()) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index b93ac98b79..4ba4a83701 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -40,7 +40,7 @@ pub use images::*; type ClipStack = Vec<(Rect, Option, Matrix)>; #[repr(u8)] -pub enum QueueFrame { +pub enum RenderQueueFrame { Yes = 1, No = 0, } @@ -524,7 +524,7 @@ impl RenderState { options.viewport_interest_area_threshold, 1.0, ), - pending_tiles: PendingTiles::new_empty(), + pending_tiles: PendingTiles::new(), nested_fills: vec![], nested_blurs: vec![], nested_shadows: vec![], @@ -1867,7 +1867,7 @@ impl RenderState { tree: ShapesPoolRef, timestamp: i32, sync_render: bool, - ) -> Result { + ) -> Result { #[cfg(feature = "stats")] self.stats.clear(); @@ -1960,7 +1960,7 @@ impl RenderState { self.apply_drawing_to_render_canvas(None, SurfaceId::Current); - let mut result = QueueFrame::No; + let mut result = RenderQueueFrame::No; if sync_render { self.render_shape_tree_sync(base_object, tree, timestamp)?; } else { @@ -2018,7 +2018,7 @@ impl RenderState { base_object: Option<&Uuid>, tree: ShapesPoolRef, timestamp: i32, - ) -> Result { + ) -> Result { performance::begin_measure!("process_animation_frame"); if self.render_in_progress { if tree.len() != 0 { @@ -2039,7 +2039,7 @@ impl RenderState { } if self.render_in_progress { - return Ok(QueueFrame::Yes); + return Ok(RenderQueueFrame::Yes); } else { // A full-quality frame is now complete. Refresh Backbuffer and regenerate // the per-shape crop cache so interactive drags can reuse pixels. @@ -2052,7 +2052,7 @@ impl RenderState { } } performance::end_measure!("process_animation_frame"); - Ok(QueueFrame::No) + Ok(RenderQueueFrame::No) } pub fn render_shape_tree_sync( @@ -2060,12 +2060,12 @@ impl RenderState { base_object: Option<&Uuid>, tree: ShapesPoolRef, timestamp: i32, - ) -> Result { + ) -> Result { if tree.len() != 0 { self.render_shape_tree_partial(base_object, tree, timestamp, false)?; } self.flush_and_submit(); - Ok(QueueFrame::No) + Ok(RenderQueueFrame::No) } pub fn render_shape_pixels( diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index 40a3125ccd..81cf2da36d 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -7,7 +7,7 @@ const SHOW_WASM_INFO: u32 = 0x08; // Render performance options // This is the extra area used for tile rendering (tiles beyond viewport). // Higher values pre-render more tiles, reducing empty squares during pan but using more memory. -const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3; +const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1; const MAX_BLOCKING_TIME_MS: i32 = 32; const NODE_BATCH_THRESHOLD: i32 = 3; const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0; diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index e9d30a0d2f..a184dfc25b 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -7,8 +7,8 @@ pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef}; pub use text_editor::*; use crate::error::{Error, Result}; -use crate::render::{RenderState, QueueFrame}; -use crate::shapes::{Shape, modifiers::grid_layout::grid_cell_data}; +use crate::render::{RenderQueueFrame, RenderState}; +use crate::shapes::{modifiers::grid_layout::grid_cell_data, Shape}; use crate::tiles; use crate::uuid::Uuid; @@ -91,14 +91,14 @@ impl State { self.render_state.render_from_cache(&self.shapes); } - pub fn render_sync(&mut self, timestamp: i32) -> Result { + pub fn render_sync(&mut self) -> Result { self.render_state - .start_render_loop(None, &self.shapes, timestamp, true) + .start_render_loop(None, &self.shapes, 0, true) } - pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result { + pub fn render_sync_shape(&mut self, id: &Uuid) -> Result { self.render_state - .start_render_loop(Some(id), &self.shapes, timestamp, true) + .start_render_loop(Some(id), &self.shapes, 0, true) } pub fn render_shape_pixels( @@ -111,7 +111,7 @@ impl State { .render_shape_pixels(id, &self.shapes, scale, timestamp) } - pub fn start_render_loop(&mut self, timestamp: i32) -> Result { + pub fn start_render_loop(&mut self, timestamp: i32) -> Result { // If zoom changed (e.g. interrupted zoom render followed by pan), the // tile index may be stale for the new viewport position. Rebuild the // index so shapes are mapped to the correct tiles. We use diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 6f0786df35..35768bdc96 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -22,43 +22,95 @@ impl Tile { pub struct TileRect(pub i32, pub i32, pub i32, pub i32); impl TileRect { + pub fn empty() -> Self { + Self(0, 0, 0, 0) + } + + #[inline] pub fn x1(&self) -> i32 { self.0 } + #[inline] pub fn y1(&self) -> i32 { self.1 } + #[inline] pub fn x2(&self) -> i32 { self.2 } + #[inline] pub fn y2(&self) -> i32 { self.3 } + #[inline] + pub fn left(&self) -> i32 { + self.0 + } + + #[inline] + pub fn top(&self) -> i32 { + self.1 + } + + #[inline] + pub fn right(&self) -> i32 { + self.2 + } + + #[inline] + pub fn bottom(&self) -> i32 { + self.3 + } + + #[inline] + pub fn x(&self) -> i32 { + self.0 + } + + #[inline] + pub fn y(&self) -> i32 { + self.1 + } + + #[inline] pub fn width(&self) -> i32 { self.x2() - self.x1() } + #[inline] + pub fn half_width(&self) -> i32 { + self.width() / 2 + } + + #[inline] pub fn height(&self) -> i32 { self.y2() - self.y1() } - pub fn center_x(&self) -> i32 { - self.x1() + self.width() / 2 + #[inline] + pub fn half_height(&self) -> i32 { + self.height() / 2 } + #[inline] + pub fn center_x(&self) -> i32 { + self.x() + self.half_width() + } + + #[inline] pub fn center_y(&self) -> i32 { - self.y1() + self.height() / 2 + self.y() + self.half_height() } pub fn contains(&self, tile: &Tile) -> bool { - tile.x() >= self.x1() - && tile.y() >= self.y1() - && tile.x() <= self.x2() - && tile.y() <= self.y2() + tile.x() >= self.left() + && tile.y() >= self.top() + && tile.x() <= self.right() + && tile.y() <= self.bottom() } } @@ -195,43 +247,76 @@ impl TileHashMap { } const VIEWPORT_DEFAULT_CAPACITY: usize = 24 * 12; +const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = 64; // This structure keeps the list of tiles that are in the pending list, the // ones that are going to be rendered. pub struct PendingTiles { pub list: Vec, + pub spiral: Vec, + pub spiral_rect: TileRect, } impl PendingTiles { - pub fn new_empty() -> Self { + pub fn new() -> Self { Self { list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), + spiral: Vec::with_capacity(VIEWPORT_SPIRAL_DEFAULT_CAPACITY), + spiral_rect: TileRect::empty(), } } - // Generate tiles ordered by distance to the center (closest processed first). - fn generate_spiral(rect: &TileRect) -> Vec { - let cx = rect.center_x(); - let cy = rect.center_y(); - - // TileRect is inclusive (x1..=x2, y1..=y2). - let mut tiles = Vec::new(); - for x in rect.x1()..=rect.x2() { - for y in rect.y1()..=rect.y2() { - tiles.push(Tile(x, y)); - } + // Generate tiles in spiral order from center + fn generate_spiral(columns: i32, rows: i32) -> Vec { + let total = columns * rows; + if total <= 0 { + return Vec::new(); } - // We pop() from the end, so keep nearest-to-center tiles at the end. - tiles.sort_unstable_by(|a, b| { - let da = (a.x() - cx).abs() + (a.y() - cy).abs(); - let db = (b.x() - cx).abs() + (b.y() - cy).abs(); - da.cmp(&db) - .then_with(|| a.x().cmp(&b.x())) - .then_with(|| a.y().cmp(&b.y())) - }); - tiles.reverse(); - tiles + let mut result = Vec::with_capacity(total as usize); + let mut cx = 0; + let mut cy = 0; + + let ratio = (columns as f32 / rows as f32).ceil() as i32; + + let mut direction_current = 0; + let mut direction_total_x = ratio; + let mut direction_total_y = 1; + let mut direction = 0; + let mut current = 0; + + result.push(Tile(cx, cy)); + while current < total { + match direction { + 0 => cx += 1, + 1 => cy += 1, + 2 => cx -= 1, + 3 => cy -= 1, + _ => unreachable!("Invalid direction"), + } + + result.push(Tile(cx, cy)); + + direction_current += 1; + let direction_total = if direction % 2 == 0 { + direction_total_x + } else { + direction_total_y + }; + + if direction_current == direction_total { + if direction % 2 == 0 { + direction_total_x += 1; + } else { + direction_total_y += 1; + } + direction = (direction + 1) % 4; + direction_current = 0; + } + current += 1; + } + result.reverse(); + result } pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces, only_visible: bool) { @@ -247,7 +332,22 @@ impl PendingTiles { } else { &tile_viewbox.interest_rect }; - let spiral = Self::generate_spiral(spiral_rect); + + // If the spiral rect doesn't change we do not + // need to recalculate anything. + // if self.spiral_rect == *spiral_rect { + // return; + // } + + self.spiral_rect = *spiral_rect; + + // We do not regenerate spiral if the spiral_rect + // doesn't change. The spiral_rect is based on the + // viewbox so, if the viewbox doesn't change + // the spiral should not change. + if self.spiral.len() != (spiral_rect.width() * spiral_rect.height()) as usize { + self.spiral = Self::generate_spiral(spiral_rect.width(), spiral_rect.height()); + } // Partition tiles into 4 priority groups (highest priority = processed last due to pop()): // 1. visible + cached (fastest - just blit from cache) @@ -259,7 +359,9 @@ impl PendingTiles { let mut interest_cached = Vec::new(); let mut interest_uncached = Vec::new(); - for tile in spiral { + let center_tile = Tile(spiral_rect.center_x(), spiral_rect.center_y()); + for spiral_tile in self.spiral.iter() { + let tile = Tile(spiral_tile.0 + center_tile.0, spiral_tile.1 + center_tile.1); let is_visible = tile_viewbox.visible_rect.contains(&tile); let is_cached = surfaces.has_cached_tile_surface(tile);