♻️ Change how rendering spiral is generated

This commit is contained in:
Aitor Moreno 2026-04-30 09:37:55 +02:00
parent 8666610920
commit 103afe7e8b
6 changed files with 160 additions and 55 deletions

View File

@ -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;

View File

@ -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<QueueFrame> {
pub extern "C" fn render(timestamp: i32) -> Result<RenderQueueFrame> {
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<QueueFrame> {
}
}
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(())

View File

@ -40,7 +40,7 @@ pub use images::*;
type ClipStack = Vec<(Rect, Option<Corners>, 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<QueueFrame> {
) -> Result<RenderQueueFrame> {
#[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<QueueFrame> {
) -> Result<RenderQueueFrame> {
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<QueueFrame> {
) -> Result<RenderQueueFrame> {
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(

View File

@ -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;

View File

@ -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<QueueFrame> {
pub fn render_sync(&mut self) -> Result<RenderQueueFrame> {
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<QueueFrame> {
pub fn render_sync_shape(&mut self, id: &Uuid) -> Result<RenderQueueFrame> {
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<QueueFrame> {
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<RenderQueueFrame> {
// 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

View File

@ -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<Tile>,
pub spiral: Vec<Tile>,
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<Tile> {
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<Tile> {
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);