mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🐛 Avoid sequential tile draws and flicker during shape transforms
This commit is contained in:
parent
c42eb6ff86
commit
98c8bb1746
@ -38,6 +38,23 @@
|
||||
(def ^:private xf:without-uuid-zero
|
||||
(remove #(= % uuid/zero)))
|
||||
|
||||
;; Tracks whether the WASM renderer is currently in "interactive
|
||||
;; transform" mode (a drag / resize / rotate gesture in progress).
|
||||
;; Paired with `set-modifiers-start` / `set-modifiers-end` so the
|
||||
;; native side only toggles once per gesture, regardless of how many
|
||||
;; `set-wasm-modifiers` calls fire in between.
|
||||
(defonce ^:private interactive-transform-active? (atom false))
|
||||
|
||||
(defn- ensure-interactive-transform-start!
|
||||
[]
|
||||
(when (compare-and-set! interactive-transform-active? false true)
|
||||
(wasm.api/set-modifiers-start)))
|
||||
|
||||
(defn- ensure-interactive-transform-end!
|
||||
[]
|
||||
(when (compare-and-set! interactive-transform-active? true false)
|
||||
(wasm.api/set-modifiers-end)))
|
||||
|
||||
(def ^:private transform-attrs
|
||||
#{:selrect
|
||||
:points
|
||||
@ -279,6 +296,11 @@
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "render-wasm/v1")
|
||||
;; End interactive transform mode BEFORE cleaning modifiers so
|
||||
;; the final full-quality render triggered by subsequent shape
|
||||
;; updates is not still classified as "interactive" (which would
|
||||
;; skip shadows / blur).
|
||||
(ensure-interactive-transform-end!)
|
||||
(wasm.api/clean-modifiers)
|
||||
(set-wasm-props! (dsh/lookup-page-objects state) (:wasm-props state) [])))
|
||||
|
||||
@ -624,6 +646,12 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; Entering an interactive transform (drag/resize/rotate). Flip
|
||||
;; the renderer into fast + atlas-backdrop mode so the live
|
||||
;; preview is cheap, tiles never appear sequentially and the main
|
||||
;; thread is not blocked. The pair is closed in
|
||||
;; `clear-local-transform`.
|
||||
(ensure-interactive-transform-start!)
|
||||
(wasm.api/clean-modifiers)
|
||||
(let [prev-wasm-props (:prev-wasm-props state)
|
||||
wasm-props (:wasm-props state)
|
||||
@ -764,6 +792,7 @@
|
||||
(ptk/reify ::set-wasm-rotation-modifiers
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(ensure-interactive-transform-start!)
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
ids (sequence xf-rotation-shape shapes)
|
||||
|
||||
|
||||
@ -1517,6 +1517,23 @@
|
||||
[]
|
||||
(h/call wasm/internal-module "_clean_modifiers"))
|
||||
|
||||
(defn set-modifiers-start
|
||||
"Enter interactive transform mode (drag / resize / rotate). Enables
|
||||
fast-mode effect skipping in the renderer and activates an atlas
|
||||
backdrop so tiles do not appear sequentially or flicker while the
|
||||
gesture is in progress."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_set_modifiers_start")))
|
||||
|
||||
(defn set-modifiers-end
|
||||
"Leave interactive transform mode. Cancels any pending async render
|
||||
scheduled under it; the caller is expected to trigger a full-quality
|
||||
render (via `request-render`) once the gesture is committed."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_set_modifiers_end")))
|
||||
|
||||
(defn set-modifiers
|
||||
[modifiers]
|
||||
|
||||
|
||||
@ -401,6 +401,42 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enter interactive transform mode (drag / resize / rotate of a
|
||||
/// shape). Activates the same expensive-effect skipping as pan/zoom
|
||||
/// (`fast_mode`) but keeps per-frame flushing enabled so the Target is
|
||||
/// presented every rAF, and triggers atlas-backed backdrops so
|
||||
/// invalidated tiles do not appear sequentially or flicker.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_modifiers_start");
|
||||
let opts = &mut state.render_state.options;
|
||||
opts.set_fast_mode(true);
|
||||
opts.set_interactive_transform(true);
|
||||
performance::end_measure!("set_modifiers_start");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leave interactive transform mode and cancel any pending async
|
||||
/// render scheduled under it. The caller is responsible for triggering
|
||||
/// a final full-quality render (typically via `_render`) once the
|
||||
/// modifiers have been committed.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_modifiers_end");
|
||||
let opts = &mut state.render_state.options;
|
||||
opts.set_fast_mode(false);
|
||||
opts.set_interactive_transform(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clear_focus_mode() -> Result<()> {
|
||||
|
||||
@ -1666,6 +1666,24 @@ impl RenderState {
|
||||
|
||||
self.cache_cleared_this_render = false;
|
||||
self.reset_canvas();
|
||||
|
||||
// During an interactive shape transform (drag/resize/rotate) the
|
||||
// Target is repainted tile-by-tile. If only a subset of the
|
||||
// invalidated tiles finishes in this rAF the remaining area
|
||||
// would either show stale content from the previous frame or,
|
||||
// on buffer swaps, show blank pixels — either way the user
|
||||
// perceives tiles appearing sequentially. Paint the persistent
|
||||
// 1:1 atlas as a stable backdrop so every flush presents a
|
||||
// coherent picture: unchanged tiles come from the atlas and
|
||||
// invalidated tiles are overwritten on top as they finish.
|
||||
if self.options.is_interactive_transform() && self.surfaces.has_atlas() {
|
||||
self.surfaces.draw_atlas_to_target(
|
||||
self.viewbox,
|
||||
self.options.dpr(),
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
@ -1744,12 +1762,16 @@ impl RenderState {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
|
||||
}
|
||||
|
||||
// In fast mode (pan/zoom in progress), render_from_cache owns
|
||||
// the Target surface — skip flush so we don't present stale
|
||||
// tile positions. The rAF still populates the Cache surface
|
||||
// and tile HashMap so render_from_cache progressively shows
|
||||
// more complete content.
|
||||
if !self.options.is_fast_mode() {
|
||||
// In a pure viewport interaction (pan/zoom), render_from_cache
|
||||
// owns the Target surface — skip flush so we don't present
|
||||
// stale tile positions. The rAF still populates the Cache
|
||||
// surface and tile HashMap so render_from_cache progressively
|
||||
// shows more complete content.
|
||||
//
|
||||
// During interactive shape transforms (drag/resize/rotate) we
|
||||
// still need to flush every rAF so the user sees the updated
|
||||
// shape position — render_from_cache is not in the loop here.
|
||||
if !self.options.is_viewport_interaction() {
|
||||
self.flush_and_submit();
|
||||
}
|
||||
|
||||
@ -1887,8 +1909,26 @@ impl RenderState {
|
||||
|
||||
#[inline]
|
||||
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
|
||||
iteration % NODE_BATCH_THRESHOLD == 0
|
||||
&& performance::get_time() - timestamp > MAX_BLOCKING_TIME_MS
|
||||
if iteration % NODE_BATCH_THRESHOLD != 0 {
|
||||
return false;
|
||||
}
|
||||
if performance::get_time() - timestamp <= MAX_BLOCKING_TIME_MS {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During interactive shape transforms we must complete every
|
||||
// visible tile in a single rAF so the user never sees tiles
|
||||
// popping in sequentially. Only yield once all visible work is
|
||||
// done and we are processing the interest-area pre-render.
|
||||
if self.options.is_interactive_transform() {
|
||||
if let Some(tile) = self.current_tile {
|
||||
if self.tile_viewbox.is_visible(&tile) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@ -9,6 +9,11 @@ pub struct RenderOptions {
|
||||
pub flags: u32,
|
||||
pub dpr: Option<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
|
||||
/// `render_from_cache` drives target presentation).
|
||||
interactive_transform: bool,
|
||||
/// Minimum on-screen size (CSS px at 1:1 zoom) above which vector antialiasing is enabled.
|
||||
pub antialias_threshold: f32,
|
||||
}
|
||||
@ -19,6 +24,7 @@ impl Default for RenderOptions {
|
||||
flags: 0,
|
||||
dpr: None,
|
||||
fast_mode: false,
|
||||
interactive_transform: false,
|
||||
antialias_threshold: 7.0,
|
||||
}
|
||||
}
|
||||
@ -42,6 +48,26 @@ impl RenderOptions {
|
||||
self.fast_mode = enabled;
|
||||
}
|
||||
|
||||
/// Interactive transform is ON while the user is dragging, resizing
|
||||
/// or rotating a shape. Callers use it to keep per-frame flushing
|
||||
/// enabled and to render visible tiles in a single frame so tiles
|
||||
/// never appear sequentially or flicker during the gesture.
|
||||
pub fn is_interactive_transform(&self) -> bool {
|
||||
self.interactive_transform
|
||||
}
|
||||
|
||||
pub fn set_interactive_transform(&mut self, enabled: bool) {
|
||||
self.interactive_transform = enabled;
|
||||
}
|
||||
|
||||
/// True only when the viewport is the one being moved (pan/zoom)
|
||||
/// and the dedicated `render_from_cache` path owns Target
|
||||
/// presentation. In this mode `process_animation_frame` must not
|
||||
/// flush to avoid presenting stale tile positions.
|
||||
pub fn is_viewport_interaction(&self) -> bool {
|
||||
self.fast_mode && !self.interactive_transform
|
||||
}
|
||||
|
||||
pub fn dpr(&self) -> f32 {
|
||||
self.dpr.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user