mirror of
https://github.com/penpot/penpot.git
synced 2026-07-02 12:25:42 +00:00
WIP
This commit is contained in:
parent
5f1bef6bd9
commit
13533e1ff7
@ -197,11 +197,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :panning] true)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(dwvw/maybe-view-interaction-start! state))))
|
||||
(assoc-in [:workspace-local :panning] true)))))
|
||||
|
||||
(defn start-panning []
|
||||
(ptk/reify ::start-panning
|
||||
@ -229,8 +225,4 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :panning)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(dwvw/maybe-view-interaction-end! state))))
|
||||
(update :workspace-local dissoc :panning)))))
|
||||
|
||||
@ -19,12 +19,12 @@
|
||||
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
|
||||
(wasm.api/sync-workspace-local-viewport! state)))
|
||||
|
||||
(defn maybe-view-interaction-start!
|
||||
#_(defn maybe-view-interaction-start!
|
||||
[state]
|
||||
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
|
||||
(wasm.api/view-interaction-start!)))
|
||||
|
||||
(defn maybe-view-interaction-end!
|
||||
#_(defn maybe-view-interaction-end!
|
||||
[state]
|
||||
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
|
||||
(wasm.api/view-interaction-end!)))
|
||||
(wasm.api/view-interaction-end!)))
|
||||
|
||||
@ -199,7 +199,6 @@
|
||||
(when (and (not (dwvw/render-context-lost? state))
|
||||
(not (get-in state [:workspace-local :zooming])))
|
||||
(rx/concat
|
||||
(rx/of (fn [s] (dwvw/maybe-view-interaction-start! s) s))
|
||||
(rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
|
||||
(->> stream
|
||||
(rx/filter mse/pointer-event?)
|
||||
@ -215,7 +214,4 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :zooming)))
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(dwvw/maybe-view-interaction-end! state))))
|
||||
(update :workspace-local dissoc :zooming)))))
|
||||
|
||||
@ -362,7 +362,7 @@
|
||||
([]
|
||||
(internal-render 0))
|
||||
([timestamp]
|
||||
(set! wasm/internal-frame-type (h/call wasm/internal-module "_render" timestamp wasm/internal-frame-type))
|
||||
(set! wasm/internal-frame-type (h/call wasm/internal-module "_render2" timestamp wasm/internal-frame-type))
|
||||
(when (= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
(request-render "frame-type-partial"))))
|
||||
|
||||
@ -1297,13 +1297,13 @@
|
||||
(= result 1))
|
||||
false))
|
||||
|
||||
(defn view-interaction-start!
|
||||
#_(defn view-interaction-start!
|
||||
[]
|
||||
(when-not @view-interaction-active?
|
||||
(h/call wasm/internal-module "_set_view_start")
|
||||
(reset! view-interaction-active? true)))
|
||||
|
||||
(defn view-interaction-end!
|
||||
#_(defn view-interaction-end!
|
||||
[]
|
||||
(when @view-interaction-active?
|
||||
(perf/begin-measure "render-finish")
|
||||
@ -1311,33 +1311,35 @@
|
||||
(perf/end-measure "render-finish")
|
||||
(reset! view-interaction-active? false)))
|
||||
|
||||
(def render-finish
|
||||
(letfn [(do-render []
|
||||
;; Check if context is still initialized before executing
|
||||
;; to prevent errors when navigating quickly
|
||||
(when (initialized?)
|
||||
(view-interaction-end!)
|
||||
;; Use async _render: visible tiles render synchronously
|
||||
;; (no yield), interest-area tiles render progressively
|
||||
;; via rAF. _set_view_end already rebuilt the tile
|
||||
;; index. For pan, most tiles are cached so the render
|
||||
;; completes in the first frame. For zoom, interest-
|
||||
;; area tiles (~3 tile margin) don't block the main
|
||||
;; thread.
|
||||
(internal-render)))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
#_(def render-finish
|
||||
(letfn [(do-render []
|
||||
;; Check if context is still initialized before executing
|
||||
;; to prevent errors when navigating quickly
|
||||
(when (initialized?)
|
||||
#_(view-interaction-end!)
|
||||
;; Use async _render: visible tiles render synchronously
|
||||
;; (no yield), interest-area tiles render progressively
|
||||
;; via rAF. _set_view_end already rebuilt the tile
|
||||
;; index. For pan, most tiles are cached so the render
|
||||
;; completes in the first frame. For zoom, interest-
|
||||
;; area tiles (~3 tile margin) don't block the main
|
||||
;; thread.
|
||||
(internal-render)))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
|
||||
(defn set-view-box
|
||||
[zoom vbox]
|
||||
(perf/begin-measure "set-view-box")
|
||||
(view-interaction-start!)
|
||||
#_(view-interaction-start!)
|
||||
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
||||
(perf/end-measure "set-view-box")
|
||||
|
||||
(perf/begin-measure "render-from-cache")
|
||||
(h/call wasm/internal-module "_render_from_cache" 0)
|
||||
(render-finish)
|
||||
(perf/end-measure "render-from-cache"))
|
||||
#_(perf/begin-measure "render-from-cache")
|
||||
#_(h/call wasm/internal-module "_render_from_cache" 0)
|
||||
#_(render-finish)
|
||||
#_(view-interaction-end!)
|
||||
(internal-render)
|
||||
#_(perf/end-measure "render-from-cache"))
|
||||
|
||||
(defn sync-workspace-local-viewport!
|
||||
"Pushes `[:workspace-local :zoom]` and `:vbox` into WASM."
|
||||
|
||||
@ -103,6 +103,40 @@ pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render2(timestamp: i32, flags: u8) -> Result<FrameType> {
|
||||
with_state!(state, {
|
||||
let render_state = get_render_state();
|
||||
render_state.rebuild_touched_tiles(&state.shapes);
|
||||
|
||||
// Drain the throttled modifier-tile invalidation accumulated
|
||||
// since the previous rAF. set_modifiers skips this work during
|
||||
// interactive_transform; we do it once here, with the current
|
||||
// modifier set, so the cost is paid once per rAF rather than
|
||||
// once per pointer move.
|
||||
if render_state.options.is_interactive_transform() {
|
||||
// Collect into an owned Vec to release the immutable borrow on
|
||||
// `state.shapes` before the mutable `rebuild_modifier_tiles` call.
|
||||
let ids = state.shapes.modifier_ids().to_vec();
|
||||
if !ids.is_empty() {
|
||||
render_state.rebuild_modifier_tiles(&mut state.shapes, &ids)?;
|
||||
}
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render(timestamp: i32, flags: u8) -> Result<FrameType> {
|
||||
@ -215,21 +249,21 @@ pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
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.
|
||||
// `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
|
||||
// render at the new viewport position.
|
||||
state.render_from_cache();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
// #[no_mangle]
|
||||
// #[wasm_error]
|
||||
// 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.
|
||||
// // `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
|
||||
// // render at the new viewport position.
|
||||
// state.render_from_cache();
|
||||
// });
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
@ -311,13 +345,13 @@ static mut VIEW_INTERACTION_START: i32 = 0;
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_start() -> Result<()> {
|
||||
#[cfg(feature = "profile-macros")]
|
||||
unsafe {
|
||||
VIEW_INTERACTION_START = performance::get_time();
|
||||
}
|
||||
performance::begin_measure!("set_view_start");
|
||||
get_render_state().options.set_fast_mode(true);
|
||||
performance::end_measure!("set_view_start");
|
||||
// #[cfg(feature = "profile-macros")]
|
||||
// unsafe {
|
||||
// VIEW_INTERACTION_START = performance::get_time();
|
||||
// }
|
||||
// performance::begin_measure!("set_view_start");
|
||||
// get_render_state().options.set_fast_mode(true);
|
||||
// performance::end_measure!("set_view_start");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -329,32 +363,19 @@ pub extern "C" fn set_view_start() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_end() -> Result<()> {
|
||||
with_state!(state, {
|
||||
performance::begin_measure!("set_view_end");
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(false);
|
||||
render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
// with_state!(state, {
|
||||
// performance::begin_measure!("set_view_end");
|
||||
// let render_state = get_render_state();
|
||||
// render_state.options.set_fast_mode(false);
|
||||
// render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
|
||||
if render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else if render_state.zoom_changed() {
|
||||
// Zoom changed: tile sizes differ so all cached tile
|
||||
// textures are invalid (wrong scale). Rebuild the tile
|
||||
// index and clear the tile texture cache, but *preserve*
|
||||
// the cache canvas so render_from_cache can show a scaled
|
||||
// preview of the old content while new tiles render.
|
||||
render_state.rebuild_tile_index(&state.shapes);
|
||||
render_state.surfaces.invalidate_tile_cache();
|
||||
} else {
|
||||
// Pure pan at the same zoom level: tile contents have not
|
||||
// changed — only the viewport position moved. Update the
|
||||
// tile index (which tiles are in the interest area) but
|
||||
// keep cached tile textures so the render can blit them
|
||||
// instead of re-drawing every visible tile from scratch.
|
||||
render_state.rebuild_tile_index(&state.shapes);
|
||||
}
|
||||
performance::end_measure!("set_view_end");
|
||||
});
|
||||
// render_state.rebuild_tile_index(&state.shapes);
|
||||
// if render_state.zoom_changed() {
|
||||
// render_state.surfaces.invalidate_tile_cache();
|
||||
// }
|
||||
|
||||
// performance::end_measure!("set_view_end");
|
||||
// });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -368,7 +389,7 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
performance::begin_measure!("set_modifiers_start");
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(true);
|
||||
// render_state.options.set_fast_mode(true);
|
||||
render_state.options.set_interactive_transform(true);
|
||||
performance::end_measure!("set_modifiers_start");
|
||||
Ok(())
|
||||
@ -383,7 +404,7 @@ pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
performance::begin_measure!("set_modifiers_end");
|
||||
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);
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
Ok(())
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
mod background_blur;
|
||||
pub mod cache;
|
||||
mod debug;
|
||||
pub mod drag_crop;
|
||||
pub mod drop_shadow;
|
||||
pub mod enter_exit;
|
||||
pub mod export;
|
||||
@ -29,12 +27,11 @@ pub mod walk;
|
||||
|
||||
use skia_safe::{self as skia, Matrix, RRect, Rect};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashSet};
|
||||
|
||||
use options::RenderOptions;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
pub use drag_crop::InteractiveDragCrop;
|
||||
pub use focus_mode::FocusMode;
|
||||
pub(crate) use walk::{get_simplified_children, sort_z_index};
|
||||
pub use walk::{ClipStack, NodeRenderState, RenderStats};
|
||||
@ -115,9 +112,6 @@ pub(crate) struct RenderState {
|
||||
/// Preview render mode - when true, uses simplified rendering for progressive loading
|
||||
pub preview_mode: bool,
|
||||
pub export_context: Option<(Rect, f32)>,
|
||||
/// 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 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
|
||||
@ -125,46 +119,14 @@ pub(crate) struct RenderState {
|
||||
/// (must composite to present the work). Reset when current_tile
|
||||
/// changes.
|
||||
pub current_tile_had_shapes: bool,
|
||||
/// During interactive transforms we keep `Target` between rAFs. Seed the
|
||||
/// interactive backdrop exactly once per gesture (first rAF) so we don't
|
||||
/// repeatedly overwrite tiles that have already been updated.
|
||||
pub interactive_target_seeded: bool,
|
||||
/// When true, the next `start_render_loop` keeps the last presented `Target`
|
||||
/// pixels instead of clearing the canvas. Set after incremental shape updates
|
||||
/// (e.g. adding a rect) so the workspace stays visible while only affected
|
||||
/// tiles are re-rendered asynchronously.
|
||||
pub preserve_target_during_render: bool,
|
||||
/// GPU crops from `Backbuffer` or tile atlas keyed by shape id. Filled on full-frame completion; during
|
||||
/// drag, entries for the moved top-level selection are ensured here
|
||||
pub backbuffer_crop_cache: HashMap<Uuid, InteractiveDragCrop>,
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
/// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an
|
||||
/// interactive transform (drag/resize/rotate).
|
||||
///
|
||||
/// We only reuse cached pixels when it is safe and visually correct:
|
||||
/// - **Top-level only**: cache entries are built for direct children of the root.
|
||||
/// - **Moved node**: only allow cache reuse for *pure translations* (no scale/rotate/skew),
|
||||
/// because other transforms would require resampling and can diverge from the live render.
|
||||
/// - **Other cached nodes**: if the moving bounds overlap this cached crop, invalidate it so
|
||||
/// we don't show stale content while something moves over/inside it.
|
||||
fn should_use_cached_top_level_during_interactive(
|
||||
&mut self,
|
||||
node_id: Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
moved_ids: &[Uuid],
|
||||
moved_bounds: Option<Rect>,
|
||||
) -> bool {
|
||||
drag_crop::should_use_cached_top_level_during_interactive(
|
||||
self,
|
||||
node_id,
|
||||
tree,
|
||||
moved_ids,
|
||||
moved_bounds,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
|
||||
// This needs to be done once per WebGL context.
|
||||
let sampling_options =
|
||||
@ -219,11 +181,9 @@ impl RenderState {
|
||||
ignore_nested_blurs: false,
|
||||
preview_mode: false,
|
||||
export_context: None,
|
||||
cache_cleared_this_render: false,
|
||||
current_tile_had_shapes: false,
|
||||
interactive_target_seeded: false,
|
||||
preserve_target_during_render: false,
|
||||
backbuffer_crop_cache: HashMap::default(),
|
||||
// backbuffer_crop_cache: HashMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -360,10 +320,6 @@ impl RenderState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) {
|
||||
self.surfaces.flush(SurfaceId::Backbuffer);
|
||||
}
|
||||
|
||||
pub fn flush_and_submit(&mut self) {
|
||||
self.surfaces.flush_and_submit(SurfaceId::Target);
|
||||
}
|
||||
@ -469,10 +425,6 @@ impl RenderState {
|
||||
self.flush_and_submit();
|
||||
}
|
||||
|
||||
pub fn apply_render_to_final_canvas(&mut self) -> Result<()> {
|
||||
cache::apply_render_to_final_canvas(self)
|
||||
}
|
||||
|
||||
/// 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");
|
||||
@ -616,22 +568,24 @@ impl RenderState {
|
||||
s.canvas().save();
|
||||
});
|
||||
}
|
||||
let fast_mode = self.options.is_fast_mode();
|
||||
// let fast_mode = self.options.is_fast_mode();
|
||||
// Skip anti-aliasing entirely during fast_mode (interactive
|
||||
// gestures + pan/zoom). AA edge sampling is per-pixel and adds
|
||||
// up across many shapes; reverts to full quality on commit.
|
||||
let antialias = !fast_mode
|
||||
&& shape.should_use_antialias(self.get_scale_fast(), self.options.antialias_threshold);
|
||||
let skip_effects = fast_mode;
|
||||
let antialias = true;
|
||||
// && shape.should_use_antialias(self.get_scale_fast(), self.options.antialias_threshold);
|
||||
let skip_effects = false;
|
||||
|
||||
let has_nested_fills = self
|
||||
.nested_fills
|
||||
.last()
|
||||
.is_some_and(|fills| !fills.is_empty());
|
||||
|
||||
let has_inherited_blur = !self.ignore_nested_blurs
|
||||
&& self.nested_blurs.iter().flatten().any(|blur| {
|
||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
|
||||
});
|
||||
|
||||
let can_render_directly = apply_to_current_surface
|
||||
&& clip_bounds.is_none()
|
||||
&& offset.is_none()
|
||||
@ -1218,13 +1172,13 @@ impl RenderState {
|
||||
self.surfaces.update_render_context(self.render_area, scale);
|
||||
}
|
||||
|
||||
fn rebuild_backbuffer_crop_cache(&mut self, tree: ShapesPoolRef) {
|
||||
drag_crop::rebuild_backbuffer_crop_cache(self, tree)
|
||||
}
|
||||
// fn rebuild_backbuffer_crop_cache(&mut self, tree: ShapesPoolRef) {
|
||||
// drag_crop::rebuild_backbuffer_crop_cache(self, tree)
|
||||
// }
|
||||
|
||||
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
|
||||
cache::render_from_cache(self, shapes)
|
||||
}
|
||||
// pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
|
||||
// cache::render_from_cache(self, shapes)
|
||||
// }
|
||||
|
||||
/// Render a preview of the shapes during loading.
|
||||
/// This rebuilds tiles for touched shapes and renders synchronously.
|
||||
@ -1234,8 +1188,8 @@ impl RenderState {
|
||||
|
||||
// Enable fast_mode during preview to skip expensive effects (blur, shadows).
|
||||
// Restore the previous state afterward so the final render is full quality.
|
||||
let current_fast_mode = self.options.is_fast_mode();
|
||||
self.options.set_fast_mode(true);
|
||||
// let current_fast_mode = self.options.is_fast_mode();
|
||||
// self.options.set_fast_mode(true);
|
||||
|
||||
// Skip tile rebuilding during preview - we'll do it at the end
|
||||
// Just rebuild tiles for touched shapes and render synchronously
|
||||
@ -1244,7 +1198,7 @@ impl RenderState {
|
||||
// Use the sync render path
|
||||
self.start_render_loop(None, tree, timestamp, true)?;
|
||||
|
||||
self.options.set_fast_mode(current_fast_mode);
|
||||
// self.options.set_fast_mode(current_fast_mode);
|
||||
|
||||
performance::end_measure!("render_preview");
|
||||
performance::end_timed_log!("render_preview", _start);
|
||||
@ -1304,50 +1258,50 @@ impl RenderState {
|
||||
self.tile_viewbox.update(&self.viewbox);
|
||||
self.focus_mode.reset();
|
||||
|
||||
// render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
|
||||
// render_state.rebuild_tile_index(&state.shapes);
|
||||
// if render_state.zoom_changed() {
|
||||
// render_state.surfaces.invalidate_tile_cache();
|
||||
// }
|
||||
|
||||
performance::begin_measure!("render");
|
||||
performance::begin_measure!("start_render_loop");
|
||||
|
||||
// Compute and set document-space bounds (1 unit == 1 doc px @ 100% zoom)
|
||||
// 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.atlas.set_doc_bounds(doc_bounds);
|
||||
|
||||
self.cache_cleared_this_render = false;
|
||||
let preserve_target = self.preserve_target_during_render;
|
||||
self.preserve_target_during_render = false;
|
||||
|
||||
if self.options.is_interactive_transform() {
|
||||
// Keep `Target` as the previous frame and overwrite only the tiles
|
||||
// that changed. This avoids clearing + redrawing an atlas backdrop
|
||||
// every rAF during drag (a common source of GPU work/stalls).
|
||||
self.surfaces
|
||||
.reset_interactive_transform(self.background_color);
|
||||
if !self.interactive_target_seeded {
|
||||
// Seed from the last presented frame; this is stable even when
|
||||
// fast_mode skips cache updates and regardless of atlas coverage.
|
||||
self.interactive_target_seeded = true;
|
||||
}
|
||||
} else if preserve_target || self.zoom_changed() {
|
||||
// Shape updates or zoom-end: keep the last presented frame on screen
|
||||
// while tiles are re-rendered asynchronously. During zoom the
|
||||
// preview from render_from_cache stays visible until the full-
|
||||
// quality pass completes.
|
||||
self.surfaces
|
||||
.reset_interactive_transform(self.background_color);
|
||||
self.surfaces.seed_backbuffer_from_target();
|
||||
self.interactive_target_seeded = false;
|
||||
} else {
|
||||
self.reset_canvas();
|
||||
self.interactive_target_seeded = false;
|
||||
// Paint rulers/frame now so they survive the progressive frames
|
||||
// instead of blanking until the first full `present_frame`.
|
||||
// Skip on sync renders (thumbnails/exports)
|
||||
if !sync_render {
|
||||
ui::render(self, tree);
|
||||
self.flush_and_submit();
|
||||
}
|
||||
}
|
||||
// if self.options.is_interactive_transform() {
|
||||
// // Keep `Target` as the previous frame and overwrite only the tiles
|
||||
// // that changed. This avoids clearing + redrawing an atlas backdrop
|
||||
// // every rAF during drag (a common source of GPU work/stalls).
|
||||
// self.surfaces
|
||||
// .reset_interactive_transform(self.background_color);
|
||||
// if !self.interactive_target_seeded {
|
||||
// // Seed from the last presented frame; this is stable even when
|
||||
// // fast_mode skips cache updates and regardless of atlas coverage.
|
||||
// self.interactive_target_seeded = true;
|
||||
// }
|
||||
// } else if preserve_target || self.zoom_changed() {
|
||||
// // Shape updates or zoom-end: keep the last presented frame on screen
|
||||
// // while tiles are re-rendered asynchronously. During zoom the
|
||||
// // preview from render_from_cache stays visible until the full-
|
||||
// // quality pass completes.
|
||||
// self.surfaces
|
||||
// .reset_interactive_transform(self.background_color);
|
||||
// self.surfaces.seed_backbuffer_from_target();
|
||||
// self.interactive_target_seeded = false;
|
||||
// } else {
|
||||
// self.reset_canvas();
|
||||
// self.interactive_target_seeded = false;
|
||||
// // Paint rulers/frame now so they survive the progressive frames
|
||||
// // instead of blanking until the first full `present_frame`.
|
||||
// // Skip on sync renders (thumbnails/exports)
|
||||
// if !sync_render {
|
||||
// ui::render(self, tree);
|
||||
// self.flush_and_submit();
|
||||
// }
|
||||
// }
|
||||
|
||||
// Viewer fixed-scroll passes reuse the same WASM context; `reset` does not
|
||||
// clear Backbuffer, so pass 2 would otherwise keep pass-1 pixels in regions
|
||||
@ -1361,15 +1315,16 @@ impl RenderState {
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
|
||||
// NOTE: Why we're scaling in here these surfaces?
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().scale((scale, scale));
|
||||
});
|
||||
|
||||
self.surfaces.resize_cache_from_viewbox(
|
||||
&self.viewbox,
|
||||
&self.cached_viewbox,
|
||||
self.options.dpr_viewport_interest_area_threshold,
|
||||
)?;
|
||||
// self.surfaces.resize_cache_from_viewbox(
|
||||
// &self.viewbox,
|
||||
// &self.cached_viewbox,
|
||||
// self.options.dpr_viewport_interest_area_threshold,
|
||||
// )?;
|
||||
|
||||
// FIXME - review debug
|
||||
// debug::render_debug_tiles_for_viewbox(self);
|
||||
@ -1384,7 +1339,7 @@ impl RenderState {
|
||||
|
||||
performance::end_timed_log!("tile_cache_update", _tile_start);
|
||||
|
||||
self.draw_shape_surface_stack_into(None, SurfaceId::Current);
|
||||
// self.draw_shape_surface_stack_into(None, SurfaceId::Current);
|
||||
|
||||
#[allow(unused)]
|
||||
let mut frame_type = FrameType::None;
|
||||
@ -1401,17 +1356,6 @@ impl RenderState {
|
||||
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
|
||||
// asynchronously. Without this, panning right after a zoom
|
||||
// would keep scaling the Cache surface by the old zoom ratio
|
||||
// (pixelated/wrong-scale tiles) because the async render
|
||||
// never completes — each pan frame cancels it.
|
||||
if self.cache_cleared_this_render {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
}
|
||||
|
||||
performance::end_measure!("start_render_loop");
|
||||
@ -1419,36 +1363,36 @@ impl RenderState {
|
||||
Ok(frame_type)
|
||||
}
|
||||
|
||||
fn compute_document_bounds(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
) -> Option<skia::Rect> {
|
||||
let ids: Vec<Uuid> = if let Some(id) = base_object {
|
||||
vec![*id]
|
||||
} else {
|
||||
let root = tree.get(&Uuid::nil())?;
|
||||
root.children_ids(false)
|
||||
};
|
||||
// fn compute_document_bounds(
|
||||
// &mut self,
|
||||
// base_object: Option<&Uuid>,
|
||||
// tree: ShapesPoolRef,
|
||||
// ) -> Option<skia::Rect> {
|
||||
// let ids: Vec<Uuid> = if let Some(id) = base_object {
|
||||
// vec![*id]
|
||||
// } else {
|
||||
// let root = tree.get(&Uuid::nil())?;
|
||||
// root.children_ids(false)
|
||||
// };
|
||||
|
||||
let mut acc: Option<skia::Rect> = None;
|
||||
for id in ids.iter() {
|
||||
let Some(shape) = tree.get(id) else {
|
||||
continue;
|
||||
};
|
||||
let r = shape.extrect(tree, 1.0);
|
||||
if r.is_empty() {
|
||||
continue;
|
||||
}
|
||||
acc = Some(if let Some(mut a) = acc {
|
||||
a.join(r);
|
||||
a
|
||||
} else {
|
||||
r
|
||||
});
|
||||
}
|
||||
acc
|
||||
}
|
||||
// let mut acc: Option<skia::Rect> = None;
|
||||
// for id in ids.iter() {
|
||||
// let Some(shape) = tree.get(id) else {
|
||||
// continue;
|
||||
// };
|
||||
// let r = shape.extrect(tree, 1.0);
|
||||
// if r.is_empty() {
|
||||
// continue;
|
||||
// }
|
||||
// acc = Some(if let Some(mut a) = acc {
|
||||
// a.join(r);
|
||||
// a
|
||||
// } else {
|
||||
// r
|
||||
// });
|
||||
// }
|
||||
// acc
|
||||
// }
|
||||
|
||||
pub fn continue_render_loop(
|
||||
&mut self,
|
||||
@ -1464,32 +1408,22 @@ impl RenderState {
|
||||
// `draw_atlas` needs a snapshot of the tile atlas. Partial frames are not
|
||||
// presented (only flushed), so defer composition to the final frame and
|
||||
// avoid re-snapshotting up to 4096² on every rAF during async tile work.
|
||||
if !self.options.is_interactive_transform() && matches!(frame_type, FrameType::Full) {
|
||||
// if !self.options.is_interactive_transform() && matches!(frame_type, FrameType::Full) {
|
||||
self.surfaces.draw_tile_atlas_to_backbuffer(
|
||||
&self.viewbox,
|
||||
&self.tile_viewbox,
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
// }
|
||||
|
||||
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();
|
||||
self.present_frame(tree);
|
||||
}
|
||||
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");
|
||||
@ -1619,14 +1553,6 @@ impl RenderState {
|
||||
enter_exit::render_shape_exit(self, element, visited_mask, clip_bounds, target_surface)
|
||||
}
|
||||
|
||||
pub fn get_current_tile_bounds(&mut self) -> Result<Rect> {
|
||||
let tile = self
|
||||
.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
let offset = self.viewbox.get_offset();
|
||||
Ok(tile.get_rect_with_offset(&offset))
|
||||
}
|
||||
|
||||
pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect {
|
||||
let scale = self.get_scale();
|
||||
let offset_x = self.viewbox.area.left * scale;
|
||||
@ -1719,48 +1645,6 @@ impl RenderState {
|
||||
target_surface = SurfaceId::Export;
|
||||
}
|
||||
|
||||
// During interactive transforms we compute the union of the current bounds of all
|
||||
// modified shapes (doc-space @ 100% zoom, scale=1.0). This is used as a cheap overlap
|
||||
// guard to decide when cached top-level crops are unsafe to reuse (something is moving
|
||||
// over/inside them), without doing expensive ancestor walks per node.
|
||||
//
|
||||
// `modifier_ids` is pre-computed once here and reused throughout the loop to avoid
|
||||
// repeated allocations (formerly O(N_shapes) HashMap builds) per node.
|
||||
let modifier_ids = tree.modifier_ids();
|
||||
let moved_bounds = if self.options.is_interactive_transform() && !modifier_ids.is_empty() {
|
||||
let mut acc: Option<Rect> = None;
|
||||
for id in modifier_ids.iter() {
|
||||
// Current (post-modifier) bounds
|
||||
if let Some(s) = tree.get(id) {
|
||||
let r = s.extrect(tree, 1.0);
|
||||
acc = Some(match acc {
|
||||
None => r,
|
||||
Some(mut prev) => {
|
||||
prev.join(r);
|
||||
prev
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pre-modifier bounds: important so cached top-level crops that still contain the
|
||||
// shape at its original position are considered "unsafe" even after the shape
|
||||
// has moved away (e.g. dragging a child out of a clipped frame).
|
||||
if let Some(raw) = tree.get_raw(id) {
|
||||
let r0 = raw.extrect(tree, 1.0);
|
||||
acc = Some(match acc {
|
||||
None => r0,
|
||||
Some(mut prev) => {
|
||||
prev.join(r0);
|
||||
prev
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
acc
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
while let Some(node_render_state) = self.pending_nodes.pop() {
|
||||
let node_id = node_render_state.id;
|
||||
let visited_children = node_render_state.visited_children;
|
||||
@ -1869,71 +1753,6 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive drag cache: if this node is cacheable during interactive transform,
|
||||
// draw it directly from Backbuffer crop on the current tile surface and skip
|
||||
// traversing/rendering the subtree.
|
||||
if self.options.is_interactive_transform() {
|
||||
let use_cached = self.should_use_cached_top_level_during_interactive(
|
||||
node_id,
|
||||
tree,
|
||||
modifier_ids,
|
||||
moved_bounds,
|
||||
);
|
||||
|
||||
if use_cached {
|
||||
if let Some(crop) = self.backbuffer_crop_cache.get(&node_id) {
|
||||
let crop_image = &crop.image;
|
||||
let crop_src_selrect = crop.src_selrect;
|
||||
|
||||
let cur_selrect = tree.get(&node_id).map(|s| s.selrect());
|
||||
let (dx, dy) = match cur_selrect {
|
||||
Some(cur) => (
|
||||
cur.left - crop_src_selrect.left,
|
||||
cur.top - crop_src_selrect.top,
|
||||
),
|
||||
None => (0.0, 0.0),
|
||||
};
|
||||
let scale = self.get_scale_fast();
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
|
||||
let canvas = self.surfaces.canvas(target_surface);
|
||||
canvas.save();
|
||||
canvas.reset_matrix();
|
||||
// If the crop includes shadows/blur (extrect pixels outside the fill/stroke
|
||||
// silhouette), do NOT apply the silhouette clip or we'd cut those pixels.
|
||||
let should_clip_crop = element.shadows.is_empty() && element.blur.is_none();
|
||||
if should_clip_crop {
|
||||
if let Some(clip_path) = element.drag_crop_clip_path() {
|
||||
let mut doc_to_tile = Matrix::new_identity();
|
||||
// Map document-space coordinates into tile pixels.
|
||||
// Rendering surfaces apply: scale(scale) then translate(translation) in doc units.
|
||||
// Equivalent point mapping: (doc + translation) * scale.
|
||||
doc_to_tile.post_translate((translation.0, translation.1));
|
||||
doc_to_tile.post_scale((scale, scale), None);
|
||||
let clip_path = clip_path.make_transform(&doc_to_tile);
|
||||
canvas.clip_path(&clip_path, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
}
|
||||
let doc_left =
|
||||
crop.capture_vb_left + (crop.capture_src_left as f32 / scale) + dx;
|
||||
let doc_top =
|
||||
crop.capture_vb_top + (crop.capture_src_top as f32 / scale) + dy;
|
||||
|
||||
let x = (doc_left + translation.0) * scale;
|
||||
let y = (doc_top + translation.1) * scale;
|
||||
let bw = crop_image.width() as f32;
|
||||
let bh = crop_image.height() as f32;
|
||||
let dst = skia::Rect::from_xywh(x, y, bw, bh);
|
||||
canvas.draw_image_rect(crop_image, None, dst, &skia::Paint::default());
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let can_flatten = element.can_flatten() && !self.focus_mode.should_focus(&element.id);
|
||||
|
||||
// Skip render_shape_enter/exit for flattened containers
|
||||
@ -1947,7 +1766,6 @@ impl RenderState {
|
||||
// the layer blur (which would make it more diffused than without clipping)
|
||||
let shadow_before_layer = !node_render_state.is_root()
|
||||
&& self.focus_mode.is_active()
|
||||
&& !self.options.is_fast_mode()
|
||||
&& !matches!(element.shape_type, Type::Text(_))
|
||||
&& Self::frame_clip_layer_blur(element).is_some()
|
||||
&& element.drop_shadows_visible().next().is_some();
|
||||
@ -1975,8 +1793,8 @@ impl RenderState {
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
// Skip expensive drop shadow rendering in fast mode (during pan/zoom).
|
||||
let skip_shadows = self.options.is_fast_mode();
|
||||
|
||||
// let skip_shadows = self.options.is_fast_mode();
|
||||
let skip_shadows = false;
|
||||
// Skip shadow block when already rendered before the layer (frame_clip_layer_blur)
|
||||
let shadows_already_rendered = Self::frame_clip_layer_blur(element).is_some();
|
||||
|
||||
@ -2155,23 +1973,22 @@ impl RenderState {
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::uncached");
|
||||
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
// Composite if the walker did work in this PAF (`!is_empty`) OR
|
||||
// the tile has unfinished work from a previous PAF
|
||||
// (`current_tile_had_shapes` was set when we populated pending_nodes
|
||||
// for this tile).
|
||||
if !is_empty || self.current_tile_had_shapes {
|
||||
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_into_backbuffer(
|
||||
&tile_rect,
|
||||
self.background_color,
|
||||
surfaces::DrawOnCache::Yes,
|
||||
);
|
||||
} else {
|
||||
self.apply_render_to_final_canvas()?;
|
||||
}
|
||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
|
||||
let current_tile = *self
|
||||
.current_tile
|
||||
.as_ref()
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
|
||||
self.surfaces.draw_current_tile_into_tile_atlas(
|
||||
&self.tile_viewbox,
|
||||
¤t_tile,
|
||||
);
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render_workspace_current_tile(
|
||||
@ -2232,7 +2049,8 @@ impl RenderState {
|
||||
// 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 {
|
||||
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() {
|
||||
@ -2271,9 +2089,7 @@ impl RenderState {
|
||||
// cached_viewbox here would make zoom_changed() return false,
|
||||
// so set_view_end would skip tile invalidation and the next
|
||||
// full render would reuse the low-quality tiles.
|
||||
if !self.options.is_fast_mode() {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
self.cached_viewbox = self.viewbox;
|
||||
|
||||
Ok(FrameType::Full)
|
||||
}
|
||||
@ -2338,27 +2154,6 @@ impl RenderState {
|
||||
|
||||
let mut result = HashSet::<tiles::Tile>::with_capacity(old_tiles.len());
|
||||
|
||||
// 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`
|
||||
// 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
|
||||
// change. Clearing the full raw extrect here removes all such residual
|
||||
// content without growing the atlas.
|
||||
//
|
||||
// We intentionally skip this when there is NO modifier so that plain
|
||||
// zoom / pan tile-index rebuilds do NOT invalidate valid atlas content.
|
||||
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
|
||||
.atlas
|
||||
.clear_doc_rect_in_atlas_clipped(old_extrect);
|
||||
}
|
||||
}
|
||||
|
||||
// First, remove the shape from all tiles where it was previously located
|
||||
for tile in old_tiles {
|
||||
self.tiles.remove_shape_at(tile, shape.id);
|
||||
@ -2478,9 +2273,7 @@ impl RenderState {
|
||||
// Zoom changes world tile size: a partial cache update would mix scales in the
|
||||
// mosaic and glitch. Same zoom as last finished render (typical pan): drop only
|
||||
// tile textures and keep the cache canvas for render_from_cache.
|
||||
if self.zoom_changed() {
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
} else {
|
||||
if !self.zoom_changed() {
|
||||
self.surfaces.invalidate_tile_cache();
|
||||
}
|
||||
|
||||
@ -2515,7 +2308,6 @@ impl RenderState {
|
||||
}
|
||||
|
||||
// Invalidate changed tiles - old content stays visible until new tiles render
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
for tile in all_tiles {
|
||||
self.remove_cached_tile(tile);
|
||||
}
|
||||
@ -2640,7 +2432,7 @@ impl RenderState {
|
||||
|
||||
pub fn prepare_context_loss_cleanup(&mut self) {
|
||||
// Drop cached GPU-backed snapshots before dropping the render state.
|
||||
self.backbuffer_crop_cache.clear();
|
||||
// self.backbuffer_crop_cache.clear();
|
||||
self.surfaces.invalidate_tile_cache();
|
||||
// Mark context as abandoned so resource destructors avoid issuing
|
||||
// GL commands when the browser has already lost/restored the context.
|
||||
|
||||
@ -7,9 +7,12 @@ pub fn render_background_blur(
|
||||
shape: &Shape,
|
||||
target_surface: SurfaceId,
|
||||
) {
|
||||
/*
|
||||
if render_state.options.is_fast_mode() {
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if matches!(shape.shape_type, Type::Text(_)) || matches!(shape.shape_type, Type::SVGRaw(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::tiles;
|
||||
|
||||
pub fn apply_render_to_final_canvas(render_state: &mut crate::render::RenderState) -> 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 render_state.options.is_interactive_transform() {
|
||||
let tile_rect = render_state.get_current_aligned_tile_bounds()?;
|
||||
render_state.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
render_state.background_color,
|
||||
surfaces::DrawOnCache::No,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Viewer masked passes render a partial scene. Reusing the tile texture cache would
|
||||
// SrcOver-blend onto textures from the previous pass and leak pixels into the blob.
|
||||
if render_state.viewer_masked_pass() {
|
||||
// Use viewbox-aligned bounds (not grid-snapped) to match interactive-transform
|
||||
// compositing and avoid a visible offset vs the DOM canvas.
|
||||
let tile_rect = render_state.get_current_tile_bounds()?;
|
||||
render_state.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
render_state.background_color,
|
||||
surfaces::DrawOnCache::No,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let fast_mode = render_state.options.is_fast_mode();
|
||||
// Decide *now* (at the first real cache blit) whether we need to clear Cache.
|
||||
// This avoids clearing Cache on renders that don't actually paint tiles (e.g. hover/UI),
|
||||
// while still preventing stale pixels from surviving across full-quality renders.
|
||||
if !fast_mode && !render_state.cache_cleared_this_render {
|
||||
render_state.surfaces.clear_cache(render_state.background_color);
|
||||
render_state.cache_cleared_this_render = true;
|
||||
}
|
||||
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
||||
// positions would be wrong — only save to the tile HashMap.
|
||||
let tile_rect = render_state.get_current_aligned_tile_bounds()?;
|
||||
|
||||
let current_tile = *render_state
|
||||
.current_tile
|
||||
.as_ref()
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
|
||||
render_state.surfaces.draw_current_tile_into_tile_atlas(
|
||||
&render_state.tile_viewbox,
|
||||
¤t_tile,
|
||||
&tile_rect,
|
||||
fast_mode,
|
||||
render_state.render_area,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_from_cache(render_state: &mut crate::render::RenderState, shapes: ShapesPoolRef) {
|
||||
let _start = performance::begin_timed_log!("render_from_cache");
|
||||
performance::begin_measure!("render_from_cache");
|
||||
let bg_color = render_state.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 render_state.options.is_fast_mode() && !render_state.surfaces.atlas.is_empty() {
|
||||
render_state.surfaces
|
||||
.draw_atlas_to_backbuffer(render_state.viewbox, bg_color);
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
||||
if render_state.cached_viewbox.area.width() > 0.0 {
|
||||
// Scale and translate the target according to the cached data
|
||||
let navigate_zoom = render_state.viewbox.zoom / render_state.cached_viewbox.zoom;
|
||||
|
||||
let interest = render_state.options.dpr_viewport_interest_area_threshold;
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(&render_state.cached_viewbox, interest);
|
||||
let offset_x = render_state.viewbox.area.left * render_state.cached_viewbox.zoom * render_state.options.dpr;
|
||||
let offset_y = render_state.viewbox.area.top * render_state.cached_viewbox.zoom * render_state.options.dpr;
|
||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||
|
||||
// For zoom-out, prefer cache only if it fully covers the viewport.
|
||||
// Otherwise, atlas will provide a more correct full-viewport preview.
|
||||
let zooming_out = render_state.viewbox.zoom < render_state.cached_viewbox.zoom;
|
||||
if zooming_out {
|
||||
let cache_dim = render_state.surfaces.cache_dimensions();
|
||||
let cache_w = cache_dim.width as f32;
|
||||
let cache_h = cache_dim.height as f32;
|
||||
|
||||
// Viewport in target pixels.
|
||||
let vw = render_state.viewbox.dpr_width().max(1.0);
|
||||
let vh = render_state.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).
|
||||
// => cache = (target / navigate_zoom) - translate
|
||||
let inv = if navigate_zoom.abs() > f32::EPSILON {
|
||||
1.0 / navigate_zoom
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
let min_x = cx0.min(cx1);
|
||||
let min_y = cy0.min(cy1);
|
||||
let max_x = cx0.max(cx1);
|
||||
let max_y = cy0.max(cy1);
|
||||
|
||||
let cache_covers =
|
||||
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 !render_state.surfaces.atlas.is_empty() {
|
||||
render_state.surfaces
|
||||
.draw_atlas_to_backbuffer(render_state.viewbox, bg_color);
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw directly from cache surface, avoiding snapshot overhead
|
||||
render_state.surfaces.draw_cache_to_backbuffer();
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
}
|
||||
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
}
|
||||
@ -36,16 +36,6 @@ fn render_debug_view(render_state: &mut RenderState) {
|
||||
.draw_rect(rect, &paint);
|
||||
}
|
||||
|
||||
pub fn render_debug_cache_surface(render_state: &mut RenderState) {
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
canvas.save();
|
||||
canvas.scale((0.1, 0.1));
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_into(SurfaceId::Cache, SurfaceId::Debug, None);
|
||||
render_state.surfaces.canvas(SurfaceId::Debug).restore();
|
||||
}
|
||||
|
||||
pub fn render_wasm_label(render_state: &mut RenderState) {
|
||||
if render_state.preview_mode || !render_state.options.show_wasm_info() {
|
||||
return;
|
||||
@ -274,38 +264,6 @@ pub extern "C" fn capture_frames(capture_frames: i32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_cache_console() -> Result<()> {
|
||||
console_debug_surface(get_render_state(), SurfaceId::Cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_cache_base64() -> Result<()> {
|
||||
console_debug_surface_base64(get_render_state(), SurfaceId::Cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_atlas_console() -> Result<()> {
|
||||
console_debug_surface(get_render_state(), SurfaceId::Atlas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_atlas_base64() -> Result<()> {
|
||||
console_debug_surface_base64(get_render_state(), SurfaceId::Atlas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@ -1,330 +0,0 @@
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
use super::RenderState;
|
||||
use super::SurfaceId;
|
||||
use crate::get_gpu_state;
|
||||
use crate::math;
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
pub struct InteractiveDragCrop {
|
||||
pub src_doc_bounds: Rect,
|
||||
pub src_selrect: Rect,
|
||||
/// Viewbox origin (doc-space) at capture time.
|
||||
pub capture_vb_left: f32,
|
||||
pub capture_vb_top: f32,
|
||||
/// Backbuffer pixel origin used for `snapshot_rect` (so we can do 1:1 blits).
|
||||
pub capture_src_left: i32,
|
||||
pub capture_src_top: i32,
|
||||
pub image: skia::Image,
|
||||
}
|
||||
|
||||
/// 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 [`GpuState::max_texture_size`] (same budget as the atlas).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn drag_crop_snapshot_window_px(
|
||||
max_side_px: i32,
|
||||
out_w: i32,
|
||||
out_h: i32,
|
||||
viewport_doc: Rect,
|
||||
vb_left: f32,
|
||||
vb_top: f32,
|
||||
scale: f32,
|
||||
src_left_px: i32,
|
||||
src_top_px: i32,
|
||||
src_doc_bounds: Rect,
|
||||
) -> (i32, i32, i32, i32) {
|
||||
let cap = max_side_px.max(1);
|
||||
if out_w <= cap && out_h <= cap {
|
||||
return (0, 0, out_w, out_h);
|
||||
}
|
||||
let win_w = out_w.min(cap);
|
||||
let win_h = out_h.min(cap);
|
||||
|
||||
let mut vis = viewport_doc;
|
||||
let has_vis = vis.intersect(src_doc_bounds);
|
||||
let (cx, cy) = if !has_vis || vis.is_empty() {
|
||||
(out_w as f32 * 0.5, out_h as f32 * 0.5)
|
||||
} else {
|
||||
let lx0 = (vis.left - vb_left) * scale - src_left_px as f32;
|
||||
let ly0 = (vis.top - vb_top) * scale - src_top_px as f32;
|
||||
let lx1 = (vis.right - vb_left) * scale - src_left_px as f32;
|
||||
let ly1 = (vis.bottom - vb_top) * scale - src_top_px as f32;
|
||||
((lx0 + lx1) * 0.5, (ly0 + ly1) * 0.5)
|
||||
};
|
||||
|
||||
let mut ox = (cx - win_w as f32 * 0.5).round() as i32;
|
||||
let mut oy = (cy - win_h as f32 * 0.5).round() as i32;
|
||||
ox = ox.clamp(0, out_w - win_w);
|
||||
oy = oy.clamp(0, out_h - win_h);
|
||||
(ox, oy, win_w, win_h)
|
||||
}
|
||||
|
||||
/// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an
|
||||
/// interactive transform (drag/resize/rotate).
|
||||
///
|
||||
/// We only reuse cached pixels when it is safe and visually correct:
|
||||
/// - **Top-level only**: cache entries are built for direct children of the root.
|
||||
/// - **Moved node**: only allow cache reuse for *pure translations* (no scale/rotate/skew),
|
||||
/// because other transforms would require resampling and can diverge from the live render.
|
||||
/// - **Other cached nodes**: if the moving bounds overlap this cached crop, invalidate it so
|
||||
/// we don't show stale content while something moves over/inside it.
|
||||
pub fn should_use_cached_top_level_during_interactive(
|
||||
render_state: &mut RenderState,
|
||||
node_id: Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
moved_ids: &[Uuid],
|
||||
moved_bounds: Option<Rect>,
|
||||
) -> bool {
|
||||
if !render_state.backbuffer_crop_cache.contains_key(&node_id) {
|
||||
return false;
|
||||
}
|
||||
let Some(raw) = tree.get_raw(&node_id) else {
|
||||
return false;
|
||||
};
|
||||
if raw.parent_id != Some(Uuid::nil()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this top-level shape itself is being moved, always allow using its cached pixels.
|
||||
// BUT only for pure translations. For non-translation transforms (scale/rotate/skew),
|
||||
// cached pixels won't match the live result (and may require resampling), so render live.
|
||||
if moved_ids.contains(&node_id) {
|
||||
let Some(m) = tree.get_modifier(&node_id) else {
|
||||
return false;
|
||||
};
|
||||
// Only allow using the cached pixels for pure translations.
|
||||
// For non-translation transforms (scale/rotate/skew), cached pixels won't match.
|
||||
// If the transform is the identity means a reflow, we need to redraw as well.
|
||||
if math::identitish(m) || !math::is_move_only_matrix(m) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additionally require this node to be safe to serve from a rectangular backbuffer
|
||||
// crop while moving; otherwise it must be rendered live (e.g. text, overflow frames).
|
||||
return tree
|
||||
.get(&node_id)
|
||||
.is_some_and(|s| s.is_safe_for_drag_crop_cache(tree));
|
||||
}
|
||||
|
||||
// If the moving content overlaps this cached crop, do not use the cached pixels
|
||||
// for this frame. We intentionally keep the cache entry: overlap is typically
|
||||
// transient during drag, and once the moving content leaves the area the crop
|
||||
// becomes valid again (stationary shape unchanged).
|
||||
if let Some(moved) = moved_bounds {
|
||||
let intersects = render_state
|
||||
.backbuffer_crop_cache
|
||||
.get(&node_id)
|
||||
.is_some_and(|crop| moved.intersects(crop.src_doc_bounds));
|
||||
|
||||
if intersects {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn rebuild_backbuffer_crop_cache(render_state: &mut RenderState, tree: ShapesPoolRef) {
|
||||
render_state.backbuffer_crop_cache.clear();
|
||||
|
||||
// Collect candidate shapes that are "recortable" and visible in the current viewport.
|
||||
|
||||
// This is intentionally conservative; we only cache shapes that do not overlap with
|
||||
// ANY other candidate to guarantee the pixels under their bounds belong exclusively
|
||||
// to that shape in Backbuffer.
|
||||
let viewport = render_state.viewbox.area;
|
||||
let scale = render_state.get_scale();
|
||||
let mut candidates: Vec<(Uuid, Rect, Rect)> = Vec::new(); // (id, doc_bounds, selrect)
|
||||
|
||||
let root_ids: Vec<Uuid> = match tree.get(&Uuid::nil()) {
|
||||
Some(root) => root.children_ids(false),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
for shape_id in root_ids {
|
||||
let Some(shape) = tree.get(&shape_id) else {
|
||||
continue;
|
||||
};
|
||||
if shape.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let doc_bounds = shape.extrect(tree, 1.0);
|
||||
if !doc_bounds.intersects(viewport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also require selrect to be visible; used for drag delta placement.
|
||||
let selrect = shape.selrect();
|
||||
if !selrect.intersects(viewport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push((shape.id, doc_bounds, selrect));
|
||||
}
|
||||
|
||||
// Filter out any candidate that overlaps with any other candidate.
|
||||
// Sort by left edge so the inner loop can break early once no further
|
||||
// x-overlap is possible, reducing comparisons from O(N²) to O(N log N)
|
||||
// in typical layouts where shapes are spread out.
|
||||
candidates.sort_unstable_by(|a, b| {
|
||||
a.1.left
|
||||
.partial_cmp(&b.1.left)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let n = candidates.len();
|
||||
let mut is_overlapping = vec![false; n];
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
if candidates[j].1.left >= candidates[i].1.right {
|
||||
break; // sorted: no further x-overlap possible for i
|
||||
}
|
||||
if is_overlapping[i] && is_overlapping[j] {
|
||||
continue; // both already excluded, skip check
|
||||
}
|
||||
if candidates[i].1.intersects(candidates[j].1) {
|
||||
is_overlapping[i] = true;
|
||||
is_overlapping[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let non_overlapping: Vec<(Uuid, Rect, Rect)> = candidates
|
||||
.iter()
|
||||
.zip(is_overlapping.iter())
|
||||
.filter_map(|((id, bounds, selrect), ov)| {
|
||||
if !ov {
|
||||
Some((*id, *bounds, *selrect))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let vb_left = render_state.viewbox.area.left;
|
||||
let vb_top = render_state.viewbox.area.top;
|
||||
let (bb_w, bb_h) = render_state.surfaces.surface_size(SurfaceId::Backbuffer);
|
||||
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 = render_state.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.
|
||||
// Created lazily on first use and grown if a later shape needs more space.
|
||||
let mut scratch_surface: Option<skia::Surface> = None;
|
||||
|
||||
for (id, doc_bounds, selrect) in non_overlapping {
|
||||
let left = ((doc_bounds.left - vb_left) * scale).floor() as i32;
|
||||
let top = ((doc_bounds.top - vb_top) * scale).floor() as i32;
|
||||
let right = ((doc_bounds.right - vb_left) * scale).ceil() as i32;
|
||||
let bottom = ((doc_bounds.bottom - vb_top) * scale).ceil() as i32;
|
||||
if right <= left || bottom <= top {
|
||||
continue;
|
||||
}
|
||||
let src_irect = skia::IRect::new(left, top, right, bottom);
|
||||
|
||||
let src_doc_bounds = Rect::new(
|
||||
src_irect.left as f32 / scale + vb_left,
|
||||
src_irect.top as f32 / scale + vb_top,
|
||||
src_irect.right as f32 / scale + vb_left,
|
||||
src_irect.bottom as f32 / scale + vb_top,
|
||||
);
|
||||
|
||||
let full_w = src_irect.width();
|
||||
let full_h = src_irect.height();
|
||||
let (win_ox, win_oy, win_w, win_h) = drag_crop_snapshot_window_px(
|
||||
max_snap_px,
|
||||
full_w,
|
||||
full_h,
|
||||
viewport,
|
||||
vb_left,
|
||||
vb_top,
|
||||
scale,
|
||||
src_irect.left,
|
||||
src_irect.top,
|
||||
src_doc_bounds,
|
||||
);
|
||||
let window_irect = skia::IRect::new(
|
||||
src_irect.left + win_ox,
|
||||
src_irect.top + win_oy,
|
||||
src_irect.left + win_ox + win_w,
|
||||
src_irect.top + win_oy + win_h,
|
||||
);
|
||||
|
||||
let src_doc_window = Rect::new(
|
||||
window_irect.left as f32 / scale + vb_left,
|
||||
window_irect.top as f32 / scale + vb_top,
|
||||
window_irect.right as f32 / scale + vb_left,
|
||||
window_irect.bottom as f32 / scale + vb_top,
|
||||
);
|
||||
|
||||
let in_backbuffer = window_irect.left >= 0
|
||||
&& window_irect.top >= 0
|
||||
&& window_irect.right <= bb_w
|
||||
&& window_irect.bottom <= bb_h;
|
||||
|
||||
let backbuffer_snap = if in_backbuffer {
|
||||
render_state
|
||||
.surfaces
|
||||
.snapshot_rect(SurfaceId::Backbuffer, window_irect)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let image = if let Some(img) = backbuffer_snap {
|
||||
img
|
||||
} else {
|
||||
// Ensure the scratch surface is large enough for this window.
|
||||
// Grow (reallocate) only when necessary so that the common case
|
||||
// of similarly-sized shapes pays zero extra allocation cost.
|
||||
let needs_alloc = scratch_surface
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.width() < win_w || s.height() < win_h);
|
||||
if needs_alloc {
|
||||
scratch_surface = get_gpu_state()
|
||||
.create_surface_with_isize(
|
||||
"drag_crop_scratch".to_string(),
|
||||
skia::ISize::new(win_w, win_h),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
let Some(scratch) = scratch_surface.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let Some(img) = render_state
|
||||
.surfaces
|
||||
.try_snapshot_doc_rect_from_tiles_and_atlas(
|
||||
scratch,
|
||||
atlas_snap.as_ref(),
|
||||
src_doc_window,
|
||||
window_irect,
|
||||
win_w,
|
||||
win_h,
|
||||
vb_left,
|
||||
vb_top,
|
||||
scale,
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
img
|
||||
};
|
||||
|
||||
render_state.backbuffer_crop_cache.insert(
|
||||
id,
|
||||
InteractiveDragCrop {
|
||||
src_doc_bounds: src_doc_window,
|
||||
src_selrect: selrect,
|
||||
capture_vb_left: vb_left,
|
||||
capture_vb_top: vb_top,
|
||||
capture_src_left: window_irect.left,
|
||||
capture_src_top: window_irect.top,
|
||||
image,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -335,8 +335,7 @@ pub fn render_element_drop_shadows_and_composite(
|
||||
}
|
||||
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
let antialias = !render_state.options.is_fast_mode()
|
||||
&& element.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
let antialias = element.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
render_state.surfaces.canvas(target_surface).save();
|
||||
render_state.clip_target_surface_to_stack(clips, target_surface, scale, antialias);
|
||||
render_state
|
||||
|
||||
@ -27,8 +27,7 @@ pub fn render_shape_enter(
|
||||
render_state.surfaces.canvas(target_surface).save();
|
||||
if let Some(clips) = clip_bounds {
|
||||
let scale = render_state.get_scale();
|
||||
let antialias = !render_state.options.is_fast_mode()
|
||||
&& element
|
||||
let antialias = element
|
||||
.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
render_state.clip_target_surface_to_stack(
|
||||
clips,
|
||||
@ -40,15 +39,13 @@ pub fn render_shape_enter(
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
if !render_state.options.is_fast_mode() {
|
||||
if let Some(blur) = element.masked_group_layer_blur() {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(blur.value * scale);
|
||||
if let Some(filter) =
|
||||
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||
{
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
if let Some(blur) = element.masked_group_layer_blur() {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(blur.value * scale);
|
||||
if let Some(filter) =
|
||||
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||
{
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,13 +78,11 @@ pub fn render_shape_enter(
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
if !render_state.options.is_fast_mode() {
|
||||
if let Some(frame_blur) = layer_blur::frame_clip_layer_blur(element) {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(frame_blur.value * scale);
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
if let Some(frame_blur) = layer_blur::frame_clip_layer_blur(element) {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(frame_blur.value * scale);
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// Render options flags
|
||||
const DEBUG_VISIBLE: u32 = 0x01;
|
||||
const PROFILE_REBUILD_TILES: u32 = 0x02;
|
||||
const TEXT_EDITOR_V3: u32 = 0x04;
|
||||
const SHOW_WASM_INFO: u32 = 0x08;
|
||||
|
||||
@ -17,7 +16,6 @@ const ANTIALIAS_THRESHOLD: f32 = 7.0;
|
||||
pub struct RenderOptions {
|
||||
pub flags: u32,
|
||||
pub dpr: f32,
|
||||
fast_mode: bool,
|
||||
/// Active while the user is interacting with a shape (drag, resize,
|
||||
/// rotate). Implies `fast_mode` semantics for expensive effects but
|
||||
/// keeps per-frame flushing enabled (unlike pan/zoom, where
|
||||
@ -38,7 +36,6 @@ impl Default for RenderOptions {
|
||||
Self {
|
||||
flags: 0,
|
||||
dpr: 1.0,
|
||||
fast_mode: false,
|
||||
interactive_transform: false,
|
||||
antialias_threshold: ANTIALIAS_THRESHOLD,
|
||||
viewport_interest_area_threshold: VIEWPORT_INTEREST_AREA_THRESHOLD,
|
||||
@ -56,19 +53,6 @@ impl RenderOptions {
|
||||
self.flags & DEBUG_VISIBLE == DEBUG_VISIBLE
|
||||
}
|
||||
|
||||
pub fn is_profile_rebuild_tiles(&self) -> bool {
|
||||
self.flags & PROFILE_REBUILD_TILES == PROFILE_REBUILD_TILES
|
||||
}
|
||||
|
||||
/// Use fast mode to enable / disable expensive operations
|
||||
pub fn is_fast_mode(&self) -> bool {
|
||||
self.fast_mode
|
||||
}
|
||||
|
||||
pub fn set_fast_mode(&mut self, enabled: bool) {
|
||||
self.fast_mode = enabled;
|
||||
}
|
||||
|
||||
pub fn set_capture_frames(&mut self, capture_frames: i32) {
|
||||
self.capture_frames = capture_frames;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ use crate::{get_gpu_state, performance};
|
||||
|
||||
use skia_safe::{self as skia, IRect, Paint, RRect, Rect};
|
||||
|
||||
use super::{gpu_state::GpuState, tiles, tiles::Tile, tiles::TileRect, tiles::TileViewbox};
|
||||
use super::{tiles, tiles::Tile, tiles::TileViewbox};
|
||||
use crate::math::Point;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
@ -25,28 +25,6 @@ const TILE_DRAWABLE_RECT: IRect = IRect {
|
||||
right: TILE_MARGIN_SIZE + TILE_SIZE,
|
||||
bottom: TILE_MARGIN_SIZE + TILE_SIZE,
|
||||
};
|
||||
const DOC_ATLAS_MAX_DIM: i32 = 4096;
|
||||
|
||||
pub fn get_cache_size(viewbox: &Viewbox, interest: i32) -> skia::ISize {
|
||||
// First we retrieve the extended area of the viewport that we could render.
|
||||
let TileRect(isx, isy, iex, iey) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(viewbox, interest);
|
||||
|
||||
let dx = if isx.signum() != iex.signum() { 1 } else { 0 };
|
||||
let dy = if isy.signum() != iey.signum() { 1 } else { 0 };
|
||||
|
||||
(
|
||||
((iex - isx).abs() + dx) * TILE_SIZE,
|
||||
((iey - isy).abs() + dy) * TILE_SIZE,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DrawOnCache {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
@ -54,7 +32,6 @@ pub enum DrawOnCache {
|
||||
pub enum SurfaceId {
|
||||
Target = 0b000_0000_0001,
|
||||
Filter = 0b000_0000_0010,
|
||||
Cache = 0b000_0000_0100,
|
||||
Current = 0b000_0000_1000,
|
||||
Fills = 0b000_0001_0000,
|
||||
Strokes = 0b000_0010_0000,
|
||||
@ -64,346 +41,14 @@ pub enum SurfaceId {
|
||||
Export = 0b010_0000_0000,
|
||||
UI = 0b100_0000_0000,
|
||||
Debug = 0b100_0000_0001,
|
||||
Atlas = 0b100_0000_0010,
|
||||
Backbuffer = 0b100_0000_0100,
|
||||
TileAtlas = 0b100_0000_1000,
|
||||
}
|
||||
|
||||
pub struct DocAtlas {
|
||||
// Persistent 1:1 document-space atlas that gets incrementally updated as tiles render.
|
||||
// It grows dynamically to include any rendered document rect.
|
||||
pub surface: skia::Surface,
|
||||
pub origin: skia::Point,
|
||||
pub size: skia::ISize,
|
||||
/// Atlas pixel density relative to document pixels (1.0 == 1:1 doc px).
|
||||
/// When the atlas would exceed `max_texture_size`, this value is
|
||||
/// reduced so the atlas stays within the fixed texture cap.
|
||||
pub scale: f32,
|
||||
/// Optional document-space bounds (1 unit == 1 doc px @ 100% zoom) used to
|
||||
/// clamp atlas writes/clears so the atlas doesn't grow due to outlier tile rects.
|
||||
pub doc_bounds: Option<skia::Rect>,
|
||||
/// Tracks the last document-space rect written to the atlas per tile.
|
||||
/// Used to clear old content without clearing the whole (potentially huge) tile rect.
|
||||
pub tile_doc_rects: HashMap<Tile, skia::Rect>,
|
||||
}
|
||||
|
||||
impl DocAtlas {
|
||||
pub fn try_new() -> Result<Self> {
|
||||
// Keep atlas as a regular surface like the rest. Start with a tiny
|
||||
// transparent surface and grow it on demand.
|
||||
let mut surface =
|
||||
get_gpu_state().create_surface_with_dimensions("atlas".to_string(), 1, 1)?;
|
||||
|
||||
surface.canvas().clear(skia::Color::TRANSPARENT);
|
||||
|
||||
Ok(Self {
|
||||
surface,
|
||||
origin: skia::Point::new(0.0, 0.0),
|
||||
size: skia::ISize::new(0, 0),
|
||||
scale: 1.0,
|
||||
doc_bounds: None,
|
||||
tile_doc_rects: HashMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.size.width <= 0 || self.size.height <= 0
|
||||
}
|
||||
|
||||
/// Sets the document-space bounds used to clamp atlas updates.
|
||||
/// Pass `None` to disable clamping.
|
||||
pub fn set_doc_bounds(&mut self, bounds: Option<skia::Rect>) {
|
||||
self.doc_bounds = bounds.filter(|b| !b.is_empty());
|
||||
}
|
||||
|
||||
fn clamp_doc_rect_to_bounds(&self, doc_rect: skia::Rect) -> skia::Rect {
|
||||
if doc_rect.is_empty() {
|
||||
return doc_rect;
|
||||
}
|
||||
if let Some(bounds) = self.doc_bounds {
|
||||
let mut r = doc_rect;
|
||||
if r.intersect(bounds) {
|
||||
r
|
||||
} else {
|
||||
skia::Rect::new_empty()
|
||||
}
|
||||
} else {
|
||||
doc_rect
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_atlas_contains(
|
||||
&mut self,
|
||||
gpu_state: &mut GpuState,
|
||||
doc_rect: skia::Rect,
|
||||
) -> Result<()> {
|
||||
if doc_rect.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Current atlas bounds in document space (1 unit == 1 px).
|
||||
let current_left = self.origin.x;
|
||||
let current_top = self.origin.y;
|
||||
let scale = self.scale.max(0.01);
|
||||
let current_right = current_left + (self.size.width as f32) / scale;
|
||||
let current_bottom = current_top + (self.size.height as f32) / scale;
|
||||
|
||||
let mut new_left = current_left;
|
||||
let mut new_top = current_top;
|
||||
let mut new_right = current_right;
|
||||
let mut new_bottom = current_bottom;
|
||||
|
||||
// If atlas is empty/uninitialized, seed to rect (expanded to tile boundaries for fewer reallocs).
|
||||
let needs_init = self.size.width <= 0 || self.size.height <= 0;
|
||||
if needs_init {
|
||||
new_left = doc_rect.left.floor();
|
||||
new_top = doc_rect.top.floor();
|
||||
new_right = doc_rect.right.ceil();
|
||||
new_bottom = doc_rect.bottom.ceil();
|
||||
} else {
|
||||
new_left = new_left.min(doc_rect.left.floor());
|
||||
new_top = new_top.min(doc_rect.top.floor());
|
||||
new_right = new_right.max(doc_rect.right.ceil());
|
||||
new_bottom = new_bottom.max(doc_rect.bottom.ceil());
|
||||
}
|
||||
|
||||
// Geometric over-allocation: pad by 25% of extent to reduce realloc frequency.
|
||||
let doc_extent_w = new_right - new_left;
|
||||
let doc_extent_h = new_bottom - new_top;
|
||||
let pad = (doc_extent_w.min(doc_extent_h) * 0.25_f32).max(TILE_SIZE as f32);
|
||||
new_left -= pad;
|
||||
new_top -= pad;
|
||||
new_right += pad;
|
||||
new_bottom += pad;
|
||||
|
||||
let doc_w = (new_right - new_left).max(1.0);
|
||||
let doc_h = (new_bottom - new_top).max(1.0);
|
||||
|
||||
// Compute atlas scale needed to fit within the fixed texture cap.
|
||||
// Keep the highest possible scale (closest to 1.0) that still fits.
|
||||
let cap = gpu_state
|
||||
.max_texture_size()
|
||||
.clamp(TILE_SIZE, DOC_ATLAS_MAX_DIM) as f32;
|
||||
|
||||
let required_scale = (cap / doc_w).min(cap / doc_h).clamp(0.01, 1.0);
|
||||
|
||||
// Never upscale the atlas (it would add blur and churn).
|
||||
let new_scale = self.scale.min(required_scale).max(0.01);
|
||||
|
||||
let new_w = (doc_w * new_scale).ceil().clamp(1.0, cap) as i32;
|
||||
let new_h = (doc_h * new_scale).ceil().clamp(1.0, cap) as i32;
|
||||
|
||||
// Fast path: existing atlas already contains the rect.
|
||||
if !needs_init
|
||||
&& doc_rect.left >= current_left
|
||||
&& doc_rect.top >= current_top
|
||||
&& doc_rect.right <= current_right
|
||||
&& doc_rect.bottom <= current_bottom
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut new_surface =
|
||||
gpu_state.create_surface_with_dimensions("atlas".to_string(), new_w, new_h)?;
|
||||
new_surface.canvas().clear(skia::Color::TRANSPARENT);
|
||||
|
||||
// Copy old atlas into the new one with offset.
|
||||
if !needs_init {
|
||||
let old_scale = self.scale.max(0.01);
|
||||
let scale_ratio = new_scale / old_scale;
|
||||
let dx = (current_left - new_left) * new_scale;
|
||||
let dy = (current_top - new_top) * new_scale;
|
||||
|
||||
let image = self.surface.image_snapshot();
|
||||
let src =
|
||||
skia::Rect::from_xywh(0.0, 0.0, self.size.width as f32, self.size.height as f32);
|
||||
let dst = skia::Rect::from_xywh(
|
||||
dx,
|
||||
dy,
|
||||
(self.size.width as f32) * scale_ratio,
|
||||
(self.size.height as f32) * scale_ratio,
|
||||
);
|
||||
new_surface.canvas().draw_image_rect(
|
||||
&image,
|
||||
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||
dst,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
|
||||
self.origin = skia::Point::new(new_left, new_top);
|
||||
self.size = skia::ISize::new(new_w, new_h);
|
||||
self.scale = new_scale;
|
||||
gpu_state.delete_surface(&mut self.surface);
|
||||
self.surface = new_surface;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn blit_tile_image_into_atlas(
|
||||
&mut self,
|
||||
gpu_state: &mut GpuState,
|
||||
tile_image: &skia::Image,
|
||||
tile_doc_rect: skia::Rect,
|
||||
) -> Result<()> {
|
||||
if tile_doc_rect.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Clamp to document bounds (if any) and compute a matching source-rect in tile pixels.
|
||||
let mut clipped_doc_rect = tile_doc_rect;
|
||||
if let Some(bounds) = self.doc_bounds {
|
||||
if !clipped_doc_rect.intersect(bounds) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if clipped_doc_rect.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.ensure_atlas_contains(gpu_state, clipped_doc_rect)?;
|
||||
|
||||
// Destination is document-space rect mapped into atlas pixel coords.
|
||||
let dst = skia::Rect::from_xywh(
|
||||
(clipped_doc_rect.left - self.origin.x) * self.scale,
|
||||
(clipped_doc_rect.top - self.origin.y) * self.scale,
|
||||
clipped_doc_rect.width() * self.scale,
|
||||
clipped_doc_rect.height() * self.scale,
|
||||
);
|
||||
|
||||
// Compute source rect in tile_image pixel coordinates.
|
||||
let img_w = tile_image.width() as f32;
|
||||
let img_h = tile_image.height() as f32;
|
||||
let tw = tile_doc_rect.width().max(1.0);
|
||||
let th = tile_doc_rect.height().max(1.0);
|
||||
|
||||
let sx = ((clipped_doc_rect.left - tile_doc_rect.left) / tw) * img_w;
|
||||
let sy = ((clipped_doc_rect.top - tile_doc_rect.top) / th) * img_h;
|
||||
let sw = (clipped_doc_rect.width() / tw) * img_w;
|
||||
let sh = (clipped_doc_rect.height() / th) * img_h;
|
||||
let src = skia::Rect::from_xywh(sx, sy, sw, sh);
|
||||
|
||||
self.surface.canvas().draw_image_rect(
|
||||
tile_image,
|
||||
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||
dst,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears a doc-space rect from the atlas **without** growing it.
|
||||
///
|
||||
/// Unlike [`clear_doc_rect_in_atlas`], this method clips `doc_rect` to the
|
||||
/// current atlas bounds and skips silently if there is no overlap. Use this
|
||||
/// when evicting stale shape content (e.g. before a drag re-render) where
|
||||
/// growing the atlas to accommodate an out-of-range rect would be wasteful.
|
||||
pub fn clear_doc_rect_in_atlas_clipped(&mut self, doc_rect: skia::Rect) {
|
||||
if self.is_empty() || doc_rect.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let scale = self.scale.max(0.01);
|
||||
let atlas_doc_right = self.origin.x + (self.size.width as f32) / scale;
|
||||
let atlas_doc_bottom = self.origin.y + (self.size.height as f32) / scale;
|
||||
|
||||
// Intersect with current atlas bounds in doc space.
|
||||
let mut clipped = doc_rect;
|
||||
let atlas_bounds = skia::Rect::from_ltrb(
|
||||
self.origin.x,
|
||||
self.origin.y,
|
||||
atlas_doc_right,
|
||||
atlas_doc_bottom,
|
||||
);
|
||||
if !clipped.intersect(atlas_bounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply doc_bounds clamping.
|
||||
if let Some(bounds) = self.doc_bounds {
|
||||
if !clipped.intersect(bounds) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if clipped.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dst = skia::Rect::from_xywh(
|
||||
(clipped.left - self.origin.x) * scale,
|
||||
(clipped.top - self.origin.y) * scale,
|
||||
clipped.width() * scale,
|
||||
clipped.height() * scale,
|
||||
);
|
||||
|
||||
let canvas = self.surface.canvas();
|
||||
canvas.save();
|
||||
canvas.clip_rect(dst, None, true);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
pub fn clear_doc_rect_in_atlas(
|
||||
&mut self,
|
||||
gpu_state: &mut GpuState,
|
||||
doc_rect: skia::Rect,
|
||||
) -> Result<()> {
|
||||
let doc_rect = self.clamp_doc_rect_to_bounds(doc_rect);
|
||||
if doc_rect.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.ensure_atlas_contains(gpu_state, doc_rect)?;
|
||||
|
||||
// Destination is document-space rect mapped into atlas pixel coords.
|
||||
let dst = skia::Rect::from_xywh(
|
||||
(doc_rect.left - self.origin.x) * self.scale,
|
||||
(doc_rect.top - self.origin.y) * self.scale,
|
||||
doc_rect.width() * self.scale,
|
||||
doc_rect.height() * self.scale,
|
||||
);
|
||||
|
||||
let canvas = self.surface.canvas();
|
||||
canvas.save();
|
||||
canvas.clip_rect(dst, None, true);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
canvas.restore();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the last atlas region written by `tile` (if any).
|
||||
///
|
||||
/// This avoids clearing the entire logical tile rect which, at very low
|
||||
/// zoom levels, can be enormous in document space and would unnecessarily
|
||||
/// grow / rescale the atlas.
|
||||
pub fn clear_tile_in_atlas(&mut self, gpu_state: &mut GpuState, tile: Tile) -> Result<()> {
|
||||
if let Some(doc_rect) = self.tile_doc_rects.remove(&tile) {
|
||||
self.clear_doc_rect_in_atlas(gpu_state, doc_rect)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a snapshot of the atlas together with its scale and origin, so the
|
||||
/// caller can take it **once** per `rebuild_backbuffer_crop_cachef` and share it
|
||||
/// across all shapes that need the tile/atlas fallback path — avoiding an
|
||||
/// `image_snapshot` (and potential GPU flush) per shape.
|
||||
pub fn snapshot_for_drag_crop(&mut self) -> Option<(skia::Image, f32, skia::Point)> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
self.surface.image_snapshot(),
|
||||
self.scale.max(0.01),
|
||||
self.origin,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Surfaces {
|
||||
// is the final destination surface, the one that it is represented in the canvas element.
|
||||
target: skia::Surface,
|
||||
filter: skia::Surface,
|
||||
cache: skia::Surface,
|
||||
// keeps the current render
|
||||
current: skia::Surface,
|
||||
// keeps the current shape's fills
|
||||
@ -427,9 +72,7 @@ pub struct Surfaces {
|
||||
// Atlas used to keep tiles.
|
||||
tile_atlas: skia::Surface,
|
||||
tile_atlas_image: Option<skia::Image>,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
pub atlas: DocAtlas,
|
||||
sampling_options: skia::SamplingOptions,
|
||||
atlas_sampling_options: skia::SamplingOptions,
|
||||
pub margins: skia::ISize,
|
||||
@ -456,7 +99,6 @@ impl Surfaces {
|
||||
|
||||
let target = gpu_state.create_target_surface(width, height)?;
|
||||
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims)?;
|
||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?;
|
||||
let backbuffer =
|
||||
gpu_state.create_surface_with_dimensions("backbuffer".to_string(), width, height)?;
|
||||
|
||||
@ -487,11 +129,9 @@ impl Surfaces {
|
||||
|
||||
// 512, why not?
|
||||
let tiles = TileTextureCache::new(tile_atlas.width(), 512);
|
||||
let atlas = DocAtlas::try_new()?;
|
||||
Ok(Self {
|
||||
target,
|
||||
filter,
|
||||
cache,
|
||||
current,
|
||||
drop_shadows,
|
||||
inner_shadows,
|
||||
@ -505,7 +145,6 @@ impl Surfaces {
|
||||
tile_atlas,
|
||||
tile_atlas_image: None,
|
||||
tiles,
|
||||
atlas,
|
||||
sampling_options,
|
||||
atlas_sampling_options: skia::SamplingOptions::new(
|
||||
skia::FilterMode::Nearest,
|
||||
@ -554,44 +193,6 @@ impl Surfaces {
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw the persistent atlas onto the backbuffer using the current viewbox transform.
|
||||
/// Intended for fast pan/zoom-out previews (avoids per-tile composition).
|
||||
/// Clears Backbuffer to `background` first so atlas-uncovered regions don't
|
||||
/// show stale content when the atlas only partially covers the viewport.
|
||||
pub fn draw_atlas_to_backbuffer(&mut self, viewbox: Viewbox, background: skia::Color) {
|
||||
if self.atlas.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas = self.backbuffer.canvas();
|
||||
canvas.save();
|
||||
canvas.reset_matrix();
|
||||
let size = canvas.base_layer_size();
|
||||
canvas.clip_rect(
|
||||
skia::Rect::from_xywh(0.0, 0.0, size.width as f32, size.height as f32),
|
||||
None,
|
||||
true,
|
||||
);
|
||||
canvas.clear(background);
|
||||
|
||||
let s = viewbox.get_scale();
|
||||
let scale = self.atlas.scale.max(0.01);
|
||||
canvas.translate((
|
||||
(self.atlas.origin.x + viewbox.pan.x) * s,
|
||||
(self.atlas.origin.y + viewbox.pan.y) * s,
|
||||
));
|
||||
canvas.scale((s / scale, s / scale));
|
||||
|
||||
self.atlas.surface.draw(
|
||||
canvas,
|
||||
(0.0, 0.0),
|
||||
self.sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
pub fn margins(&self) -> skia::ISize {
|
||||
self.margins
|
||||
}
|
||||
@ -625,7 +226,7 @@ impl Surfaces {
|
||||
pub fn base64_snapshot_rect(
|
||||
&mut self,
|
||||
id: SurfaceId,
|
||||
irect: skia::IRect,
|
||||
irect: IRect,
|
||||
) -> Result<Option<String>> {
|
||||
let surface = self.get_mut(id);
|
||||
if let Some(image) = surface.image_snapshot_with_bounds(irect) {
|
||||
@ -641,7 +242,7 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot_rect(&mut self, id: SurfaceId, irect: skia::IRect) -> Option<skia::Image> {
|
||||
pub fn snapshot_rect(&mut self, id: SurfaceId, irect: IRect) -> Option<skia::Image> {
|
||||
let surface = self.get_mut(id);
|
||||
surface.image_snapshot_with_bounds(irect)
|
||||
}
|
||||
@ -718,22 +319,6 @@ impl Surfaces {
|
||||
);
|
||||
}
|
||||
|
||||
/// Draws the cache surface directly to the backbuffer canvas.
|
||||
/// This avoids creating an intermediate snapshot, reducing GPU stalls.
|
||||
pub fn draw_cache_to_backbuffer(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.cache.draw(
|
||||
self.backbuffer.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn cache_dimensions(&self) -> skia::ISize {
|
||||
skia::ISize::new(self.cache.width(), self.cache.height())
|
||||
}
|
||||
|
||||
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
||||
performance::begin_measure!("apply_mut::flags");
|
||||
if ids & SurfaceId::Target as u32 != 0 {
|
||||
@ -745,9 +330,6 @@ impl Surfaces {
|
||||
if ids & SurfaceId::Current as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Current));
|
||||
}
|
||||
if ids & SurfaceId::Cache as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Cache));
|
||||
}
|
||||
if ids & SurfaceId::Backbuffer as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Backbuffer));
|
||||
}
|
||||
@ -781,7 +363,7 @@ impl Surfaces {
|
||||
|
||||
pub fn get_render_context_translation(
|
||||
&mut self,
|
||||
render_area: skia::Rect,
|
||||
render_area: Rect,
|
||||
scale: f32,
|
||||
) -> (f32, f32) {
|
||||
(
|
||||
@ -790,7 +372,7 @@ impl Surfaces {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
|
||||
pub fn update_render_context(&mut self, render_area: Rect, scale: f32) {
|
||||
let translation = self.get_render_context_translation(render_area, scale);
|
||||
|
||||
// When context changes (zoom/pan/tile), clear all render surfaces first
|
||||
@ -821,7 +403,6 @@ impl Surfaces {
|
||||
match id {
|
||||
SurfaceId::Target => &mut self.target,
|
||||
SurfaceId::Filter => &mut self.filter,
|
||||
SurfaceId::Cache => &mut self.cache,
|
||||
SurfaceId::Backbuffer => &mut self.backbuffer,
|
||||
SurfaceId::Current => &mut self.current,
|
||||
SurfaceId::DropShadows => &mut self.drop_shadows,
|
||||
@ -832,7 +413,6 @@ impl Surfaces {
|
||||
SurfaceId::Debug => &mut self.debug,
|
||||
SurfaceId::UI => &mut self.ui,
|
||||
SurfaceId::Export => &mut self.export,
|
||||
SurfaceId::Atlas => &mut self.atlas.surface,
|
||||
SurfaceId::TileAtlas => &mut self.tile_atlas,
|
||||
}
|
||||
}
|
||||
@ -842,7 +422,6 @@ impl Surfaces {
|
||||
match id {
|
||||
SurfaceId::Target => &self.target,
|
||||
SurfaceId::Filter => &self.filter,
|
||||
SurfaceId::Cache => &self.cache,
|
||||
SurfaceId::Backbuffer => &self.backbuffer,
|
||||
SurfaceId::Current => &self.current,
|
||||
SurfaceId::DropShadows => &self.drop_shadows,
|
||||
@ -853,7 +432,6 @@ impl Surfaces {
|
||||
SurfaceId::Debug => &self.debug,
|
||||
SurfaceId::UI => &self.ui,
|
||||
SurfaceId::Export => &self.export,
|
||||
SurfaceId::Atlas => &self.atlas.surface,
|
||||
SurfaceId::TileAtlas => &self.tile_atlas,
|
||||
}
|
||||
}
|
||||
@ -940,41 +518,6 @@ impl Surfaces {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize_cache(
|
||||
&mut self,
|
||||
cache_dims: skia::ISize,
|
||||
interest_area_threshold: i32,
|
||||
) -> Result<()> {
|
||||
self.cache = self
|
||||
.target
|
||||
.new_surface_with_dimensions(cache_dims)
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
self.cache.canvas().reset_matrix();
|
||||
self.cache.canvas().translate((
|
||||
(interest_area_threshold * TILE_SIZE) as f32,
|
||||
(interest_area_threshold * TILE_SIZE) as f32,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize_cache_from_viewbox(
|
||||
&mut self,
|
||||
viewbox: &Viewbox,
|
||||
cached_viewbox: &Viewbox,
|
||||
interest_area_threshold: i32,
|
||||
) -> Result<()> {
|
||||
let viewbox_cache_size = get_cache_size(viewbox, interest_area_threshold);
|
||||
let cached_viewbox_cache_size = get_cache_size(cached_viewbox, interest_area_threshold);
|
||||
// Only resize cache if the new size is larger than the cached size
|
||||
// This avoids unnecessary surface recreations when the cache size decreases
|
||||
if viewbox_cache_size.width > cached_viewbox_cache_size.width
|
||||
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
|
||||
{
|
||||
return self.resize_cache(viewbox_cache_size, interest_area_threshold);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw_rect_to(
|
||||
&mut self,
|
||||
id: SurfaceId,
|
||||
@ -1073,7 +616,7 @@ impl Surfaces {
|
||||
self.backbuffer.canvas().clear(color);
|
||||
}
|
||||
|
||||
pub fn clear_backbuffer_rect(&mut self, rect: skia::Rect, color: skia::Color) {
|
||||
pub fn clear_backbuffer_rect(&mut self, rect: Rect, color: skia::Color) {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color(color);
|
||||
self.backbuffer.canvas().draw_rect(rect, &paint);
|
||||
@ -1150,51 +693,21 @@ impl Surfaces {
|
||||
self.clear_all_dirty();
|
||||
}
|
||||
|
||||
/// Clears the whole cache surface without disturbing its configured transform.
|
||||
pub fn clear_cache(&mut self, color: skia::Color) {
|
||||
let canvas = self.cache.canvas();
|
||||
canvas.save();
|
||||
canvas.reset_matrix();
|
||||
canvas.clear(color);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
pub fn draw_current_tile_into_tile_atlas(
|
||||
&mut self,
|
||||
tile_viewbox: &TileViewbox,
|
||||
tile: &Tile,
|
||||
tile_rect: &skia::Rect,
|
||||
skip_cache_surface: bool,
|
||||
tile_doc_rect: skia::Rect,
|
||||
) {
|
||||
let gpu_state = get_gpu_state();
|
||||
let rect = TILE_DRAWABLE_RECT;
|
||||
|
||||
let tile_image_opt = self.current.image_snapshot_with_bounds(rect);
|
||||
if let Some(tile_image) = tile_image_opt {
|
||||
if !skip_cache_surface {
|
||||
// Draw to cache surface for render_from_cache
|
||||
self.cache.canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
None,
|
||||
tile_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
|
||||
// Incrementally update persistent 1:1 atlas in document space.
|
||||
// `tile_doc_rect` is in world/document coordinates (1 unit == 1 px at 100%).
|
||||
let _ = self
|
||||
.atlas
|
||||
.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect);
|
||||
self.atlas.tile_doc_rects.insert(*tile, tile_doc_rect);
|
||||
|
||||
// Draws current tile into tile atlas
|
||||
if let Some(tile_ref) = self.tiles.add(tile_viewbox, tile) {
|
||||
self.tile_atlas.canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
None,
|
||||
tile_ref.rect,
|
||||
tile_ref,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
@ -1205,181 +718,11 @@ impl Surfaces {
|
||||
self.tiles.has(tile)
|
||||
}
|
||||
|
||||
/// Builds a 1:1 workspace-pixel snapshot for `src_doc_bounds` / `src_irect` into
|
||||
/// `scratch`, then returns the sub-region `[0, out_w) × [0, out_h)` as an image.
|
||||
///
|
||||
/// `scratch` must be at least `out_w × out_h` pixels — the caller is responsible
|
||||
/// for allocating (and **reusing across shapes**) a surface large enough to hold
|
||||
/// the largest window needed in one `rebuild_backbuffer_crop_cache` pass.
|
||||
///
|
||||
/// `atlas_snap` is a pre-snapshotted view of the persistent atlas produced by
|
||||
/// [`Surfaces::atlas.snapshot_for_drag_crop`]; pass `None` when no atlas exists.
|
||||
///
|
||||
/// For each tile cell intersecting `src_doc_bounds`: draws from
|
||||
/// [`TileTextureCache`] when present; otherwise samples the atlas.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn try_snapshot_doc_rect_from_tiles_and_atlas(
|
||||
&mut self,
|
||||
scratch: &mut skia::Surface,
|
||||
atlas_snap: Option<&(skia::Image, f32, skia::Point)>,
|
||||
src_doc_bounds: skia::Rect,
|
||||
src_irect: IRect,
|
||||
out_w: i32,
|
||||
out_h: i32,
|
||||
vb_left: f32,
|
||||
vb_top: f32,
|
||||
scale: f32,
|
||||
) -> Option<skia::Image> {
|
||||
if out_w <= 0 || out_h <= 0 || src_doc_bounds.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let canvas = scratch.canvas();
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
let tile_size = tiles::get_tile_size(scale);
|
||||
let tr = tiles::get_tiles_for_rect(src_doc_bounds, tile_size);
|
||||
let ix0 = src_irect.left as f32;
|
||||
let iy0 = src_irect.top as f32;
|
||||
let paint = skia::Paint::default();
|
||||
|
||||
for ty in tr.y1()..=tr.y2() {
|
||||
for tx in tr.x1()..=tr.x2() {
|
||||
let tile = Tile(tx, ty);
|
||||
let tile_doc = tiles::get_tile_rect(tile, scale);
|
||||
let mut clip_doc = tile_doc;
|
||||
if !clip_doc.intersect(src_doc_bounds) || clip_doc.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dst = skia::Rect::from_ltrb(
|
||||
(clip_doc.left - vb_left) * scale - ix0,
|
||||
(clip_doc.top - vb_top) * scale - iy0,
|
||||
(clip_doc.right - vb_left) * scale - ix0,
|
||||
(clip_doc.bottom - vb_top) * scale - iy0,
|
||||
);
|
||||
|
||||
if let Some(tile_ref) = self.tiles.get(tile) {
|
||||
let bounds = skia::IRect::from_ltrb(
|
||||
tile_ref.rect.left as i32,
|
||||
tile_ref.rect.top as i32,
|
||||
tile_ref.rect.right as i32,
|
||||
tile_ref.rect.bottom as i32,
|
||||
);
|
||||
let Some(tile_image) = self.tile_atlas.image_snapshot_with_bounds(bounds)
|
||||
else {
|
||||
panic!("Cannot retrieve tile image");
|
||||
};
|
||||
let iw = tile_image.width() as f32;
|
||||
let ih = tile_image.height() as f32;
|
||||
let td_w = tile_doc.width().max(1e-6);
|
||||
let td_h = tile_doc.height().max(1e-6);
|
||||
|
||||
let src = skia::Rect::from_ltrb(
|
||||
((clip_doc.left - tile_doc.left) / td_w) * iw,
|
||||
((clip_doc.top - tile_doc.top) / td_h) * ih,
|
||||
((clip_doc.right - tile_doc.left) / td_w) * iw,
|
||||
((clip_doc.bottom - tile_doc.top) / td_h) * ih,
|
||||
);
|
||||
|
||||
canvas.draw_image_rect(
|
||||
tile_image,
|
||||
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||
dst,
|
||||
&paint,
|
||||
);
|
||||
} else {
|
||||
let snap = atlas_snap?;
|
||||
let (atlas, a_scale, origin) = (&snap.0, snap.1, snap.2);
|
||||
let sx = (clip_doc.left - origin.x) * a_scale;
|
||||
let sy = (clip_doc.top - origin.y) * a_scale;
|
||||
let sw = clip_doc.width() * a_scale;
|
||||
let sh = clip_doc.height() * a_scale;
|
||||
if sw <= 0.0 || sh <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let src = skia::Rect::from_xywh(sx, sy, sw, sh);
|
||||
canvas.draw_image_rect(
|
||||
atlas,
|
||||
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||
dst,
|
||||
&paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scratch.image_snapshot_with_bounds(IRect::new(0, 0, out_w, out_h))
|
||||
}
|
||||
|
||||
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
|
||||
let gpu_state = get_gpu_state();
|
||||
// Mark tile as invalid
|
||||
// Old content stays visible until new tile overwrites it atomically,
|
||||
// preventing flickering during tile re-renders.
|
||||
self.tiles.remove(tile);
|
||||
// Also clear the corresponding region in the persistent atlas to avoid
|
||||
// leaving stale pixels when shapes move/delete.
|
||||
let _ = self.atlas.clear_tile_in_atlas(gpu_state, tile);
|
||||
}
|
||||
|
||||
/// Draws the current tile directly to the backbuffer and cache surfaces without
|
||||
/// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't
|
||||
/// populate the tile texture cache (suitable for one-shot renders like tests).
|
||||
pub fn draw_current_tile_into_backbuffer(
|
||||
&mut self,
|
||||
tile_rect: &skia::Rect,
|
||||
_color: skia::Color,
|
||||
draw_on_cache: DrawOnCache,
|
||||
) {
|
||||
let sampling_options = self.sampling_options;
|
||||
let src_rect = IRect::from_xywh(
|
||||
self.margins.width,
|
||||
self.margins.height,
|
||||
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
|
||||
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||
);
|
||||
let src_rect_f = skia::Rect::from(src_rect);
|
||||
|
||||
let backbuffer_canvas = self.backbuffer.canvas();
|
||||
|
||||
// Draw background
|
||||
// let mut paint = skia::Paint::default();
|
||||
// paint.set_color(color);
|
||||
// backbuffer_canvas.draw_rect(tile_rect, &paint);
|
||||
|
||||
// Draw current surface directly to target (no snapshot)
|
||||
self.current.draw(
|
||||
backbuffer_canvas,
|
||||
(
|
||||
tile_rect.left - src_rect_f.left,
|
||||
tile_rect.top - src_rect_f.top,
|
||||
),
|
||||
sampling_options,
|
||||
None,
|
||||
);
|
||||
|
||||
// Also draw to cache for render_from_cache
|
||||
if draw_on_cache == DrawOnCache::Yes {
|
||||
self.current.draw(
|
||||
self.cache.canvas(),
|
||||
(
|
||||
tile_rect.left - src_rect_f.left,
|
||||
tile_rect.top - src_rect_f.top,
|
||||
),
|
||||
sampling_options,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Full cache reset: clears both the tile texture cache and the cache canvas.
|
||||
/// Used by `rebuild_tiles` (full rebuild). For shallow rebuilds that preserve
|
||||
/// the cache canvas for scaled previews, use `invalidate_tile_cache` instead.
|
||||
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
||||
self.tiles.clear();
|
||||
self.atlas.tile_doc_rects.clear();
|
||||
self.cache.canvas().clear(color);
|
||||
}
|
||||
|
||||
/// Invalidate the tile texture cache without clearing the cache canvas.
|
||||
@ -1388,7 +731,6 @@ impl Surfaces {
|
||||
/// content while new tiles are being rendered.
|
||||
pub fn invalidate_tile_cache(&mut self) {
|
||||
self.tiles.clear();
|
||||
self.atlas.tile_doc_rects.clear();
|
||||
self.tile_atlas_image = None;
|
||||
}
|
||||
|
||||
@ -1396,7 +738,7 @@ impl Surfaces {
|
||||
self.tiles.gc();
|
||||
}
|
||||
|
||||
pub fn resize_export_surface(&mut self, scale: f32, rect: skia::Rect) {
|
||||
pub fn resize_export_surface(&mut self, scale: f32, rect: Rect) {
|
||||
let target_w = (scale * rect.width()).ceil() as i32;
|
||||
let target_h = (scale * rect.height()).ceil() as i32;
|
||||
|
||||
@ -1448,22 +790,7 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TileAtlasTextureRef {
|
||||
pub index: usize,
|
||||
pub rect: skia::Rect,
|
||||
}
|
||||
|
||||
impl TileAtlasTextureRef {
|
||||
pub fn new(index: usize, rect: skia::Rect) -> Self {
|
||||
Self { index, rect }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TileAtlasTextureProvider {
|
||||
pub index: usize,
|
||||
pub length: usize,
|
||||
pub in_use: Vec<bool>,
|
||||
pub rects: Vec<Rect>,
|
||||
}
|
||||
|
||||
@ -1480,36 +807,16 @@ impl TileAtlasTextureProvider {
|
||||
rects.push(Rect::new(left, top, right, bottom));
|
||||
}
|
||||
Self {
|
||||
index: 0,
|
||||
length: length as usize,
|
||||
in_use: vec![false; length as usize],
|
||||
rects,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self) -> Option<TileAtlasTextureRef> {
|
||||
let start = self.index;
|
||||
loop {
|
||||
if !self.in_use[self.index] {
|
||||
self.in_use[self.index] = true;
|
||||
return Some(TileAtlasTextureRef::new(self.index, self.rects[self.index]));
|
||||
}
|
||||
|
||||
self.index = (self.index + 1) % self.length;
|
||||
if self.index == start {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
pub fn allocate(&mut self) -> Option<Rect> {
|
||||
self.rects.pop()
|
||||
}
|
||||
|
||||
pub fn deallocate(&mut self, reference: TileAtlasTextureRef) -> bool {
|
||||
// In this case the user of the provider it's trying to release
|
||||
// a reference already freed.
|
||||
if !self.in_use[reference.index] {
|
||||
return false;
|
||||
}
|
||||
self.in_use[reference.index] = false;
|
||||
self.index = reference.index;
|
||||
pub fn deallocate(&mut self, rect: Rect) -> bool {
|
||||
self.rects.push(rect);
|
||||
true
|
||||
}
|
||||
}
|
||||
@ -1519,8 +826,8 @@ pub struct TileTextureCache {
|
||||
is_updated: bool,
|
||||
provider: TileAtlasTextureProvider,
|
||||
transforms: Vec<skia::RSXform>,
|
||||
textures: Vec<skia::Rect>,
|
||||
grid: HashMap<Tile, TileAtlasTextureRef>,
|
||||
textures: Vec<Rect>,
|
||||
grid: HashMap<Tile, Rect>,
|
||||
removed: HashSet<Tile>,
|
||||
}
|
||||
|
||||
@ -1587,7 +894,7 @@ impl TileTextureCache {
|
||||
if self.textures.len() != tile_viewbox.visible_rect.len() as usize {
|
||||
self.textures.resize(
|
||||
tile_viewbox.visible_rect.len() as usize,
|
||||
skia::Rect::new_empty(),
|
||||
Rect::new_empty(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1613,10 +920,10 @@ impl TileTextureCache {
|
||||
self.transforms[index].ty = y as f32 * self.tile_size - offset.y;
|
||||
|
||||
self.textures[index].set_ltrb(
|
||||
tile_ref.rect.left,
|
||||
tile_ref.rect.top,
|
||||
tile_ref.rect.right,
|
||||
tile_ref.rect.bottom,
|
||||
tile_ref.left,
|
||||
tile_ref.top,
|
||||
tile_ref.right,
|
||||
tile_ref.bottom,
|
||||
);
|
||||
|
||||
index += 1;
|
||||
@ -1628,7 +935,7 @@ impl TileTextureCache {
|
||||
self.grid.contains_key(&tile) && !self.removed.contains(&tile)
|
||||
}
|
||||
|
||||
pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> Option<TileAtlasTextureRef> {
|
||||
pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> Option<Rect> {
|
||||
if self.grid.len() > TEXTURES_CACHE_CAPACITY {
|
||||
// First we try to remove the obsolete tiles.
|
||||
self.gc();
|
||||
@ -1643,7 +950,7 @@ impl TileTextureCache {
|
||||
|
||||
let tile_ref = self.provider.allocate()?;
|
||||
|
||||
self.grid.insert(*tile, tile_ref.clone());
|
||||
self.grid.insert(*tile, tile_ref);
|
||||
|
||||
if self.removed.contains(tile) {
|
||||
self.removed.remove(tile);
|
||||
@ -1653,19 +960,19 @@ impl TileTextureCache {
|
||||
Some(tile_ref)
|
||||
}
|
||||
|
||||
pub fn get(&mut self, tile: Tile) -> Option<&TileAtlasTextureRef> {
|
||||
if self.removed.contains(&tile) {
|
||||
return None;
|
||||
}
|
||||
self.grid.get(&tile)
|
||||
}
|
||||
// pub fn get(&mut self, tile: Tile) -> Option<&Rect> {
|
||||
// if self.removed.contains(&tile) {
|
||||
// return None;
|
||||
// }
|
||||
// self.grid.get(&tile)
|
||||
// }
|
||||
|
||||
pub fn remove(&mut self, tile: Tile) {
|
||||
if let Some(tile_ref) = self.grid.get(&tile) {
|
||||
if tile_ref.index < self.textures.len() {
|
||||
self.textures[tile_ref.index].set_empty();
|
||||
}
|
||||
}
|
||||
// if let Some(tile_ref) = self.grid.get(&tile) {
|
||||
// if tile_ref.index < self.textures.len() {
|
||||
// self.textures[tile_ref.index].set_empty();
|
||||
// }
|
||||
// }
|
||||
self.is_updated = true;
|
||||
self.removed.insert(tile);
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ pub fn precompute_viewer_visible_set(render_state: &mut RenderState, tree: Shape
|
||||
let Some(ref include) = render_state.include_filter else {
|
||||
return;
|
||||
};
|
||||
let mut visible = include.clone();
|
||||
let mut visible: HashSet<Uuid> = include.clone();
|
||||
for id in include.iter() {
|
||||
let mut current_id = id;
|
||||
while let Some(raw) = tree.get_raw(current_id) {
|
||||
|
||||
@ -1382,108 +1382,6 @@ impl Shape {
|
||||
}
|
||||
}
|
||||
|
||||
/// Same `concat` applied around [`center`](Self::center) as in `render_shape` (non-text branch).
|
||||
fn shape_document_transform(&self) -> Matrix {
|
||||
let c = self.center();
|
||||
let mut m = self.transform;
|
||||
m.post_translate(c);
|
||||
m.pre_translate(-c);
|
||||
m
|
||||
}
|
||||
|
||||
/// Fill silhouette only, document space (matches fill rendering).
|
||||
fn drag_crop_fill_clip_path_skia(&self) -> Option<skia::Path> {
|
||||
match &self.shape_type {
|
||||
Type::Rect(r) => {
|
||||
let p = Path::new(shape_to_path::rect_segments(self, r.corners));
|
||||
Some(p.to_skia_path(self.svg_attrs.as_ref()))
|
||||
}
|
||||
Type::Circle => {
|
||||
let p = Path::new(shape_to_path::circle_segments(self));
|
||||
Some(p.to_skia_path(self.svg_attrs.as_ref()))
|
||||
}
|
||||
Type::Path(_) | Type::Bool(_) => {
|
||||
let sk = self.get_skia_path()?;
|
||||
Some(sk.make_transform(&self.shape_document_transform()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this shape may use the backbuffer crop fast path during interactive drag.
|
||||
///
|
||||
/// Conservative: only effects and fills that match what we snapshot and clip in
|
||||
/// [`drag_crop_clip_path`](Self::drag_crop_clip_path). Text is never safe (glyph layout,
|
||||
/// no `drag_crop_clip_path`).
|
||||
pub fn is_safe_for_drag_crop_cache(&self, shapes_pool: ShapesPoolRef) -> bool {
|
||||
if matches!(self.shape_type, Type::Text(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a frame shows overflow (clip_content=false) and its visible content exceeds the
|
||||
// frame bounds, a cached crop anchored to the frame can easily become incorrect while
|
||||
// moving (children can extend beyond selrect). Be conservative and render live.
|
||||
if matches!(self.shape_type, Type::Frame(_)) && !self.clip_content {
|
||||
let extrect = self.extrect(shapes_pool, 1.0);
|
||||
let sr = self.selrect;
|
||||
let exceeds = extrect.left < sr.left
|
||||
|| extrect.top < sr.top
|
||||
|| extrect.right > sr.right
|
||||
|| extrect.bottom > sr.bottom;
|
||||
if exceeds {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
self.blur.is_none()
|
||||
&& self.background_blur.is_none()
|
||||
&& self.shadows.is_empty()
|
||||
&& (self.opacity - 1.0).abs() <= 1e-4
|
||||
&& self.blend_mode().0 == skia::BlendMode::SrcOver
|
||||
}
|
||||
|
||||
/// Fill + visible strokes in **document space** for clipping interactive drag textures.
|
||||
///
|
||||
/// The backbuffer crop uses an axis-aligned `extrect`; we clip the blit so backdrop pixels
|
||||
/// outside the real silhouette (fill and stroke regions) are not smeared. Strokes use
|
||||
/// [`stroke_to_path`](stroke_to_path) like the main renderer, then union with the fill path.
|
||||
pub fn drag_crop_clip_path(&self) -> Option<skia::Path> {
|
||||
let mut acc = self.drag_crop_fill_clip_path_skia()?;
|
||||
if !self.has_visible_strokes() {
|
||||
return Some(acc);
|
||||
}
|
||||
|
||||
let shape_path = match &self.shape_type {
|
||||
Type::Rect(r) => Path::new(shape_to_path::rect_segments(self, r.corners)),
|
||||
Type::Circle => Path::new(shape_to_path::circle_segments(self)),
|
||||
Type::Path(_) | Type::Bool(_) => self.shape_type.path()?.clone(),
|
||||
_ => return Some(acc),
|
||||
};
|
||||
|
||||
let path_transform = self.to_path_transform();
|
||||
let apply_doc_transform = path_transform.is_some();
|
||||
|
||||
for stroke in self.visible_strokes() {
|
||||
let Some(stroke_region) = stroke_to_path(
|
||||
stroke,
|
||||
&shape_path,
|
||||
path_transform.as_ref(),
|
||||
&self.selrect,
|
||||
self.svg_attrs.as_ref(),
|
||||
true,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let mut sk = stroke_region.to_skia_path(self.svg_attrs.as_ref());
|
||||
if apply_doc_transform {
|
||||
sk = sk.make_transform(&self.shape_document_transform());
|
||||
}
|
||||
acc = acc.op(&sk, skia::PathOp::Union).unwrap_or(acc);
|
||||
}
|
||||
|
||||
Some(acc)
|
||||
}
|
||||
|
||||
fn transform_selrect(&mut self, transform: &Matrix) {
|
||||
if math::is_move_only_matrix(transform) {
|
||||
let tx = transform.translate_x();
|
||||
|
||||
@ -65,9 +65,9 @@ impl State {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_from_cache(&mut self) {
|
||||
get_render_state().render_from_cache(&self.shapes);
|
||||
}
|
||||
// pub fn render_from_cache(&mut self) {
|
||||
// get_render_state().render_from_cache(&self.shapes);
|
||||
// }
|
||||
|
||||
pub fn render_ui_only(&mut self) {
|
||||
get_render_state().render_ui_only(&self.shapes);
|
||||
@ -102,16 +102,18 @@ impl State {
|
||||
|
||||
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
|
||||
// index so shapes are mapped to the correct tiles. We use
|
||||
// rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile
|
||||
// texture cache — otherwise cached tiles with shadows/blur would be
|
||||
// cleared and re-rendered in fast mode without effects.
|
||||
|
||||
render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
render_state.rebuild_tile_index(&self.shapes);
|
||||
if render_state.zoom_changed() {
|
||||
render_state.rebuild_tile_index(&self.shapes);
|
||||
render_state.surfaces.invalidate_tile_cache();
|
||||
}
|
||||
render_state.start_render_loop(None, &self.shapes, timestamp, false)
|
||||
render_state.start_render_loop(
|
||||
None,
|
||||
&self.shapes,
|
||||
timestamp,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
pub fn continue_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
|
||||
@ -30,16 +30,6 @@ impl Tile {
|
||||
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)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user