mirror of
https://github.com/penpot/penpot.git
synced 2026-08-08 05:48:50 +00:00
⚡ Present viewport before interest-area tile work
Defer the interest-ring until visible tiles are done, present via FrameType::ViewportReady, then fill interest on later Partial frames so large files show the viewport without waiting on off-screen work.
This commit is contained in:
parent
288193f3e7
commit
53e048bbae
@ -362,6 +362,19 @@
|
||||
(def ^:const FRAME_TYPE_NONE 0) ;; This type should never "leak".
|
||||
(def ^:const FRAME_TYPE_PARTIAL 1) ;; A frame needs more render calls to end.
|
||||
(def ^:const FRAME_TYPE_FULL 2) ;; A frame was full.
|
||||
(def ^:const FRAME_TYPE_VIEWPORT_READY 3) ;; Viewport presented; interest tiles may still be pending.
|
||||
|
||||
(defn- needs-more-render-frames?
|
||||
"True when WASM still has progressive tile work (visible or interest ring)."
|
||||
[]
|
||||
(or (= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
(= wasm/internal-frame-type FRAME_TYPE_VIEWPORT_READY)))
|
||||
|
||||
(defn- frame-presented-target?
|
||||
"True when this frame recomposited Target (full or early viewport present)."
|
||||
[]
|
||||
(not= wasm/internal-frame-type FRAME_TYPE_PARTIAL))
|
||||
|
||||
(def ^:const RENDER-FLAG-SYNC-TILES 4) ;; Rebuild tile index without ending fast mode (pan/zoom pause).
|
||||
|
||||
(defn- internal-render
|
||||
@ -371,7 +384,7 @@
|
||||
(internal-render timestamp wasm/internal-frame-type))
|
||||
([timestamp flags]
|
||||
(set! wasm/internal-frame-type (h/call wasm/internal-module "_render" timestamp flags))
|
||||
(when (= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
(when (needs-more-render-frames?)
|
||||
(request-render "frame-type-partial"))))
|
||||
|
||||
(defn- build-reload-payload
|
||||
@ -468,13 +481,13 @@
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-update-blink timestamp)
|
||||
;; Only repaint the overlay when this frame recomposited Target (a full
|
||||
;; frame). A partial frame is flushed but not presented — Target still
|
||||
;; shows the last presented frame with the overlay already on it — so
|
||||
;; repainting the translucent selection over it stacks another layer
|
||||
;; every progressive frame: it darkens, then snaps back when the final
|
||||
;; frame presents from the clean Backbuffer (the blink at the end of a
|
||||
;; zoom over a selection, gh-10709).
|
||||
(when (not= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
;; frame or early viewport present). A partial frame is flushed but not
|
||||
;; presented - Target still shows the last presented frame with the
|
||||
;; overlay already on it - so repainting the translucent selection over
|
||||
;; it stacks another layer every progressive frame: it darkens, then
|
||||
;; snaps back when the final frame presents from the clean Backbuffer
|
||||
;; (the blink at the end of a zoom over a selection, gh-10709).
|
||||
(when (frame-presented-target?)
|
||||
(text-editor/text-editor-render-overlay))
|
||||
;; Drain editor events. Only content/layout changes need a full shape
|
||||
;; re-render; selection/style changes are already reflected by the
|
||||
@ -1376,14 +1389,14 @@
|
||||
|
||||
(defn- render-text-editor-overlay-after-frame!
|
||||
"Repaint the overlay after a direct `internal-render`, but only when that
|
||||
render recomposited Target (a full frame). A partial frame is only flushed —
|
||||
Target keeps the last presented frame with the overlay already on it — so
|
||||
repainting the translucent selection then stacks another layer and it visibly
|
||||
darkens across the progressive frames before snapping back on the final
|
||||
present (the blink at the end of a zoom over a selection, gh-10709). The
|
||||
final full frame's own repaint keeps the overlay in place."
|
||||
render recomposited Target (a full frame or early viewport present). A partial
|
||||
frame is only flushed - Target keeps the last presented frame with the overlay
|
||||
already on it - so repainting the translucent selection then stacks another
|
||||
layer and it visibly darkens across the progressive frames before snapping
|
||||
back on the final present (the blink at the end of a zoom over a selection,
|
||||
gh-10709). The final full frame's own repaint keeps the overlay in place."
|
||||
[]
|
||||
(when (not= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
(when (frame-presented-target?)
|
||||
(render-text-editor-overlay-if-active!)))
|
||||
|
||||
(defn finalize-view-interaction!
|
||||
|
||||
@ -49,6 +49,10 @@ pub enum FrameType {
|
||||
None = 0,
|
||||
Partial = 1,
|
||||
Full = 2,
|
||||
/// Viewport tiles are presented; interest-ring work may still be pending.
|
||||
/// Frontend should keep requesting frames (like Partial) but may treat the
|
||||
/// Target as freshly composited (like Full) for overlays.
|
||||
ViewportReady = 3,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -417,6 +421,9 @@ pub(crate) struct RenderState {
|
||||
/// are skipped. A full skip made flush_and_submit very slow (Skia ops-task
|
||||
/// ordering); doing it per shape was wasted GPU work.
|
||||
pub drop_shadows_ops_warmed: bool,
|
||||
/// Visible tiles were already presented this pass; interest-ring fill may
|
||||
/// still be running. Final Full should not re-present.
|
||||
pub viewport_presented: bool,
|
||||
}
|
||||
|
||||
pub struct InteractiveDragCrop {
|
||||
@ -601,6 +608,7 @@ impl RenderState {
|
||||
backbuffer_crop_cache: HashMap::default(),
|
||||
tile_atlas_flushed: false,
|
||||
drop_shadows_ops_warmed: false,
|
||||
viewport_presented: false,
|
||||
})
|
||||
}
|
||||
|
||||
@ -2195,6 +2203,7 @@ impl RenderState {
|
||||
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
self.viewport_presented = false;
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
@ -2366,9 +2375,14 @@ impl RenderState {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, allow_stop)?;
|
||||
|
||||
// `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) {
|
||||
// presented (only flushed), so defer composition until the viewport is
|
||||
// ready and avoid re-snapshotting up to 4096² on every rAF during async
|
||||
// tile work.
|
||||
let should_compose = !self.options.is_interactive_transform()
|
||||
&& matches!(frame_type, FrameType::Full | FrameType::ViewportReady)
|
||||
&& !self.viewport_presented;
|
||||
|
||||
if should_compose {
|
||||
self.surfaces.draw_tile_atlas_to_backbuffer(
|
||||
&self.viewbox,
|
||||
&self.tile_viewbox,
|
||||
@ -2382,23 +2396,33 @@ impl RenderState {
|
||||
}
|
||||
FrameType::Partial => {
|
||||
// Drain tile GPU work (Current / tile atlas / cache) without
|
||||
// presenting Target and without re-snapshotting the tile atlas —
|
||||
// composition stays deferred to Full (`01fc3c3e7d`). A Backbuffer
|
||||
// flush alone left commands queued until present_frame's
|
||||
// flush_and_submit, which stalled the browser on large files.
|
||||
// presenting Target and without re-snapshotting the tile atlas.
|
||||
// Composition stays deferred until ViewportReady/Full.
|
||||
crate::get_gpu_state().context.flush_and_submit();
|
||||
}
|
||||
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.
|
||||
FrameType::ViewportReady => {
|
||||
// Visible tiles are done: present now so the user sees the
|
||||
// viewport without waiting for interest-ring pre-render.
|
||||
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);
|
||||
self.viewport_presented = true;
|
||||
wapi::notify_tiles_render_complete!();
|
||||
crate::get_gpu_state().context.flush_and_submit();
|
||||
}
|
||||
FrameType::Full => {
|
||||
if !self.viewport_presented {
|
||||
// A full-quality frame is now complete (no early viewport
|
||||
// present). Rebuild crop cache and present.
|
||||
if !self.options.is_fast_mode() && !self.options.is_interactive_transform() {
|
||||
self.rebuild_backbuffer_crop_cache(tree);
|
||||
}
|
||||
self.present_frame(tree);
|
||||
wapi::notify_tiles_render_complete!();
|
||||
}
|
||||
// If we already presented at ViewportReady, interest fill is
|
||||
// done; Target already shows the viewport.
|
||||
performance::end_measure!("render");
|
||||
}
|
||||
}
|
||||
@ -3776,8 +3800,18 @@ impl RenderState {
|
||||
flattened: false,
|
||||
}));
|
||||
} else {
|
||||
// If there are no more pending tiles, stop.
|
||||
should_stop = true;
|
||||
// Visible tiles finished. Promote deferred interest-ring work
|
||||
// so pan/zoom pre-render still happens, but yield first when
|
||||
// allowed so continue_render_loop can present the viewport.
|
||||
if self.pending_tiles.promote_deferred_interest() {
|
||||
if allow_stop {
|
||||
should_stop = true;
|
||||
}
|
||||
// Sync path (allow_stop=false): keep looping on interest
|
||||
// tiles in the same call without an early present.
|
||||
} else {
|
||||
should_stop = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3795,6 +3829,12 @@ impl RenderState {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
|
||||
// Visible done with interest still queued and we yielded: present
|
||||
// viewport now, keep Partial-like rAFs for the ring.
|
||||
if allow_stop && !self.pending_tiles.list.is_empty() {
|
||||
return Ok(FrameType::ViewportReady);
|
||||
}
|
||||
|
||||
Ok(FrameType::Full)
|
||||
}
|
||||
|
||||
|
||||
@ -323,6 +323,8 @@ pub struct PendingTiles {
|
||||
pub visible_uncached: Vec<Tile>,
|
||||
pub interest_cached: Vec<Tile>,
|
||||
pub interest_uncached: Vec<Tile>,
|
||||
/// Interest-ring tiles deferred until after the viewport has been presented.
|
||||
deferred_interest: Vec<Tile>,
|
||||
}
|
||||
|
||||
impl PendingTiles {
|
||||
@ -335,14 +337,16 @@ impl PendingTiles {
|
||||
visible_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
interest_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
interest_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
deferred_interest: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces, only_visible: bool) {
|
||||
self.list.clear();
|
||||
self.deferred_interest.clear();
|
||||
|
||||
// During interactive transform, skip the interest-area ring
|
||||
// entirely — the user is dragging, every rAF is on the critical
|
||||
// entirely: the user is dragging, every rAF is on the critical
|
||||
// path, and pre-rendering tiles outside the viewport is wasted
|
||||
// work that just gets evicted on the next pointer move. The ring
|
||||
// is repopulated naturally on gesture end / on idle rAFs.
|
||||
@ -394,10 +398,29 @@ impl PendingTiles {
|
||||
}
|
||||
}
|
||||
|
||||
self.list.extend(self.interest_uncached.iter());
|
||||
self.list.extend(self.interest_cached.iter());
|
||||
self.list.extend(self.visible_uncached.iter());
|
||||
self.list.extend(self.visible_cached.iter());
|
||||
// Visible tiles first. Interest-ring work is deferred so we can present
|
||||
// as soon as the viewport is ready (see `promote_deferred_interest`).
|
||||
// Interactive/`only_visible` already excludes the ring from `tile_rect`.
|
||||
if only_visible {
|
||||
self.list.extend(self.visible_uncached.iter());
|
||||
self.list.extend(self.visible_cached.iter());
|
||||
} else {
|
||||
self.deferred_interest
|
||||
.extend(self.interest_uncached.iter());
|
||||
self.deferred_interest.extend(self.interest_cached.iter());
|
||||
self.list.extend(self.visible_uncached.iter());
|
||||
self.list.extend(self.visible_cached.iter());
|
||||
}
|
||||
}
|
||||
|
||||
/// Move deferred interest-ring tiles onto the pending list.
|
||||
/// Returns true when there is interest work left to do.
|
||||
pub fn promote_deferred_interest(&mut self) -> bool {
|
||||
if self.deferred_interest.is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.list.append(&mut self.deferred_interest);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<Tile> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user