From 47ce2ad6cd3d72a88f91c426340ee38606aa7a58 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 14 Apr 2026 16:18:05 +0200 Subject: [PATCH] :wrench: Add loading between pages using previous thumbnail --- frontend/src/app/render_wasm/api.cljs | 62 ++++++++++++++++----- frontend/src/app/render_wasm/api/webgl.cljs | 31 +++++++++-- render-wasm/src/render.rs | 7 ++- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index b9e0458631..ff99e54d44 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -991,7 +991,11 @@ (letfn [(do-render [] ;; Check if context is still initialized before executing ;; to prevent errors when navigating quickly - (when (and wasm/context-initialized? (not @wasm/context-lost?)) + (when (and wasm/context-initialized? (not @wasm/context-lost?) + ;; Skip during bulk loading — the blurred previous-page + ;; preview must stay visible until set-objects finishes + ;; and renders synchronously. + (not @shapes-loading?)) (perf/begin-measure "render-finish") (h/call wasm/internal-module "_set_view_end") (perf/end-measure "render-finish") @@ -1007,15 +1011,17 @@ (defn set-view-box [zoom vbox] - (perf/begin-measure "set-view-box") - (h/call wasm/internal-module "_set_view_start") - (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) - (perf/end-measure "set-view-box") + ;; Skip during bulk loading — preserve blurred previous-page preview + (when-not @shapes-loading? + (perf/begin-measure "set-view-box") + (h/call wasm/internal-module "_set_view_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) + (perf/end-measure "render-from-cache"))) (defn update-text-rect! [id] @@ -1204,7 +1210,23 @@ (update-text-layouts text-ids))) (if render-callback (render-callback) - (request-render "set-objects-complete")) + ;; When on-shapes-ready is set (page transition), use + ;; synchronous render so the complete scene appears in + ;; one frame right after the blur is cleared. Without + ;; this, the async _render path flushes partial tiles + ;; progressively, causing visible tile-by-tile loading. + (if on-shapes-ready + (do + ;; Cancel the rAF that end-shapes-loading! just + ;; scheduled — we are about to render synchronously + ;; and don't want a redundant progressive render on + ;; the next frame. + (when-let [fid wasm/internal-frame-id] + (js/cancelAnimationFrame fid) + (set! wasm/internal-frame-id nil) + (reset! pending-render false)) + (render-sync)) + (request-render "set-objects-complete"))) (ug/dispatch! (ug/event "penpot:wasm:set-objects")) (resolve nil) @@ -1252,11 +1274,25 @@ ;; Rebuild the tile index so _render knows which shapes ;; map to which tiles after a page switch. (h/call wasm/internal-module "_set_view_end") + + ;; Run text layouts before the first render so text shapes + ;; are correctly sized immediately. + (let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)] + (when (seq text-ids) + (update-text-layouts text-ids))) + + ;; When doing a page transition, render immediately with whatever + ;; content is available (shapes without images). Images load in + ;; the background and trigger a re-render when ready. + (when on-shapes-ready + (render-sync)) + (process-pending shapes thumbnails full (fn [] - (if render-callback - (render-callback) - (request-render "set-objects-sync-complete")) + (when-not on-shapes-ready + (if render-callback + (render-callback) + (request-render "set-objects-sync-complete"))) (ug/dispatch! (ug/event "penpot:wasm:set-objects")))))) (defn- shapes-in-tree-order diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs index eac0c42df7..926b119581 100644 --- a/frontend/src/app/render_wasm/api/webgl.cljs +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -48,11 +48,28 @@ ;; FIXME: temporary function until we are able to keep the same across pages. (defn- draw-imagedata-to-webgl - "Draws ImageData to a WebGL2 context by creating a texture" + "Draws ImageData to a WebGL2 context by creating a texture. + After _init, Skia owns the GL context and may have left a non-default + FBO bound plus scissor/stencil/depth enabled. We must reset all + relevant GL state so the full-screen quad actually lands on the + default framebuffer (the visible canvas)." [gl image-data] (let [width (.-width ^js image-data) height (.-height ^js image-data) texture (.createTexture ^js gl)] + + ;; Bind the default framebuffer (FBO 0) — Skia may have left one + ;; of its offscreen FBOs bound. + (.bindFramebuffer ^js gl (.-FRAMEBUFFER ^js gl) nil) + + ;; Disable GPU tests that Skia may have enabled — any of these + ;; can silently discard our fragments. + (.disable ^js gl (.-SCISSOR_TEST ^js gl)) + (.disable ^js gl (.-STENCIL_TEST ^js gl)) + (.disable ^js gl (.-DEPTH_TEST ^js gl)) + (.disable ^js gl (.-BLEND ^js gl)) + (.colorMask ^js gl true true true true) + ;; Bind texture and set parameters (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture) (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl)) @@ -156,6 +173,9 @@ void main() { [] (when wasm/canvas (let [context wasm/gl-context] + ;; Bind default framebuffer so we clear the visible canvas, + ;; not whichever offscreen FBO Skia may have left active. + (.bindFramebuffer ^js context (.-FRAMEBUFFER ^js context) nil) (.clearColor ^js context 0 0 0 0.0) (.clear ^js context (.-COLOR_BUFFER_BIT ^js context)) (.clear ^js context (.-DEPTH_BUFFER_BIT ^js context)) @@ -172,10 +192,11 @@ void main() { (let [context wasm/gl-context width (.-width wasm/canvas) height (.-height wasm/canvas) - buffer (js/Uint8ClampedArray. (* width height 4)) - _ (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer) - image-data (js/ImageData. buffer width height)] - (set! wasm/canvas-pixels image-data)))) + buffer (js/Uint8ClampedArray. (* width height 4))] + ;; Bind default framebuffer — Skia may have left an offscreen FBO active. + (.bindFramebuffer ^js context (.-FRAMEBUFFER ^js context) nil) + (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer) + (set! wasm/canvas-pixels (js/ImageData. buffer width height))))) (defn draw-thumbnail-to-canvas "Loads an image from `uri` and draws it stretched to fill the WebGL canvas. diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index c247341590..6d9909f7cd 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2779,8 +2779,13 @@ impl RenderState { } } else { performance::begin_measure!("render_shape_tree::uncached"); + // Only allow stopping (yielding) if the current tile is NOT visible. + // This ensures all visible tiles render synchronously before showing, + // eliminating empty squares during zoom. Interest-area tiles can still yield. + let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile); + let can_stop = allow_stop && !tile_is_visible; let (is_empty, early_return) = self - .render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?; + .render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?; if early_return { return Ok(());