From f2412ab0c704c52fae041809c7d9e0628f1b5f3f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 2 Oct 2026 09:30:28 +0200 Subject: [PATCH] WIP --- frontend/src/app/main/ui/workspace.cljs | 82 ++++++++++++++++++++----- frontend/src/app/render_wasm/api.cljs | 67 +++++++++++++++++++- 2 files changed, 132 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index c626b36a29..33412b86dc 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -44,11 +44,13 @@ [app.main.ui.workspace.tokens.themes.create-modal] [app.main.ui.workspace.viewport :refer [viewport*]] [app.main.ui.workspace.webgl-unavailable-modal] + [app.render-wasm.api :as wasm.api] [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [goog.events :as events] [okulary.core :as l] + [promesa.core :as p] [rumext.v2 :as mf])) (mf/defc workspace-content* @@ -193,6 +195,53 @@ :layout layout}] [:> workspace-loader*]))) +(mf/defc early-wasm-batch-canvas* + "Optional preview canvas while get-file is still in flight. If the batch + arrives first, paints geometry immediately. Unmounts as soon as the file + loads — never delays workspace-inner." + {::mf/private true} + [{:keys [background]}] + (let [canvas-ref (mf/use-ref nil)] + (mf/with-effect [background] + (when-let [canvas (mf/ref-val canvas-ref)] + (let [unmounted? (volatile! false) + interval-id (volatile! nil) + try-paint + (fn [] + (when-not @unmounted? + (when (wasm.api/try-early-structural-paint! + {:background background}) + (when-let [id @interval-id] + (js/clearInterval id) + (vreset! interval-id nil)))))] + (->> @wasm.api/module + (p/fmap + (fn [ready?] + (when (and ready? (not @unmounted?)) + (when (try + (wasm.api/init-canvas-context canvas) + (catch :default e + (js/console.error + "early-wasm-batch-canvas init failed" e) + false)) + (wasm.api/resize-canvas! canvas) + (try-paint) + (vreset! interval-id (js/setInterval try-paint 50))))))) + (fn [] + (vreset! unmounted? true) + (when-let [id @interval-id] (js/clearInterval id)) + (wasm.api/clear-canvas))))) + + [:canvas {:id "render-early-batch" + :data-testid "canvas-wasm-early-batch" + :ref canvas-ref + :style {:position "absolute" + :inset 0 + :width "100%" + :height "100%" + :z-index 2 + :pointer-events "none"}}])) + (mf/defc workspace* {::mf/wrap [mf/memo]} [{:keys [team-id project-id file-id page-id layout-name]}] @@ -219,11 +268,20 @@ design-tokens? (features/use-feature "design-tokens/v1") wasm-renderer-enabled? (features/use-feature "render-wasm/v1") + wasm-batch-enabled? (wasm.api/wasm-shapes-batch-enabled?) first-frame-rendered? (mf/use-state false) background-color (:background-color wglobal)] + (mf/with-effect [] + (let [handle-wasm-render + (fn [_] + (reset! first-frame-rendered? true)) + listener-key (events/listen js/document "penpot:wasm:render" handle-wasm-render)] + (fn [] + (events/unlistenByKey listener-key)))) + (mf/with-effect [] (st/emit! (dps/initialize-persistence) (dpl/update-plugins-permissions-peek))) @@ -252,14 +310,6 @@ (mf/with-effect [file-id page-id] (reset! first-frame-rendered? false)) - (mf/with-effect [] - (let [handle-wasm-render - (fn [_] - (reset! first-frame-rendered? true)) - listener-key (events/listen js/document "penpot:wasm:render" handle-wasm-render)] - (fn [] - (events/unlistenByKey listener-key)))) - [:> (mf/provider ctx/current-project-id) {:value project-id} [:> (mf/provider ctx/current-file-id) {:value file-id} [:> (mf/provider ctx/current-page-id) {:value page-id} @@ -272,6 +322,12 @@ :touch-action "none" :position "relative"}} [:> context-menu*] + ;; Parallel preview only: never blocks get-file / workspace-inner. + (when (and wasm-renderer-enabled? + wasm-batch-enabled? + (uuid? page-id) + (not file-loaded?)) + [:> early-wasm-batch-canvas* {:background background-color}]) (when (and file-loaded? page-id) [:> workspace-inner* {:page-id page-id @@ -279,12 +335,10 @@ :file file :wglobal wglobal :layout layout}]) - (when (or (not (and file-loaded? page-id)) - ;; in wasm renderer, extend the pixel loader until the first frame is rendered - ;; but do not apply it when switching pages - (and wasm-renderer-enabled? - (not file-loaded?) - (not @first-frame-rendered?))) + (when (or (not page-id) + (and (not file-loaded?) + (or (not wasm-renderer-enabled?) + (not @first-frame-rendered?)))) [:> workspace-loader*])]]]]]])) (mf/defc workspace-page* diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 7ee82acda2..5a6659cb2f 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -76,13 +76,72 @@ (contains? cf/flags :wasm-shapes-batch)) ;; Prefetched `_set_shapes_batch` bytes for the next cold load. -;; Shape: {:file-id uuid :page-id uuid :promise Promise} +;; Shape: {:file-id uuid :page-id uuid :promise Promise :u8 Uint8Array? +;; :preview-painted? bool} (defonce ^:private cold-load-batch* (atom nil)) +(declare paint-after-structural-ingest!) +(declare stop-progressive-render!) + (defn clear-cold-load-batch! [] (reset! cold-load-batch* nil)) +(defn- batch-shape-count + "Shape count is the leading LE u32 of an encode-shapes-batch buffer." + [u8] + (when (and u8 (>= (.-byteLength ^js u8) 4)) + (let [view (js/DataView. (.-buffer ^js u8) + (.-byteOffset ^js u8) + (.-byteLength ^js u8))] + (.getUint32 view 0 true)))) + +(defn- store-batch-bytes! + [file-id page-id u8] + (swap! cold-load-batch* + (fn [entry] + (if (and entry + (= file-id (:file-id entry)) + (= page-id (:page-id entry))) + (assoc entry :u8 u8) + entry)))) + +(defn try-early-structural-paint! + "Flush + sync-paint a prefetched server batch without CLJS shape maps. + Fires only when an early canvas is already live AND the batch arrived + before get-file unmounted it. Never blocks get-file or workspace mount. + Does not consume the batch — set-objects re-ingests on the real viewport." + ([] + (try-early-structural-paint! nil)) + ([{:keys [zoom vbox background]}] + (when (and (wasm-shapes-batch-enabled?) (wasm/live?)) + (let [{:keys [u8 preview-painted?] :as entry} @cold-load-batch* + n (batch-shape-count u8)] + (when (and (some? entry) + (some? u8) + (number? n) + (pos? n) + (not preview-painted?)) + (let [zoom (or zoom 1) + vbox (or vbox {:x 0 :y 0 :width 1920 :height 1080}) + rgba (when background (sr-clr/hex->u32argb background 1))] + (stop-progressive-render!) + (when rgba + (h/call wasm/internal-module "_set_canvas_background" rgba)) + (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) + (h/call wasm/internal-module "_init_shapes_pool" n) + (upload/flush-shapes-batch-bytes! u8) + (paint-after-structural-ingest! nil) + (swap! cold-load-batch* + (fn [e] + (cond-> e + (and e (= (:file-id e) (:file-id entry)) + (= (:page-id e) (:page-id entry))) + (assoc :preview-painted? true)))) + (ug/dispatch! (ug/event "penpot:wasm:render")) + (js/console.info "[wasm-batch] early structural paint" n "shapes") + true)))))) + (defn prefetch-cold-load-batch! "Start downloading the server-encoded shapes batch for `file-id`/`page-id`. Safe to call multiple times; only the latest pair is kept." @@ -91,7 +150,7 @@ (let [t0 (js/performance.now) prom (p/create - (fn [resolve reject] + (fn [resolve _reject] (->> (rp/cmd! :get-file-wasm-shapes-batch {:file-id file-id :page-id page-id}) @@ -104,7 +163,9 @@ "[wasm-batch] download" (.-byteLength u8) "bytes in" (.toFixed ms 1) "ms") - (resolve u8))) + (store-batch-bytes! file-id page-id u8) + (resolve u8) + (try-early-structural-paint!))) (fn [err] (js/console.warn "[wasm-batch] download failed" err) (resolve nil))))))]