🐛 Fix minor glitches

This commit is contained in:
Aitor Moreno 2026-05-19 11:57:27 +02:00
parent dd9d183958
commit 06a52b7951
7 changed files with 283 additions and 211 deletions

View File

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

View File

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

View File

@ -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,
&current_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).

View File

@ -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")]

View File

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

View File

@ -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),

View File

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