♻️ Refactor render pipeline (#9891)

* ♻️ Refactor viewbox

* 🎉 Add draw_atlas alternative to draw tiles

* 🐛 Fix minor glitches

* ♻️ Change how process_animation_frame works

* ♻️ Refactor document atlas

* ♻️ Refactor max texture size

* ♻️ Refactor entrypoints and dead_code
This commit is contained in:
Aitor Moreno 2026-06-02 09:38:52 +02:00 committed by GitHub
parent 7bf519a127
commit d0f6d5b3a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1164 additions and 1151 deletions

View File

@ -414,7 +414,8 @@ test("[Taiga #9929] Paste text in workspace", async ({ page, context }) => {
.getByText("Lorem ipsum dolor");
});
test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
// I've skipped this test because it doesn't make sense with the new render.
test.skip("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
page,
context,
}) => {

View File

@ -306,6 +306,20 @@
(declare set-shape-vertical-align fonts-from-text-content)
(declare reload-renderer!)
;; These are the type of frames we have in our
;; render pipeline.
(def ^:const FRAME_TYPE_NONE 0) ;; This type should never "leak".
(def ^:const FRAME_TYPE_PARTIAL 1) ;; A frame needs more render calls to end.
(def ^:const FRAME_TYPE_FULL 2) ;; A frame was full.
(defn- internal-render
([]
(internal-render 0))
([timestamp]
(set! wasm/internal-frame-type (h/call wasm/internal-module "_render" timestamp wasm/internal-frame-type))
(when (= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
(request-render "frame-type-partial"))))
(defn- build-reload-payload
"Builds renderer reload payload from current application state.
Avoids keeping heavyweight object snapshots in memory."
@ -337,8 +351,8 @@
;; This should never be called from the outside.
(defn- render
[timestamp]
(when (initialized?)
(h/call wasm/internal-module "_render" timestamp)
(when (and wasm/context-initialized? (not @wasm/context-lost?))
(internal-render timestamp)
;; Update text editor blink (so cursor toggles) using the same timestamp
(try
@ -1201,7 +1215,7 @@
;; completes in the first frame. For zoom, interest-
;; area tiles (~3 tile margin) don't block the main
;; thread.
(h/call wasm/internal-module "_render" 0)))]
(internal-render)))]
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
(defn set-view-box
@ -1804,50 +1818,19 @@
(contains? cf/flags :render-wasm-info)
(bit-or 2r00000000000000000000000000001000)))
(defn- wasm-aa-threshold-from-route-params
"Reads optional `aa_threshold` query param from the router"
[]
(defn- wasm-get-numeric-value
[name]
(when-let [raw (let [p (rt/get-params @st/state)]
(:aa_threshold p))]
(get p name))]
(let [n (if (string? raw) (js/parseFloat raw) raw)]
(when (and (number? n) (not (js/isNaN n)) (pos? n))
n))))
(defn- wasm-blur-downscale-threshold-from-route-params
"Reads optional `aa_threshold` query param from the router"
[]
(when-let [raw (let [p (rt/get-params @st/state)]
(:blur_downscale_threshold p))]
(let [n (if (string? raw) (js/parseFloat raw) raw)]
(when (and (number? n) (not (js/isNaN n)) (pos? n))
n))))
(defn- wasm-max-blocking-time-ms-from-route-params
"Reads optional `aa_threshold` query param from the router"
[]
(when-let [raw (let [p (rt/get-params @st/state)]
(:max_blocking_time_ms p))]
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
(when (and (number? n) (not (js/isNaN n)) (pos? n))
n))))
(defn- wasm-node-batch-threshold-from-route-params
"Reads optional `aa_threshold` query param from the router"
[]
(when-let [raw (let [p (rt/get-params @st/state)]
(:node_batch_threshold p))]
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
(when (and (number? n) (not (js/isNaN n)) (pos? n))
n))))
(defn- wasm-viewport-interest-area-threshold-from-route-params
"Reads optional `aa_threshold` query param from the router"
[]
(when-let [raw (let [p (rt/get-params @st/state)]
(:viewport_interest_area_threshold p))]
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
(when (and (number? n) (not (js/isNaN n)) (pos? n))
n))))
(defn- wasm-set-param-from-route-params-if-present
[param-name]
(when-let [value (wasm-get-numeric-value param-name)]
(let [setter-name (str/concat "_set_" (name param-name))]
(h/call wasm/internal-module setter-name value))))
(defn set-canvas-size
[canvas]
@ -1914,18 +1897,13 @@
;; Initialize Wasm Render Engine
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
(h/call wasm/internal-module "_set_render_options" flags dpr)
(when-let [t (wasm-aa-threshold-from-route-params)]
(h/call wasm/internal-module "_set_antialias_threshold" t))
(when-let [t (wasm-viewport-interest-area-threshold-from-route-params)]
(h/call wasm/internal-module "_set_viewport_interest_area_threshold" t))
(when-let [t (wasm-max-blocking-time-ms-from-route-params)]
(h/call wasm/internal-module "_set_max_blocking_time_ms" t))
(when-let [t (wasm-node-batch-threshold-from-route-params)]
(h/call wasm/internal-module "_set_node_batch_threshold" t))
(when-let [t (wasm-blur-downscale-threshold-from-route-params)]
(h/call wasm/internal-module "_set_blur_downscale_threshold" t))
(when-let [max-tex (webgl/max-texture-size context)]
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
;; Configurable parameters.
(wasm-set-param-from-route-params-if-present :antialias_threshold)
(wasm-set-param-from-route-params-if-present :viewport_interest_area_threshold)
(wasm-set-param-from-route-params-if-present :max_blocking_time_ms)
(wasm-set-param-from-route-params-if-present :node_batch_threshold)
(wasm-set-param-from-route-params-if-present :blur_downscale_threshold)
;; Set browser and canvas size only after initialization
(h/call wasm/internal-module "_set_browser" browser)

View File

@ -11,15 +11,6 @@
[app.render-wasm.wasm :as wasm]
[promesa.core :as p]))
(defn max-texture-size
"Returns `gl.MAX_TEXTURE_SIZE` (max dimension of a 2D texture), or nil if
unavailable."
[gl]
(when gl
(let [n (.getParameter ^js gl (.-MAX_TEXTURE_SIZE ^js gl))]
(when (and (number? n) (pos? n) (js/isFinite n))
(js/Math.floor n)))))
(defn get-webgl-context
"Gets the WebGL context from the WASM module"
[]

View File

@ -8,6 +8,7 @@
(:require ["./api/shared.js" :as shared]))
(defonce internal-frame-id nil)
(defonce internal-frame-type 0)
(defonce internal-module #js {})
;; Reference to the HTML canvas element.

View File

@ -37,8 +37,6 @@
[app.util.debug :as dbg]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.object :as obj]
[app.util.timers :as timers]
[beicon.v2.core :as rx]
[cljs.pprint :refer [pprint]]
[cuerdas.core :as str]
@ -135,6 +133,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 +219,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

@ -32,6 +32,7 @@ export EM_MALLOC="dlmalloc"
export EMCC_CFLAGS="--no-entry \
--js-library src/js/wapi.js \
-sMALLOC=$EM_MALLOC \
-sINVOKE_RUN=0 \
-sALLOW_TABLE_GROWTH=0 \
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_HEAP=$EM_INITIAL_HEAP \

View File

@ -1,5 +1,8 @@
use macros::wasm_error;
#[cfg(target_arch = "wasm32")]
use crate::emscripten::init_gl;
use crate::mem;
use crate::render::{gpu_state::GpuState, RenderState};
use crate::state::{State, TextEditorState};
@ -107,6 +110,7 @@ fn design_init() {
}
}
/// Initializes TextEditorState.
fn text_editor_init() {
unsafe {
let text_editor_state = TextEditorState::new();
@ -117,6 +121,8 @@ fn text_editor_init() {
#[no_mangle]
#[wasm_error]
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
#[cfg(target_arch = "wasm32")]
init_gl!();
gpu_init();
render_init(width, height);
text_editor_init();
@ -130,7 +136,6 @@ pub extern "C" fn clean_up() -> Result<()> {
// Cancel the current animation frame if it exists so
// it won't try to render without context
let render_state = get_render_state();
render_state.cancel_animation_frame();
render_state.prepare_context_loss_cleanup();
unsafe { DESIGN_STATE = std::ptr::null_mut() }
mem::free_bytes()?;

View File

@ -1,18 +1,4 @@
addToLibrary({
wapi_requestAnimationFrame: function wapi_requestAnimationFrame() {
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
setTimeout(Module._process_animation_frame);
} else {
return window.requestAnimationFrame(Module._process_animation_frame);
}
},
wapi_cancelAnimationFrame: function wapi_cancelAnimationFrame(frameId) {
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
clearTimeout(frameId);
} else {
return window.cancelAnimationFrame(frameId);
}
},
wapi_notifyTilesRenderComplete: function wapi_notifyTilesRenderComplete() {
// The corresponding listener lives on `document` (main thread), so in a
// worker context we simply skip the dispatch instead of crashing.

View File

@ -19,6 +19,7 @@ use std::collections::HashMap;
#[allow(unused_imports)]
use crate::error::{Error, Result};
use crate::render::{FrameType, RenderFlag};
use globals::{get_design_state, get_gpu_state, get_render_state};
@ -89,15 +90,6 @@ pub extern "C" fn set_antialias_threshold(threshold: f32) -> Result<()> {
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_max_atlas_texture_size(max_px: i32) -> Result<()> {
get_render_state()
.surfaces
.set_max_atlas_texture_size(max_px);
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
@ -112,7 +104,7 @@ pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn render(timestamp: i32) -> Result<()> {
pub extern "C" fn render(timestamp: i32, flags: u8) -> Result<FrameType> {
with_state!(state, {
state.rebuild_touched_tiles();
// Drain the throttled modifier-tile invalidation accumulated
@ -128,11 +120,17 @@ pub extern "C" fn render(timestamp: i32) -> Result<()> {
state.rebuild_modifier_tiles(&ids)?;
}
}
state
.start_render_loop(timestamp)
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
let frame_type = if flags & RenderFlag::Partial as u8 == RenderFlag::Partial as u8 {
state
.continue_render_loop(timestamp)
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?
} else {
state
.start_render_loop(timestamp)
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?
};
return Ok(frame_type);
});
Ok(())
}
#[no_mangle]
@ -179,7 +177,7 @@ pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
with_state!(state, {
// Don't cancel the animation frame — let the async render
// continue populating the tile HashMap in the background.
// process_animation_frame skips flush_and_submit in fast
// `continue_render_loop` skips flush_and_submit in fast
// mode so it won't present stale Target content. The
// tile HashMap is position-independent, so tiles rendered
// for the old viewport can be reused by the next full
@ -239,16 +237,6 @@ pub extern "C" fn render_loading_overlay() -> Result<()> {
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
let result = with_state!(state, { state.process_animation_frame(timestamp) });
if let Err(err) = result {
eprintln!("process_animation_frame error: {}", err);
}
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn reset_canvas() -> Result<()> {
@ -301,12 +289,7 @@ pub extern "C" fn set_view_end() -> Result<()> {
performance::begin_measure!("set_view_end");
let render_state = get_render_state();
render_state.options.set_fast_mode(false);
render_state.cancel_animation_frame();
let scale = render_state.get_scale();
render_state
.tile_viewbox
.update(render_state.viewbox, scale);
render_state.tile_viewbox.update(&render_state.viewbox);
if render_state.options.is_profile_rebuild_tiles() {
state.rebuild_tiles();
@ -358,7 +341,6 @@ pub extern "C" fn set_modifiers_end() -> Result<()> {
let render_state = get_render_state();
render_state.options.set_fast_mode(false);
render_state.options.set_interactive_transform(false);
render_state.cancel_animation_frame();
performance::end_measure!("set_modifiers_end");
Ok(())
}
@ -946,7 +928,9 @@ pub fn free_gpu_resources() {
get_render_state().free_gpu_resources();
}
fn main() {
#[cfg(target_arch = "wasm32")]
init_gl!();
pub fn main() {
// Why an empty main?
// Right now with the target `wasm32-unknown-emscripten` it is not possible
// to compile a rust project without a main and generate the necessary glue
// code. So the only option is to compile it with an empty main.
}

View File

@ -6,6 +6,7 @@ pub type Rect = skia::Rect;
pub type Matrix = skia::Matrix;
pub type Vector = skia::Vector;
pub type Point = skia::Point;
pub type Size = skia::Size;
const THRESHOLD: f32 = 0.001;

View File

@ -1,10 +1,8 @@
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")]
pub fn get_time() -> i32 {
crate::get_now!() as i32
}
#[allow(dead_code)]
#[cfg(not(target_arch = "wasm32"))]
pub fn get_time() -> i32 {
let now = std::time::Instant::now();

View File

@ -38,6 +38,21 @@ pub use images::*;
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
#[repr(u8)]
pub enum FrameType {
None = 0,
Partial = 1,
Full = 2,
}
#[allow(dead_code)]
#[repr(u8)]
pub enum RenderFlag {
None = 0,
Partial = 1,
Full = 2,
}
#[derive(Debug)]
pub struct NodeRenderState {
pub id: Uuid,
@ -334,10 +349,6 @@ pub(crate) struct RenderState {
pub cached_viewbox: Viewbox,
pub images: ImageStore,
pub background_color: skia::Color,
// Identifier of the current requestAnimationFrame call, if any.
pub render_request_id: Option<i32>,
// Indicates whether the rendering process has pending frames.
pub render_in_progress: bool,
// Stack of nodes pending to be rendered.
pending_nodes: Vec<NodeRenderState>,
pub current_tile: Option<tiles::Tile>,
@ -370,7 +381,7 @@ pub(crate) struct RenderState {
/// Cleared at the beginning of a render pass; set to true after we clear Cache the first
/// time we are about to blit a tile into Cache for this pass.
pub cache_cleared_this_render: bool,
/// True iff the current tile had shapes assigned to it when we
/// True if the current tile had shapes assigned to it when we
/// started rendering it. Lets us distinguish a genuinely empty
/// tile (skip composite, just clear) from a tile whose walker
/// finished its work in a previous PAF and is now being resumed
@ -401,7 +412,7 @@ pub struct InteractiveDragCrop {
/// Chooses a window inside the full workspace-pixel crop `[0, out_w) × [0, out_h)` with each side
/// at most `max_side_px` (**without scaling**): centered on the projection of
/// `viewport_doc ∩ src_doc_bounds`, or on the full crop if that intersection is empty.
/// `max_side_px` should match [`Surfaces::max_texture_dimension_px`] (same budget as the atlas).
/// `max_side_px` should match [`GpuState::max_texture_size`] (same budget as the atlas).
#[allow(clippy::too_many_arguments)]
fn drag_crop_snapshot_window_px(
max_side_px: i32,
@ -441,22 +452,6 @@ fn drag_crop_snapshot_window_px(
(ox, oy, win_w, win_h)
}
pub fn get_cache_size(viewbox: Viewbox, scale: f32, 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, scale);
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).
@ -554,8 +549,6 @@ impl RenderState {
cached_viewbox: Viewbox::new(0., 0.),
images: ImageStore::new(),
background_color: skia::Color::TRANSPARENT,
render_request_id: None,
render_in_progress: false,
pending_nodes: vec![],
current_tile: None,
sampling_options,
@ -563,9 +556,8 @@ 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,
1.0,
),
pending_tiles: PendingTiles::new(),
nested_fills: vec![],
@ -787,10 +779,12 @@ impl RenderState {
self.tile_viewbox
.set_interest(self.options.dpr_viewport_interest_area_threshold);
self.resize(
self.viewbox.width.floor() as i32,
self.viewbox.height.floor() as i32,
self.viewbox.width().floor() as i32,
self.viewbox.height().floor() as i32,
)?;
self.fonts.set_scale_debug_font(dpr);
self.viewbox.set_dpr(dpr);
self.surfaces.set_dpr(dpr);
}
Ok(())
}
@ -836,7 +830,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.get_scale());
self.tile_viewbox.update(&self.viewbox);
Ok(())
}
@ -901,23 +895,13 @@ impl RenderState {
self.flush_and_submit();
}
#[allow(dead_code)]
pub fn get_canvas_at(&mut self, surface_id: SurfaceId) -> &skia::Canvas {
self.surfaces.canvas(surface_id)
}
#[allow(dead_code)]
pub fn restore_canvas(&mut self, surface_id: SurfaceId) {
self.surfaces.canvas(surface_id).restore();
}
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> {
pub fn apply_render_to_final_canvas(&mut self) -> Result<()> {
// During interactive transforms we render tiles directly into Target; updating the cache
// (snapshot -> atlas blit -> tiles.add) can force GPU stalls. Defer cache rebuild until
// 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,
@ -942,7 +926,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,
@ -950,12 +934,15 @@ impl RenderState {
self.render_area,
);
let rect = self.get_current_tile_bounds()?;
self.surfaces
.draw_cached_tile_surface(current_tile, rect, self.background_color);
.draw_cached_tile_into_backbuffer(current_tile, &rect);
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();
@ -1663,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)
@ -1691,14 +1678,6 @@ impl RenderState {
self.surfaces.update_render_context(self.render_area, scale);
}
pub fn cancel_animation_frame(&mut self) {
if self.render_in_progress {
if let Some(frame_id) = self.render_request_id {
wapi::cancel_animation_frame!(frame_id);
}
}
}
fn rebuild_backbuffer_crop_cache(&mut self, tree: ShapesPoolRef) {
self.backbuffer_crop_cache.clear();
@ -1778,12 +1757,12 @@ impl RenderState {
let vb_left = self.viewbox.area.left;
let vb_top = self.viewbox.area.top;
let (bb_w, bb_h) = self.surfaces.surface_size(SurfaceId::Backbuffer);
let max_snap_px = self.surfaces.max_texture_dimension_px();
let max_snap_px = get_gpu_state().max_texture_size();
// Snapshot the atlas once for the whole pass so that all shapes sharing
// the tile/atlas fallback path reuse the same GPU image rather than each
// triggering a separate `image_snapshot` flush.
let atlas_snap = self.surfaces.atlas_snapshot_for_drag_crop();
let atlas_snap = self.surfaces.atlas.snapshot_for_drag_crop();
// Scratch surface reused across all shapes that need the tile/atlas
// fallback — avoids one WebGL texture allocation per shape.
@ -1901,16 +1880,14 @@ impl RenderState {
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
let _start = performance::begin_timed_log!("render_from_cache");
performance::begin_measure!("render_from_cache");
let cached_scale = self.get_cached_scale();
let bg_color = self.background_color;
// During fast mode (pan/zoom), if a previous full-quality render still has pending tiles,
// always prefer the persistent atlas. The atlas is incrementally updated as tiles finish,
// and drawing from it avoids mixing a partially-updated Cache surface with missing tiles.
if self.options.is_fast_mode() && self.render_in_progress && self.surfaces.has_atlas() {
if self.options.is_fast_mode() && !self.surfaces.atlas.is_empty() {
self.surfaces
.draw_atlas_to_backbuffer(self.viewbox, self.options.dpr, bg_color);
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
self.present_frame(shapes);
performance::end_measure!("render_from_cache");
@ -1925,11 +1902,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,
interest,
cached_scale,
);
tiles::get_tiles_for_viewbox_with_interest(&self.cached_viewbox, interest);
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr;
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr;
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
@ -1944,8 +1917,8 @@ impl RenderState {
let cache_h = cache_dim.height as f32;
// Viewport in target pixels.
let vw = (self.viewbox.width * self.options.dpr).max(1.0);
let vh = (self.viewbox.height * self.options.dpr).max(1.0);
let vw = self.viewbox.dpr_width().max(1.0);
let vh = self.viewbox.dpr_height().max(1.0);
// Inverse-map viewport corners into cache coordinates.
// target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords).
@ -1956,8 +1929,11 @@ impl RenderState {
0.0
};
let cx0 = (0.0 * inv) - translate_x;
let cy0 = (0.0 * inv) - translate_y;
// let cx0 = (0.0 * inv) - translate_x;
// let cy0 = (0.0 * inv) - translate_y;
// NOTA: 0.0 * inv => siempre 0
let cx0 = -translate_x;
let cy0 = -translate_y;
let cx1 = (vw * inv) - translate_x;
let cy1 = (vh * inv) - translate_y;
@ -1970,12 +1946,9 @@ impl RenderState {
min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
if !cache_covers {
// Early return only if atlas exists; otherwise keep cache path.
if self.surfaces.has_atlas() {
self.surfaces.draw_atlas_to_backbuffer(
self.viewbox,
self.options.dpr,
bg_color,
);
if !self.surfaces.atlas.is_empty() {
self.surfaces
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
self.present_frame(shapes);
performance::end_measure!("render_from_cache");
@ -1985,21 +1958,9 @@ impl RenderState {
}
}
// Setup canvas transform
{
let canvas = self.surfaces.canvas(SurfaceId::Backbuffer);
canvas.save();
canvas.scale((navigate_zoom, navigate_zoom));
canvas.translate((translate_x, translate_y));
canvas.clear(bg_color);
}
// Draw directly from cache surface, avoiding snapshot overhead
self.surfaces.draw_cache_to_backbuffer();
// Restore canvas state
self.surfaces.canvas(SurfaceId::Backbuffer).restore();
// During pure pan (same zoom), draw tiles from the HashMap
// on top of the scaled Cache surface. Cached tile textures
// include full-quality effects (shadows, blur) from the last
@ -2008,23 +1969,14 @@ 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 current_scale = self.get_scale();
let visible_rect = tiles::get_tiles_for_viewbox(self.viewbox, current_scale);
let vb_offset_x = self.viewbox.area.left * current_scale;
let vb_offset_y = self.viewbox.area.top * current_scale;
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 - vb_offset_x,
ty as f32 * tiles::TILE_SIZE - vb_offset_y,
tiles::TILE_SIZE,
tiles::TILE_SIZE,
);
self.surfaces
.draw_cached_tile_surface(tile, tile_rect, bg_color);
let rect = tile.get_rect_with_offset(&offset);
self.surfaces.draw_cached_tile_into_backbuffer(tile, &rect);
}
}
}
@ -2063,20 +2015,43 @@ impl RenderState {
Ok(())
}
/// Clears all the necessary vecs and hashmaps.
/// Also garbage collects surfaces.
fn clear(&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>,
tree: ShapesPoolRef,
timestamp: i32,
sync_render: bool,
) -> Result<()> {
#[cfg(feature = "stats")]
self.stats.clear();
) -> Result<FrameType> {
self.clear(tree);
let _start = performance::begin_timed_log!("start_render_loop");
let scale = self.get_scale();
self.tile_viewbox.update(self.viewbox, scale);
self.tile_viewbox.update(&self.viewbox);
self.focus_mode.reset();
performance::begin_measure!("render");
@ -2086,7 +2061,7 @@ impl RenderState {
// to clamp atlas updates. This prevents zoom-out tiles from forcing atlas
// growth far beyond real content.
let doc_bounds = self.compute_document_bounds(base_object, tree);
self.surfaces.set_atlas_doc_bounds(doc_bounds);
self.surfaces.atlas.set_doc_bounds(doc_bounds);
self.cache_cleared_this_render = false;
if self.options.is_interactive_transform() {
@ -2109,63 +2084,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,
scale,
self.surfaces.resize_cache_from_viewbox(
&self.viewbox,
&self.cached_viewbox,
self.options.dpr_viewport_interest_area_threshold,
);
let cached_viewbox_cache_size = get_cache_size(
self.cached_viewbox,
scale,
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,
)?;
}
)?;
// 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;
self.render_in_progress = true;
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
self.draw_shape_surface_stack_into(None, SurfaceId::Current);
#[allow(unused)]
let mut frame_type = FrameType::None;
if sync_render {
self.render_shape_tree_sync(base_object, tree, timestamp)?;
frame_type = self.render_shape_tree_sync(base_object, tree, timestamp)?;
} else {
self.process_animation_frame(base_object, tree, timestamp)?;
frame_type = self.continue_render_loop(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
@ -2180,7 +2136,7 @@ impl RenderState {
performance::end_measure!("start_render_loop");
performance::end_timed_log!("start_render_loop", _start);
Ok(())
Ok(frame_type)
}
fn compute_document_bounds(
@ -2214,37 +2170,45 @@ impl RenderState {
acc
}
pub fn process_animation_frame(
pub fn continue_render_loop(
&mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<()> {
performance::begin_measure!("process_animation_frame");
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
) -> Result<FrameType> {
performance::begin_measure!("continue_render_loop");
let frame_type = self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
if self.render_in_progress {
// Partial frame: just flush GPU work. The display shows the last
// fully submitted frame; no need to copy or draw UI overlays here.
self.flush();
self.cancel_animation_frame();
self.render_request_id = Some(wapi::request_animation_frame!());
} else {
// A full-quality frame is now complete. Rebuild the per-shape crop
// cache from the clean Backbuffer (no UI overlay yet) so that
// interactive drag backgrounds don't include the grid overlay.
if !self.options.is_fast_mode() && !self.options.is_interactive_transform() {
self.rebuild_backbuffer_crop_cache(tree);
}
// present_frame: copy clean Backbuffer → Target, draw UI/debug
// overlays on Target only, then flush. Backbuffer stays overlay-free.
self.present_frame(tree);
wapi::notify_tiles_render_complete!();
performance::end_measure!("render");
if !self.options.is_interactive_transform() {
self.surfaces
.draw_tile_atlas_to_backbuffer(&self.viewbox, &self.tile_viewbox);
}
performance::end_measure!("process_animation_frame");
Ok(())
match frame_type {
FrameType::None => {
panic!("FrameType::None");
}
FrameType::Partial => {
// Partial frame: just flush GPU work. The display shows the last
// fully submitted frame; no need to copy or draw UI overlays here.
self.flush();
}
FrameType::Full => {
// A full-quality frame is now complete. Rebuild the per-shape crop
// cache from the clean Backbuffer (no UI overlay yet) so that
// interactive drag backgrounds don't include the grid overlay.
if !self.options.is_fast_mode() && !self.options.is_interactive_transform() {
self.rebuild_backbuffer_crop_cache(tree);
}
// present_frame: copy clean Backbuffer → Target, draw UI/debug
// overlays on Target only, then flush. Backbuffer stays overlay-free.
self.present_frame(tree);
wapi::notify_tiles_render_complete!();
performance::end_measure!("render");
}
}
performance::end_measure!("continue_render_loop");
Ok(frame_type)
}
pub fn render_shape_tree_sync(
@ -2252,10 +2216,10 @@ impl RenderState {
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<()> {
) -> Result<FrameType> {
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
self.present_frame(tree);
Ok(())
Ok(FrameType::Full)
}
pub fn render_shape_pixels(
@ -2560,18 +2524,11 @@ impl RenderState {
}
pub fn get_current_tile_bounds(&mut self) -> Result<Rect> {
let tiles::Tile(tile_x, tile_y) = self
let tile = self
.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
let scale = self.get_scale();
let offset_x = self.viewbox.area.left * scale;
let offset_y = self.viewbox.area.top * scale;
Ok(Rect::from_xywh(
(tile_x as f32 * tiles::TILE_SIZE) - offset_x,
(tile_y as f32 * tiles::TILE_SIZE) - offset_y,
tiles::TILE_SIZE,
tiles::TILE_SIZE,
))
let offset = self.viewbox.get_offset();
Ok(tile.get_rect_with_offset(&offset))
}
pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect {
@ -3337,7 +3294,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
@ -3406,6 +3363,7 @@ impl RenderState {
}
iteration += 1;
}
Ok((is_empty, false))
}
@ -3415,7 +3373,7 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
) -> Result<()> {
) -> Result<FrameType> {
let mut should_stop = false;
let root_ids = {
if let Some(shape_id) = base_object {
@ -3430,53 +3388,20 @@ impl RenderState {
while !should_stop {
if let Some(current_tile) = self.current_tile {
if self.surfaces.has_cached_tile_surface(current_tile) {
performance::begin_measure!("render_shape_tree::cached");
// During interactive transforms, `Target` is preserved and seeded once
// from Backbuffer. Cached tiles are therefore already visible and
// re-blitting them costs extra GPU work.
let tile_rect = self.get_current_tile_bounds()?;
if !self.options.is_interactive_transform() {
self.surfaces.draw_cached_tile_surface(
current_tile,
tile_rect,
self.background_color,
);
}
// Also draw the cached tile to the Cache surface so
// render_from_cache (used during pan) has the full scene.
// apply_render_to_final_canvas clears Cache on the first
// uncached tile, but cached tiles must also be present.
if !self.options.is_fast_mode() {
if !self.cache_cleared_this_render {
self.surfaces.clear_cache(self.background_color);
self.cache_cleared_this_render = true;
}
let aligned_rect = self.get_aligned_tile_bounds(current_tile);
self.surfaces.draw_cached_tile_to_cache(
current_tile,
&aligned_rect,
self.background_color,
);
}
performance::end_measure!("render_shape_tree::cached");
if self.options.is_debug_visible() {
debug::render_workspace_current_tile(
self,
"Cached".to_string(),
current_tile,
tile_rect,
);
}
} else {
// 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(());
return Ok(FrameType::Partial);
}
performance::end_measure!("render_shape_tree::uncached");
@ -3489,13 +3414,13 @@ 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,
);
} else {
self.apply_render_to_final_canvas(tile_rect)?;
self.apply_render_to_final_canvas()?;
}
if self.options.is_debug_visible() {
@ -3506,10 +3431,9 @@ 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) {
self.surfaces.remove_cached_tile_surface(current_tile);
}
}
@ -3527,62 +3451,65 @@ 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).
// An async render can complete while fast mode is active
@ -3595,7 +3522,7 @@ impl RenderState {
self.cached_viewbox = self.viewbox;
}
Ok(())
Ok(FrameType::Full)
}
/*
@ -3648,7 +3575,7 @@ impl RenderState {
shape: &Shape,
tree: ShapesPoolRef,
) -> HashSet<tiles::Tile> {
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
let tile_rect = self.get_tiles_for_shape(shape, tree);
// Collect old tiles to avoid borrow conflict with remove_shape_at
let old_tiles: Vec<_> = self
@ -3661,7 +3588,7 @@ impl RenderState {
// When the shape has an active modifier (i.e. is being moved/resized),
// clear its OLD doc-space extent from the atlas using the raw
// (pre-modifier) shape. The per-tile clearing done later via
// `clear_tile_in_atlas` only covers tiles tracked in `atlas_tile_doc_rects`
// `clear_tile_in_atlas` only covers tiles tracked in `atlas.tile_doc_rects`
// at the current zoom level. However, the atlas may also contain stale
// pixels from previous zoom levels (tiles are larger / smaller in doc
// space at different zoom scales) that were never re-tracked after a zoom
@ -3673,7 +3600,9 @@ impl RenderState {
if tree.get_modifier(&shape.id).is_some() {
if let Some(raw_shape) = tree.get_raw(&shape.id) {
let old_extrect = raw_shape.extrect(tree, 1.0);
self.surfaces.clear_doc_rect_in_atlas_clipped(old_extrect);
self.surfaces
.atlas
.clear_doc_rect_in_atlas_clipped(old_extrect);
}
}
@ -3684,7 +3613,7 @@ impl RenderState {
}
// Then, add the shape to the new tiles
for tile in (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y))) {
for tile in tile_rect.iter(true) {
self.tiles.add_shape_at(tile, shape.id);
result.insert(tile);
}
@ -3713,16 +3642,13 @@ impl RenderState {
shape: &Shape,
tree: ShapesPoolRef,
) -> Vec<tiles::Tile> {
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
let tile_rect = self.get_tiles_for_shape(shape, tree);
let old_tiles: HashSet<tiles::Tile> = self
.tiles
.get_tiles_of(shape.id)
.map_or(HashSet::new(), |tiles| tiles.iter().copied().collect());
let new_tiles: HashSet<tiles::Tile> = (rsx..=rex)
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)))
.collect();
let new_tiles: HashSet<tiles::Tile> = tile_rect.iter(true).collect();
// Tiles where shape is being removed from index (left interest area)
let removed: Vec<_> = old_tiles.difference(&new_tiles).copied().collect();
@ -3747,18 +3673,16 @@ impl RenderState {
}
/*
* Add the tiles forthe shape to the index.
* Add the tiles for the shape to the index.
* returns the tiles that have been updated
*/
pub fn add_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec<tiles::Tile> {
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
let tiles: Vec<_> = (rsx..=rex)
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)))
.collect();
performance::begin_measure!("add_shape_tiles");
let tiles: Vec<tiles::Tile> = self.get_tiles_for_shape(shape, tree).iter(true).collect();
for tile in tiles.iter() {
self.tiles.add_shape_at(*tile, shape.id);
}
performance::end_measure!("add_shape_tiles");
tiles
}
@ -3771,8 +3695,9 @@ impl RenderState {
/// survive so that fast-mode renders during pan still show shadows/blur.
pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) {
let zoom_changed = self.zoom_changed();
let mut nodes = vec![Uuid::nil()];
performance::begin_measure!("rebuild_tile_index");
let mut nodes = Vec::<Uuid>::with_capacity(64);
nodes.push(Uuid::nil());
while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) {
if shape_id != Uuid::nil() {
@ -3789,6 +3714,7 @@ impl RenderState {
}
}
}
performance::end_measure!("rebuild_tile_index");
}
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
@ -3927,11 +3853,7 @@ impl RenderState {
if let Some((_, export_scale)) = self.export_context {
return export_scale;
}
self.viewbox.zoom() * self.options.dpr
}
pub fn get_cached_scale(&self) -> f32 {
self.cached_viewbox.zoom() * self.options.dpr
self.viewbox.get_scale()
}
pub fn zoom_changed(&self) -> bool {

View File

@ -1,3 +1,4 @@
#![allow(dead_code)]
use super::{tiles, RenderState, SurfaceId};
#[cfg(target_arch = "wasm32")]
@ -22,7 +23,6 @@ fn get_debug_rect(rect: Rect) -> Rect {
)
}
#[allow(dead_code)]
fn render_debug_view(render_state: &mut RenderState) {
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
@ -36,7 +36,6 @@ fn render_debug_view(render_state: &mut RenderState) {
.draw_rect(rect, &paint);
}
#[allow(dead_code)]
pub fn render_debug_cache_surface(render_state: &mut RenderState) {
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
canvas.save();
@ -78,7 +77,6 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
}
}
#[allow(dead_code)]
pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
let tiles::TileRect(sx, sy, ex, ey) = render_state.tile_viewbox.interest_rect;
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
@ -91,7 +89,6 @@ pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
}
// Renders the tiles in the viewbox
#[allow(dead_code)]
pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
let scale = render_state.get_scale();
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
@ -101,30 +98,25 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
paint.set_stroke_width(1.);
let tile_size = tiles::get_tile_size(scale);
let tiles::TileRect(sx, sy, ex, ey) =
tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size);
let tile_rect = tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size);
let tiles::TileRect(sx, sy, ex, ey) = tile_rect;
let str_rect = format!("{} {} {} {}", sx, sy, ex, ey);
let debug_font = render_state.fonts.debug_font();
canvas.draw_str(str_rect, skia::Point::new(100.0, 100.0), debug_font, &paint);
let tile_size = tiles::get_tile_size(scale);
for y in sy..=ey {
for x in sx..=ex {
let rect = Rect::from_xywh(
x as f32 * tile_size,
y as f32 * tile_size,
tile_size,
tile_size,
);
let debug_rect = get_debug_rect(rect);
let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.);
let str = format!("{}:{}", x, y);
let debug_font = render_state.fonts.debug_font();
paint.set_style(skia::PaintStyle::Fill);
canvas.draw_str(str, p, debug_font, &paint);
canvas.draw_rect(debug_rect, &paint);
}
for tile in tile_rect.iter(true) {
let tiles::Tile(x, y) = tile;
let rect = tile.get_rect_with_size(tile_size);
let debug_rect = get_debug_rect(rect);
let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.);
let str = format!("{}:{}", x, y);
let debug_font = render_state.fonts.debug_font();
paint.set_style(skia::PaintStyle::Fill);
canvas.draw_str(str, p, debug_font, &paint);
canvas.draw_rect(debug_rect, &paint);
}
}
@ -187,13 +179,11 @@ pub fn render_debug_shape(
}
}
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")]
pub fn trap() {
run_script!("debugger");
}
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")]
#[derive(Debug, PartialEq)]
pub enum SurfaceBackendKind {
@ -203,7 +193,6 @@ pub enum SurfaceBackendKind {
Unknown,
}
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")]
pub fn classify_surface_backend(surface: &mut skia::Surface) -> SurfaceBackendKind {
if skia::gpu::surfaces::get_backend_texture(
@ -231,7 +220,6 @@ pub fn classify_surface_backend(surface: &mut skia::Surface) -> SurfaceBackendKi
SurfaceBackendKind::Unknown
}
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")]
pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
let base64_image = render_state
@ -242,7 +230,6 @@ pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"));
}
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")]
pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceId) {
let base64_image = render_state
@ -253,7 +240,6 @@ pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceI
println!("{}", base64_image);
}
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")]
pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect: skia::Rect) {
let int_rect = skia::IRect::from_ltrb(
@ -273,6 +259,16 @@ 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

@ -5,6 +5,9 @@ use skia_safe::gpu::{
};
use skia_safe::{self as skia, ISize};
const MIN_MAX_TEXTURE_SIZE: i32 = 512;
const MAX_MAX_TEXTURE_SIZE: i32 = 8192 * 2;
#[derive(Debug, Clone)]
pub struct GpuState {
pub context: DirectContext,
@ -28,6 +31,7 @@ impl GpuState {
let context = gpu::direct_contexts::make_gl(interface, Some(&context_options)).ok_or(
Error::CriticalError("Failed to create GL context".to_string()),
)?;
let framebuffer_info = {
let mut fboid: gl::types::GLint = 0;
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
@ -47,6 +51,12 @@ impl GpuState {
})
}
pub fn max_texture_size(&self) -> i32 {
self.context
.max_texture_size()
.clamp(MIN_MAX_TEXTURE_SIZE, MAX_MAX_TEXTURE_SIZE)
}
fn delete_gl_texture(&mut self, texture_id: gl::types::GLuint) -> bool {
unsafe {
gl::DeleteTextures(1, &texture_id);

View File

@ -30,6 +30,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 {
@ -45,6 +46,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,
}
}
}
@ -67,6 +69,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

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,8 @@ use crate::{
error::Result,
math::Rect,
shapes::{
calculate_position_data, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
Stroke, StrokeKind, TextContent,
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind,
TextContent,
},
utils::{get_fallback_fonts, get_font_collection},
};
@ -12,7 +12,7 @@ use skia_safe::{
self as skia,
canvas::SaveLayerRec,
textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle},
Canvas, ImageFilter, Paint, Path,
Canvas, ImageFilter, Paint,
};
pub fn stroke_paragraph_builder_group_from_text(
@ -552,78 +552,6 @@ pub fn calculate_decoration_metrics(
)
}
#[allow(dead_code)]
fn calculate_total_paragraphs_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 {
paragraphs
.iter_mut()
.map(|p| {
let mut paragraph = p.build();
paragraph.layout(width);
paragraph.height()
})
.sum()
}
#[allow(dead_code)]
fn calculate_all_paragraphs_height(
paragraph_groups: &mut [Vec<ParagraphBuilder>],
width: f32,
) -> f32 {
paragraph_groups
.iter_mut()
.map(|group| {
// For stroke groups, only count the first paragraph to avoid double-counting
if group.len() > 1 {
let mut paragraph = group[0].build();
paragraph.layout(width);
paragraph.height()
} else {
calculate_total_paragraphs_height(group, width)
}
})
.sum()
}
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_as_path(
render_state: &mut RenderState,
paths: &Vec<(Path, Paint)>,
surface_id: Option<SurfaceId>,
) {
let canvas = render_state
.surfaces
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Fills));
for (path, paint) in paths {
// Note: path can be empty
canvas.draw_path(path, paint);
}
}
#[allow(dead_code)]
pub fn render_position_data(
render_state: &mut RenderState,
surface_id: SurfaceId,
shape: &Shape,
text_content: &TextContent,
) {
let position_data = calculate_position_data(shape, text_content, false);
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
paint.set_stroke_width(2.);
for pd in position_data {
let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height);
render_state
.surfaces
.canvas_and_mark_dirty(surface_id)
.draw_rect(rect, &paint);
}
}
// How to use it?
// Type::Text(text_content) => {
// self.surfaces

View File

@ -1,7 +1,6 @@
use crate::uuid::Uuid;
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum Layout {
FlexLayout(LayoutData, FlexData),
GridLayout(LayoutData, GridData),

View File

@ -501,7 +501,6 @@ pub fn propagate_modifiers(
}
}
// #[allow(dead_code)]
Ok(modifiers
.iter()
.map(|(key, val)| TransformEntry::from_input(*key, *val))

View File

@ -1,5 +1,3 @@
#![allow(dead_code)]
use crate::error::{Error, Result};
use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt};
use crate::shapes::{
@ -123,7 +121,7 @@ struct ChildAxis {
max_across_size: f32,
is_fill_main: bool,
is_fill_across: bool,
z_index: i32,
_z_index: i32, // unused
bounds: Bounds,
}
@ -146,7 +144,7 @@ impl ChildAxis {
max_across_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
is_fill_main: child.is_layout_horizontal_fill(),
is_fill_across: child.is_layout_vertical_fill(),
z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
_z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
bounds: *child_bounds,
}
} else {
@ -164,7 +162,7 @@ impl ChildAxis {
max_main_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
is_fill_main: child.is_layout_vertical_fill(),
is_fill_across: child.is_layout_horizontal_fill(),
z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
_z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
bounds: *child_bounds,
}
};

View File

@ -115,7 +115,6 @@ impl TextContentSize {
#[derive(Debug, Clone, Copy, Default)]
pub struct TextPositionWithAffinity {
#[allow(dead_code)]
pub position_with_affinity: PositionWithAffinity,
pub paragraph: usize,
pub offset: usize,
@ -244,9 +243,9 @@ impl TextContentLayout {
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TextDecorationSegment {
#[allow(dead_code)]
pub kind: skia::textlayout::TextDecoration,
pub text_style: skia::textlayout::TextStyle,
pub y: f32,
@ -392,16 +391,6 @@ impl TextContent {
self.bounds = Rect::from_xywh(x, y, w, h);
}
#[allow(dead_code)]
pub fn x(&self) -> f32 {
self.bounds.x()
}
#[allow(dead_code)]
pub fn y(&self) -> f32 {
self.bounds.y()
}
pub fn add_paragraph(&mut self, paragraph: Paragraph) {
self.paragraphs.push(paragraph);
self.content_version = self.content_version.wrapping_add(1);
@ -621,10 +610,6 @@ impl TextContent {
let mut offset_y = 0.0;
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
// IMPORTANT! I'm keeping this because I think it should be better to have the span index
// cached the same way we keep the paragraph index.
#[allow(dead_code)]
let mut _span_index: usize = 0;
for (paragraph_index, layout_paragraph) in layout_paragraphs.enumerate() {
let start_y = offset_y;
let end_y = offset_y + layout_paragraph.height();
@ -652,15 +637,8 @@ impl TextContent {
// in which span we are.
let mut computed_position: usize = 0;
// This could be useful in the future as part of the TextPositionWithAffinity.
#[allow(dead_code)]
let mut _span_offset: usize = 0;
// If paragraph has no spans, default to span 0, offset 0
if paragraph.children().is_empty() {
_span_index = 0;
_span_offset = 0;
} else {
if !paragraph.children().is_empty() {
for span in paragraph.children() {
let length = span.text.chars().count();
let start_position = computed_position;
@ -670,19 +648,15 @@ impl TextContent {
// Handle empty spans: if the span is empty and current position
// matches the start, this is the right span
if length == 0 && current_position == start_position {
_span_offset = 0;
break;
}
if start_position <= current_position
&& end_position >= current_position
{
_span_offset =
position_with_affinity.position as usize - start_position;
break;
}
computed_position += length;
_span_index += 1;
}
}
@ -1152,11 +1126,6 @@ impl Paragraph {
}
}
#[allow(dead_code)]
fn set_children(&mut self, children: Vec<TextSpan>) {
self.children = children;
}
pub fn children(&self) -> &[TextSpan] {
&self.children
}
@ -1165,11 +1134,6 @@ impl Paragraph {
&mut self.children
}
#[allow(dead_code)]
fn add_span(&mut self, span: TextSpan) {
self.children.push(span);
}
pub fn line_height(&self) -> f32 {
self.line_height
}
@ -1306,11 +1270,6 @@ impl TextSpan {
self.text = text;
}
#[allow(dead_code)]
pub fn fills(&self) -> &[shapes::Fill] {
&self.fills
}
pub fn to_style(
&self,
content_bounds: &Rect,
@ -1415,7 +1374,6 @@ impl TextSpan {
}
}
#[allow(dead_code)]
#[derive(Debug, Copy, Clone)]
pub struct PositionData {
pub paragraph: u32,
@ -1429,21 +1387,17 @@ pub struct PositionData {
pub direction: u32,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct ParagraphLayout {
pub paragraph: skia::textlayout::Paragraph,
pub x: f32,
pub y: f32,
pub spans: Vec<crate::shapes::TextSpan>,
pub decorations: Vec<TextDecorationSegment>,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct TextLayoutData {
pub position_data: Vec<PositionData>,
pub content_rect: Rect,
pub paragraphs: Vec<ParagraphLayout>,
}
@ -1508,12 +1462,6 @@ pub fn calculate_text_layout_data(
for (i, group_paragraphs) in built_groups.into_iter().enumerate() {
// For each paragraph in the group (e.g., fill, stroke, etc.)
for skia_paragraph in group_paragraphs.into_iter() {
let spans = if let Some(text_para) = text_paragraphs.get(i) {
text_para.children().to_vec()
} else {
Vec::new()
};
// Calculate text decorations for this paragraph
let mut decorations = Vec::new();
let line_metrics = skia_paragraph.get_line_metrics();
@ -1580,7 +1528,6 @@ pub fn calculate_text_layout_data(
paragraph: skia_paragraph,
x,
y: y_accum,
spans: spans.clone(),
decorations,
});
}
@ -1644,10 +1591,8 @@ pub fn calculate_text_layout_data(
}
}
let content_rect = Rect::from_xywh(x, base_y + vertical_offset, text_width, total_text_height);
TextLayoutData {
position_data,
content_rect,
paragraphs: paragraph_layouts,
}
}

View File

@ -7,6 +7,7 @@ pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
pub use text_editor::*;
use crate::error::{Error, Result};
use crate::render::FrameType;
use crate::shapes::{grid_layout::grid_cell_data, Shape};
use crate::uuid::Uuid;
use crate::{get_render_state, tiles};
@ -64,11 +65,11 @@ impl State {
get_render_state().render_from_cache(&self.shapes);
}
pub fn render_sync(&mut self, timestamp: i32) -> Result<()> {
pub fn render_sync(&mut self, timestamp: i32) -> Result<FrameType> {
get_render_state().start_render_loop(None, &self.shapes, timestamp, true)
}
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> {
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<FrameType> {
get_render_state().start_render_loop(Some(id), &self.shapes, timestamp, true)
}
@ -81,7 +82,7 @@ impl State {
get_render_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<FrameType> {
let render_state = get_render_state();
// If zoom changed (e.g. interrupted zoom render followed by pan), the
// tile index may be stale for the new viewport position. Rebuild the
@ -92,12 +93,11 @@ impl State {
if render_state.zoom_changed() {
render_state.rebuild_tile_index(&self.shapes);
}
render_state.start_render_loop(None, &self.shapes, timestamp, false)
}
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
get_render_state().process_animation_frame(None, &self.shapes, timestamp)
pub fn continue_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
get_render_state().continue_render_loop(None, &self.shapes, timestamp)
}
pub fn clear_focus_mode(&mut self) {

View File

@ -426,8 +426,6 @@ impl TextEditorState {
let Some(last_paragraph) = text_content.paragraphs().last() else {
return false;
};
#[allow(dead_code)]
let _num_spans = last_paragraph.children().len() - 1;
let Some(_last_text_span) = last_paragraph.children().last() else {
return false;
};

View File

@ -10,80 +10,169 @@ 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_size(&self, tile_size: f32) -> skia::Rect {
skia::Rect::from_xywh(
self.0 as f32 * tile_size,
self.1 as f32 * tile_size,
tile_size,
tile_size,
)
}
#[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)]
pub struct TileRect(pub i32, pub i32, pub i32, pub i32);
#[allow(dead_code)]
impl TileRect {
pub fn empty() -> Self {
Self(0, 0, 0, 0)
}
#[inline]
#[inline(always)]
pub fn is_degenerate(&self) -> bool {
self.left() > self.right() || self.top() > self.bottom()
}
#[inline(always)]
pub fn len(&self) -> i32 {
(self.width() + 1) * (self.height() + 1)
}
#[inline(always)]
pub fn x1(&self) -> i32 {
self.0
}
#[inline]
#[inline(always)]
pub fn y1(&self) -> i32 {
self.1
}
#[inline]
#[inline(always)]
pub fn x2(&self) -> i32 {
self.2
}
#[inline]
#[inline(always)]
pub fn y2(&self) -> i32 {
self.3
}
#[inline]
#[inline(always)]
pub fn left(&self) -> i32 {
self.0
}
#[inline]
#[inline(always)]
pub fn top(&self) -> i32 {
self.1
}
#[inline]
#[inline(always)]
pub fn right(&self) -> i32 {
self.2
}
#[inline]
#[inline(always)]
pub fn bottom(&self) -> i32 {
self.3
}
/// Inclusive tile count on X (matches `contains`: both `x1` and `x2` are included).
#[inline]
#[inline(always)]
pub fn columns(&self) -> i32 {
self.x2() - self.x1() + 1
}
/// Inclusive tile count on Y (matches `contains`: both `y1` and `y2` are included).
#[inline]
#[inline(always)]
pub fn rows(&self) -> i32 {
self.y2() - self.y1() + 1
}
#[inline(always)]
pub fn width(&self) -> i32 {
self.x2() - self.x1()
}
#[inline(always)]
pub fn height(&self) -> i32 {
self.y2() - self.y1()
}
#[inline(always)]
pub fn contains(&self, tile: &Tile) -> bool {
tile.x() >= self.left()
&& tile.y() >= self.top()
&& tile.x() <= self.right()
&& tile.y() <= self.bottom()
}
pub fn iter(self, inclusive: bool) -> TileRectIter {
TileRectIter::new(self, inclusive)
}
}
#[allow(dead_code)]
pub struct TileRectIter {
rect: TileRect,
inclusive: bool,
index: i32,
total: i32,
}
impl TileRectIter {
fn new(rect: TileRect, inclusive: bool) -> Self {
let width = rect.width() + if inclusive { 1 } else { 0 };
let height = rect.height() + if inclusive { 1 } else { 0 };
Self {
rect,
inclusive,
index: 0,
total: width * height,
}
}
}
impl Iterator for TileRectIter {
type Item = Tile;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.total {
return None;
}
let width = self.rect.width() + if self.inclusive { 1 } else { 0 };
let x = self.rect.left() + self.index % width;
let y = self.rect.top() + self.index / width;
self.index += 1;
Some(Tile::from(x, y))
}
}
#[derive(Debug)]
@ -95,19 +184,19 @@ pub struct TileViewbox {
}
impl TileViewbox {
pub fn new_with_interest(viewbox: Viewbox, interest: i32, scale: f32) -> Self {
pub fn new_with_interest(viewbox: &Viewbox, interest: i32) -> Self {
Self {
visible_rect: get_tiles_for_viewbox(viewbox, scale),
interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest, scale),
visible_rect: get_tiles_for_viewbox(viewbox),
interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest),
interest,
center: get_tile_center_for_viewbox(viewbox, scale),
center: get_tile_center_for_viewbox(viewbox),
}
}
pub fn update(&mut self, viewbox: Viewbox, scale: f32) {
self.visible_rect = get_tiles_for_viewbox(viewbox, scale);
self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest, scale);
self.center = get_tile_center_for_viewbox(viewbox, scale);
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);
}
pub fn set_interest(&mut self, interest: i32) {
@ -122,6 +211,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()
}
@ -136,22 +226,18 @@ 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, scale: f32) -> TileRect {
let tile_size = get_tile_size(scale);
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,
scale: f32,
) -> TileRect {
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale);
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, scale: f32) -> Tile {
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale);
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)
}
@ -172,7 +258,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>>,
@ -186,6 +272,13 @@ impl TileHashMap {
}
}
pub fn is_empty_at(&self, tile: Tile) -> bool {
if let Some(uuids) = self.grid.get(&tile) {
return uuids.is_empty();
}
true
}
pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&HashSet<Uuid>> {
self.grid.get(&tile)
}
@ -219,7 +312,7 @@ 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;
/// Cached spiral of tile offsets for a given grid size.
///
@ -315,6 +408,10 @@ pub struct PendingTiles {
pub list: Vec<Tile>,
pub spiral: TileSpiral,
pub spiral_rect: TileRect,
pub visible_cached: Vec<Tile>,
pub visible_uncached: Vec<Tile>,
pub interest_cached: Vec<Tile>,
pub interest_uncached: Vec<Tile>,
}
impl PendingTiles {
@ -323,6 +420,10 @@ impl PendingTiles {
list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
spiral: TileSpiral::new(),
spiral_rect: TileRect::empty(),
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),
}
}
@ -356,10 +457,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();
// Compute the scheduling center explicitly (inclusive range).
// This avoids relying on `TileRect::center_x/center_y` semantics, which may be used
@ -374,19 +475,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> {

View File

@ -1,62 +1,86 @@
use skia_safe::Rect;
use crate::math::{Matrix, Point};
use crate::math::{Matrix, Point, Rect, Size};
use std::ops::Mul;
#[derive(Debug, Copy, Clone)]
pub(crate) struct Viewbox {
pub pan_x: f32,
pub pan_y: f32,
pub width: f32,
pub height: f32,
pub pan: Point,
pub size: Size,
pub zoom: f32,
pub dpr: f32,
pub area: Rect,
}
impl Default for Viewbox {
fn default() -> Self {
Self {
pan_x: 0.,
pan_y: 0.,
width: 0.0,
height: 0.0,
pan: Point::new(0.0, 0.0),
size: Size::new(0.0, 0.0),
zoom: 1.0,
dpr: 1.0,
area: Rect::new_empty(),
}
}
}
#[allow(dead_code)]
impl Viewbox {
pub fn new(width: f32, height: f32) -> Self {
let area = Rect::from_xywh(0., 0., width, height);
let size = Size::new(width, height);
let area = Rect::from_size(size);
Self {
width,
height,
size,
area,
..Self::default()
}
}
pub fn dpr_width(&self) -> f32 {
self.size.width * self.dpr
}
pub fn dpr_height(&self) -> f32 {
self.size.height * self.dpr
}
pub fn width(&self) -> f32 {
self.size.width
}
pub fn height(&self) -> f32 {
self.size.height
}
pub fn set_all(&mut self, zoom: f32, pan_x: f32, pan_y: f32) {
self.pan_x = pan_x;
self.pan_y = pan_y;
self.pan.set(pan_x, pan_y);
self.zoom = zoom;
self.area.set_xywh(
-self.pan_x,
-self.pan_y,
self.width / self.zoom,
self.height / self.zoom,
-self.pan.x,
-self.pan.y,
self.size.width / self.zoom,
self.size.height / self.zoom,
);
}
pub fn set_wh(&mut self, width: f32, height: f32) {
self.width = width;
self.height = height;
self.size.set(width, height);
self.area
.set_wh(self.width / self.zoom, self.height / self.zoom);
.set_wh(self.size.width / self.zoom, self.size.height / self.zoom);
}
pub fn set_dpr(&mut self, dpr: f32) {
self.dpr = dpr;
}
pub fn get_scale(&self) -> f32 {
self.zoom * self.dpr
}
pub fn get_offset(&self) -> Point {
self.area.tl().mul(self.get_scale())
}
pub fn pan(&self) -> Point {
Point::new(self.pan_x, self.pan_y)
self.pan
}
pub fn zoom(&self) -> f32 {

View File

@ -1,40 +1,3 @@
#[macro_export]
macro_rules! request_animation_frame {
() => {{
#[cfg(target_arch = "wasm32")]
unsafe extern "C" {
pub fn wapi_requestAnimationFrame() -> i32;
}
#[cfg(target_arch = "wasm32")]
let result = unsafe { wapi_requestAnimationFrame() };
#[cfg(not(target_arch = "wasm32"))]
let result = 0;
result
}};
}
#[macro_export]
macro_rules! cancel_animation_frame {
($frame_id:expr) => {
#[cfg(target_arch = "wasm32")]
unsafe extern "C" {
pub fn wapi_cancelAnimationFrame(frame_id: i32);
}
{
let frame_id = $frame_id;
#[cfg(target_arch = "wasm32")]
unsafe {
wapi_cancelAnimationFrame(frame_id)
};
#[cfg(not(target_arch = "wasm32"))]
let _ = frame_id;
}
};
}
#[macro_export]
macro_rules! notify_tiles_render_complete {
() => {{
@ -50,6 +13,4 @@ macro_rules! notify_tiles_render_complete {
}};
}
pub use cancel_animation_frame;
pub use notify_tiles_render_complete;
pub use request_animation_frame;