mirror of
https://github.com/penpot/penpot.git
synced 2026-07-23 06:28:14 +00:00
🐛 Fix minor glitches
This commit is contained in:
parent
dd9d183958
commit
06a52b7951
@ -135,6 +135,14 @@
|
||||
(wasm.mem/free)
|
||||
text)))
|
||||
|
||||
(defn ^:export wasmCaptureFrames
|
||||
[amount]
|
||||
(let [module wasm/internal-module
|
||||
f (when module (unchecked-get module "_capture_frames"))]
|
||||
(if (fn? f)
|
||||
(wasm.h/call module "_capture_frames" amount)
|
||||
(js/console.warn "[debug] render-wasm module not ready or missing _render_stats"))))
|
||||
|
||||
(defn ^:export wasmRenderStats
|
||||
[]
|
||||
(let [module wasm/internal-module
|
||||
@ -213,31 +221,6 @@
|
||||
opacity: 0.5;
|
||||
")
|
||||
|
||||
(defn ^:export fps
|
||||
"Adds a widget to keep track of the average FPS's"
|
||||
[]
|
||||
(let [last (volatile! (.now js/performance))
|
||||
avg (volatile! 0)
|
||||
node (-> (.createElement js/document "div")
|
||||
(obj/set! "id" "fps")
|
||||
(obj/set! "style" widget-style))
|
||||
body (obj/get js/document "body")
|
||||
|
||||
do-thing (fn do-thing []
|
||||
(timers/raf
|
||||
(fn []
|
||||
(let [cur (.now js/performance)
|
||||
ts (/ 1000 (* (- cur @last)))
|
||||
val (+ @avg (* (- ts @avg) 0.1))]
|
||||
|
||||
(obj/set! node "innerText" val)
|
||||
(vreset! last cur)
|
||||
(vreset! avg val)
|
||||
(do-thing)))))]
|
||||
|
||||
(.appendChild body node)
|
||||
(do-thing)))
|
||||
|
||||
(defn ^:export dump-state []
|
||||
(logjs "state" @st/state)
|
||||
nil)
|
||||
|
||||
@ -302,7 +302,7 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(false);
|
||||
render_state.cancel_animation_frame();
|
||||
render_state.tile_viewbox.update(render_state.viewbox);
|
||||
render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
|
||||
if render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
|
||||
@ -441,22 +441,6 @@ fn drag_crop_snapshot_window_px(
|
||||
(ox, oy, win_w, win_h)
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, interest: i32) -> skia::ISize {
|
||||
// First we retrieve the extended area of the viewport that we could render.
|
||||
let TileRect(isx, isy, iex, iey) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(viewbox, interest);
|
||||
|
||||
let dx = if isx.signum() != iex.signum() { 1 } else { 0 };
|
||||
let dy = if isy.signum() != iey.signum() { 1 } else { 0 };
|
||||
|
||||
let tile_size = tiles::TILE_SIZE;
|
||||
(
|
||||
((iex - isx).abs() + dx) * tile_size as i32,
|
||||
((iey - isy).abs() + dy) * tile_size as i32,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
/// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an
|
||||
/// interactive transform (drag/resize/rotate).
|
||||
@ -563,7 +547,7 @@ impl RenderState {
|
||||
render_area_with_margins: Rect::new_empty(),
|
||||
tiles,
|
||||
tile_viewbox: tiles::TileViewbox::new_with_interest(
|
||||
viewbox,
|
||||
&viewbox,
|
||||
options.dpr_viewport_interest_area_threshold,
|
||||
),
|
||||
pending_tiles: PendingTiles::new(),
|
||||
@ -837,7 +821,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_viewbox.update(self.viewbox);
|
||||
self.tile_viewbox.update(&self.viewbox);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -918,7 +902,7 @@ impl RenderState {
|
||||
// the interaction ends.
|
||||
if self.options.is_interactive_transform() {
|
||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
self.surfaces.draw_current_tile_direct(
|
||||
self.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
self.background_color,
|
||||
surfaces::DrawOnCache::No,
|
||||
@ -943,7 +927,7 @@ impl RenderState {
|
||||
.as_ref()
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
|
||||
self.surfaces.cache_current_tile_texture(
|
||||
self.surfaces.draw_current_tile_into_tile_atlas(
|
||||
&self.tile_viewbox,
|
||||
¤t_tile,
|
||||
&tile_rect,
|
||||
@ -952,11 +936,13 @@ impl RenderState {
|
||||
);
|
||||
|
||||
self.surfaces
|
||||
.draw_cached_tile_surface(current_tile, rect, self.background_color);
|
||||
.draw_cached_tile_into_backbuffer(current_tile, rect, self.background_color);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||
/// This function draws the "surface stack" into the specified "target" surface.
|
||||
pub fn draw_shape_surface_stack_into(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||
performance::begin_measure!("apply_drawing_to_render_canvas");
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
@ -1664,7 +1650,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if apply_to_current_surface {
|
||||
self.apply_drawing_to_render_canvas(Some(&shape), target_surface);
|
||||
self.draw_shape_surface_stack_into(Some(&shape), target_surface);
|
||||
}
|
||||
|
||||
// Only restore if we saved (optimization for simple shapes)
|
||||
@ -1925,7 +1911,7 @@ impl RenderState {
|
||||
let interest = self.options.dpr_viewport_interest_area_threshold;
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(
|
||||
self.cached_viewbox,
|
||||
&self.cached_viewbox,
|
||||
interest,
|
||||
);
|
||||
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr;
|
||||
@ -2005,20 +1991,15 @@ impl RenderState {
|
||||
// would be at wrong positions — skip them and let the full
|
||||
// render after set_view_end handle it.
|
||||
if !self.zoom_changed() {
|
||||
let visible_rect = tiles::get_tiles_for_viewbox(self.viewbox);
|
||||
let visible_rect = tiles::get_tiles_for_viewbox(&self.viewbox);
|
||||
let offset = self.viewbox.get_offset();
|
||||
for tx in visible_rect.x1()..=visible_rect.x2() {
|
||||
for ty in visible_rect.y1()..=visible_rect.y2() {
|
||||
let tile = tiles::Tile::from(tx, ty);
|
||||
if self.surfaces.has_cached_tile_surface(tile) {
|
||||
let tile_rect = skia::Rect::from_xywh(
|
||||
tx as f32 * tiles::TILE_SIZE - offset.x,
|
||||
ty as f32 * tiles::TILE_SIZE - offset.y,
|
||||
tiles::TILE_SIZE,
|
||||
tiles::TILE_SIZE,
|
||||
);
|
||||
let tile_rect = tile.get_rect_with_offset(&offset);
|
||||
self.surfaces
|
||||
.draw_cached_tile_surface(tile, tile_rect, bg_color);
|
||||
.draw_cached_tile_into_backbuffer(tile, tile_rect, bg_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2057,6 +2038,26 @@ impl RenderState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn gc(&mut self, tree: ShapesPoolRef) {
|
||||
#[cfg(feature = "stats")]
|
||||
self.stats.clear();
|
||||
|
||||
self.surfaces.gc();
|
||||
|
||||
self.pending_nodes.clear();
|
||||
if self.pending_nodes.capacity() < tree.len() {
|
||||
self.pending_nodes
|
||||
.reserve(tree.len() - self.pending_nodes.capacity());
|
||||
}
|
||||
// Clear nested state stacks to avoid residual fills/blurs from previous renders
|
||||
// being incorrectly applied to new frames
|
||||
self.nested_fills.clear();
|
||||
self.nested_blurs.clear();
|
||||
self.nested_shadows.clear();
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
@ -2064,13 +2065,12 @@ impl RenderState {
|
||||
timestamp: i32,
|
||||
sync_render: bool,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "stats")]
|
||||
self.stats.clear();
|
||||
self.gc(tree);
|
||||
|
||||
let _start = performance::begin_timed_log!("start_render_loop");
|
||||
let scale = self.get_scale();
|
||||
|
||||
self.tile_viewbox.update(self.viewbox);
|
||||
self.tile_viewbox.update(&self.viewbox);
|
||||
self.focus_mode.reset();
|
||||
|
||||
performance::begin_measure!("render");
|
||||
@ -2103,61 +2103,44 @@ impl RenderState {
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().scale((scale, scale));
|
||||
});
|
||||
|
||||
let viewbox_cache_size = get_cache_size(
|
||||
self.viewbox,
|
||||
self.options.dpr_viewport_interest_area_threshold,
|
||||
);
|
||||
let cached_viewbox_cache_size = get_cache_size(
|
||||
self.cached_viewbox,
|
||||
self.options.dpr_viewport_interest_area_threshold,
|
||||
);
|
||||
// Only resize cache if the new size is larger than the cached size
|
||||
// This avoids unnecessary surface recreations when the cache size decreases
|
||||
if viewbox_cache_size.width > cached_viewbox_cache_size.width
|
||||
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
|
||||
{
|
||||
self.surfaces.resize_cache(
|
||||
viewbox_cache_size,
|
||||
self.options.dpr_viewport_interest_area_threshold,
|
||||
)?;
|
||||
}
|
||||
self.surfaces.resize_cache_from_viewbox(
|
||||
&self.viewbox,
|
||||
&self.cached_viewbox,
|
||||
self.options.dpr_viewport_interest_area_threshold
|
||||
)?;
|
||||
|
||||
// FIXME - review debug
|
||||
// debug::render_debug_tiles_for_viewbox(self);
|
||||
|
||||
let _tile_start = performance::begin_timed_log!("tile_cache_update");
|
||||
|
||||
performance::begin_measure!("tile_cache");
|
||||
let only_visible = self.options.is_interactive_transform();
|
||||
self.pending_tiles
|
||||
.update(&self.tile_viewbox, &self.surfaces, only_visible);
|
||||
performance::end_measure!("tile_cache");
|
||||
performance::end_timed_log!("tile_cache_update", _tile_start);
|
||||
|
||||
self.pending_nodes.clear();
|
||||
if self.pending_nodes.capacity() < tree.len() {
|
||||
self.pending_nodes
|
||||
.reserve(tree.len() - self.pending_nodes.capacity());
|
||||
}
|
||||
// Clear nested state stacks to avoid residual fills/blurs from previous renders
|
||||
// being incorrectly applied to new frames
|
||||
self.nested_fills.clear();
|
||||
self.nested_blurs.clear();
|
||||
self.nested_shadows.clear();
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
performance::end_timed_log!("tile_cache_update", _tile_start);
|
||||
|
||||
self.render_in_progress = true;
|
||||
|
||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||
self.draw_shape_surface_stack_into(None, SurfaceId::Current);
|
||||
|
||||
if sync_render {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
} else {
|
||||
self.process_animation_frame(base_object, tree, timestamp)?;
|
||||
|
||||
// This is an option to debug frames.
|
||||
if self.options.capture_frames > 0 {
|
||||
self.options.capture_frames -= 1;
|
||||
}
|
||||
|
||||
// Update cached_viewbox after visible tiles render
|
||||
// synchronously so that render_from_cache uses the correct
|
||||
// zoom ratio even if interest-area tiles are still rendering
|
||||
@ -2215,8 +2198,9 @@ impl RenderState {
|
||||
performance::begin_measure!("process_animation_frame");
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
|
||||
|
||||
self.surfaces.draw_tile_atlas_to_backbuffer(&self.viewbox, &self.tile_viewbox);
|
||||
self.flush_and_submit();
|
||||
if !self.options.is_interactive_transform() {
|
||||
self.surfaces.draw_tile_atlas_to_backbuffer(&self.viewbox, &self.tile_viewbox);
|
||||
}
|
||||
|
||||
if self.render_in_progress {
|
||||
// Partial frame: just flush GPU work. The display shows the last
|
||||
@ -3286,7 +3270,7 @@ impl RenderState {
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
} else if visited_children {
|
||||
self.apply_drawing_to_render_canvas(Some(element), target_surface);
|
||||
self.draw_shape_surface_stack_into(Some(element), target_surface);
|
||||
}
|
||||
|
||||
// Skip nested state updates for flattened containers
|
||||
@ -3355,6 +3339,7 @@ impl RenderState {
|
||||
}
|
||||
iteration += 1;
|
||||
}
|
||||
|
||||
Ok((is_empty, false))
|
||||
}
|
||||
|
||||
@ -3379,13 +3364,18 @@ impl RenderState {
|
||||
|
||||
while !should_stop {
|
||||
if let Some(current_tile) = self.current_tile {
|
||||
// NOTA: Ahora no tenemos que cubrir el caso en el que el tile
|
||||
// no está cacheado porque se hará todo desde el draw_atlas.
|
||||
// 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.
|
||||
if !self.surfaces.has_cached_tile_surface(current_tile) {
|
||||
performance::begin_measure!("render_shape_tree::uncached");
|
||||
let (is_empty, early_return) = self
|
||||
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if self.options.capture_frames > 0 {
|
||||
debug::console_debug_surface(self, SurfaceId::Backbuffer);
|
||||
}
|
||||
|
||||
if early_return {
|
||||
return Ok(());
|
||||
}
|
||||
@ -3400,7 +3390,7 @@ impl RenderState {
|
||||
if self.options.is_interactive_transform() {
|
||||
// During drag, avoid snapshot-based caching. Draw Current directly
|
||||
// into Target (and Cache) to reduce stalls.
|
||||
self.surfaces.draw_current_tile_direct(
|
||||
self.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
self.background_color,
|
||||
surfaces::DrawOnCache::Yes,
|
||||
@ -3417,9 +3407,6 @@ impl RenderState {
|
||||
tile_rect,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Tile is uncached and has no shapes to render
|
||||
self.apply_render_to_final_canvas(tile_rect)?;
|
||||
}
|
||||
} else {
|
||||
if self.tiles.is_empty_at(current_tile) {
|
||||
@ -3442,61 +3429,68 @@ impl RenderState {
|
||||
// empty tile.
|
||||
self.current_tile_had_shapes = false;
|
||||
|
||||
if !self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
|
||||
// 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
|
||||
// which must contain the shapes behind it.
|
||||
let tile_has_bg_blur = ids.iter().any(|id| {
|
||||
tree.get(id).is_some_and(|s| {
|
||||
s.blur.is_some_and(|b| {
|
||||
!b.hidden && b.blur_type == BlurType::BackgroundBlur
|
||||
})
|
||||
})
|
||||
});
|
||||
let Some(ids) = self.tiles.get_shapes_at(next_tile) else {
|
||||
// If the tile is empty we do not need to render it.
|
||||
continue;
|
||||
};
|
||||
|
||||
// We only need first level shapes, in the same order as the parent node.
|
||||
//
|
||||
// During interactive transforms we may invalidate only the modified shapes
|
||||
// (to avoid massive ancestor eviction). However, we still composite full
|
||||
// tiles (we clear the tile rect before drawing Current), so we must render
|
||||
// all root shapes that can contribute to this tile; otherwise, unchanged
|
||||
// siblings inside the same tile would disappear.
|
||||
let mut valid_ids = Vec::with_capacity(ids.len());
|
||||
if self.options.is_interactive_transform() || tile_has_bg_blur {
|
||||
valid_ids.extend(root_ids.iter().copied());
|
||||
} else {
|
||||
for root_id in root_ids.iter() {
|
||||
if ids.contains(root_id) {
|
||||
valid_ids.push(*root_id);
|
||||
}
|
||||
}
|
||||
if self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
// If the tile is cached, then we do not need to
|
||||
// render it.
|
||||
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
|
||||
// which must contain the shapes behind it.
|
||||
let tile_has_bg_blur = ids.iter().any(|id| {
|
||||
tree.get(id).is_some_and(|s| {
|
||||
s.blur.is_some_and(|b| {
|
||||
!b.hidden && b.blur_type == BlurType::BackgroundBlur
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// We only need first level shapes, in the same order as the parent node.
|
||||
//
|
||||
// During interactive transforms we may invalidate only the modified shapes
|
||||
// (to avoid massive ancestor eviction). However, we still composite full
|
||||
// tiles (we clear the tile rect before drawing Current), so we must render
|
||||
// all root shapes that can contribute to this tile; otherwise, unchanged
|
||||
// siblings inside the same tile would disappear.
|
||||
let mut valid_ids = Vec::with_capacity(ids.len());
|
||||
if self.options.is_interactive_transform() || tile_has_bg_blur {
|
||||
valid_ids.extend(root_ids.iter().copied());
|
||||
} else {
|
||||
for root_id in root_ids.iter() {
|
||||
if ids.contains(root_id) {
|
||||
valid_ids.push(*root_id);
|
||||
}
|
||||
|
||||
if !valid_ids.is_empty() {
|
||||
self.current_tile_had_shapes = true;
|
||||
}
|
||||
|
||||
self.pending_nodes.extend(valid_ids.into_iter().map(|id| {
|
||||
NodeRenderState {
|
||||
id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if !valid_ids.is_empty() {
|
||||
self.current_tile_had_shapes = true;
|
||||
}
|
||||
|
||||
self.pending_nodes.extend(valid_ids.into_iter().map(|id| {
|
||||
NodeRenderState {
|
||||
id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// If there are no more pending tiles, stop.
|
||||
should_stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
self.render_in_progress = false;
|
||||
self.surfaces.gc();
|
||||
|
||||
// Mark cache as valid for render_from_cache.
|
||||
// Only update for full-quality renders (non-fast mode).
|
||||
|
||||
@ -4,7 +4,7 @@ use super::{tiles, RenderState, SurfaceId};
|
||||
use macros::wasm_error;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::get_render_state;
|
||||
use crate::{get_render_state};
|
||||
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
@ -273,6 +273,14 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn capture_frames(capture_frames: i32) -> Result<()> {
|
||||
get_render_state().options.set_capture_frames(capture_frames);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@ -29,6 +29,7 @@ pub struct RenderOptions {
|
||||
pub max_blocking_time_ms: i32,
|
||||
pub node_batch_threshold: i32,
|
||||
pub blur_downscale_threshold: f32,
|
||||
pub capture_frames: i32,
|
||||
}
|
||||
|
||||
impl Default for RenderOptions {
|
||||
@ -44,6 +45,7 @@ impl Default for RenderOptions {
|
||||
max_blocking_time_ms: MAX_BLOCKING_TIME_MS,
|
||||
node_batch_threshold: NODE_BATCH_THRESHOLD,
|
||||
blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD,
|
||||
capture_frames: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,6 +68,10 @@ impl RenderOptions {
|
||||
self.fast_mode = enabled;
|
||||
}
|
||||
|
||||
pub fn set_capture_frames(&mut self, capture_frames: i32) {
|
||||
self.capture_frames = capture_frames;
|
||||
}
|
||||
|
||||
/// Updates the dpr viewport interest area threshold.
|
||||
/// This function is updated when the dpr or the
|
||||
/// viewport_interest_area_threshold is changed
|
||||
|
||||
@ -5,7 +5,7 @@ use crate::{get_gpu_state, performance};
|
||||
|
||||
use skia_safe::{self as skia, IRect, Paint, RRect, Rect};
|
||||
|
||||
use super::{gpu_state::GpuState, tiles, tiles::Tile, tiles::TileViewbox, tiles::TILE_SIZE};
|
||||
use super::{gpu_state::GpuState, tiles, tiles::Tile, tiles::TileRect, tiles::TileViewbox};
|
||||
use crate::math::Point;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
@ -13,9 +13,18 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
const TEXTURES_CACHE_CAPACITY: usize = 1024;
|
||||
const TEXTURES_BATCH_DELETE: usize = 256;
|
||||
|
||||
// This is the amount of extra space we're going to give to all the surfaces to render shapes.
|
||||
// If it's too big it could affect performance.
|
||||
const TILE_SIZE: i32 = tiles::TILE_SIZE as i32;
|
||||
const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||
const TILE_MARGIN_SIZE: i32 = TILE_SIZE * TILE_SIZE_MULTIPLIER / 4;
|
||||
const TILE_DRAWABLE_RECT: IRect = IRect {
|
||||
left: TILE_MARGIN_SIZE,
|
||||
top: TILE_MARGIN_SIZE,
|
||||
right: TILE_MARGIN_SIZE + TILE_SIZE,
|
||||
bottom: TILE_MARGIN_SIZE + TILE_SIZE,
|
||||
};
|
||||
|
||||
/// Atlas texture size limits (px per side).
|
||||
///
|
||||
@ -24,9 +33,24 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||
/// [`Surfaces::set_max_atlas_texture_size`].
|
||||
/// - `MAX_ATLAS_TEXTURE_SIZE` is a hard upper bound to clamp the runtime value
|
||||
/// (defensive cap to avoid accidentally creating oversized GPU textures).
|
||||
const MAX_ATLAS_TEXTURE_SIZE: i32 = 4096;
|
||||
const MAX_ATLAS_TEXTURE_SIZE: i32 = 8192;
|
||||
const DEFAULT_MAX_ATLAS_TEXTURE_SIZE: i32 = 1024;
|
||||
|
||||
pub fn get_cache_size(viewbox: &Viewbox, interest: i32) -> skia::ISize {
|
||||
// First we retrieve the extended area of the viewport that we could render.
|
||||
let TileRect(isx, isy, iex, iey) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(viewbox, interest);
|
||||
|
||||
let dx = if isx.signum() != iex.signum() { 1 } else { 0 };
|
||||
let dy = if isy.signum() != iey.signum() { 1 } else { 0 };
|
||||
|
||||
(
|
||||
((iex - isx).abs() + dx) * TILE_SIZE,
|
||||
((iey - isy).abs() + dy) * TILE_SIZE,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DrawOnCache {
|
||||
Yes,
|
||||
@ -202,7 +226,10 @@ impl Surfaces {
|
||||
/// WebGL `MAX_TEXTURE_SIZE` reported by the browser. Values are clamped to
|
||||
/// a small minimum so the atlas logic stays well-defined.
|
||||
pub fn set_max_atlas_texture_size(&mut self, max_px: i32) {
|
||||
self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE);
|
||||
self.max_atlas_texture_size = max_px.clamp(
|
||||
TILE_SIZE,
|
||||
MAX_ATLAS_TEXTURE_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -268,7 +295,7 @@ impl Surfaces {
|
||||
}
|
||||
|
||||
// Add padding to reduce realloc frequency.
|
||||
let pad = TILE_SIZE;
|
||||
let pad = tiles::TILE_SIZE;
|
||||
new_left -= pad;
|
||||
new_top -= pad;
|
||||
new_right += pad;
|
||||
@ -279,7 +306,7 @@ impl Surfaces {
|
||||
|
||||
// Compute atlas scale needed to fit within the fixed texture cap.
|
||||
// Keep the highest possible scale (closest to 1.0) that still fits.
|
||||
let cap = self.max_atlas_texture_size.max(TILE_SIZE as i32) as f32;
|
||||
let cap = self.max_atlas_texture_size.max(TILE_SIZE) as f32;
|
||||
let required_scale = (cap / doc_w).min(cap / doc_h).clamp(0.01, 1.0);
|
||||
|
||||
// Never upscale the atlas (it would add blur and churn).
|
||||
@ -876,12 +903,39 @@ impl Surfaces {
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
self.cache.canvas().reset_matrix();
|
||||
self.cache.canvas().translate((
|
||||
(interest_area_threshold as f32 * TILE_SIZE),
|
||||
(interest_area_threshold as f32 * TILE_SIZE),
|
||||
(interest_area_threshold * TILE_SIZE) as f32,
|
||||
(interest_area_threshold * TILE_SIZE) as f32,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize_cache_from_viewbox(
|
||||
&mut self,
|
||||
viewbox: &Viewbox,
|
||||
cached_viewbox: &Viewbox,
|
||||
interest_area_threshold: i32,
|
||||
) -> Result<()> {
|
||||
let viewbox_cache_size = get_cache_size(
|
||||
viewbox,
|
||||
interest_area_threshold,
|
||||
);
|
||||
let cached_viewbox_cache_size = get_cache_size(
|
||||
cached_viewbox,
|
||||
interest_area_threshold,
|
||||
);
|
||||
// Only resize cache if the new size is larger than the cached size
|
||||
// This avoids unnecessary surface recreations when the cache size decreases
|
||||
if viewbox_cache_size.width > cached_viewbox_cache_size.width
|
||||
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
|
||||
{
|
||||
return self.resize_cache(
|
||||
viewbox_cache_size,
|
||||
interest_area_threshold,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw_rect_to(
|
||||
&mut self,
|
||||
id: SurfaceId,
|
||||
@ -1038,6 +1092,7 @@ impl Surfaces {
|
||||
self.canvas(SurfaceId::Debug)
|
||||
.clear(skia::Color::TRANSPARENT)
|
||||
.reset_matrix();
|
||||
|
||||
self.canvas(SurfaceId::UI)
|
||||
.clear(skia::Color::TRANSPARENT)
|
||||
.reset_matrix();
|
||||
@ -1054,7 +1109,7 @@ impl Surfaces {
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
pub fn cache_current_tile_texture(
|
||||
pub fn draw_current_tile_into_tile_atlas(
|
||||
&mut self,
|
||||
tile_viewbox: &TileViewbox,
|
||||
tile: &Tile,
|
||||
@ -1063,15 +1118,9 @@ impl Surfaces {
|
||||
tile_doc_rect: skia::Rect,
|
||||
) {
|
||||
let gpu_state = get_gpu_state();
|
||||
let rect = IRect::from_xywh(
|
||||
self.margins.width,
|
||||
self.margins.height,
|
||||
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
|
||||
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||
);
|
||||
let rect = TILE_DRAWABLE_RECT;
|
||||
|
||||
let tile_image_opt = self.current.image_snapshot_with_bounds(rect);
|
||||
|
||||
if let Some(tile_image) = tile_image_opt {
|
||||
if !skip_cache_surface {
|
||||
// Draw to cache surface for render_from_cache
|
||||
@ -1088,6 +1137,7 @@ impl Surfaces {
|
||||
let _ = self.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect);
|
||||
self.atlas_tile_doc_rects.insert(*tile, tile_doc_rect);
|
||||
|
||||
// Draws current tile into tile atlas
|
||||
let tile_ref = self.tiles.add(tile_viewbox, tile);
|
||||
self.tile_atlas.canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
@ -1249,13 +1299,17 @@ impl Surfaces {
|
||||
self.tile_atlas.image_snapshot_with_bounds(rect)
|
||||
}
|
||||
|
||||
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||
pub fn draw_cached_tile_into_backbuffer(&mut self, tile: Tile, rect: skia::Rect, _color: skia::Color) {
|
||||
if let Some(image) = self.get_tile_image_from_tile_atlas(tile) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
self.backbuffer.canvas().draw_rect(rect, &paint);
|
||||
self.backbuffer
|
||||
.canvas()
|
||||
let backbuffer_canvas = self.backbuffer.canvas();
|
||||
|
||||
// if color != skia::Color::TRANSPARENT {
|
||||
// let mut paint = skia::Paint::default();
|
||||
// paint.set_color(color);
|
||||
// backbuffer_canvas.draw_rect(rect, &paint);
|
||||
// }
|
||||
|
||||
backbuffer_canvas
|
||||
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
||||
}
|
||||
}
|
||||
@ -1264,16 +1318,16 @@ impl Surfaces {
|
||||
/// cache-aligned rect. This keeps the Cache surface in sync with
|
||||
/// Backbuffer so that `render_from_cache` (used during pan) has the
|
||||
/// full scene including tiles served from the texture cache.
|
||||
pub fn draw_cached_tile_to_cache(
|
||||
pub fn draw_cached_tile_into_cache(
|
||||
&mut self,
|
||||
tile: Tile,
|
||||
aligned_rect: &skia::Rect,
|
||||
color: skia::Color,
|
||||
_color: skia::Color,
|
||||
) {
|
||||
if let Some(image) = self.get_tile_image_from_tile_atlas(tile) {
|
||||
let mut bg = skia::Paint::default();
|
||||
bg.set_color(color);
|
||||
self.cache.canvas().draw_rect(aligned_rect, &bg);
|
||||
// let mut bg = skia::Paint::default();
|
||||
// bg.set_color(color);
|
||||
// self.cache.canvas().draw_rect(aligned_rect, &bg);
|
||||
self.cache.canvas().draw_image_rect(
|
||||
&image,
|
||||
None,
|
||||
@ -1286,10 +1340,10 @@ impl Surfaces {
|
||||
/// Draws the current tile directly to the backbuffer and cache surfaces without
|
||||
/// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't
|
||||
/// populate the tile texture cache (suitable for one-shot renders like tests).
|
||||
pub fn draw_current_tile_direct(
|
||||
pub fn draw_current_tile_into_backbuffer(
|
||||
&mut self,
|
||||
tile_rect: &skia::Rect,
|
||||
color: skia::Color,
|
||||
_color: skia::Color,
|
||||
draw_on_cache: DrawOnCache,
|
||||
) {
|
||||
let sampling_options = self.sampling_options;
|
||||
@ -1302,10 +1356,11 @@ impl Surfaces {
|
||||
let src_rect_f = skia::Rect::from(src_rect);
|
||||
|
||||
let backbuffer_canvas = self.backbuffer.canvas();
|
||||
|
||||
// Draw background
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
backbuffer_canvas.draw_rect(tile_rect, &paint);
|
||||
// let mut paint = skia::Paint::default();
|
||||
// paint.set_color(color);
|
||||
// backbuffer_canvas.draw_rect(tile_rect, &paint);
|
||||
|
||||
// Draw current surface directly to target (no snapshot)
|
||||
self.current.draw(
|
||||
@ -1487,8 +1542,8 @@ pub struct TileTextureCache {
|
||||
impl TileTextureCache {
|
||||
pub fn new(texture_size: i32, capacity: usize) -> Self {
|
||||
Self {
|
||||
tile_size: TILE_SIZE,
|
||||
provider: TileAtlasTextureProvider::new(texture_size, TILE_SIZE as i32),
|
||||
tile_size: tiles::TILE_SIZE,
|
||||
provider: TileAtlasTextureProvider::new(texture_size, TILE_SIZE),
|
||||
transforms: Vec::with_capacity(capacity),
|
||||
textures: Vec::with_capacity(capacity),
|
||||
grid: HashMap::with_capacity(capacity),
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use skia_safe as skia;
|
||||
use crate::render::Surfaces;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::view::Viewbox;
|
||||
use skia_safe as skia;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||
pub struct Tile(pub i32, pub i32);
|
||||
|
||||
@ -10,12 +10,26 @@ impl Tile {
|
||||
pub fn from(x: i32, y: i32) -> Self {
|
||||
Tile(x, y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn x(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn y(&self) -> i32 {
|
||||
self.1
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_rect_with_offset(&self, offset: &skia::Point) -> skia::Rect {
|
||||
skia::Rect::from_xywh(
|
||||
self.0 as f32 * TILE_SIZE - offset.x,
|
||||
self.1 as f32 * TILE_SIZE - offset.y,
|
||||
TILE_SIZE,
|
||||
TILE_SIZE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||
@ -134,7 +148,7 @@ pub struct TileViewbox {
|
||||
}
|
||||
|
||||
impl TileViewbox {
|
||||
pub fn new_with_interest(viewbox: Viewbox, interest: i32) -> Self {
|
||||
pub fn new_with_interest(viewbox: &Viewbox, interest: i32) -> Self {
|
||||
Self {
|
||||
visible_rect: get_tiles_for_viewbox(viewbox),
|
||||
interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest),
|
||||
@ -143,7 +157,7 @@ impl TileViewbox {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, viewbox: Viewbox) {
|
||||
pub fn update(&mut self, viewbox: &Viewbox) {
|
||||
self.visible_rect = get_tiles_for_viewbox(viewbox);
|
||||
self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest);
|
||||
self.center = get_tile_center_for_viewbox(viewbox);
|
||||
@ -161,6 +175,7 @@ impl TileViewbox {
|
||||
|
||||
pub const TILE_SIZE: f32 = 512.;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_tile_dimensions() -> skia::ISize {
|
||||
(TILE_SIZE as i32, TILE_SIZE as i32).into()
|
||||
}
|
||||
@ -175,17 +190,17 @@ pub fn get_tiles_for_rect(rect: skia::Rect, tile_size: f32) -> TileRect {
|
||||
TileRect(sx, sy, ex, ey)
|
||||
}
|
||||
|
||||
pub fn get_tiles_for_viewbox(viewbox: Viewbox) -> TileRect {
|
||||
pub fn get_tiles_for_viewbox(viewbox: &Viewbox) -> TileRect {
|
||||
let tile_size = get_tile_size(viewbox.get_scale());
|
||||
get_tiles_for_rect(viewbox.area, tile_size)
|
||||
}
|
||||
|
||||
pub fn get_tiles_for_viewbox_with_interest(viewbox: Viewbox, interest: i32) -> TileRect {
|
||||
pub fn get_tiles_for_viewbox_with_interest(viewbox: &Viewbox, interest: i32) -> TileRect {
|
||||
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox);
|
||||
TileRect(sx - interest, sy - interest, ex + interest, ey + interest)
|
||||
}
|
||||
|
||||
pub fn get_tile_center_for_viewbox(viewbox: Viewbox) -> Tile {
|
||||
pub fn get_tile_center_for_viewbox(viewbox: &Viewbox) -> Tile {
|
||||
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox);
|
||||
Tile((ex - sx) / 2, (ey - sy) / 2)
|
||||
}
|
||||
@ -207,7 +222,7 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect {
|
||||
skia::Rect::from_xywh(tx, ty, ts, ts)
|
||||
}
|
||||
|
||||
// This structure is usseful to keep all the shape uuids by shape id.
|
||||
// This structure is useful to keep all the shape uuids by shape id.
|
||||
pub struct TileHashMap {
|
||||
grid: HashMap<Tile, HashSet<Uuid>>,
|
||||
index: HashMap<Uuid, HashSet<Tile>>,
|
||||
@ -261,12 +276,19 @@ impl TileHashMap {
|
||||
}
|
||||
|
||||
const VIEWPORT_DEFAULT_CAPACITY: usize = 24 * 12;
|
||||
const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = 64;
|
||||
const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = VIEWPORT_DEFAULT_CAPACITY;
|
||||
|
||||
// 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>,
|
||||
|
||||
// This reduces memory allocation.
|
||||
visible_cached: Vec<Tile>,
|
||||
visible_uncached: Vec<Tile>,
|
||||
interest_cached: Vec<Tile>,
|
||||
interest_uncached: Vec<Tile>,
|
||||
|
||||
pub spiral: Vec<Tile>,
|
||||
pub spiral_rect: TileRect,
|
||||
}
|
||||
@ -275,15 +297,21 @@ impl PendingTiles {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
|
||||
visible_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
visible_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
interest_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
interest_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
|
||||
spiral: Vec::with_capacity(VIEWPORT_SPIRAL_DEFAULT_CAPACITY),
|
||||
spiral_rect: TileRect::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tiles in spiral order from center
|
||||
fn generate_spiral(columns: usize, rows: usize) -> Vec<Tile> {
|
||||
fn generate_spiral(&mut self, columns: usize, rows: usize) {
|
||||
let total = columns * rows;
|
||||
let mut result = Vec::with_capacity(total);
|
||||
|
||||
let mut cx = 0;
|
||||
let mut cy = 0;
|
||||
|
||||
@ -294,8 +322,8 @@ impl PendingTiles {
|
||||
let mut direction_total_y = 1;
|
||||
let mut direction = 0;
|
||||
|
||||
result.push(Tile(cx, cy));
|
||||
while result.len() < total {
|
||||
self.spiral.push(Tile(cx, cy));
|
||||
while self.spiral.len() < total {
|
||||
match direction {
|
||||
0 => cx += 1,
|
||||
1 => cy += 1,
|
||||
@ -304,7 +332,7 @@ impl PendingTiles {
|
||||
_ => unreachable!("Invalid direction"),
|
||||
}
|
||||
|
||||
result.push(Tile(cx, cy));
|
||||
self.spiral.push(Tile(cx, cy));
|
||||
|
||||
direction_current += 1;
|
||||
let direction_total = if direction % 2 == 0 {
|
||||
@ -323,8 +351,7 @@ impl PendingTiles {
|
||||
direction_current = 0;
|
||||
}
|
||||
}
|
||||
result.reverse();
|
||||
result
|
||||
self.spiral.reverse();
|
||||
}
|
||||
|
||||
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces, only_visible: bool) {
|
||||
@ -349,8 +376,7 @@ impl PendingTiles {
|
||||
// the spiral should not change.
|
||||
let total = (spiral_rect.width() * spiral_rect.height()) as usize;
|
||||
if self.spiral.len() < total {
|
||||
self.spiral =
|
||||
Self::generate_spiral(spiral_rect.width() as usize, spiral_rect.height() as usize);
|
||||
self.generate_spiral(spiral_rect.width() as usize, spiral_rect.height() as usize);
|
||||
}
|
||||
|
||||
// Partition tiles into 4 priority groups (highest priority = processed last due to pop()):
|
||||
@ -358,10 +384,10 @@ impl PendingTiles {
|
||||
// 2. visible + uncached (user sees these, render next)
|
||||
// 3. interest + cached (pre-rendered area, blit from cache)
|
||||
// 4. interest + uncached (lowest priority - background pre-render)
|
||||
let mut visible_cached = Vec::new();
|
||||
let mut visible_uncached = Vec::new();
|
||||
let mut interest_cached = Vec::new();
|
||||
let mut interest_uncached = Vec::new();
|
||||
self.visible_cached.clear();
|
||||
self.visible_uncached.clear();
|
||||
self.interest_cached.clear();
|
||||
self.interest_uncached.clear();
|
||||
|
||||
let center_tile = Tile(spiral_rect.center_x(), spiral_rect.center_y());
|
||||
for spiral_tile in self.spiral.iter() {
|
||||
@ -370,19 +396,19 @@ impl PendingTiles {
|
||||
let is_cached = surfaces.has_cached_tile_surface(tile);
|
||||
|
||||
match (is_visible, is_cached) {
|
||||
(true, true) => visible_cached.push(tile),
|
||||
(true, false) => visible_uncached.push(tile),
|
||||
(false, true) => interest_cached.push(tile),
|
||||
(false, false) => interest_uncached.push(tile),
|
||||
(true, true) => self.visible_cached.push(tile),
|
||||
(true, false) => self.visible_uncached.push(tile),
|
||||
(false, true) => self.interest_cached.push(tile),
|
||||
(false, false) => self.interest_uncached.push(tile),
|
||||
}
|
||||
}
|
||||
|
||||
// Build final list with lowest priority first (they get popped last)
|
||||
// Order: interest_uncached, interest_cached, visible_uncached, visible_cached
|
||||
self.list.extend(interest_uncached);
|
||||
self.list.extend(interest_cached);
|
||||
self.list.extend(visible_uncached);
|
||||
self.list.extend(visible_cached);
|
||||
self.list.extend(self.interest_uncached.iter());
|
||||
self.list.extend(self.interest_cached.iter());
|
||||
self.list.extend(self.visible_uncached.iter());
|
||||
self.list.extend(self.visible_cached.iter());
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<Tile> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user