mirror of
https://github.com/penpot/penpot.git
synced 2026-08-23 13:18:36 +00:00
⚡ Present viewport before interest and clamp paint to atlas
Present visible tiles via ViewportReady so zoom settle turns sharp without waiting on the interest ring, and paint at atlas slot size so DPR 2 does not rasterize 1024 only to downscale into 512 slots.
This commit is contained in:
parent
c6512757b8
commit
e8843b066e
@ -19,6 +19,9 @@
|
||||
|
||||
- Raster `Fill::Image`: skip `save_layer` unless the shape has an image filter; plain
|
||||
Rect/Frame (no corners) also skip the container clip (`draw_image_fill` in fills.rs).
|
||||
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
|
||||
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
|
||||
compose+present only.
|
||||
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
|
||||
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
|
||||
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
|
||||
|
||||
@ -379,6 +379,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
|
||||
@ -388,7 +401,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
|
||||
@ -485,13 +498,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
|
||||
@ -1413,14 +1426,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)]
|
||||
@ -419,6 +423,9 @@ pub(crate) struct RenderState {
|
||||
pub drop_shadows_ops_warmed: bool,
|
||||
/// Filter-surface snapshots for drop shadows, reused across tiles.
|
||||
drop_shadow_filter_cache: shadows::DropShadowFilterCache,
|
||||
/// 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 {
|
||||
@ -607,6 +614,7 @@ impl RenderState {
|
||||
tile_atlas_flushed: false,
|
||||
drop_shadows_ops_warmed: false,
|
||||
drop_shadow_filter_cache: shadows::DropShadowFilterCache::new(),
|
||||
viewport_presented: false,
|
||||
})
|
||||
}
|
||||
|
||||
@ -2309,6 +2317,7 @@ impl RenderState {
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
self.drop_shadow_filter_cache.clear();
|
||||
self.viewport_presented = false;
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
@ -2481,9 +2490,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,
|
||||
@ -2500,17 +2514,32 @@ impl RenderState {
|
||||
// `drain_partial_gpu_soft`). Full still submits via present_frame.
|
||||
Self::drain_partial_gpu_soft();
|
||||
}
|
||||
FrameType::ViewportReady => {
|
||||
// Visible tiles are done: present now so the user sees the
|
||||
// viewport without waiting for interest-ring pre-render.
|
||||
// Defer crop-cache rebuild to Full — it is expensive on large
|
||||
// HiDPI viewports and is not needed until the next drag.
|
||||
self.present_frame(tree);
|
||||
self.viewport_presented = true;
|
||||
wapi::notify_tiles_render_complete!();
|
||||
Self::drain_partial_gpu_soft();
|
||||
}
|
||||
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() {
|
||||
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!();
|
||||
} else if !self.options.is_fast_mode() && !self.options.is_interactive_transform()
|
||||
{
|
||||
// Interest fill finished after ViewportReady. Backbuffer
|
||||
// still holds the viewport compose; rebuild crop cache
|
||||
// off the sharp-snap frame.
|
||||
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");
|
||||
}
|
||||
}
|
||||
@ -4015,8 +4044,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4034,6 +4073,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)
|
||||
}
|
||||
|
||||
|
||||
@ -352,6 +352,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 {
|
||||
@ -364,14 +366,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.
|
||||
@ -423,10 +427,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