mirror of
https://github.com/penpot/penpot.git
synced 2026-06-09 08:52:05 +00:00
♻️ 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:
parent
7bf519a127
commit
d0f6d5b3a1
@ -414,7 +414,8 @@ test("[Taiga #9929] Paste text in workspace", async ({ page, context }) => {
|
||||
.getByText("Lorem ipsum dolor");
|
||||
});
|
||||
|
||||
test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
|
||||
// I've skipped this test because it doesn't make sense with the new render.
|
||||
test.skip("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
|
||||
@ -306,6 +306,20 @@
|
||||
(declare set-shape-vertical-align fonts-from-text-content)
|
||||
(declare reload-renderer!)
|
||||
|
||||
;; These are the type of frames we have in our
|
||||
;; render pipeline.
|
||||
(def ^:const FRAME_TYPE_NONE 0) ;; This type should never "leak".
|
||||
(def ^:const FRAME_TYPE_PARTIAL 1) ;; A frame needs more render calls to end.
|
||||
(def ^:const FRAME_TYPE_FULL 2) ;; A frame was full.
|
||||
|
||||
(defn- internal-render
|
||||
([]
|
||||
(internal-render 0))
|
||||
([timestamp]
|
||||
(set! wasm/internal-frame-type (h/call wasm/internal-module "_render" timestamp wasm/internal-frame-type))
|
||||
(when (= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
(request-render "frame-type-partial"))))
|
||||
|
||||
(defn- build-reload-payload
|
||||
"Builds renderer reload payload from current application state.
|
||||
Avoids keeping heavyweight object snapshots in memory."
|
||||
@ -337,8 +351,8 @@
|
||||
;; This should never be called from the outside.
|
||||
(defn- render
|
||||
[timestamp]
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_render" timestamp)
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(internal-render timestamp)
|
||||
|
||||
;; Update text editor blink (so cursor toggles) using the same timestamp
|
||||
(try
|
||||
@ -1201,7 +1215,7 @@
|
||||
;; completes in the first frame. For zoom, interest-
|
||||
;; area tiles (~3 tile margin) don't block the main
|
||||
;; thread.
|
||||
(h/call wasm/internal-module "_render" 0)))]
|
||||
(internal-render)))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
|
||||
(defn set-view-box
|
||||
@ -1804,50 +1818,19 @@
|
||||
(contains? cf/flags :render-wasm-info)
|
||||
(bit-or 2r00000000000000000000000000001000)))
|
||||
|
||||
(defn- wasm-aa-threshold-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(defn- wasm-get-numeric-value
|
||||
[name]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:aa_threshold p))]
|
||||
(get p name))]
|
||||
(let [n (if (string? raw) (js/parseFloat raw) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-blur-downscale-threshold-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:blur_downscale_threshold p))]
|
||||
(let [n (if (string? raw) (js/parseFloat raw) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-max-blocking-time-ms-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:max_blocking_time_ms p))]
|
||||
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-node-batch-threshold-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:node_batch_threshold p))]
|
||||
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-viewport-interest-area-threshold-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:viewport_interest_area_threshold p))]
|
||||
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
(defn- wasm-set-param-from-route-params-if-present
|
||||
[param-name]
|
||||
(when-let [value (wasm-get-numeric-value param-name)]
|
||||
(let [setter-name (str/concat "_set_" (name param-name))]
|
||||
(h/call wasm/internal-module setter-name value))))
|
||||
|
||||
(defn set-canvas-size
|
||||
[canvas]
|
||||
@ -1914,18 +1897,13 @@
|
||||
;; Initialize Wasm Render Engine
|
||||
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
|
||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||
(when-let [t (wasm-aa-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
||||
(when-let [t (wasm-viewport-interest-area-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_viewport_interest_area_threshold" t))
|
||||
(when-let [t (wasm-max-blocking-time-ms-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_max_blocking_time_ms" t))
|
||||
(when-let [t (wasm-node-batch-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_node_batch_threshold" t))
|
||||
(when-let [t (wasm-blur-downscale-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_blur_downscale_threshold" t))
|
||||
(when-let [max-tex (webgl/max-texture-size context)]
|
||||
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
|
||||
|
||||
;; Configurable parameters.
|
||||
(wasm-set-param-from-route-params-if-present :antialias_threshold)
|
||||
(wasm-set-param-from-route-params-if-present :viewport_interest_area_threshold)
|
||||
(wasm-set-param-from-route-params-if-present :max_blocking_time_ms)
|
||||
(wasm-set-param-from-route-params-if-present :node_batch_threshold)
|
||||
(wasm-set-param-from-route-params-if-present :blur_downscale_threshold)
|
||||
|
||||
;; Set browser and canvas size only after initialization
|
||||
(h/call wasm/internal-module "_set_browser" browser)
|
||||
|
||||
@ -11,15 +11,6 @@
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn max-texture-size
|
||||
"Returns `gl.MAX_TEXTURE_SIZE` (max dimension of a 2D texture), or nil if
|
||||
unavailable."
|
||||
[gl]
|
||||
(when gl
|
||||
(let [n (.getParameter ^js gl (.-MAX_TEXTURE_SIZE ^js gl))]
|
||||
(when (and (number? n) (pos? n) (js/isFinite n))
|
||||
(js/Math.floor n)))))
|
||||
|
||||
(defn get-webgl-context
|
||||
"Gets the WebGL context from the WASM module"
|
||||
[]
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
(:require ["./api/shared.js" :as shared]))
|
||||
|
||||
(defonce internal-frame-id nil)
|
||||
(defonce internal-frame-type 0)
|
||||
(defonce internal-module #js {})
|
||||
|
||||
;; Reference to the HTML canvas element.
|
||||
|
||||
@ -37,8 +37,6 @@
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
[cuerdas.core :as str]
|
||||
@ -135,6 +133,14 @@
|
||||
(wasm.mem/free)
|
||||
text)))
|
||||
|
||||
(defn ^:export wasmCaptureFrames
|
||||
[amount]
|
||||
(let [module wasm/internal-module
|
||||
f (when module (unchecked-get module "_capture_frames"))]
|
||||
(if (fn? f)
|
||||
(wasm.h/call module "_capture_frames" amount)
|
||||
(js/console.warn "[debug] render-wasm module not ready or missing _render_stats"))))
|
||||
|
||||
(defn ^:export wasmRenderStats
|
||||
[]
|
||||
(let [module wasm/internal-module
|
||||
@ -213,31 +219,6 @@
|
||||
opacity: 0.5;
|
||||
")
|
||||
|
||||
(defn ^:export fps
|
||||
"Adds a widget to keep track of the average FPS's"
|
||||
[]
|
||||
(let [last (volatile! (.now js/performance))
|
||||
avg (volatile! 0)
|
||||
node (-> (.createElement js/document "div")
|
||||
(obj/set! "id" "fps")
|
||||
(obj/set! "style" widget-style))
|
||||
body (obj/get js/document "body")
|
||||
|
||||
do-thing (fn do-thing []
|
||||
(timers/raf
|
||||
(fn []
|
||||
(let [cur (.now js/performance)
|
||||
ts (/ 1000 (* (- cur @last)))
|
||||
val (+ @avg (* (- ts @avg) 0.1))]
|
||||
|
||||
(obj/set! node "innerText" val)
|
||||
(vreset! last cur)
|
||||
(vreset! avg val)
|
||||
(do-thing)))))]
|
||||
|
||||
(.appendChild body node)
|
||||
(do-thing)))
|
||||
|
||||
(defn ^:export dump-state []
|
||||
(logjs "state" @st/state)
|
||||
nil)
|
||||
|
||||
@ -32,6 +32,7 @@ export EM_MALLOC="dlmalloc"
|
||||
export EMCC_CFLAGS="--no-entry \
|
||||
--js-library src/js/wapi.js \
|
||||
-sMALLOC=$EM_MALLOC \
|
||||
-sINVOKE_RUN=0 \
|
||||
-sALLOW_TABLE_GROWTH=0 \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sINITIAL_HEAP=$EM_INITIAL_HEAP \
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
use macros::wasm_error;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::emscripten::init_gl;
|
||||
|
||||
use crate::mem;
|
||||
use crate::render::{gpu_state::GpuState, RenderState};
|
||||
use crate::state::{State, TextEditorState};
|
||||
@ -107,6 +110,7 @@ fn design_init() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes TextEditorState.
|
||||
fn text_editor_init() {
|
||||
unsafe {
|
||||
let text_editor_state = TextEditorState::new();
|
||||
@ -117,6 +121,8 @@ fn text_editor_init() {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
gpu_init();
|
||||
render_init(width, height);
|
||||
text_editor_init();
|
||||
@ -130,7 +136,6 @@ pub extern "C" fn clean_up() -> Result<()> {
|
||||
// Cancel the current animation frame if it exists so
|
||||
// it won't try to render without context
|
||||
let render_state = get_render_state();
|
||||
render_state.cancel_animation_frame();
|
||||
render_state.prepare_context_loss_cleanup();
|
||||
unsafe { DESIGN_STATE = std::ptr::null_mut() }
|
||||
mem::free_bytes()?;
|
||||
|
||||
@ -1,18 +1,4 @@
|
||||
addToLibrary({
|
||||
wapi_requestAnimationFrame: function wapi_requestAnimationFrame() {
|
||||
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
|
||||
setTimeout(Module._process_animation_frame);
|
||||
} else {
|
||||
return window.requestAnimationFrame(Module._process_animation_frame);
|
||||
}
|
||||
},
|
||||
wapi_cancelAnimationFrame: function wapi_cancelAnimationFrame(frameId) {
|
||||
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
|
||||
clearTimeout(frameId);
|
||||
} else {
|
||||
return window.cancelAnimationFrame(frameId);
|
||||
}
|
||||
},
|
||||
wapi_notifyTilesRenderComplete: function wapi_notifyTilesRenderComplete() {
|
||||
// The corresponding listener lives on `document` (main thread), so in a
|
||||
// worker context we simply skip the dispatch instead of crashing.
|
||||
|
||||
@ -19,6 +19,7 @@ use std::collections::HashMap;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::{FrameType, RenderFlag};
|
||||
|
||||
use globals::{get_design_state, get_gpu_state, get_render_state};
|
||||
|
||||
@ -89,15 +90,6 @@ pub extern "C" fn set_antialias_threshold(threshold: f32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_max_atlas_texture_size(max_px: i32) -> Result<()> {
|
||||
get_render_state()
|
||||
.surfaces
|
||||
.set_max_atlas_texture_size(max_px);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||
@ -112,7 +104,7 @@ pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render(timestamp: i32) -> Result<()> {
|
||||
pub extern "C" fn render(timestamp: i32, flags: u8) -> Result<FrameType> {
|
||||
with_state!(state, {
|
||||
state.rebuild_touched_tiles();
|
||||
// Drain the throttled modifier-tile invalidation accumulated
|
||||
@ -128,11 +120,17 @@ pub extern "C" fn render(timestamp: i32) -> Result<()> {
|
||||
state.rebuild_modifier_tiles(&ids)?;
|
||||
}
|
||||
}
|
||||
state
|
||||
.start_render_loop(timestamp)
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||||
let frame_type = if flags & RenderFlag::Partial as u8 == RenderFlag::Partial as u8 {
|
||||
state
|
||||
.continue_render_loop(timestamp)
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?
|
||||
} else {
|
||||
state
|
||||
.start_render_loop(timestamp)
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?
|
||||
};
|
||||
return Ok(frame_type);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@ -179,7 +177,7 @@ pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||||
with_state!(state, {
|
||||
// Don't cancel the animation frame — let the async render
|
||||
// continue populating the tile HashMap in the background.
|
||||
// process_animation_frame skips flush_and_submit in fast
|
||||
// `continue_render_loop` skips flush_and_submit in fast
|
||||
// mode so it won't present stale Target content. The
|
||||
// tile HashMap is position-independent, so tiles rendered
|
||||
// for the old viewport can be reused by the next full
|
||||
@ -239,16 +237,6 @@ pub extern "C" fn render_loading_overlay() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||||
let result = with_state!(state, { state.process_animation_frame(timestamp) });
|
||||
if let Err(err) = result {
|
||||
eprintln!("process_animation_frame error: {}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn reset_canvas() -> Result<()> {
|
||||
@ -301,12 +289,7 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
performance::begin_measure!("set_view_end");
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(false);
|
||||
render_state.cancel_animation_frame();
|
||||
|
||||
let scale = render_state.get_scale();
|
||||
render_state
|
||||
.tile_viewbox
|
||||
.update(render_state.viewbox, scale);
|
||||
render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
|
||||
if render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
@ -358,7 +341,6 @@ pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(false);
|
||||
render_state.options.set_interactive_transform(false);
|
||||
render_state.cancel_animation_frame();
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
Ok(())
|
||||
}
|
||||
@ -946,7 +928,9 @@ pub fn free_gpu_resources() {
|
||||
get_render_state().free_gpu_resources();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
pub fn main() {
|
||||
// Why an empty main?
|
||||
// Right now with the target `wasm32-unknown-emscripten` it is not possible
|
||||
// to compile a rust project without a main and generate the necessary glue
|
||||
// code. So the only option is to compile it with an empty main.
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ pub type Rect = skia::Rect;
|
||||
pub type Matrix = skia::Matrix;
|
||||
pub type Vector = skia::Vector;
|
||||
pub type Point = skia::Point;
|
||||
pub type Size = skia::Size;
|
||||
|
||||
const THRESHOLD: f32 = 0.001;
|
||||
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_time() -> i32 {
|
||||
crate::get_now!() as i32
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn get_time() -> i32 {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
@ -38,6 +38,21 @@ pub use images::*;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum FrameType {
|
||||
None = 0,
|
||||
Partial = 1,
|
||||
Full = 2,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[repr(u8)]
|
||||
pub enum RenderFlag {
|
||||
None = 0,
|
||||
Partial = 1,
|
||||
Full = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRenderState {
|
||||
pub id: Uuid,
|
||||
@ -334,10 +349,6 @@ pub(crate) struct RenderState {
|
||||
pub cached_viewbox: Viewbox,
|
||||
pub images: ImageStore,
|
||||
pub background_color: skia::Color,
|
||||
// Identifier of the current requestAnimationFrame call, if any.
|
||||
pub render_request_id: Option<i32>,
|
||||
// Indicates whether the rendering process has pending frames.
|
||||
pub render_in_progress: bool,
|
||||
// Stack of nodes pending to be rendered.
|
||||
pending_nodes: Vec<NodeRenderState>,
|
||||
pub current_tile: Option<tiles::Tile>,
|
||||
@ -370,7 +381,7 @@ pub(crate) struct RenderState {
|
||||
/// Cleared at the beginning of a render pass; set to true after we clear Cache the first
|
||||
/// time we are about to blit a tile into Cache for this pass.
|
||||
pub cache_cleared_this_render: bool,
|
||||
/// True iff the current tile had shapes assigned to it when we
|
||||
/// True if the current tile had shapes assigned to it when we
|
||||
/// started rendering it. Lets us distinguish a genuinely empty
|
||||
/// tile (skip composite, just clear) from a tile whose walker
|
||||
/// finished its work in a previous PAF and is now being resumed
|
||||
@ -401,7 +412,7 @@ pub struct InteractiveDragCrop {
|
||||
/// Chooses a window inside the full workspace-pixel crop `[0, out_w) × [0, out_h)` with each side
|
||||
/// at most `max_side_px` (**without scaling**): centered on the projection of
|
||||
/// `viewport_doc ∩ src_doc_bounds`, or on the full crop if that intersection is empty.
|
||||
/// `max_side_px` should match [`Surfaces::max_texture_dimension_px`] (same budget as the atlas).
|
||||
/// `max_side_px` should match [`GpuState::max_texture_size`] (same budget as the atlas).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn drag_crop_snapshot_window_px(
|
||||
max_side_px: i32,
|
||||
@ -441,22 +452,6 @@ fn drag_crop_snapshot_window_px(
|
||||
(ox, oy, win_w, win_h)
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize {
|
||||
// First we retrieve the extended area of the viewport that we could render.
|
||||
let TileRect(isx, isy, iex, iey) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(viewbox, interest, scale);
|
||||
|
||||
let dx = if isx.signum() != iex.signum() { 1 } else { 0 };
|
||||
let dy = if isy.signum() != iey.signum() { 1 } else { 0 };
|
||||
|
||||
let tile_size = tiles::TILE_SIZE;
|
||||
(
|
||||
((iex - isx).abs() + dx) * tile_size as i32,
|
||||
((iey - isy).abs() + dy) * tile_size as i32,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
/// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an
|
||||
/// interactive transform (drag/resize/rotate).
|
||||
@ -554,8 +549,6 @@ impl RenderState {
|
||||
cached_viewbox: Viewbox::new(0., 0.),
|
||||
images: ImageStore::new(),
|
||||
background_color: skia::Color::TRANSPARENT,
|
||||
render_request_id: None,
|
||||
render_in_progress: false,
|
||||
pending_nodes: vec![],
|
||||
current_tile: None,
|
||||
sampling_options,
|
||||
@ -563,9 +556,8 @@ impl RenderState {
|
||||
render_area_with_margins: Rect::new_empty(),
|
||||
tiles,
|
||||
tile_viewbox: tiles::TileViewbox::new_with_interest(
|
||||
viewbox,
|
||||
&viewbox,
|
||||
options.dpr_viewport_interest_area_threshold,
|
||||
1.0,
|
||||
),
|
||||
pending_tiles: PendingTiles::new(),
|
||||
nested_fills: vec![],
|
||||
@ -787,10 +779,12 @@ impl RenderState {
|
||||
self.tile_viewbox
|
||||
.set_interest(self.options.dpr_viewport_interest_area_threshold);
|
||||
self.resize(
|
||||
self.viewbox.width.floor() as i32,
|
||||
self.viewbox.height.floor() as i32,
|
||||
self.viewbox.width().floor() as i32,
|
||||
self.viewbox.height().floor() as i32,
|
||||
)?;
|
||||
self.fonts.set_scale_debug_font(dpr);
|
||||
self.viewbox.set_dpr(dpr);
|
||||
self.surfaces.set_dpr(dpr);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -836,7 +830,7 @@ impl RenderState {
|
||||
let dpr_height = (height as f32 * self.options.dpr).floor() as i32;
|
||||
self.surfaces.resize(dpr_width, dpr_height)?;
|
||||
self.viewbox.set_wh(width as f32, height as f32);
|
||||
self.tile_viewbox.update(self.viewbox, self.get_scale());
|
||||
self.tile_viewbox.update(&self.viewbox);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -901,23 +895,13 @@ impl RenderState {
|
||||
self.flush_and_submit();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_canvas_at(&mut self, surface_id: SurfaceId) -> &skia::Canvas {
|
||||
self.surfaces.canvas(surface_id)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn restore_canvas(&mut self, surface_id: SurfaceId) {
|
||||
self.surfaces.canvas(surface_id).restore();
|
||||
}
|
||||
|
||||
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> {
|
||||
pub fn apply_render_to_final_canvas(&mut self) -> Result<()> {
|
||||
// During interactive transforms we render tiles directly into Target; updating the cache
|
||||
// (snapshot -> atlas blit -> tiles.add) can force GPU stalls. Defer cache rebuild until
|
||||
// the interaction ends.
|
||||
if self.options.is_interactive_transform() {
|
||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
self.surfaces.draw_current_tile_direct(
|
||||
self.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
self.background_color,
|
||||
surfaces::DrawOnCache::No,
|
||||
@ -942,7 +926,7 @@ impl RenderState {
|
||||
.as_ref()
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
|
||||
self.surfaces.cache_current_tile_texture(
|
||||
self.surfaces.draw_current_tile_into_tile_atlas(
|
||||
&self.tile_viewbox,
|
||||
¤t_tile,
|
||||
&tile_rect,
|
||||
@ -950,12 +934,15 @@ impl RenderState {
|
||||
self.render_area,
|
||||
);
|
||||
|
||||
let rect = self.get_current_tile_bounds()?;
|
||||
self.surfaces
|
||||
.draw_cached_tile_surface(current_tile, rect, self.background_color);
|
||||
.draw_cached_tile_into_backbuffer(current_tile, &rect);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||
/// This function draws the "surface stack" into the specified "target" surface.
|
||||
pub fn draw_shape_surface_stack_into(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||
performance::begin_measure!("apply_drawing_to_render_canvas");
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
@ -1663,7 +1650,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if apply_to_current_surface {
|
||||
self.apply_drawing_to_render_canvas(Some(&shape), target_surface);
|
||||
self.draw_shape_surface_stack_into(Some(&shape), target_surface);
|
||||
}
|
||||
|
||||
// Only restore if we saved (optimization for simple shapes)
|
||||
@ -1691,14 +1678,6 @@ impl RenderState {
|
||||
self.surfaces.update_render_context(self.render_area, scale);
|
||||
}
|
||||
|
||||
pub fn cancel_animation_frame(&mut self) {
|
||||
if self.render_in_progress {
|
||||
if let Some(frame_id) = self.render_request_id {
|
||||
wapi::cancel_animation_frame!(frame_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_backbuffer_crop_cache(&mut self, tree: ShapesPoolRef) {
|
||||
self.backbuffer_crop_cache.clear();
|
||||
|
||||
@ -1778,12 +1757,12 @@ impl RenderState {
|
||||
let vb_left = self.viewbox.area.left;
|
||||
let vb_top = self.viewbox.area.top;
|
||||
let (bb_w, bb_h) = self.surfaces.surface_size(SurfaceId::Backbuffer);
|
||||
let max_snap_px = self.surfaces.max_texture_dimension_px();
|
||||
let max_snap_px = get_gpu_state().max_texture_size();
|
||||
|
||||
// Snapshot the atlas once for the whole pass so that all shapes sharing
|
||||
// the tile/atlas fallback path reuse the same GPU image rather than each
|
||||
// triggering a separate `image_snapshot` flush.
|
||||
let atlas_snap = self.surfaces.atlas_snapshot_for_drag_crop();
|
||||
let atlas_snap = self.surfaces.atlas.snapshot_for_drag_crop();
|
||||
|
||||
// Scratch surface reused across all shapes that need the tile/atlas
|
||||
// fallback — avoids one WebGL texture allocation per shape.
|
||||
@ -1901,16 +1880,14 @@ impl RenderState {
|
||||
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
|
||||
let _start = performance::begin_timed_log!("render_from_cache");
|
||||
performance::begin_measure!("render_from_cache");
|
||||
let cached_scale = self.get_cached_scale();
|
||||
|
||||
let bg_color = self.background_color;
|
||||
|
||||
// During fast mode (pan/zoom), if a previous full-quality render still has pending tiles,
|
||||
// always prefer the persistent atlas. The atlas is incrementally updated as tiles finish,
|
||||
// and drawing from it avoids mixing a partially-updated Cache surface with missing tiles.
|
||||
if self.options.is_fast_mode() && self.render_in_progress && self.surfaces.has_atlas() {
|
||||
if self.options.is_fast_mode() && !self.surfaces.atlas.is_empty() {
|
||||
self.surfaces
|
||||
.draw_atlas_to_backbuffer(self.viewbox, self.options.dpr, bg_color);
|
||||
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
|
||||
|
||||
self.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
@ -1925,11 +1902,7 @@ impl RenderState {
|
||||
|
||||
let interest = self.options.dpr_viewport_interest_area_threshold;
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(
|
||||
self.cached_viewbox,
|
||||
interest,
|
||||
cached_scale,
|
||||
);
|
||||
tiles::get_tiles_for_viewbox_with_interest(&self.cached_viewbox, interest);
|
||||
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr;
|
||||
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr;
|
||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||
@ -1944,8 +1917,8 @@ impl RenderState {
|
||||
let cache_h = cache_dim.height as f32;
|
||||
|
||||
// Viewport in target pixels.
|
||||
let vw = (self.viewbox.width * self.options.dpr).max(1.0);
|
||||
let vh = (self.viewbox.height * self.options.dpr).max(1.0);
|
||||
let vw = self.viewbox.dpr_width().max(1.0);
|
||||
let vh = self.viewbox.dpr_height().max(1.0);
|
||||
|
||||
// Inverse-map viewport corners into cache coordinates.
|
||||
// target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords).
|
||||
@ -1956,8 +1929,11 @@ impl RenderState {
|
||||
0.0
|
||||
};
|
||||
|
||||
let cx0 = (0.0 * inv) - translate_x;
|
||||
let cy0 = (0.0 * inv) - translate_y;
|
||||
// let cx0 = (0.0 * inv) - translate_x;
|
||||
// let cy0 = (0.0 * inv) - translate_y;
|
||||
// NOTA: 0.0 * inv => siempre 0
|
||||
let cx0 = -translate_x;
|
||||
let cy0 = -translate_y;
|
||||
let cx1 = (vw * inv) - translate_x;
|
||||
let cy1 = (vh * inv) - translate_y;
|
||||
|
||||
@ -1970,12 +1946,9 @@ impl RenderState {
|
||||
min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
|
||||
if !cache_covers {
|
||||
// Early return only if atlas exists; otherwise keep cache path.
|
||||
if self.surfaces.has_atlas() {
|
||||
self.surfaces.draw_atlas_to_backbuffer(
|
||||
self.viewbox,
|
||||
self.options.dpr,
|
||||
bg_color,
|
||||
);
|
||||
if !self.surfaces.atlas.is_empty() {
|
||||
self.surfaces
|
||||
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
|
||||
|
||||
self.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
@ -1985,21 +1958,9 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// Setup canvas transform
|
||||
{
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Backbuffer);
|
||||
canvas.save();
|
||||
canvas.scale((navigate_zoom, navigate_zoom));
|
||||
canvas.translate((translate_x, translate_y));
|
||||
canvas.clear(bg_color);
|
||||
}
|
||||
|
||||
// Draw directly from cache surface, avoiding snapshot overhead
|
||||
self.surfaces.draw_cache_to_backbuffer();
|
||||
|
||||
// Restore canvas state
|
||||
self.surfaces.canvas(SurfaceId::Backbuffer).restore();
|
||||
|
||||
// During pure pan (same zoom), draw tiles from the HashMap
|
||||
// on top of the scaled Cache surface. Cached tile textures
|
||||
// include full-quality effects (shadows, blur) from the last
|
||||
@ -2008,23 +1969,14 @@ impl RenderState {
|
||||
// would be at wrong positions — skip them and let the full
|
||||
// render after set_view_end handle it.
|
||||
if !self.zoom_changed() {
|
||||
let current_scale = self.get_scale();
|
||||
let visible_rect = tiles::get_tiles_for_viewbox(self.viewbox, current_scale);
|
||||
let vb_offset_x = self.viewbox.area.left * current_scale;
|
||||
let vb_offset_y = self.viewbox.area.top * current_scale;
|
||||
|
||||
let visible_rect = tiles::get_tiles_for_viewbox(&self.viewbox);
|
||||
let offset = self.viewbox.get_offset();
|
||||
for tx in visible_rect.x1()..=visible_rect.x2() {
|
||||
for ty in visible_rect.y1()..=visible_rect.y2() {
|
||||
let tile = tiles::Tile::from(tx, ty);
|
||||
if self.surfaces.has_cached_tile_surface(tile) {
|
||||
let tile_rect = skia::Rect::from_xywh(
|
||||
tx as f32 * tiles::TILE_SIZE - vb_offset_x,
|
||||
ty as f32 * tiles::TILE_SIZE - vb_offset_y,
|
||||
tiles::TILE_SIZE,
|
||||
tiles::TILE_SIZE,
|
||||
);
|
||||
self.surfaces
|
||||
.draw_cached_tile_surface(tile, tile_rect, bg_color);
|
||||
let rect = tile.get_rect_with_offset(&offset);
|
||||
self.surfaces.draw_cached_tile_into_backbuffer(tile, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2063,20 +2015,43 @@ impl RenderState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears all the necessary vecs and hashmaps.
|
||||
/// Also garbage collects surfaces.
|
||||
fn clear(&mut self, tree: ShapesPoolRef) {
|
||||
#[cfg(feature = "stats")]
|
||||
self.stats.clear();
|
||||
|
||||
self.surfaces.gc();
|
||||
|
||||
self.pending_nodes.clear();
|
||||
if self.pending_nodes.capacity() < tree.len() {
|
||||
self.pending_nodes
|
||||
.reserve(tree.len() - self.pending_nodes.capacity());
|
||||
}
|
||||
|
||||
// Clear nested state stacks to avoid residual fills/blurs from previous renders
|
||||
// being incorrectly applied to new frames
|
||||
self.nested_fills.clear();
|
||||
self.nested_blurs.clear();
|
||||
self.nested_shadows.clear();
|
||||
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
sync_render: bool,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "stats")]
|
||||
self.stats.clear();
|
||||
) -> Result<FrameType> {
|
||||
self.clear(tree);
|
||||
|
||||
let _start = performance::begin_timed_log!("start_render_loop");
|
||||
let scale = self.get_scale();
|
||||
|
||||
self.tile_viewbox.update(self.viewbox, scale);
|
||||
self.tile_viewbox.update(&self.viewbox);
|
||||
self.focus_mode.reset();
|
||||
|
||||
performance::begin_measure!("render");
|
||||
@ -2086,7 +2061,7 @@ impl RenderState {
|
||||
// to clamp atlas updates. This prevents zoom-out tiles from forcing atlas
|
||||
// growth far beyond real content.
|
||||
let doc_bounds = self.compute_document_bounds(base_object, tree);
|
||||
self.surfaces.set_atlas_doc_bounds(doc_bounds);
|
||||
self.surfaces.atlas.set_doc_bounds(doc_bounds);
|
||||
|
||||
self.cache_cleared_this_render = false;
|
||||
if self.options.is_interactive_transform() {
|
||||
@ -2109,63 +2084,44 @@ impl RenderState {
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().scale((scale, scale));
|
||||
});
|
||||
|
||||
let viewbox_cache_size = get_cache_size(
|
||||
self.viewbox,
|
||||
scale,
|
||||
self.surfaces.resize_cache_from_viewbox(
|
||||
&self.viewbox,
|
||||
&self.cached_viewbox,
|
||||
self.options.dpr_viewport_interest_area_threshold,
|
||||
);
|
||||
let cached_viewbox_cache_size = get_cache_size(
|
||||
self.cached_viewbox,
|
||||
scale,
|
||||
self.options.dpr_viewport_interest_area_threshold,
|
||||
);
|
||||
// Only resize cache if the new size is larger than the cached size
|
||||
// This avoids unnecessary surface recreations when the cache size decreases
|
||||
if viewbox_cache_size.width > cached_viewbox_cache_size.width
|
||||
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
|
||||
{
|
||||
self.surfaces.resize_cache(
|
||||
viewbox_cache_size,
|
||||
self.options.dpr_viewport_interest_area_threshold,
|
||||
)?;
|
||||
}
|
||||
)?;
|
||||
|
||||
// FIXME - review debug
|
||||
// debug::render_debug_tiles_for_viewbox(self);
|
||||
|
||||
let _tile_start = performance::begin_timed_log!("tile_cache_update");
|
||||
|
||||
performance::begin_measure!("tile_cache");
|
||||
let only_visible = self.options.is_interactive_transform();
|
||||
self.pending_tiles
|
||||
.update(&self.tile_viewbox, &self.surfaces, only_visible);
|
||||
performance::end_measure!("tile_cache");
|
||||
|
||||
performance::end_timed_log!("tile_cache_update", _tile_start);
|
||||
|
||||
self.pending_nodes.clear();
|
||||
if self.pending_nodes.capacity() < tree.len() {
|
||||
self.pending_nodes
|
||||
.reserve(tree.len() - self.pending_nodes.capacity());
|
||||
}
|
||||
// Clear nested state stacks to avoid residual fills/blurs from previous renders
|
||||
// being incorrectly applied to new frames
|
||||
self.nested_fills.clear();
|
||||
self.nested_blurs.clear();
|
||||
self.nested_shadows.clear();
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
|
||||
self.render_in_progress = true;
|
||||
|
||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||
self.draw_shape_surface_stack_into(None, SurfaceId::Current);
|
||||
|
||||
#[allow(unused)]
|
||||
let mut frame_type = FrameType::None;
|
||||
if sync_render {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
frame_type = self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
} else {
|
||||
self.process_animation_frame(base_object, tree, timestamp)?;
|
||||
frame_type = self.continue_render_loop(base_object, tree, timestamp)?;
|
||||
|
||||
// This is an option to debug frames.
|
||||
if self.options.capture_frames > 0 {
|
||||
self.options.capture_frames -= 1;
|
||||
}
|
||||
|
||||
// Update cached_viewbox after visible tiles render
|
||||
// synchronously so that render_from_cache uses the correct
|
||||
// zoom ratio even if interest-area tiles are still rendering
|
||||
@ -2180,7 +2136,7 @@ impl RenderState {
|
||||
|
||||
performance::end_measure!("start_render_loop");
|
||||
performance::end_timed_log!("start_render_loop", _start);
|
||||
Ok(())
|
||||
Ok(frame_type)
|
||||
}
|
||||
|
||||
fn compute_document_bounds(
|
||||
@ -2214,37 +2170,45 @@ impl RenderState {
|
||||
acc
|
||||
}
|
||||
|
||||
pub fn process_animation_frame(
|
||||
pub fn continue_render_loop(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<()> {
|
||||
performance::begin_measure!("process_animation_frame");
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
|
||||
) -> Result<FrameType> {
|
||||
performance::begin_measure!("continue_render_loop");
|
||||
let frame_type = self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
|
||||
|
||||
if self.render_in_progress {
|
||||
// Partial frame: just flush GPU work. The display shows the last
|
||||
// fully submitted frame; no need to copy or draw UI overlays here.
|
||||
self.flush();
|
||||
self.cancel_animation_frame();
|
||||
self.render_request_id = Some(wapi::request_animation_frame!());
|
||||
} else {
|
||||
// A full-quality frame is now complete. Rebuild the per-shape crop
|
||||
// cache from the clean Backbuffer (no UI overlay yet) so that
|
||||
// interactive drag backgrounds don't include the grid overlay.
|
||||
if !self.options.is_fast_mode() && !self.options.is_interactive_transform() {
|
||||
self.rebuild_backbuffer_crop_cache(tree);
|
||||
}
|
||||
// present_frame: copy clean Backbuffer → Target, draw UI/debug
|
||||
// overlays on Target only, then flush. Backbuffer stays overlay-free.
|
||||
self.present_frame(tree);
|
||||
wapi::notify_tiles_render_complete!();
|
||||
performance::end_measure!("render");
|
||||
if !self.options.is_interactive_transform() {
|
||||
self.surfaces
|
||||
.draw_tile_atlas_to_backbuffer(&self.viewbox, &self.tile_viewbox);
|
||||
}
|
||||
|
||||
performance::end_measure!("process_animation_frame");
|
||||
Ok(())
|
||||
match frame_type {
|
||||
FrameType::None => {
|
||||
panic!("FrameType::None");
|
||||
}
|
||||
FrameType::Partial => {
|
||||
// Partial frame: just flush GPU work. The display shows the last
|
||||
// fully submitted frame; no need to copy or draw UI overlays here.
|
||||
self.flush();
|
||||
}
|
||||
FrameType::Full => {
|
||||
// A full-quality frame is now complete. Rebuild the per-shape crop
|
||||
// cache from the clean Backbuffer (no UI overlay yet) so that
|
||||
// interactive drag backgrounds don't include the grid overlay.
|
||||
if !self.options.is_fast_mode() && !self.options.is_interactive_transform() {
|
||||
self.rebuild_backbuffer_crop_cache(tree);
|
||||
}
|
||||
// present_frame: copy clean Backbuffer → Target, draw UI/debug
|
||||
// overlays on Target only, then flush. Backbuffer stays overlay-free.
|
||||
self.present_frame(tree);
|
||||
wapi::notify_tiles_render_complete!();
|
||||
performance::end_measure!("render");
|
||||
}
|
||||
}
|
||||
performance::end_measure!("continue_render_loop");
|
||||
Ok(frame_type)
|
||||
}
|
||||
|
||||
pub fn render_shape_tree_sync(
|
||||
@ -2252,10 +2216,10 @@ impl RenderState {
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<()> {
|
||||
) -> Result<FrameType> {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
|
||||
self.present_frame(tree);
|
||||
Ok(())
|
||||
Ok(FrameType::Full)
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
@ -2560,18 +2524,11 @@ impl RenderState {
|
||||
}
|
||||
|
||||
pub fn get_current_tile_bounds(&mut self) -> Result<Rect> {
|
||||
let tiles::Tile(tile_x, tile_y) = self
|
||||
let tile = self
|
||||
.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
let scale = self.get_scale();
|
||||
let offset_x = self.viewbox.area.left * scale;
|
||||
let offset_y = self.viewbox.area.top * scale;
|
||||
Ok(Rect::from_xywh(
|
||||
(tile_x as f32 * tiles::TILE_SIZE) - offset_x,
|
||||
(tile_y as f32 * tiles::TILE_SIZE) - offset_y,
|
||||
tiles::TILE_SIZE,
|
||||
tiles::TILE_SIZE,
|
||||
))
|
||||
let offset = self.viewbox.get_offset();
|
||||
Ok(tile.get_rect_with_offset(&offset))
|
||||
}
|
||||
|
||||
pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect {
|
||||
@ -3337,7 +3294,7 @@ impl RenderState {
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
} else if visited_children {
|
||||
self.apply_drawing_to_render_canvas(Some(element), target_surface);
|
||||
self.draw_shape_surface_stack_into(Some(element), target_surface);
|
||||
}
|
||||
|
||||
// Skip nested state updates for flattened containers
|
||||
@ -3406,6 +3363,7 @@ impl RenderState {
|
||||
}
|
||||
iteration += 1;
|
||||
}
|
||||
|
||||
Ok((is_empty, false))
|
||||
}
|
||||
|
||||
@ -3415,7 +3373,7 @@ impl RenderState {
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
allow_stop: bool,
|
||||
) -> Result<()> {
|
||||
) -> Result<FrameType> {
|
||||
let mut should_stop = false;
|
||||
let root_ids = {
|
||||
if let Some(shape_id) = base_object {
|
||||
@ -3430,53 +3388,20 @@ impl RenderState {
|
||||
|
||||
while !should_stop {
|
||||
if let Some(current_tile) = self.current_tile {
|
||||
if self.surfaces.has_cached_tile_surface(current_tile) {
|
||||
performance::begin_measure!("render_shape_tree::cached");
|
||||
// During interactive transforms, `Target` is preserved and seeded once
|
||||
// from Backbuffer. Cached tiles are therefore already visible and
|
||||
// re-blitting them costs extra GPU work.
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
if !self.options.is_interactive_transform() {
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
current_tile,
|
||||
tile_rect,
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
// Also draw the cached tile to the Cache surface so
|
||||
// render_from_cache (used during pan) has the full scene.
|
||||
// apply_render_to_final_canvas clears Cache on the first
|
||||
// uncached tile, but cached tiles must also be present.
|
||||
if !self.options.is_fast_mode() {
|
||||
if !self.cache_cleared_this_render {
|
||||
self.surfaces.clear_cache(self.background_color);
|
||||
self.cache_cleared_this_render = true;
|
||||
}
|
||||
let aligned_rect = self.get_aligned_tile_bounds(current_tile);
|
||||
self.surfaces.draw_cached_tile_to_cache(
|
||||
current_tile,
|
||||
&aligned_rect,
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::cached");
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render_workspace_current_tile(
|
||||
self,
|
||||
"Cached".to_string(),
|
||||
current_tile,
|
||||
tile_rect,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// NOTE: For now we don't need to cover the case where the tile
|
||||
// is not cached because everything will be handled from draw_atlas.
|
||||
if !self.surfaces.has_cached_tile_surface(current_tile) {
|
||||
performance::begin_measure!("render_shape_tree::uncached");
|
||||
let (is_empty, early_return) = self
|
||||
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if self.options.capture_frames > 0 {
|
||||
debug::console_debug_surface(self, SurfaceId::Backbuffer);
|
||||
}
|
||||
|
||||
if early_return {
|
||||
return Ok(());
|
||||
return Ok(FrameType::Partial);
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::uncached");
|
||||
|
||||
@ -3489,13 +3414,13 @@ impl RenderState {
|
||||
if self.options.is_interactive_transform() {
|
||||
// During drag, avoid snapshot-based caching. Draw Current directly
|
||||
// into Target (and Cache) to reduce stalls.
|
||||
self.surfaces.draw_current_tile_direct(
|
||||
self.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
self.background_color,
|
||||
surfaces::DrawOnCache::Yes,
|
||||
);
|
||||
} else {
|
||||
self.apply_render_to_final_canvas(tile_rect)?;
|
||||
self.apply_render_to_final_canvas()?;
|
||||
}
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
@ -3506,10 +3431,9 @@ impl RenderState {
|
||||
tile_rect,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Tile is uncached and has no shapes to render
|
||||
self.apply_render_to_final_canvas(tile_rect)?;
|
||||
}
|
||||
} else if self.tiles.is_empty_at(current_tile) {
|
||||
self.surfaces.remove_cached_tile_surface(current_tile);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3527,62 +3451,65 @@ impl RenderState {
|
||||
// empty tile.
|
||||
self.current_tile_had_shapes = false;
|
||||
|
||||
if !self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
|
||||
// Check if any shape on this tile has a background blur.
|
||||
// If so, we need ALL root shapes rendered (not just those
|
||||
// assigned to this tile) because the blur snapshots Current
|
||||
// which must contain the shapes behind it.
|
||||
let tile_has_bg_blur = ids.iter().any(|id| {
|
||||
tree.get(id).is_some_and(|s| {
|
||||
s.blur.is_some_and(|b| {
|
||||
!b.hidden && b.blur_type == BlurType::BackgroundBlur
|
||||
})
|
||||
})
|
||||
});
|
||||
let Some(ids) = self.tiles.get_shapes_at(next_tile) else {
|
||||
// If the tile is empty we do not need to render it.
|
||||
continue;
|
||||
};
|
||||
|
||||
// We only need first level shapes, in the same order as the parent node.
|
||||
//
|
||||
// During interactive transforms we may invalidate only the modified shapes
|
||||
// (to avoid massive ancestor eviction). However, we still composite full
|
||||
// tiles (we clear the tile rect before drawing Current), so we must render
|
||||
// all root shapes that can contribute to this tile; otherwise, unchanged
|
||||
// siblings inside the same tile would disappear.
|
||||
let mut valid_ids = Vec::with_capacity(ids.len());
|
||||
if self.options.is_interactive_transform() || tile_has_bg_blur {
|
||||
valid_ids.extend(root_ids.iter().copied());
|
||||
} else {
|
||||
for root_id in root_ids.iter() {
|
||||
if ids.contains(root_id) {
|
||||
valid_ids.push(*root_id);
|
||||
}
|
||||
}
|
||||
if self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
// If the tile is cached, then we do not need to
|
||||
// render it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if any shape on this tile has a background blur.
|
||||
// If so, we need ALL root shapes rendered (not just those
|
||||
// assigned to this tile) because the blur snapshots Current
|
||||
// which must contain the shapes behind it.
|
||||
let tile_has_bg_blur = ids.iter().any(|id| {
|
||||
tree.get(id).is_some_and(|s| {
|
||||
s.blur
|
||||
.is_some_and(|b| !b.hidden && b.blur_type == BlurType::BackgroundBlur)
|
||||
})
|
||||
});
|
||||
|
||||
// We only need first level shapes, in the same order as the parent node.
|
||||
//
|
||||
// During interactive transforms we may invalidate only the modified shapes
|
||||
// (to avoid massive ancestor eviction). However, we still composite full
|
||||
// tiles (we clear the tile rect before drawing Current), so we must render
|
||||
// all root shapes that can contribute to this tile; otherwise, unchanged
|
||||
// siblings inside the same tile would disappear.
|
||||
let mut valid_ids = Vec::with_capacity(ids.len());
|
||||
if self.options.is_interactive_transform() || tile_has_bg_blur {
|
||||
valid_ids.extend(root_ids.iter().copied());
|
||||
} else {
|
||||
for root_id in root_ids.iter() {
|
||||
if ids.contains(root_id) {
|
||||
valid_ids.push(*root_id);
|
||||
}
|
||||
|
||||
if !valid_ids.is_empty() {
|
||||
self.current_tile_had_shapes = true;
|
||||
}
|
||||
|
||||
self.pending_nodes.extend(valid_ids.into_iter().map(|id| {
|
||||
NodeRenderState {
|
||||
id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if !valid_ids.is_empty() {
|
||||
self.current_tile_had_shapes = true;
|
||||
}
|
||||
|
||||
self.pending_nodes
|
||||
.extend(valid_ids.into_iter().map(|id| NodeRenderState {
|
||||
id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
}));
|
||||
} else {
|
||||
// If there are no more pending tiles, stop.
|
||||
should_stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
self.render_in_progress = false;
|
||||
self.surfaces.gc();
|
||||
|
||||
// Mark cache as valid for render_from_cache.
|
||||
// Only update for full-quality renders (non-fast mode).
|
||||
// An async render can complete while fast mode is active
|
||||
@ -3595,7 +3522,7 @@ impl RenderState {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(FrameType::Full)
|
||||
}
|
||||
|
||||
/*
|
||||
@ -3648,7 +3575,7 @@ impl RenderState {
|
||||
shape: &Shape,
|
||||
tree: ShapesPoolRef,
|
||||
) -> HashSet<tiles::Tile> {
|
||||
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
||||
let tile_rect = self.get_tiles_for_shape(shape, tree);
|
||||
|
||||
// Collect old tiles to avoid borrow conflict with remove_shape_at
|
||||
let old_tiles: Vec<_> = self
|
||||
@ -3661,7 +3588,7 @@ impl RenderState {
|
||||
// When the shape has an active modifier (i.e. is being moved/resized),
|
||||
// clear its OLD doc-space extent from the atlas using the raw
|
||||
// (pre-modifier) shape. The per-tile clearing done later via
|
||||
// `clear_tile_in_atlas` only covers tiles tracked in `atlas_tile_doc_rects`
|
||||
// `clear_tile_in_atlas` only covers tiles tracked in `atlas.tile_doc_rects`
|
||||
// at the current zoom level. However, the atlas may also contain stale
|
||||
// pixels from previous zoom levels (tiles are larger / smaller in doc
|
||||
// space at different zoom scales) that were never re-tracked after a zoom
|
||||
@ -3673,7 +3600,9 @@ impl RenderState {
|
||||
if tree.get_modifier(&shape.id).is_some() {
|
||||
if let Some(raw_shape) = tree.get_raw(&shape.id) {
|
||||
let old_extrect = raw_shape.extrect(tree, 1.0);
|
||||
self.surfaces.clear_doc_rect_in_atlas_clipped(old_extrect);
|
||||
self.surfaces
|
||||
.atlas
|
||||
.clear_doc_rect_in_atlas_clipped(old_extrect);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3684,7 +3613,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
// Then, add the shape to the new tiles
|
||||
for tile in (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y))) {
|
||||
for tile in tile_rect.iter(true) {
|
||||
self.tiles.add_shape_at(tile, shape.id);
|
||||
result.insert(tile);
|
||||
}
|
||||
@ -3713,16 +3642,13 @@ impl RenderState {
|
||||
shape: &Shape,
|
||||
tree: ShapesPoolRef,
|
||||
) -> Vec<tiles::Tile> {
|
||||
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
||||
|
||||
let tile_rect = self.get_tiles_for_shape(shape, tree);
|
||||
let old_tiles: HashSet<tiles::Tile> = self
|
||||
.tiles
|
||||
.get_tiles_of(shape.id)
|
||||
.map_or(HashSet::new(), |tiles| tiles.iter().copied().collect());
|
||||
|
||||
let new_tiles: HashSet<tiles::Tile> = (rsx..=rex)
|
||||
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)))
|
||||
.collect();
|
||||
let new_tiles: HashSet<tiles::Tile> = tile_rect.iter(true).collect();
|
||||
|
||||
// Tiles where shape is being removed from index (left interest area)
|
||||
let removed: Vec<_> = old_tiles.difference(&new_tiles).copied().collect();
|
||||
@ -3747,18 +3673,16 @@ impl RenderState {
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the tiles forthe shape to the index.
|
||||
* Add the tiles for the shape to the index.
|
||||
* returns the tiles that have been updated
|
||||
*/
|
||||
pub fn add_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec<tiles::Tile> {
|
||||
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
||||
let tiles: Vec<_> = (rsx..=rex)
|
||||
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)))
|
||||
.collect();
|
||||
|
||||
performance::begin_measure!("add_shape_tiles");
|
||||
let tiles: Vec<tiles::Tile> = self.get_tiles_for_shape(shape, tree).iter(true).collect();
|
||||
for tile in tiles.iter() {
|
||||
self.tiles.add_shape_at(*tile, shape.id);
|
||||
}
|
||||
performance::end_measure!("add_shape_tiles");
|
||||
tiles
|
||||
}
|
||||
|
||||
@ -3771,8 +3695,9 @@ impl RenderState {
|
||||
/// survive so that fast-mode renders during pan still show shadows/blur.
|
||||
pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) {
|
||||
let zoom_changed = self.zoom_changed();
|
||||
|
||||
let mut nodes = vec![Uuid::nil()];
|
||||
performance::begin_measure!("rebuild_tile_index");
|
||||
let mut nodes = Vec::<Uuid>::with_capacity(64);
|
||||
nodes.push(Uuid::nil());
|
||||
while let Some(shape_id) = nodes.pop() {
|
||||
if let Some(shape) = tree.get(&shape_id) {
|
||||
if shape_id != Uuid::nil() {
|
||||
@ -3789,6 +3714,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
}
|
||||
performance::end_measure!("rebuild_tile_index");
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
||||
@ -3927,11 +3853,7 @@ impl RenderState {
|
||||
if let Some((_, export_scale)) = self.export_context {
|
||||
return export_scale;
|
||||
}
|
||||
self.viewbox.zoom() * self.options.dpr
|
||||
}
|
||||
|
||||
pub fn get_cached_scale(&self) -> f32 {
|
||||
self.cached_viewbox.zoom() * self.options.dpr
|
||||
self.viewbox.get_scale()
|
||||
}
|
||||
|
||||
pub fn zoom_changed(&self) -> bool {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
#![allow(dead_code)]
|
||||
use super::{tiles, RenderState, SurfaceId};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@ -22,7 +23,6 @@ fn get_debug_rect(rect: Rect) -> Rect {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn render_debug_view(render_state: &mut RenderState) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
@ -36,7 +36,6 @@ fn render_debug_view(render_state: &mut RenderState) {
|
||||
.draw_rect(rect, &paint);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn render_debug_cache_surface(render_state: &mut RenderState) {
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
canvas.save();
|
||||
@ -78,7 +77,6 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
|
||||
let tiles::TileRect(sx, sy, ex, ey) = render_state.tile_viewbox.interest_rect;
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
@ -91,7 +89,6 @@ pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
|
||||
}
|
||||
|
||||
// Renders the tiles in the viewbox
|
||||
#[allow(dead_code)]
|
||||
pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
|
||||
let scale = render_state.get_scale();
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
@ -101,30 +98,25 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
|
||||
paint.set_stroke_width(1.);
|
||||
|
||||
let tile_size = tiles::get_tile_size(scale);
|
||||
let tiles::TileRect(sx, sy, ex, ey) =
|
||||
tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size);
|
||||
let tile_rect = tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size);
|
||||
let tiles::TileRect(sx, sy, ex, ey) = tile_rect;
|
||||
|
||||
let str_rect = format!("{} {} {} {}", sx, sy, ex, ey);
|
||||
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
canvas.draw_str(str_rect, skia::Point::new(100.0, 100.0), debug_font, &paint);
|
||||
|
||||
let tile_size = tiles::get_tile_size(scale);
|
||||
for y in sy..=ey {
|
||||
for x in sx..=ex {
|
||||
let rect = Rect::from_xywh(
|
||||
x as f32 * tile_size,
|
||||
y as f32 * tile_size,
|
||||
tile_size,
|
||||
tile_size,
|
||||
);
|
||||
let debug_rect = get_debug_rect(rect);
|
||||
let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.);
|
||||
let str = format!("{}:{}", x, y);
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
canvas.draw_str(str, p, debug_font, &paint);
|
||||
canvas.draw_rect(debug_rect, &paint);
|
||||
}
|
||||
for tile in tile_rect.iter(true) {
|
||||
let tiles::Tile(x, y) = tile;
|
||||
let rect = tile.get_rect_with_size(tile_size);
|
||||
let debug_rect = get_debug_rect(rect);
|
||||
let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.);
|
||||
let str = format!("{}:{}", x, y);
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
canvas.draw_str(str, p, debug_font, &paint);
|
||||
canvas.draw_rect(debug_rect, &paint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,13 +179,11 @@ pub fn render_debug_shape(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn trap() {
|
||||
run_script!("debugger");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum SurfaceBackendKind {
|
||||
@ -203,7 +193,6 @@ pub enum SurfaceBackendKind {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn classify_surface_backend(surface: &mut skia::Surface) -> SurfaceBackendKind {
|
||||
if skia::gpu::surfaces::get_backend_texture(
|
||||
@ -231,7 +220,6 @@ pub fn classify_surface_backend(surface: &mut skia::Surface) -> SurfaceBackendKi
|
||||
SurfaceBackendKind::Unknown
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
|
||||
let base64_image = render_state
|
||||
@ -242,7 +230,6 @@ pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
|
||||
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceId) {
|
||||
let base64_image = render_state
|
||||
@ -253,7 +240,6 @@ pub fn console_debug_surface_base64(render_state: &mut RenderState, id: SurfaceI
|
||||
println!("{}", base64_image);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect: skia::Rect) {
|
||||
let int_rect = skia::IRect::from_ltrb(
|
||||
@ -273,6 +259,16 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn capture_frames(capture_frames: i32) -> Result<()> {
|
||||
get_render_state()
|
||||
.options
|
||||
.set_capture_frames(capture_frames);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@ -5,6 +5,9 @@ use skia_safe::gpu::{
|
||||
};
|
||||
use skia_safe::{self as skia, ISize};
|
||||
|
||||
const MIN_MAX_TEXTURE_SIZE: i32 = 512;
|
||||
const MAX_MAX_TEXTURE_SIZE: i32 = 8192 * 2;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GpuState {
|
||||
pub context: DirectContext,
|
||||
@ -28,6 +31,7 @@ impl GpuState {
|
||||
let context = gpu::direct_contexts::make_gl(interface, Some(&context_options)).ok_or(
|
||||
Error::CriticalError("Failed to create GL context".to_string()),
|
||||
)?;
|
||||
|
||||
let framebuffer_info = {
|
||||
let mut fboid: gl::types::GLint = 0;
|
||||
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
|
||||
@ -47,6 +51,12 @@ impl GpuState {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn max_texture_size(&self) -> i32 {
|
||||
self.context
|
||||
.max_texture_size()
|
||||
.clamp(MIN_MAX_TEXTURE_SIZE, MAX_MAX_TEXTURE_SIZE)
|
||||
}
|
||||
|
||||
fn delete_gl_texture(&mut self, texture_id: gl::types::GLuint) -> bool {
|
||||
unsafe {
|
||||
gl::DeleteTextures(1, &texture_id);
|
||||
|
||||
@ -30,6 +30,7 @@ pub struct RenderOptions {
|
||||
pub max_blocking_time_ms: i32,
|
||||
pub node_batch_threshold: i32,
|
||||
pub blur_downscale_threshold: f32,
|
||||
pub capture_frames: i32,
|
||||
}
|
||||
|
||||
impl Default for RenderOptions {
|
||||
@ -45,6 +46,7 @@ impl Default for RenderOptions {
|
||||
max_blocking_time_ms: MAX_BLOCKING_TIME_MS,
|
||||
node_batch_threshold: NODE_BATCH_THRESHOLD,
|
||||
blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD,
|
||||
capture_frames: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,6 +69,10 @@ impl RenderOptions {
|
||||
self.fast_mode = enabled;
|
||||
}
|
||||
|
||||
pub fn set_capture_frames(&mut self, capture_frames: i32) {
|
||||
self.capture_frames = capture_frames;
|
||||
}
|
||||
|
||||
/// Updates the dpr viewport interest area threshold.
|
||||
/// This function is updated when the dpr or the
|
||||
/// viewport_interest_area_threshold is changed
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,8 +3,8 @@ use crate::{
|
||||
error::Result,
|
||||
math::Rect,
|
||||
shapes::{
|
||||
calculate_position_data, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
|
||||
Stroke, StrokeKind, TextContent,
|
||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind,
|
||||
TextContent,
|
||||
},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
@ -12,7 +12,7 @@ use skia_safe::{
|
||||
self as skia,
|
||||
canvas::SaveLayerRec,
|
||||
textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle},
|
||||
Canvas, ImageFilter, Paint, Path,
|
||||
Canvas, ImageFilter, Paint,
|
||||
};
|
||||
|
||||
pub fn stroke_paragraph_builder_group_from_text(
|
||||
@ -552,78 +552,6 @@ pub fn calculate_decoration_metrics(
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn calculate_total_paragraphs_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 {
|
||||
paragraphs
|
||||
.iter_mut()
|
||||
.map(|p| {
|
||||
let mut paragraph = p.build();
|
||||
paragraph.layout(width);
|
||||
paragraph.height()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn calculate_all_paragraphs_height(
|
||||
paragraph_groups: &mut [Vec<ParagraphBuilder>],
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
paragraph_groups
|
||||
.iter_mut()
|
||||
.map(|group| {
|
||||
// For stroke groups, only count the first paragraph to avoid double-counting
|
||||
if group.len() > 1 {
|
||||
let mut paragraph = group[0].build();
|
||||
paragraph.layout(width);
|
||||
paragraph.height()
|
||||
} else {
|
||||
calculate_total_paragraphs_height(group, width)
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
// Render text paths (unused)
|
||||
#[allow(dead_code)]
|
||||
pub fn render_as_path(
|
||||
render_state: &mut RenderState,
|
||||
paths: &Vec<(Path, Paint)>,
|
||||
surface_id: Option<SurfaceId>,
|
||||
) {
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Fills));
|
||||
|
||||
for (path, paint) in paths {
|
||||
// Note: path can be empty
|
||||
canvas.draw_path(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn render_position_data(
|
||||
render_state: &mut RenderState,
|
||||
surface_id: SurfaceId,
|
||||
shape: &Shape,
|
||||
text_content: &TextContent,
|
||||
) {
|
||||
let position_data = calculate_position_data(shape, text_content, false);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
|
||||
paint.set_stroke_width(2.);
|
||||
|
||||
for pd in position_data {
|
||||
let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas_and_mark_dirty(surface_id)
|
||||
.draw_rect(rect, &paint);
|
||||
}
|
||||
}
|
||||
|
||||
// How to use it?
|
||||
// Type::Text(text_content) => {
|
||||
// self.surfaces
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Layout {
|
||||
FlexLayout(LayoutData, FlexData),
|
||||
GridLayout(LayoutData, GridData),
|
||||
|
||||
@ -501,7 +501,6 @@ pub fn propagate_modifiers(
|
||||
}
|
||||
}
|
||||
|
||||
// #[allow(dead_code)]
|
||||
Ok(modifiers
|
||||
.iter()
|
||||
.map(|(key, val)| TransformEntry::from_input(*key, *val))
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt};
|
||||
use crate::shapes::{
|
||||
@ -123,7 +121,7 @@ struct ChildAxis {
|
||||
max_across_size: f32,
|
||||
is_fill_main: bool,
|
||||
is_fill_across: bool,
|
||||
z_index: i32,
|
||||
_z_index: i32, // unused
|
||||
bounds: Bounds,
|
||||
}
|
||||
|
||||
@ -146,7 +144,7 @@ impl ChildAxis {
|
||||
max_across_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
|
||||
is_fill_main: child.is_layout_horizontal_fill(),
|
||||
is_fill_across: child.is_layout_vertical_fill(),
|
||||
z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
|
||||
_z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
|
||||
bounds: *child_bounds,
|
||||
}
|
||||
} else {
|
||||
@ -164,7 +162,7 @@ impl ChildAxis {
|
||||
max_main_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
|
||||
is_fill_main: child.is_layout_vertical_fill(),
|
||||
is_fill_across: child.is_layout_horizontal_fill(),
|
||||
z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
|
||||
_z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
|
||||
bounds: *child_bounds,
|
||||
}
|
||||
};
|
||||
|
||||
@ -115,7 +115,6 @@ impl TextContentSize {
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TextPositionWithAffinity {
|
||||
#[allow(dead_code)]
|
||||
pub position_with_affinity: PositionWithAffinity,
|
||||
pub paragraph: usize,
|
||||
pub offset: usize,
|
||||
@ -244,9 +243,9 @@ impl TextContentLayout {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextDecorationSegment {
|
||||
#[allow(dead_code)]
|
||||
pub kind: skia::textlayout::TextDecoration,
|
||||
pub text_style: skia::textlayout::TextStyle,
|
||||
pub y: f32,
|
||||
@ -392,16 +391,6 @@ impl TextContent {
|
||||
self.bounds = Rect::from_xywh(x, y, w, h);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn x(&self) -> f32 {
|
||||
self.bounds.x()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn y(&self) -> f32 {
|
||||
self.bounds.y()
|
||||
}
|
||||
|
||||
pub fn add_paragraph(&mut self, paragraph: Paragraph) {
|
||||
self.paragraphs.push(paragraph);
|
||||
self.content_version = self.content_version.wrapping_add(1);
|
||||
@ -621,10 +610,6 @@ impl TextContent {
|
||||
let mut offset_y = 0.0;
|
||||
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
||||
|
||||
// IMPORTANT! I'm keeping this because I think it should be better to have the span index
|
||||
// cached the same way we keep the paragraph index.
|
||||
#[allow(dead_code)]
|
||||
let mut _span_index: usize = 0;
|
||||
for (paragraph_index, layout_paragraph) in layout_paragraphs.enumerate() {
|
||||
let start_y = offset_y;
|
||||
let end_y = offset_y + layout_paragraph.height();
|
||||
@ -652,15 +637,8 @@ impl TextContent {
|
||||
// in which span we are.
|
||||
let mut computed_position: usize = 0;
|
||||
|
||||
// This could be useful in the future as part of the TextPositionWithAffinity.
|
||||
#[allow(dead_code)]
|
||||
let mut _span_offset: usize = 0;
|
||||
|
||||
// If paragraph has no spans, default to span 0, offset 0
|
||||
if paragraph.children().is_empty() {
|
||||
_span_index = 0;
|
||||
_span_offset = 0;
|
||||
} else {
|
||||
if !paragraph.children().is_empty() {
|
||||
for span in paragraph.children() {
|
||||
let length = span.text.chars().count();
|
||||
let start_position = computed_position;
|
||||
@ -670,19 +648,15 @@ impl TextContent {
|
||||
// Handle empty spans: if the span is empty and current position
|
||||
// matches the start, this is the right span
|
||||
if length == 0 && current_position == start_position {
|
||||
_span_offset = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if start_position <= current_position
|
||||
&& end_position >= current_position
|
||||
{
|
||||
_span_offset =
|
||||
position_with_affinity.position as usize - start_position;
|
||||
break;
|
||||
}
|
||||
computed_position += length;
|
||||
_span_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1152,11 +1126,6 @@ impl Paragraph {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn set_children(&mut self, children: Vec<TextSpan>) {
|
||||
self.children = children;
|
||||
}
|
||||
|
||||
pub fn children(&self) -> &[TextSpan] {
|
||||
&self.children
|
||||
}
|
||||
@ -1165,11 +1134,6 @@ impl Paragraph {
|
||||
&mut self.children
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn add_span(&mut self, span: TextSpan) {
|
||||
self.children.push(span);
|
||||
}
|
||||
|
||||
pub fn line_height(&self) -> f32 {
|
||||
self.line_height
|
||||
}
|
||||
@ -1306,11 +1270,6 @@ impl TextSpan {
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn fills(&self) -> &[shapes::Fill] {
|
||||
&self.fills
|
||||
}
|
||||
|
||||
pub fn to_style(
|
||||
&self,
|
||||
content_bounds: &Rect,
|
||||
@ -1415,7 +1374,6 @@ impl TextSpan {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct PositionData {
|
||||
pub paragraph: u32,
|
||||
@ -1429,21 +1387,17 @@ pub struct PositionData {
|
||||
pub direction: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct ParagraphLayout {
|
||||
pub paragraph: skia::textlayout::Paragraph,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub spans: Vec<crate::shapes::TextSpan>,
|
||||
pub decorations: Vec<TextDecorationSegment>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct TextLayoutData {
|
||||
pub position_data: Vec<PositionData>,
|
||||
pub content_rect: Rect,
|
||||
pub paragraphs: Vec<ParagraphLayout>,
|
||||
}
|
||||
|
||||
@ -1508,12 +1462,6 @@ pub fn calculate_text_layout_data(
|
||||
for (i, group_paragraphs) in built_groups.into_iter().enumerate() {
|
||||
// For each paragraph in the group (e.g., fill, stroke, etc.)
|
||||
for skia_paragraph in group_paragraphs.into_iter() {
|
||||
let spans = if let Some(text_para) = text_paragraphs.get(i) {
|
||||
text_para.children().to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Calculate text decorations for this paragraph
|
||||
let mut decorations = Vec::new();
|
||||
let line_metrics = skia_paragraph.get_line_metrics();
|
||||
@ -1580,7 +1528,6 @@ pub fn calculate_text_layout_data(
|
||||
paragraph: skia_paragraph,
|
||||
x,
|
||||
y: y_accum,
|
||||
spans: spans.clone(),
|
||||
decorations,
|
||||
});
|
||||
}
|
||||
@ -1644,10 +1591,8 @@ pub fn calculate_text_layout_data(
|
||||
}
|
||||
}
|
||||
|
||||
let content_rect = Rect::from_xywh(x, base_y + vertical_offset, text_width, total_text_height);
|
||||
TextLayoutData {
|
||||
position_data,
|
||||
content_rect,
|
||||
paragraphs: paragraph_layouts,
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
||||
pub use text_editor::*;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::FrameType;
|
||||
use crate::shapes::{grid_layout::grid_cell_data, Shape};
|
||||
use crate::uuid::Uuid;
|
||||
use crate::{get_render_state, tiles};
|
||||
@ -64,11 +65,11 @@ impl State {
|
||||
get_render_state().render_from_cache(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn render_sync(&mut self, timestamp: i32) -> Result<()> {
|
||||
pub fn render_sync(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
get_render_state().start_render_loop(None, &self.shapes, timestamp, true)
|
||||
}
|
||||
|
||||
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> {
|
||||
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<FrameType> {
|
||||
get_render_state().start_render_loop(Some(id), &self.shapes, timestamp, true)
|
||||
}
|
||||
|
||||
@ -81,7 +82,7 @@ impl State {
|
||||
get_render_state().render_shape_pixels(id, &self.shapes, scale, timestamp)
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
let render_state = get_render_state();
|
||||
// If zoom changed (e.g. interrupted zoom render followed by pan), the
|
||||
// tile index may be stale for the new viewport position. Rebuild the
|
||||
@ -92,12 +93,11 @@ impl State {
|
||||
if render_state.zoom_changed() {
|
||||
render_state.rebuild_tile_index(&self.shapes);
|
||||
}
|
||||
|
||||
render_state.start_render_loop(None, &self.shapes, timestamp, false)
|
||||
}
|
||||
|
||||
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
|
||||
get_render_state().process_animation_frame(None, &self.shapes, timestamp)
|
||||
pub fn continue_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
get_render_state().continue_render_loop(None, &self.shapes, timestamp)
|
||||
}
|
||||
|
||||
pub fn clear_focus_mode(&mut self) {
|
||||
|
||||
@ -426,8 +426,6 @@ impl TextEditorState {
|
||||
let Some(last_paragraph) = text_content.paragraphs().last() else {
|
||||
return false;
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
let _num_spans = last_paragraph.children().len() - 1;
|
||||
let Some(_last_text_span) = last_paragraph.children().last() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@ -10,80 +10,169 @@ impl Tile {
|
||||
pub fn from(x: i32, y: i32) -> Self {
|
||||
Tile(x, y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn x(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn y(&self) -> i32 {
|
||||
self.1
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_rect_with_size(&self, tile_size: f32) -> skia::Rect {
|
||||
skia::Rect::from_xywh(
|
||||
self.0 as f32 * tile_size,
|
||||
self.1 as f32 * tile_size,
|
||||
tile_size,
|
||||
tile_size,
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_rect_with_offset(&self, offset: &skia::Point) -> skia::Rect {
|
||||
skia::Rect::from_xywh(
|
||||
self.0 as f32 * TILE_SIZE - offset.x,
|
||||
self.1 as f32 * TILE_SIZE - offset.y,
|
||||
TILE_SIZE,
|
||||
TILE_SIZE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||
pub struct TileRect(pub i32, pub i32, pub i32, pub i32);
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TileRect {
|
||||
pub fn empty() -> Self {
|
||||
Self(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn is_degenerate(&self) -> bool {
|
||||
self.left() > self.right() || self.top() > self.bottom()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> i32 {
|
||||
(self.width() + 1) * (self.height() + 1)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn x1(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn y1(&self) -> i32 {
|
||||
self.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn x2(&self) -> i32 {
|
||||
self.2
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn y2(&self) -> i32 {
|
||||
self.3
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn left(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn top(&self) -> i32 {
|
||||
self.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn right(&self) -> i32 {
|
||||
self.2
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn bottom(&self) -> i32 {
|
||||
self.3
|
||||
}
|
||||
|
||||
/// Inclusive tile count on X (matches `contains`: both `x1` and `x2` are included).
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn columns(&self) -> i32 {
|
||||
self.x2() - self.x1() + 1
|
||||
}
|
||||
|
||||
/// Inclusive tile count on Y (matches `contains`: both `y1` and `y2` are included).
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn rows(&self) -> i32 {
|
||||
self.y2() - self.y1() + 1
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn width(&self) -> i32 {
|
||||
self.x2() - self.x1()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn height(&self) -> i32 {
|
||||
self.y2() - self.y1()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn contains(&self, tile: &Tile) -> bool {
|
||||
tile.x() >= self.left()
|
||||
&& tile.y() >= self.top()
|
||||
&& tile.x() <= self.right()
|
||||
&& tile.y() <= self.bottom()
|
||||
}
|
||||
|
||||
pub fn iter(self, inclusive: bool) -> TileRectIter {
|
||||
TileRectIter::new(self, inclusive)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct TileRectIter {
|
||||
rect: TileRect,
|
||||
inclusive: bool,
|
||||
index: i32,
|
||||
total: i32,
|
||||
}
|
||||
|
||||
impl TileRectIter {
|
||||
fn new(rect: TileRect, inclusive: bool) -> Self {
|
||||
let width = rect.width() + if inclusive { 1 } else { 0 };
|
||||
let height = rect.height() + if inclusive { 1 } else { 0 };
|
||||
Self {
|
||||
rect,
|
||||
inclusive,
|
||||
index: 0,
|
||||
total: width * height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TileRectIter {
|
||||
type Item = Tile;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index >= self.total {
|
||||
return None;
|
||||
}
|
||||
|
||||
let width = self.rect.width() + if self.inclusive { 1 } else { 0 };
|
||||
|
||||
let x = self.rect.left() + self.index % width;
|
||||
let y = self.rect.top() + self.index / width;
|
||||
|
||||
self.index += 1;
|
||||
|
||||
Some(Tile::from(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -95,19 +184,19 @@ pub struct TileViewbox {
|
||||
}
|
||||
|
||||
impl TileViewbox {
|
||||
pub fn new_with_interest(viewbox: Viewbox, interest: i32, scale: f32) -> Self {
|
||||
pub fn new_with_interest(viewbox: &Viewbox, interest: i32) -> Self {
|
||||
Self {
|
||||
visible_rect: get_tiles_for_viewbox(viewbox, scale),
|
||||
interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest, scale),
|
||||
visible_rect: get_tiles_for_viewbox(viewbox),
|
||||
interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest),
|
||||
interest,
|
||||
center: get_tile_center_for_viewbox(viewbox, scale),
|
||||
center: get_tile_center_for_viewbox(viewbox),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, viewbox: Viewbox, scale: f32) {
|
||||
self.visible_rect = get_tiles_for_viewbox(viewbox, scale);
|
||||
self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest, scale);
|
||||
self.center = get_tile_center_for_viewbox(viewbox, scale);
|
||||
pub fn update(&mut self, viewbox: &Viewbox) {
|
||||
self.visible_rect = get_tiles_for_viewbox(viewbox);
|
||||
self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest);
|
||||
self.center = get_tile_center_for_viewbox(viewbox);
|
||||
}
|
||||
|
||||
pub fn set_interest(&mut self, interest: i32) {
|
||||
@ -122,6 +211,7 @@ impl TileViewbox {
|
||||
|
||||
pub const TILE_SIZE: f32 = 512.;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_tile_dimensions() -> skia::ISize {
|
||||
(TILE_SIZE as i32, TILE_SIZE as i32).into()
|
||||
}
|
||||
@ -136,22 +226,18 @@ pub fn get_tiles_for_rect(rect: skia::Rect, tile_size: f32) -> TileRect {
|
||||
TileRect(sx, sy, ex, ey)
|
||||
}
|
||||
|
||||
pub fn get_tiles_for_viewbox(viewbox: Viewbox, scale: f32) -> TileRect {
|
||||
let tile_size = get_tile_size(scale);
|
||||
pub fn get_tiles_for_viewbox(viewbox: &Viewbox) -> TileRect {
|
||||
let tile_size = get_tile_size(viewbox.get_scale());
|
||||
get_tiles_for_rect(viewbox.area, tile_size)
|
||||
}
|
||||
|
||||
pub fn get_tiles_for_viewbox_with_interest(
|
||||
viewbox: Viewbox,
|
||||
interest: i32,
|
||||
scale: f32,
|
||||
) -> TileRect {
|
||||
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale);
|
||||
pub fn get_tiles_for_viewbox_with_interest(viewbox: &Viewbox, interest: i32) -> TileRect {
|
||||
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox);
|
||||
TileRect(sx - interest, sy - interest, ex + interest, ey + interest)
|
||||
}
|
||||
|
||||
pub fn get_tile_center_for_viewbox(viewbox: Viewbox, scale: f32) -> Tile {
|
||||
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox, scale);
|
||||
pub fn get_tile_center_for_viewbox(viewbox: &Viewbox) -> Tile {
|
||||
let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox);
|
||||
Tile((ex - sx) / 2, (ey - sy) / 2)
|
||||
}
|
||||
|
||||
@ -172,7 +258,7 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect {
|
||||
skia::Rect::from_xywh(tx, ty, ts, ts)
|
||||
}
|
||||
|
||||
// This structure is usseful to keep all the shape uuids by shape id.
|
||||
// This structure is useful to keep all the shape uuids by shape id.
|
||||
pub struct TileHashMap {
|
||||
grid: HashMap<Tile, HashSet<Uuid>>,
|
||||
index: HashMap<Uuid, HashSet<Tile>>,
|
||||
@ -186,6 +272,13 @@ impl TileHashMap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty_at(&self, tile: Tile) -> bool {
|
||||
if let Some(uuids) = self.grid.get(&tile) {
|
||||
return uuids.is_empty();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&HashSet<Uuid>> {
|
||||
self.grid.get(&tile)
|
||||
}
|
||||
@ -219,7 +312,7 @@ impl TileHashMap {
|
||||
}
|
||||
|
||||
const VIEWPORT_DEFAULT_CAPACITY: usize = 24 * 12;
|
||||
const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = 64;
|
||||
const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = VIEWPORT_DEFAULT_CAPACITY;
|
||||
|
||||
/// Cached spiral of tile offsets for a given grid size.
|
||||
///
|
||||
@ -315,6 +408,10 @@ pub struct PendingTiles {
|
||||
pub list: Vec<Tile>,
|
||||
pub spiral: TileSpiral,
|
||||
pub spiral_rect: TileRect,
|
||||
pub visible_cached: Vec<Tile>,
|
||||
pub visible_uncached: Vec<Tile>,
|
||||
pub interest_cached: Vec<Tile>,
|
||||
pub interest_uncached: Vec<Tile>,
|
||||
}
|
||||
|
||||
impl PendingTiles {
|
||||
@ -323,6 +420,10 @@ impl PendingTiles {
|
||||
list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
spiral: TileSpiral::new(),
|
||||
spiral_rect: TileRect::empty(),
|
||||
visible_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
visible_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
interest_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
interest_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,10 +457,10 @@ impl PendingTiles {
|
||||
// 2. visible + uncached (user sees these, render next)
|
||||
// 3. interest + cached (pre-rendered area, blit from cache)
|
||||
// 4. interest + uncached (lowest priority - background pre-render)
|
||||
let mut visible_cached = Vec::new();
|
||||
let mut visible_uncached = Vec::new();
|
||||
let mut interest_cached = Vec::new();
|
||||
let mut interest_uncached = Vec::new();
|
||||
self.visible_cached.clear();
|
||||
self.visible_uncached.clear();
|
||||
self.interest_cached.clear();
|
||||
self.interest_uncached.clear();
|
||||
|
||||
// Compute the scheduling center explicitly (inclusive range).
|
||||
// This avoids relying on `TileRect::center_x/center_y` semantics, which may be used
|
||||
@ -374,19 +475,19 @@ impl PendingTiles {
|
||||
let is_cached = surfaces.has_cached_tile_surface(tile);
|
||||
|
||||
match (is_visible, is_cached) {
|
||||
(true, true) => visible_cached.push(tile),
|
||||
(true, false) => visible_uncached.push(tile),
|
||||
(false, true) => interest_cached.push(tile),
|
||||
(false, false) => interest_uncached.push(tile),
|
||||
(true, true) => self.visible_cached.push(tile),
|
||||
(true, false) => self.visible_uncached.push(tile),
|
||||
(false, true) => self.interest_cached.push(tile),
|
||||
(false, false) => self.interest_uncached.push(tile),
|
||||
}
|
||||
}
|
||||
|
||||
// Build final list with lowest priority first (they get popped last)
|
||||
// Order: interest_uncached, interest_cached, visible_uncached, visible_cached
|
||||
self.list.extend(interest_uncached);
|
||||
self.list.extend(interest_cached);
|
||||
self.list.extend(visible_uncached);
|
||||
self.list.extend(visible_cached);
|
||||
self.list.extend(self.interest_uncached.iter());
|
||||
self.list.extend(self.interest_cached.iter());
|
||||
self.list.extend(self.visible_uncached.iter());
|
||||
self.list.extend(self.visible_cached.iter());
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<Tile> {
|
||||
|
||||
@ -1,62 +1,86 @@
|
||||
use skia_safe::Rect;
|
||||
|
||||
use crate::math::{Matrix, Point};
|
||||
use crate::math::{Matrix, Point, Rect, Size};
|
||||
use std::ops::Mul;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct Viewbox {
|
||||
pub pan_x: f32,
|
||||
pub pan_y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub pan: Point,
|
||||
pub size: Size,
|
||||
pub zoom: f32,
|
||||
pub dpr: f32,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl Default for Viewbox {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pan_x: 0.,
|
||||
pan_y: 0.,
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
pan: Point::new(0.0, 0.0),
|
||||
size: Size::new(0.0, 0.0),
|
||||
zoom: 1.0,
|
||||
dpr: 1.0,
|
||||
area: Rect::new_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Viewbox {
|
||||
pub fn new(width: f32, height: f32) -> Self {
|
||||
let area = Rect::from_xywh(0., 0., width, height);
|
||||
let size = Size::new(width, height);
|
||||
let area = Rect::from_size(size);
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
size,
|
||||
area,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dpr_width(&self) -> f32 {
|
||||
self.size.width * self.dpr
|
||||
}
|
||||
|
||||
pub fn dpr_height(&self) -> f32 {
|
||||
self.size.height * self.dpr
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f32 {
|
||||
self.size.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> f32 {
|
||||
self.size.height
|
||||
}
|
||||
|
||||
pub fn set_all(&mut self, zoom: f32, pan_x: f32, pan_y: f32) {
|
||||
self.pan_x = pan_x;
|
||||
self.pan_y = pan_y;
|
||||
self.pan.set(pan_x, pan_y);
|
||||
self.zoom = zoom;
|
||||
self.area.set_xywh(
|
||||
-self.pan_x,
|
||||
-self.pan_y,
|
||||
self.width / self.zoom,
|
||||
self.height / self.zoom,
|
||||
-self.pan.x,
|
||||
-self.pan.y,
|
||||
self.size.width / self.zoom,
|
||||
self.size.height / self.zoom,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_wh(&mut self, width: f32, height: f32) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.size.set(width, height);
|
||||
self.area
|
||||
.set_wh(self.width / self.zoom, self.height / self.zoom);
|
||||
.set_wh(self.size.width / self.zoom, self.size.height / self.zoom);
|
||||
}
|
||||
|
||||
pub fn set_dpr(&mut self, dpr: f32) {
|
||||
self.dpr = dpr;
|
||||
}
|
||||
|
||||
pub fn get_scale(&self) -> f32 {
|
||||
self.zoom * self.dpr
|
||||
}
|
||||
|
||||
pub fn get_offset(&self) -> Point {
|
||||
self.area.tl().mul(self.get_scale())
|
||||
}
|
||||
|
||||
pub fn pan(&self) -> Point {
|
||||
Point::new(self.pan_x, self.pan_y)
|
||||
self.pan
|
||||
}
|
||||
|
||||
pub fn zoom(&self) -> f32 {
|
||||
|
||||
@ -1,40 +1,3 @@
|
||||
#[macro_export]
|
||||
macro_rules! request_animation_frame {
|
||||
() => {{
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe extern "C" {
|
||||
pub fn wapi_requestAnimationFrame() -> i32;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let result = unsafe { wapi_requestAnimationFrame() };
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let result = 0;
|
||||
|
||||
result
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! cancel_animation_frame {
|
||||
($frame_id:expr) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe extern "C" {
|
||||
pub fn wapi_cancelAnimationFrame(frame_id: i32);
|
||||
}
|
||||
|
||||
{
|
||||
let frame_id = $frame_id;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe {
|
||||
wapi_cancelAnimationFrame(frame_id)
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = frame_id;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! notify_tiles_render_complete {
|
||||
() => {{
|
||||
@ -50,6 +13,4 @@ macro_rules! notify_tiles_render_complete {
|
||||
}};
|
||||
}
|
||||
|
||||
pub use cancel_animation_frame;
|
||||
pub use notify_tiles_render_complete;
|
||||
pub use request_animation_frame;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user