This commit is contained in:
Alejandro Alonso 2026-10-02 09:30:28 +02:00
parent 10a2c9bc5c
commit f2412ab0c7
2 changed files with 132 additions and 17 deletions

View File

@ -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*

View File

@ -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<Uint8Array|nil>}
;; 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))))))]