♻️ 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"); .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, page,
context, context,
}) => { }) => {

View File

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

View File

@ -11,15 +11,6 @@
[app.render-wasm.wasm :as wasm] [app.render-wasm.wasm :as wasm]
[promesa.core :as p])) [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 (defn get-webgl-context
"Gets the WebGL context from the WASM module" "Gets the WebGL context from the WASM module"
[] []

View File

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

View File

@ -37,8 +37,6 @@
[app.util.debug :as dbg] [app.util.debug :as dbg]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.http :as http] [app.util.http :as http]
[app.util.object :as obj]
[app.util.timers :as timers]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -135,6 +133,14 @@
(wasm.mem/free) (wasm.mem/free)
text))) 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 (defn ^:export wasmRenderStats
[] []
(let [module wasm/internal-module (let [module wasm/internal-module
@ -213,31 +219,6 @@
opacity: 0.5; 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 [] (defn ^:export dump-state []
(logjs "state" @st/state) (logjs "state" @st/state)
nil) nil)

View File

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

View File

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

View File

@ -1,18 +1,4 @@
addToLibrary({ 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() { wapi_notifyTilesRenderComplete: function wapi_notifyTilesRenderComplete() {
// The corresponding listener lives on `document` (main thread), so in a // The corresponding listener lives on `document` (main thread), so in a
// worker context we simply skip the dispatch instead of crashing. // worker context we simply skip the dispatch instead of crashing.

View File

@ -19,6 +19,7 @@ use std::collections::HashMap;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::render::{FrameType, RenderFlag};
use globals::{get_design_state, get_gpu_state, get_render_state}; 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(()) 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] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> { 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] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn render(timestamp: i32) -> Result<()> { pub extern "C" fn render(timestamp: i32, flags: u8) -> Result<FrameType> {
with_state!(state, { with_state!(state, {
state.rebuild_touched_tiles(); state.rebuild_touched_tiles();
// Drain the throttled modifier-tile invalidation accumulated // 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.rebuild_modifier_tiles(&ids)?;
} }
} }
state let frame_type = if flags & RenderFlag::Partial as u8 == RenderFlag::Partial as u8 {
.start_render_loop(timestamp) state
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?; .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] #[no_mangle]
@ -179,7 +177,7 @@ pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
with_state!(state, { with_state!(state, {
// Don't cancel the animation frame — let the async render // Don't cancel the animation frame — let the async render
// continue populating the tile HashMap in the background. // 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 // mode so it won't present stale Target content. The
// tile HashMap is position-independent, so tiles rendered // tile HashMap is position-independent, so tiles rendered
// for the old viewport can be reused by the next full // for the old viewport can be reused by the next full
@ -239,16 +237,6 @@ pub extern "C" fn render_loading_overlay() -> Result<()> {
Ok(()) 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] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn reset_canvas() -> Result<()> { 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"); performance::begin_measure!("set_view_end");
let render_state = get_render_state(); let render_state = get_render_state();
render_state.options.set_fast_mode(false); render_state.options.set_fast_mode(false);
render_state.cancel_animation_frame(); render_state.tile_viewbox.update(&render_state.viewbox);
let scale = render_state.get_scale();
render_state
.tile_viewbox
.update(render_state.viewbox, scale);
if render_state.options.is_profile_rebuild_tiles() { if render_state.options.is_profile_rebuild_tiles() {
state.rebuild_tiles(); state.rebuild_tiles();
@ -358,7 +341,6 @@ pub extern "C" fn set_modifiers_end() -> Result<()> {
let render_state = get_render_state(); let render_state = get_render_state();
render_state.options.set_fast_mode(false); render_state.options.set_fast_mode(false);
render_state.options.set_interactive_transform(false); render_state.options.set_interactive_transform(false);
render_state.cancel_animation_frame();
performance::end_measure!("set_modifiers_end"); performance::end_measure!("set_modifiers_end");
Ok(()) Ok(())
} }
@ -946,7 +928,9 @@ pub fn free_gpu_resources() {
get_render_state().free_gpu_resources(); get_render_state().free_gpu_resources();
} }
fn main() { pub fn main() {
#[cfg(target_arch = "wasm32")] // Why an empty main?
init_gl!(); // 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 Matrix = skia::Matrix;
pub type Vector = skia::Vector; pub type Vector = skia::Vector;
pub type Point = skia::Point; pub type Point = skia::Point;
pub type Size = skia::Size;
const THRESHOLD: f32 = 0.001; const THRESHOLD: f32 = 0.001;

View File

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

View File

@ -38,6 +38,21 @@ pub use images::*;
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>; 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)] #[derive(Debug)]
pub struct NodeRenderState { pub struct NodeRenderState {
pub id: Uuid, pub id: Uuid,
@ -334,10 +349,6 @@ pub(crate) struct RenderState {
pub cached_viewbox: Viewbox, pub cached_viewbox: Viewbox,
pub images: ImageStore, pub images: ImageStore,
pub background_color: skia::Color, 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. // Stack of nodes pending to be rendered.
pending_nodes: Vec<NodeRenderState>, pending_nodes: Vec<NodeRenderState>,
pub current_tile: Option<tiles::Tile>, 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 /// 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. /// time we are about to blit a tile into Cache for this pass.
pub cache_cleared_this_render: bool, 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 /// started rendering it. Lets us distinguish a genuinely empty
/// tile (skip composite, just clear) from a tile whose walker /// tile (skip composite, just clear) from a tile whose walker
/// finished its work in a previous PAF and is now being resumed /// 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 /// 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 /// 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. /// `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)] #[allow(clippy::too_many_arguments)]
fn drag_crop_snapshot_window_px( fn drag_crop_snapshot_window_px(
max_side_px: i32, max_side_px: i32,
@ -441,22 +452,6 @@ fn drag_crop_snapshot_window_px(
(ox, oy, win_w, win_h) (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 { impl RenderState {
/// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an /// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an
/// interactive transform (drag/resize/rotate). /// interactive transform (drag/resize/rotate).
@ -554,8 +549,6 @@ impl RenderState {
cached_viewbox: Viewbox::new(0., 0.), cached_viewbox: Viewbox::new(0., 0.),
images: ImageStore::new(), images: ImageStore::new(),
background_color: skia::Color::TRANSPARENT, background_color: skia::Color::TRANSPARENT,
render_request_id: None,
render_in_progress: false,
pending_nodes: vec![], pending_nodes: vec![],
current_tile: None, current_tile: None,
sampling_options, sampling_options,
@ -563,9 +556,8 @@ impl RenderState {
render_area_with_margins: Rect::new_empty(), render_area_with_margins: Rect::new_empty(),
tiles, tiles,
tile_viewbox: tiles::TileViewbox::new_with_interest( tile_viewbox: tiles::TileViewbox::new_with_interest(
viewbox, &viewbox,
options.dpr_viewport_interest_area_threshold, options.dpr_viewport_interest_area_threshold,
1.0,
), ),
pending_tiles: PendingTiles::new(), pending_tiles: PendingTiles::new(),
nested_fills: vec![], nested_fills: vec![],
@ -787,10 +779,12 @@ impl RenderState {
self.tile_viewbox self.tile_viewbox
.set_interest(self.options.dpr_viewport_interest_area_threshold); .set_interest(self.options.dpr_viewport_interest_area_threshold);
self.resize( self.resize(
self.viewbox.width.floor() as i32, self.viewbox.width().floor() as i32,
self.viewbox.height.floor() as i32, self.viewbox.height().floor() as i32,
)?; )?;
self.fonts.set_scale_debug_font(dpr); self.fonts.set_scale_debug_font(dpr);
self.viewbox.set_dpr(dpr);
self.surfaces.set_dpr(dpr);
} }
Ok(()) Ok(())
} }
@ -836,7 +830,7 @@ impl RenderState {
let dpr_height = (height as f32 * self.options.dpr).floor() as i32; let dpr_height = (height as f32 * self.options.dpr).floor() as i32;
self.surfaces.resize(dpr_width, dpr_height)?; self.surfaces.resize(dpr_width, dpr_height)?;
self.viewbox.set_wh(width as f32, height as f32); 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(()) Ok(())
} }
@ -901,23 +895,13 @@ impl RenderState {
self.flush_and_submit(); self.flush_and_submit();
} }
#[allow(dead_code)] pub fn apply_render_to_final_canvas(&mut self) -> Result<()> {
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<()> {
// During interactive transforms we render tiles directly into Target; updating the cache // 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 // (snapshot -> atlas blit -> tiles.add) can force GPU stalls. Defer cache rebuild until
// the interaction ends. // the interaction ends.
if self.options.is_interactive_transform() { if self.options.is_interactive_transform() {
let tile_rect = self.get_current_aligned_tile_bounds()?; let tile_rect = self.get_current_aligned_tile_bounds()?;
self.surfaces.draw_current_tile_direct( self.surfaces.draw_current_tile_into_backbuffer(
&tile_rect, &tile_rect,
self.background_color, self.background_color,
surfaces::DrawOnCache::No, surfaces::DrawOnCache::No,
@ -942,7 +926,7 @@ impl RenderState {
.as_ref() .as_ref()
.ok_or(Error::CriticalError("Current tile not found".to_string()))?; .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, &self.tile_viewbox,
&current_tile, &current_tile,
&tile_rect, &tile_rect,
@ -950,12 +934,15 @@ impl RenderState {
self.render_area, self.render_area,
); );
let rect = self.get_current_tile_bounds()?;
self.surfaces self.surfaces
.draw_cached_tile_surface(current_tile, rect, self.background_color); .draw_cached_tile_into_backbuffer(current_tile, &rect);
Ok(()) 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"); performance::begin_measure!("apply_drawing_to_render_canvas");
let paint = skia::Paint::default(); let paint = skia::Paint::default();
@ -1663,7 +1650,7 @@ impl RenderState {
} }
if apply_to_current_surface { 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) // Only restore if we saved (optimization for simple shapes)
@ -1691,14 +1678,6 @@ impl RenderState {
self.surfaces.update_render_context(self.render_area, scale); 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) { fn rebuild_backbuffer_crop_cache(&mut self, tree: ShapesPoolRef) {
self.backbuffer_crop_cache.clear(); self.backbuffer_crop_cache.clear();
@ -1778,12 +1757,12 @@ impl RenderState {
let vb_left = self.viewbox.area.left; let vb_left = self.viewbox.area.left;
let vb_top = self.viewbox.area.top; let vb_top = self.viewbox.area.top;
let (bb_w, bb_h) = self.surfaces.surface_size(SurfaceId::Backbuffer); 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 // 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 // the tile/atlas fallback path reuse the same GPU image rather than each
// triggering a separate `image_snapshot` flush. // 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 // Scratch surface reused across all shapes that need the tile/atlas
// fallback — avoids one WebGL texture allocation per shape. // fallback — avoids one WebGL texture allocation per shape.
@ -1901,16 +1880,14 @@ impl RenderState {
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) { pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
let _start = performance::begin_timed_log!("render_from_cache"); let _start = performance::begin_timed_log!("render_from_cache");
performance::begin_measure!("render_from_cache"); performance::begin_measure!("render_from_cache");
let cached_scale = self.get_cached_scale();
let bg_color = self.background_color; let bg_color = self.background_color;
// During fast mode (pan/zoom), if a previous full-quality render still has pending tiles, // 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, // 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. // 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 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); self.present_frame(shapes);
performance::end_measure!("render_from_cache"); performance::end_measure!("render_from_cache");
@ -1925,11 +1902,7 @@ impl RenderState {
let interest = self.options.dpr_viewport_interest_area_threshold; let interest = self.options.dpr_viewport_interest_area_threshold;
let TileRect(start_tile_x, start_tile_y, _, _) = let TileRect(start_tile_x, start_tile_y, _, _) =
tiles::get_tiles_for_viewbox_with_interest( tiles::get_tiles_for_viewbox_with_interest(&self.cached_viewbox, interest);
self.cached_viewbox,
interest,
cached_scale,
);
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr; 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 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; 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; let cache_h = cache_dim.height as f32;
// Viewport in target pixels. // Viewport in target pixels.
let vw = (self.viewbox.width * self.options.dpr).max(1.0); let vw = self.viewbox.dpr_width().max(1.0);
let vh = (self.viewbox.height * self.options.dpr).max(1.0); let vh = self.viewbox.dpr_height().max(1.0);
// Inverse-map viewport corners into cache coordinates. // Inverse-map viewport corners into cache coordinates.
// target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords). // target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords).
@ -1956,8 +1929,11 @@ impl RenderState {
0.0 0.0
}; };
let cx0 = (0.0 * inv) - translate_x; // let cx0 = (0.0 * inv) - translate_x;
let cy0 = (0.0 * inv) - translate_y; // 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 cx1 = (vw * inv) - translate_x;
let cy1 = (vh * inv) - translate_y; 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; min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
if !cache_covers { if !cache_covers {
// Early return only if atlas exists; otherwise keep cache path. // Early return only if atlas exists; otherwise keep cache path.
if self.surfaces.has_atlas() { if !self.surfaces.atlas.is_empty() {
self.surfaces.draw_atlas_to_backbuffer( self.surfaces
self.viewbox, .draw_atlas_to_backbuffer(self.viewbox, bg_color);
self.options.dpr,
bg_color,
);
self.present_frame(shapes); self.present_frame(shapes);
performance::end_measure!("render_from_cache"); 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 // Draw directly from cache surface, avoiding snapshot overhead
self.surfaces.draw_cache_to_backbuffer(); 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 // During pure pan (same zoom), draw tiles from the HashMap
// on top of the scaled Cache surface. Cached tile textures // on top of the scaled Cache surface. Cached tile textures
// include full-quality effects (shadows, blur) from the last // 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 // would be at wrong positions — skip them and let the full
// render after set_view_end handle it. // render after set_view_end handle it.
if !self.zoom_changed() { if !self.zoom_changed() {
let current_scale = self.get_scale(); let visible_rect = tiles::get_tiles_for_viewbox(&self.viewbox);
let visible_rect = tiles::get_tiles_for_viewbox(self.viewbox, current_scale); let offset = self.viewbox.get_offset();
let vb_offset_x = self.viewbox.area.left * current_scale;
let vb_offset_y = self.viewbox.area.top * current_scale;
for tx in visible_rect.x1()..=visible_rect.x2() { for tx in visible_rect.x1()..=visible_rect.x2() {
for ty in visible_rect.y1()..=visible_rect.y2() { for ty in visible_rect.y1()..=visible_rect.y2() {
let tile = tiles::Tile::from(tx, ty); let tile = tiles::Tile::from(tx, ty);
if self.surfaces.has_cached_tile_surface(tile) { if self.surfaces.has_cached_tile_surface(tile) {
let tile_rect = skia::Rect::from_xywh( let rect = tile.get_rect_with_offset(&offset);
tx as f32 * tiles::TILE_SIZE - vb_offset_x, self.surfaces.draw_cached_tile_into_backbuffer(tile, &rect);
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);
} }
} }
} }
@ -2063,20 +2015,43 @@ impl RenderState {
Ok(()) 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( pub fn start_render_loop(
&mut self, &mut self,
base_object: Option<&Uuid>, base_object: Option<&Uuid>,
tree: ShapesPoolRef, tree: ShapesPoolRef,
timestamp: i32, timestamp: i32,
sync_render: bool, sync_render: bool,
) -> Result<()> { ) -> Result<FrameType> {
#[cfg(feature = "stats")] self.clear(tree);
self.stats.clear();
let _start = performance::begin_timed_log!("start_render_loop"); let _start = performance::begin_timed_log!("start_render_loop");
let scale = self.get_scale(); let scale = self.get_scale();
self.tile_viewbox.update(self.viewbox, scale); self.tile_viewbox.update(&self.viewbox);
self.focus_mode.reset(); self.focus_mode.reset();
performance::begin_measure!("render"); performance::begin_measure!("render");
@ -2086,7 +2061,7 @@ impl RenderState {
// to clamp atlas updates. This prevents zoom-out tiles from forcing atlas // to clamp atlas updates. This prevents zoom-out tiles from forcing atlas
// growth far beyond real content. // growth far beyond real content.
let doc_bounds = self.compute_document_bounds(base_object, tree); 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; self.cache_cleared_this_render = false;
if self.options.is_interactive_transform() { if self.options.is_interactive_transform() {
@ -2109,63 +2084,44 @@ impl RenderState {
| SurfaceId::Fills as u32 | SurfaceId::Fills as u32
| SurfaceId::InnerShadows as u32 | SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32; | SurfaceId::TextDropShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| { self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().scale((scale, scale)); s.canvas().scale((scale, scale));
}); });
let viewbox_cache_size = get_cache_size( self.surfaces.resize_cache_from_viewbox(
self.viewbox, &self.viewbox,
scale, &self.cached_viewbox,
self.options.dpr_viewport_interest_area_threshold, 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 // FIXME - review debug
// debug::render_debug_tiles_for_viewbox(self); // debug::render_debug_tiles_for_viewbox(self);
let _tile_start = performance::begin_timed_log!("tile_cache_update"); let _tile_start = performance::begin_timed_log!("tile_cache_update");
performance::begin_measure!("tile_cache"); performance::begin_measure!("tile_cache");
let only_visible = self.options.is_interactive_transform(); let only_visible = self.options.is_interactive_transform();
self.pending_tiles self.pending_tiles
.update(&self.tile_viewbox, &self.surfaces, only_visible); .update(&self.tile_viewbox, &self.surfaces, only_visible);
performance::end_measure!("tile_cache"); performance::end_measure!("tile_cache");
performance::end_timed_log!("tile_cache_update", _tile_start); performance::end_timed_log!("tile_cache_update", _tile_start);
self.pending_nodes.clear(); self.draw_shape_surface_stack_into(None, SurfaceId::Current);
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);
#[allow(unused)]
let mut frame_type = FrameType::None;
if sync_render { if sync_render {
self.render_shape_tree_sync(base_object, tree, timestamp)?; frame_type = self.render_shape_tree_sync(base_object, tree, timestamp)?;
} else { } 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 // Update cached_viewbox after visible tiles render
// synchronously so that render_from_cache uses the correct // synchronously so that render_from_cache uses the correct
// zoom ratio even if interest-area tiles are still rendering // zoom ratio even if interest-area tiles are still rendering
@ -2180,7 +2136,7 @@ impl RenderState {
performance::end_measure!("start_render_loop"); performance::end_measure!("start_render_loop");
performance::end_timed_log!("start_render_loop", _start); performance::end_timed_log!("start_render_loop", _start);
Ok(()) Ok(frame_type)
} }
fn compute_document_bounds( fn compute_document_bounds(
@ -2214,37 +2170,45 @@ impl RenderState {
acc acc
} }
pub fn process_animation_frame( pub fn continue_render_loop(
&mut self, &mut self,
base_object: Option<&Uuid>, base_object: Option<&Uuid>,
tree: ShapesPoolRef, tree: ShapesPoolRef,
timestamp: i32, timestamp: i32,
) -> Result<()> { ) -> Result<FrameType> {
performance::begin_measure!("process_animation_frame"); performance::begin_measure!("continue_render_loop");
self.render_shape_tree_partial(base_object, tree, timestamp, true)?; let frame_type = self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
if self.render_in_progress { if !self.options.is_interactive_transform() {
// Partial frame: just flush GPU work. The display shows the last self.surfaces
// fully submitted frame; no need to copy or draw UI overlays here. .draw_tile_atlas_to_backbuffer(&self.viewbox, &self.tile_viewbox);
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");
} }
performance::end_measure!("process_animation_frame"); match frame_type {
Ok(()) 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( pub fn render_shape_tree_sync(
@ -2252,10 +2216,10 @@ impl RenderState {
base_object: Option<&Uuid>, base_object: Option<&Uuid>,
tree: ShapesPoolRef, tree: ShapesPoolRef,
timestamp: i32, timestamp: i32,
) -> Result<()> { ) -> Result<FrameType> {
self.render_shape_tree_partial(base_object, tree, timestamp, false)?; self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
self.present_frame(tree); self.present_frame(tree);
Ok(()) Ok(FrameType::Full)
} }
pub fn render_shape_pixels( pub fn render_shape_pixels(
@ -2560,18 +2524,11 @@ impl RenderState {
} }
pub fn get_current_tile_bounds(&mut self) -> Result<Rect> { pub fn get_current_tile_bounds(&mut self) -> Result<Rect> {
let tiles::Tile(tile_x, tile_y) = self let tile = self
.current_tile .current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?; .ok_or(Error::CriticalError("Current tile not found".to_string()))?;
let scale = self.get_scale(); let offset = self.viewbox.get_offset();
let offset_x = self.viewbox.area.left * scale; Ok(tile.get_rect_with_offset(&offset))
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,
))
} }
pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect { pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect {
@ -3337,7 +3294,7 @@ impl RenderState {
.canvas(SurfaceId::DropShadows) .canvas(SurfaceId::DropShadows)
.clear(skia::Color::TRANSPARENT); .clear(skia::Color::TRANSPARENT);
} else if visited_children { } 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 // Skip nested state updates for flattened containers
@ -3406,6 +3363,7 @@ impl RenderState {
} }
iteration += 1; iteration += 1;
} }
Ok((is_empty, false)) Ok((is_empty, false))
} }
@ -3415,7 +3373,7 @@ impl RenderState {
tree: ShapesPoolRef, tree: ShapesPoolRef,
timestamp: i32, timestamp: i32,
allow_stop: bool, allow_stop: bool,
) -> Result<()> { ) -> Result<FrameType> {
let mut should_stop = false; let mut should_stop = false;
let root_ids = { let root_ids = {
if let Some(shape_id) = base_object { if let Some(shape_id) = base_object {
@ -3430,53 +3388,20 @@ impl RenderState {
while !should_stop { while !should_stop {
if let Some(current_tile) = self.current_tile { if let Some(current_tile) = self.current_tile {
if self.surfaces.has_cached_tile_surface(current_tile) { // NOTE: For now we don't need to cover the case where the tile
performance::begin_measure!("render_shape_tree::cached"); // is not cached because everything will be handled from draw_atlas.
// During interactive transforms, `Target` is preserved and seeded once if !self.surfaces.has_cached_tile_surface(current_tile) {
// 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 {
performance::begin_measure!("render_shape_tree::uncached"); performance::begin_measure!("render_shape_tree::uncached");
let (is_empty, early_return) = self let (is_empty, early_return) = self
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?; .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 { if early_return {
return Ok(()); return Ok(FrameType::Partial);
} }
performance::end_measure!("render_shape_tree::uncached"); performance::end_measure!("render_shape_tree::uncached");
@ -3489,13 +3414,13 @@ impl RenderState {
if self.options.is_interactive_transform() { if self.options.is_interactive_transform() {
// During drag, avoid snapshot-based caching. Draw Current directly // During drag, avoid snapshot-based caching. Draw Current directly
// into Target (and Cache) to reduce stalls. // into Target (and Cache) to reduce stalls.
self.surfaces.draw_current_tile_direct( self.surfaces.draw_current_tile_into_backbuffer(
&tile_rect, &tile_rect,
self.background_color, self.background_color,
surfaces::DrawOnCache::Yes, surfaces::DrawOnCache::Yes,
); );
} else { } else {
self.apply_render_to_final_canvas(tile_rect)?; self.apply_render_to_final_canvas()?;
} }
if self.options.is_debug_visible() { if self.options.is_debug_visible() {
@ -3506,10 +3431,9 @@ impl RenderState {
tile_rect, 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. // empty tile.
self.current_tile_had_shapes = false; self.current_tile_had_shapes = false;
if !self.surfaces.has_cached_tile_surface(next_tile) { let Some(ids) = self.tiles.get_shapes_at(next_tile) else {
if let Some(ids) = self.tiles.get_shapes_at(next_tile) { // If the tile is empty we do not need to render it.
// Check if any shape on this tile has a background blur. continue;
// 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. if self.surfaces.has_cached_tile_surface(next_tile) {
// // If the tile is cached, then we do not need to
// During interactive transforms we may invalidate only the modified shapes // render it.
// (to avoid massive ancestor eviction). However, we still composite full continue;
// 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. // Check if any shape on this tile has a background blur.
let mut valid_ids = Vec::with_capacity(ids.len()); // If so, we need ALL root shapes rendered (not just those
if self.options.is_interactive_transform() || tile_has_bg_blur { // assigned to this tile) because the blur snapshots Current
valid_ids.extend(root_ids.iter().copied()); // which must contain the shapes behind it.
} else { let tile_has_bg_blur = ids.iter().any(|id| {
for root_id in root_ids.iter() { tree.get(id).is_some_and(|s| {
if ids.contains(root_id) { s.blur
valid_ids.push(*root_id); .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 { } else {
// If there are no more pending tiles, stop.
should_stop = true; should_stop = true;
} }
} }
self.render_in_progress = false;
self.surfaces.gc();
// Mark cache as valid for render_from_cache. // Mark cache as valid for render_from_cache.
// Only update for full-quality renders (non-fast mode). // Only update for full-quality renders (non-fast mode).
// An async render can complete while fast mode is active // An async render can complete while fast mode is active
@ -3595,7 +3522,7 @@ impl RenderState {
self.cached_viewbox = self.viewbox; self.cached_viewbox = self.viewbox;
} }
Ok(()) Ok(FrameType::Full)
} }
/* /*
@ -3648,7 +3575,7 @@ impl RenderState {
shape: &Shape, shape: &Shape,
tree: ShapesPoolRef, tree: ShapesPoolRef,
) -> HashSet<tiles::Tile> { ) -> 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 // Collect old tiles to avoid borrow conflict with remove_shape_at
let old_tiles: Vec<_> = self let old_tiles: Vec<_> = self
@ -3661,7 +3588,7 @@ impl RenderState {
// When the shape has an active modifier (i.e. is being moved/resized), // 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 // clear its OLD doc-space extent from the atlas using the raw
// (pre-modifier) shape. The per-tile clearing done later via // (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 // at the current zoom level. However, the atlas may also contain stale
// pixels from previous zoom levels (tiles are larger / smaller in doc // pixels from previous zoom levels (tiles are larger / smaller in doc
// space at different zoom scales) that were never re-tracked after a zoom // 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 tree.get_modifier(&shape.id).is_some() {
if let Some(raw_shape) = tree.get_raw(&shape.id) { if let Some(raw_shape) = tree.get_raw(&shape.id) {
let old_extrect = raw_shape.extrect(tree, 1.0); 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 // 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); self.tiles.add_shape_at(tile, shape.id);
result.insert(tile); result.insert(tile);
} }
@ -3713,16 +3642,13 @@ impl RenderState {
shape: &Shape, shape: &Shape,
tree: ShapesPoolRef, tree: ShapesPoolRef,
) -> Vec<tiles::Tile> { ) -> 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 let old_tiles: HashSet<tiles::Tile> = self
.tiles .tiles
.get_tiles_of(shape.id) .get_tiles_of(shape.id)
.map_or(HashSet::new(), |tiles| tiles.iter().copied().collect()); .map_or(HashSet::new(), |tiles| tiles.iter().copied().collect());
let new_tiles: HashSet<tiles::Tile> = (rsx..=rex) let new_tiles: HashSet<tiles::Tile> = tile_rect.iter(true).collect();
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)))
.collect();
// Tiles where shape is being removed from index (left interest area) // Tiles where shape is being removed from index (left interest area)
let removed: Vec<_> = old_tiles.difference(&new_tiles).copied().collect(); 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 * returns the tiles that have been updated
*/ */
pub fn add_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec<tiles::Tile> { 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); performance::begin_measure!("add_shape_tiles");
let tiles: Vec<_> = (rsx..=rex) let tiles: Vec<tiles::Tile> = self.get_tiles_for_shape(shape, tree).iter(true).collect();
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)))
.collect();
for tile in tiles.iter() { for tile in tiles.iter() {
self.tiles.add_shape_at(*tile, shape.id); self.tiles.add_shape_at(*tile, shape.id);
} }
performance::end_measure!("add_shape_tiles");
tiles tiles
} }
@ -3771,8 +3695,9 @@ impl RenderState {
/// survive so that fast-mode renders during pan still show shadows/blur. /// survive so that fast-mode renders during pan still show shadows/blur.
pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) { pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) {
let zoom_changed = self.zoom_changed(); let zoom_changed = self.zoom_changed();
performance::begin_measure!("rebuild_tile_index");
let mut nodes = vec![Uuid::nil()]; let mut nodes = Vec::<Uuid>::with_capacity(64);
nodes.push(Uuid::nil());
while let Some(shape_id) = nodes.pop() { while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) { if let Some(shape) = tree.get(&shape_id) {
if shape_id != Uuid::nil() { 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) { pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
@ -3927,11 +3853,7 @@ impl RenderState {
if let Some((_, export_scale)) = self.export_context { if let Some((_, export_scale)) = self.export_context {
return export_scale; return export_scale;
} }
self.viewbox.zoom() * self.options.dpr self.viewbox.get_scale()
}
pub fn get_cached_scale(&self) -> f32 {
self.cached_viewbox.zoom() * self.options.dpr
} }
pub fn zoom_changed(&self) -> bool { pub fn zoom_changed(&self) -> bool {

View File

@ -1,3 +1,4 @@
#![allow(dead_code)]
use super::{tiles, RenderState, SurfaceId}; use super::{tiles, RenderState, SurfaceId};
#[cfg(target_arch = "wasm32")] #[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) { fn render_debug_view(render_state: &mut RenderState) {
let mut paint = skia::Paint::default(); let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke); paint.set_style(skia::PaintStyle::Stroke);
@ -36,7 +36,6 @@ fn render_debug_view(render_state: &mut RenderState) {
.draw_rect(rect, &paint); .draw_rect(rect, &paint);
} }
#[allow(dead_code)]
pub fn render_debug_cache_surface(render_state: &mut RenderState) { pub fn render_debug_cache_surface(render_state: &mut RenderState) {
let canvas = render_state.surfaces.canvas(SurfaceId::Debug); let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
canvas.save(); 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) { 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 tiles::TileRect(sx, sy, ex, ey) = render_state.tile_viewbox.interest_rect;
let canvas = render_state.surfaces.canvas(SurfaceId::Debug); 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 // Renders the tiles in the viewbox
#[allow(dead_code)]
pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) { pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
let scale = render_state.get_scale(); let scale = render_state.get_scale();
let canvas = render_state.surfaces.canvas(SurfaceId::Debug); 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.); paint.set_stroke_width(1.);
let tile_size = tiles::get_tile_size(scale); let tile_size = tiles::get_tile_size(scale);
let tiles::TileRect(sx, sy, ex, ey) = let tile_rect = tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size);
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 str_rect = format!("{} {} {} {}", sx, sy, ex, ey);
let debug_font = render_state.fonts.debug_font(); let debug_font = render_state.fonts.debug_font();
canvas.draw_str(str_rect, skia::Point::new(100.0, 100.0), debug_font, &paint); canvas.draw_str(str_rect, skia::Point::new(100.0, 100.0), debug_font, &paint);
let tile_size = tiles::get_tile_size(scale); let tile_size = tiles::get_tile_size(scale);
for y in sy..=ey { for tile in tile_rect.iter(true) {
for x in sx..=ex { let tiles::Tile(x, y) = tile;
let rect = Rect::from_xywh( let rect = tile.get_rect_with_size(tile_size);
x as f32 * tile_size, let debug_rect = get_debug_rect(rect);
y as f32 * tile_size, let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.);
tile_size, let str = format!("{}:{}", x, y);
tile_size, let debug_font = render_state.fonts.debug_font();
); paint.set_style(skia::PaintStyle::Fill);
let debug_rect = get_debug_rect(rect); canvas.draw_str(str, p, debug_font, &paint);
let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.); canvas.draw_rect(debug_rect, &paint);
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")] #[cfg(target_arch = "wasm32")]
pub fn trap() { pub fn trap() {
run_script!("debugger"); run_script!("debugger");
} }
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum SurfaceBackendKind { pub enum SurfaceBackendKind {
@ -203,7 +193,6 @@ pub enum SurfaceBackendKind {
Unknown, Unknown,
} }
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn classify_surface_backend(surface: &mut skia::Surface) -> SurfaceBackendKind { pub fn classify_surface_backend(surface: &mut skia::Surface) -> SurfaceBackendKind {
if skia::gpu::surfaces::get_backend_texture( if skia::gpu::surfaces::get_backend_texture(
@ -231,7 +220,6 @@ pub fn classify_surface_backend(surface: &mut skia::Surface) -> SurfaceBackendKi
SurfaceBackendKind::Unknown SurfaceBackendKind::Unknown
} }
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) { pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
let base64_image = render_state 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;')")); 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")] #[cfg(target_arch = "wasm32")]
pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceId) { pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceId) {
let base64_image = render_state let base64_image = render_state
@ -253,7 +240,6 @@ pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceI
println!("{}", base64_image); println!("{}", base64_image);
} }
#[allow(dead_code)]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect: skia::Rect) { pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect: skia::Rect) {
let int_rect = skia::IRect::from_ltrb( 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] #[no_mangle]
#[wasm_error] #[wasm_error]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]

View File

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

View File

@ -30,6 +30,7 @@ pub struct RenderOptions {
pub max_blocking_time_ms: i32, pub max_blocking_time_ms: i32,
pub node_batch_threshold: i32, pub node_batch_threshold: i32,
pub blur_downscale_threshold: f32, pub blur_downscale_threshold: f32,
pub capture_frames: i32,
} }
impl Default for RenderOptions { impl Default for RenderOptions {
@ -45,6 +46,7 @@ impl Default for RenderOptions {
max_blocking_time_ms: MAX_BLOCKING_TIME_MS, max_blocking_time_ms: MAX_BLOCKING_TIME_MS,
node_batch_threshold: NODE_BATCH_THRESHOLD, node_batch_threshold: NODE_BATCH_THRESHOLD,
blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD, blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD,
capture_frames: 0,
} }
} }
} }
@ -67,6 +69,10 @@ impl RenderOptions {
self.fast_mode = enabled; 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. /// Updates the dpr viewport interest area threshold.
/// This function is updated when the dpr or the /// This function is updated when the dpr or the
/// viewport_interest_area_threshold is changed /// 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, error::Result,
math::Rect, math::Rect,
shapes::{ shapes::{
calculate_position_data, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind,
Stroke, StrokeKind, TextContent, TextContent,
}, },
utils::{get_fallback_fonts, get_font_collection}, utils::{get_fallback_fonts, get_font_collection},
}; };
@ -12,7 +12,7 @@ use skia_safe::{
self as skia, self as skia,
canvas::SaveLayerRec, canvas::SaveLayerRec,
textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle}, textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle},
Canvas, ImageFilter, Paint, Path, Canvas, ImageFilter, Paint,
}; };
pub fn stroke_paragraph_builder_group_from_text( 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? // How to use it?
// Type::Text(text_content) => { // Type::Text(text_content) => {
// self.surfaces // self.surfaces

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,80 +10,169 @@ impl Tile {
pub fn from(x: i32, y: i32) -> Self { pub fn from(x: i32, y: i32) -> Self {
Tile(x, y) Tile(x, y)
} }
#[inline(always)]
pub fn x(&self) -> i32 { pub fn x(&self) -> i32 {
self.0 self.0
} }
#[inline(always)]
pub fn y(&self) -> i32 { pub fn y(&self) -> i32 {
self.1 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)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct TileRect(pub i32, pub i32, pub i32, pub i32); pub struct TileRect(pub i32, pub i32, pub i32, pub i32);
#[allow(dead_code)]
impl TileRect { impl TileRect {
pub fn empty() -> Self { pub fn empty() -> Self {
Self(0, 0, 0, 0) 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 { pub fn x1(&self) -> i32 {
self.0 self.0
} }
#[inline] #[inline(always)]
pub fn y1(&self) -> i32 { pub fn y1(&self) -> i32 {
self.1 self.1
} }
#[inline] #[inline(always)]
pub fn x2(&self) -> i32 { pub fn x2(&self) -> i32 {
self.2 self.2
} }
#[inline] #[inline(always)]
pub fn y2(&self) -> i32 { pub fn y2(&self) -> i32 {
self.3 self.3
} }
#[inline] #[inline(always)]
pub fn left(&self) -> i32 { pub fn left(&self) -> i32 {
self.0 self.0
} }
#[inline] #[inline(always)]
pub fn top(&self) -> i32 { pub fn top(&self) -> i32 {
self.1 self.1
} }
#[inline] #[inline(always)]
pub fn right(&self) -> i32 { pub fn right(&self) -> i32 {
self.2 self.2
} }
#[inline] #[inline(always)]
pub fn bottom(&self) -> i32 { pub fn bottom(&self) -> i32 {
self.3 self.3
} }
/// Inclusive tile count on X (matches `contains`: both `x1` and `x2` are included). /// Inclusive tile count on X (matches `contains`: both `x1` and `x2` are included).
#[inline] #[inline(always)]
pub fn columns(&self) -> i32 { pub fn columns(&self) -> i32 {
self.x2() - self.x1() + 1 self.x2() - self.x1() + 1
} }
/// Inclusive tile count on Y (matches `contains`: both `y1` and `y2` are included). /// Inclusive tile count on Y (matches `contains`: both `y1` and `y2` are included).
#[inline] #[inline(always)]
pub fn rows(&self) -> i32 { pub fn rows(&self) -> i32 {
self.y2() - self.y1() + 1 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 { pub fn contains(&self, tile: &Tile) -> bool {
tile.x() >= self.left() tile.x() >= self.left()
&& tile.y() >= self.top() && tile.y() >= self.top()
&& tile.x() <= self.right() && tile.x() <= self.right()
&& tile.y() <= self.bottom() && 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)] #[derive(Debug)]
@ -95,19 +184,19 @@ pub struct TileViewbox {
} }
impl 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 { Self {
visible_rect: get_tiles_for_viewbox(viewbox, scale), visible_rect: get_tiles_for_viewbox(viewbox),
interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest, scale), interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest),
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) { pub fn update(&mut self, viewbox: &Viewbox) {
self.visible_rect = get_tiles_for_viewbox(viewbox, scale); self.visible_rect = get_tiles_for_viewbox(viewbox);
self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest, scale); self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest);
self.center = get_tile_center_for_viewbox(viewbox, scale); self.center = get_tile_center_for_viewbox(viewbox);
} }
pub fn set_interest(&mut self, interest: i32) { pub fn set_interest(&mut self, interest: i32) {
@ -122,6 +211,7 @@ impl TileViewbox {
pub const TILE_SIZE: f32 = 512.; pub const TILE_SIZE: f32 = 512.;
#[inline(always)]
pub fn get_tile_dimensions() -> skia::ISize { pub fn get_tile_dimensions() -> skia::ISize {
(TILE_SIZE as i32, TILE_SIZE as i32).into() (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) TileRect(sx, sy, ex, ey)
} }
pub fn get_tiles_for_viewbox(viewbox: Viewbox, scale: f32) -> TileRect { pub fn get_tiles_for_viewbox(viewbox: &Viewbox) -> TileRect {
let tile_size = get_tile_size(scale); let tile_size = get_tile_size(viewbox.get_scale());
get_tiles_for_rect(viewbox.area, tile_size) get_tiles_for_rect(viewbox.area, tile_size)
} }
pub fn get_tiles_for_viewbox_with_interest( pub fn get_tiles_for_viewbox_with_interest(viewbox: &Viewbox, interest: i32) -> TileRect {
viewbox: Viewbox, let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox);
interest: i32,
scale: f32,
) -> TileRect {
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale);
TileRect(sx - interest, sy - interest, ex + interest, ey + interest) TileRect(sx - interest, sy - interest, ex + interest, ey + interest)
} }
pub fn get_tile_center_for_viewbox(viewbox: Viewbox, scale: f32) -> Tile { pub fn get_tile_center_for_viewbox(viewbox: &Viewbox) -> Tile {
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale); let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox);
Tile((ex - sx) / 2, (ey - sy) / 2) 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) 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 { pub struct TileHashMap {
grid: HashMap<Tile, HashSet<Uuid>>, grid: HashMap<Tile, HashSet<Uuid>>,
index: HashMap<Uuid, HashSet<Tile>>, 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>> { pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&HashSet<Uuid>> {
self.grid.get(&tile) self.grid.get(&tile)
} }
@ -219,7 +312,7 @@ impl TileHashMap {
} }
const VIEWPORT_DEFAULT_CAPACITY: usize = 24 * 12; 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. /// Cached spiral of tile offsets for a given grid size.
/// ///
@ -315,6 +408,10 @@ pub struct PendingTiles {
pub list: Vec<Tile>, pub list: Vec<Tile>,
pub spiral: TileSpiral, pub spiral: TileSpiral,
pub spiral_rect: TileRect, 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 { impl PendingTiles {
@ -323,6 +420,10 @@ impl PendingTiles {
list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
spiral: TileSpiral::new(), spiral: TileSpiral::new(),
spiral_rect: TileRect::empty(), 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) // 2. visible + uncached (user sees these, render next)
// 3. interest + cached (pre-rendered area, blit from cache) // 3. interest + cached (pre-rendered area, blit from cache)
// 4. interest + uncached (lowest priority - background pre-render) // 4. interest + uncached (lowest priority - background pre-render)
let mut visible_cached = Vec::new(); self.visible_cached.clear();
let mut visible_uncached = Vec::new(); self.visible_uncached.clear();
let mut interest_cached = Vec::new(); self.interest_cached.clear();
let mut interest_uncached = Vec::new(); self.interest_uncached.clear();
// Compute the scheduling center explicitly (inclusive range). // Compute the scheduling center explicitly (inclusive range).
// This avoids relying on `TileRect::center_x/center_y` semantics, which may be used // 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); let is_cached = surfaces.has_cached_tile_surface(tile);
match (is_visible, is_cached) { match (is_visible, is_cached) {
(true, true) => visible_cached.push(tile), (true, true) => self.visible_cached.push(tile),
(true, false) => visible_uncached.push(tile), (true, false) => self.visible_uncached.push(tile),
(false, true) => interest_cached.push(tile), (false, true) => self.interest_cached.push(tile),
(false, false) => interest_uncached.push(tile), (false, false) => self.interest_uncached.push(tile),
} }
} }
// Build final list with lowest priority first (they get popped last) // Build final list with lowest priority first (they get popped last)
// Order: interest_uncached, interest_cached, visible_uncached, visible_cached // Order: interest_uncached, interest_cached, visible_uncached, visible_cached
self.list.extend(interest_uncached); self.list.extend(self.interest_uncached.iter());
self.list.extend(interest_cached); self.list.extend(self.interest_cached.iter());
self.list.extend(visible_uncached); self.list.extend(self.visible_uncached.iter());
self.list.extend(visible_cached); self.list.extend(self.visible_cached.iter());
} }
pub fn pop(&mut self) -> Option<Tile> { pub fn pop(&mut self) -> Option<Tile> {

View File

@ -1,62 +1,86 @@
use skia_safe::Rect; use crate::math::{Matrix, Point, Rect, Size};
use std::ops::Mul;
use crate::math::{Matrix, Point};
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub(crate) struct Viewbox { pub(crate) struct Viewbox {
pub pan_x: f32, pub pan: Point,
pub pan_y: f32, pub size: Size,
pub width: f32,
pub height: f32,
pub zoom: f32, pub zoom: f32,
pub dpr: f32,
pub area: Rect, pub area: Rect,
} }
impl Default for Viewbox { impl Default for Viewbox {
fn default() -> Self { fn default() -> Self {
Self { Self {
pan_x: 0., pan: Point::new(0.0, 0.0),
pan_y: 0., size: Size::new(0.0, 0.0),
width: 0.0,
height: 0.0,
zoom: 1.0, zoom: 1.0,
dpr: 1.0,
area: Rect::new_empty(), area: Rect::new_empty(),
} }
} }
} }
#[allow(dead_code)]
impl Viewbox { impl Viewbox {
pub fn new(width: f32, height: f32) -> Self { 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 { Self {
width, size,
height,
area, area,
..Self::default() ..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) { pub fn set_all(&mut self, zoom: f32, pan_x: f32, pan_y: f32) {
self.pan_x = pan_x; self.pan.set(pan_x, pan_y);
self.pan_y = pan_y;
self.zoom = zoom; self.zoom = zoom;
self.area.set_xywh( self.area.set_xywh(
-self.pan_x, -self.pan.x,
-self.pan_y, -self.pan.y,
self.width / self.zoom, self.size.width / self.zoom,
self.height / self.zoom, self.size.height / self.zoom,
); );
} }
pub fn set_wh(&mut self, width: f32, height: f32) { pub fn set_wh(&mut self, width: f32, height: f32) {
self.width = width; self.size.set(width, height);
self.height = height;
self.area 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 { pub fn pan(&self) -> Point {
Point::new(self.pan_x, self.pan_y) self.pan
} }
pub fn zoom(&self) -> f32 { 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_export]
macro_rules! notify_tiles_render_complete { 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 notify_tiles_render_complete;
pub use request_animation_frame;