mirror of
https://github.com/penpot/penpot.git
synced 2026-08-29 16:18:48 +00:00
⚡ Batch WASM shape upload to speed up page switches
Upload structural shape attrs (base, children, blur, shadows, flex, layout-item) via multi-shape `_set_shapes_batch` FFI in chunks of 512, then apply host attrs with use-shape selection.
This commit is contained in:
parent
03cd3fa70f
commit
5c5eb22c9a
456
common/src/app/common/render_wasm/api/upload.cljs
Normal file
456
common/src/app/common/render_wasm/api/upload.cljs
Normal file
@ -0,0 +1,456 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.common.render-wasm.api.upload
|
||||
"Enlarged per-shape + multi-shape structural upload for WASM cold load.
|
||||
|
||||
Writes a binary batch consumed by `_set_shapes_batch`. Remaining
|
||||
host-specific attrs (image bytes, text, path, grid tracks) are applied
|
||||
afterwards via the existing per-shape setters."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.render-wasm.helpers :as h]
|
||||
[app.common.render-wasm.mem :as mem]
|
||||
[app.common.render-wasm.serializers :as sr]
|
||||
[app.common.render-wasm.serializers.color :as sr-clr]
|
||||
[app.common.render-wasm.wasm :as wasm]
|
||||
[app.common.types.fills :as types.fills]
|
||||
[app.common.types.fills.impl :as types.fills.impl]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
(def ^:const BASE-PROPS-SIZE 104)
|
||||
(def ^:const FLAG-CLIP-CONTENT 0x01)
|
||||
(def ^:const FLAG-HIDDEN 0x02)
|
||||
|
||||
(def ^:const SECTION-CHILDREN 0x01)
|
||||
(def ^:const SECTION-BLUR-LAYER 0x02)
|
||||
(def ^:const SECTION-BLUR-BG 0x04)
|
||||
(def ^:const SECTION-SHADOWS 0x08)
|
||||
(def ^:const SECTION-MASKED 0x10)
|
||||
(def ^:const SECTION-BOOL-TYPE 0x20)
|
||||
(def ^:const SECTION-GROW-TYPE 0x40)
|
||||
(def ^:const SECTION-LAYOUT-ITEM 0x80)
|
||||
(def ^:const SECTION-FLEX 0x100)
|
||||
(def ^:const SECTION-FILLS 0x200)
|
||||
(def ^:const SECTION-STROKES 0x400)
|
||||
|
||||
;; Stroke header before RawFillData (must match upload_batch.rs).
|
||||
(def ^:const STROKE-HEADER-U8-SIZE 36)
|
||||
(def ^:const STROKE-ALIGN-CENTER 0)
|
||||
(def ^:const STROKE-ALIGN-INNER 1)
|
||||
(def ^:const STROKE-ALIGN-OUTER 2)
|
||||
|
||||
(defn- write-uuid!
|
||||
[dview offset id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
(.setUint32 dview offset (aget buffer 0) true)
|
||||
(.setUint32 dview (+ offset 4) (aget buffer 1) true)
|
||||
(.setUint32 dview (+ offset 8) (aget buffer 2) true)
|
||||
(.setUint32 dview (+ offset 12) (aget buffer 3) true)
|
||||
(+ offset 16)))
|
||||
|
||||
(defn- write-base-props!
|
||||
"Write the 104-byte RawBasePropsData at `offset`. Returns next offset."
|
||||
[dview offset shape]
|
||||
(let [id (dm/get-prop shape :id)
|
||||
parent-id (get shape :parent-id)
|
||||
shape-type (dm/get-prop shape :type)
|
||||
clip-content (if (= shape-type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
hidden (get shape :hidden false)
|
||||
flags (cond-> 0
|
||||
clip-content (bit-or FLAG-CLIP-CONTENT)
|
||||
hidden (bit-or FLAG-HIDDEN))
|
||||
blend-mode (sr/translate-blend-mode (get shape :blend-mode))
|
||||
constraint-h (sr/translate-constraint-h (or (get shape :constraints-h) :none))
|
||||
constraint-v (sr/translate-constraint-v (or (get shape :constraints-v) :none))
|
||||
opacity (d/nilv (get shape :opacity) 1.0)
|
||||
rotation (d/nilv (get shape :rotation) 0.0)
|
||||
transform (get shape :transform)
|
||||
[ta tb tc td te tf]
|
||||
(if (some? transform)
|
||||
[(dm/get-prop transform :a)
|
||||
(dm/get-prop transform :b)
|
||||
(dm/get-prop transform :c)
|
||||
(dm/get-prop transform :d)
|
||||
(dm/get-prop transform :e)
|
||||
(dm/get-prop transform :f)]
|
||||
[1.0 0.0 0.0 1.0 0.0 0.0])
|
||||
selrect (get shape :selrect)
|
||||
[sx1 sy1 sx2 sy2]
|
||||
(if (some? selrect)
|
||||
[(dm/get-prop selrect :x1)
|
||||
(dm/get-prop selrect :y1)
|
||||
(dm/get-prop selrect :x2)
|
||||
(dm/get-prop selrect :y2)]
|
||||
[0.0 0.0 0.0 0.0])
|
||||
r1 (d/nilv (get shape :r1) 0.0)
|
||||
r2 (d/nilv (get shape :r2) 0.0)
|
||||
r3 (d/nilv (get shape :r3) 0.0)
|
||||
r4 (d/nilv (get shape :r4) 0.0)]
|
||||
|
||||
(write-uuid! dview offset id)
|
||||
(write-uuid! dview (+ offset 16) (d/nilv parent-id uuid/zero))
|
||||
(.setUint8 dview (+ offset 32) (sr/translate-shape-type shape-type))
|
||||
(.setUint8 dview (+ offset 33) flags)
|
||||
(.setUint8 dview (+ offset 34) blend-mode)
|
||||
(.setUint8 dview (+ offset 35) constraint-h)
|
||||
(.setUint8 dview (+ offset 36) constraint-v)
|
||||
(.setFloat32 dview (+ offset 40) opacity true)
|
||||
(.setFloat32 dview (+ offset 44) rotation true)
|
||||
(.setFloat32 dview (+ offset 48) ta true)
|
||||
(.setFloat32 dview (+ offset 52) tb true)
|
||||
(.setFloat32 dview (+ offset 56) tc true)
|
||||
(.setFloat32 dview (+ offset 60) td true)
|
||||
(.setFloat32 dview (+ offset 64) te true)
|
||||
(.setFloat32 dview (+ offset 68) tf true)
|
||||
(.setFloat32 dview (+ offset 72) sx1 true)
|
||||
(.setFloat32 dview (+ offset 76) sy1 true)
|
||||
(.setFloat32 dview (+ offset 80) sx2 true)
|
||||
(.setFloat32 dview (+ offset 84) sy2 true)
|
||||
(.setFloat32 dview (+ offset 88) r1 true)
|
||||
(.setFloat32 dview (+ offset 92) r2 true)
|
||||
(.setFloat32 dview (+ offset 96) r3 true)
|
||||
(.setFloat32 dview (+ offset 100) r4 true)
|
||||
(+ offset BASE-PROPS-SIZE)))
|
||||
|
||||
(defn- write-blur!
|
||||
[dview offset blur]
|
||||
(.setUint8 dview offset (if (get blur :hidden) 1 0))
|
||||
(.setFloat32 dview (+ offset 4) (get blur :value 0) true)
|
||||
(+ offset 8))
|
||||
|
||||
(defn- write-shadow!
|
||||
[dview offset shadow]
|
||||
(let [color (get shadow :color)
|
||||
rgba (sr-clr/hex->u32argb (get color :color)
|
||||
(get color :opacity))]
|
||||
(.setUint32 dview offset rgba true)
|
||||
(.setFloat32 dview (+ offset 4) (get shadow :blur 0) true)
|
||||
(.setFloat32 dview (+ offset 8) (get shadow :spread 0) true)
|
||||
(.setFloat32 dview (+ offset 12) (get shadow :offset-x 0) true)
|
||||
(.setFloat32 dview (+ offset 16) (get shadow :offset-y 0) true)
|
||||
(.setUint8 dview (+ offset 20) (sr/translate-shadow-style (get shadow :style)))
|
||||
(.setUint8 dview (+ offset 21) (if (get shadow :hidden) 1 0))
|
||||
(+ offset 24)))
|
||||
|
||||
(defn- write-flex!
|
||||
[dview offset shape]
|
||||
(let [dir (-> (get shape :layout-flex-dir :row)
|
||||
(sr/translate-layout-flex-dir))
|
||||
gap (get shape :layout-gap)
|
||||
row-gap (get gap :row-gap 0)
|
||||
column-gap (get gap :column-gap 0)
|
||||
align-items (-> (get shape :layout-align-items) sr/translate-layout-align-items)
|
||||
align-content (-> (get shape :layout-align-content) sr/translate-layout-align-content)
|
||||
justify-items (-> (get shape :layout-justify-items) sr/translate-layout-justify-items)
|
||||
justify-content (-> (get shape :layout-justify-content) sr/translate-layout-justify-content)
|
||||
wrap-type (-> (get shape :layout-wrap-type) sr/translate-layout-wrap-type)
|
||||
padding (get shape :layout-padding)
|
||||
padding-top (get padding :p1 0)
|
||||
padding-right (get padding :p2 0)
|
||||
padding-bottom (get padding :p3 0)
|
||||
padding-left (get padding :p4 0)]
|
||||
(.setUint8 dview offset dir)
|
||||
(.setUint8 dview (+ offset 1) align-items)
|
||||
(.setUint8 dview (+ offset 2) align-content)
|
||||
(.setUint8 dview (+ offset 3) justify-items)
|
||||
(.setUint8 dview (+ offset 4) justify-content)
|
||||
(.setUint8 dview (+ offset 5) wrap-type)
|
||||
(.setFloat32 dview (+ offset 8) row-gap true)
|
||||
(.setFloat32 dview (+ offset 12) column-gap true)
|
||||
(.setFloat32 dview (+ offset 16) padding-top true)
|
||||
(.setFloat32 dview (+ offset 20) padding-right true)
|
||||
(.setFloat32 dview (+ offset 24) padding-bottom true)
|
||||
(.setFloat32 dview (+ offset 28) padding-left true)
|
||||
(+ offset 32)))
|
||||
|
||||
(defn- write-layout-item!
|
||||
[dview offset shape]
|
||||
(let [margins (get shape :layout-item-margin)
|
||||
margin-top (get margins :m1 0)
|
||||
margin-right (get margins :m2 0)
|
||||
margin-bottom (get margins :m3 0)
|
||||
margin-left (get margins :m4 0)
|
||||
h-sizing (-> (get shape :layout-item-h-sizing) sr/translate-layout-sizing)
|
||||
v-sizing (-> (get shape :layout-item-v-sizing) sr/translate-layout-sizing)
|
||||
align-self (-> (get shape :layout-item-align-self) sr/translate-align-self)
|
||||
max-h (get shape :layout-item-max-h)
|
||||
min-h (get shape :layout-item-min-h)
|
||||
max-w (get shape :layout-item-max-w)
|
||||
min-w (get shape :layout-item-min-w)
|
||||
is-absolute (boolean (get shape :layout-item-absolute))
|
||||
z-index (get shape :layout-item-z-index)
|
||||
flags (cond-> 0
|
||||
(some? max-h) (bit-or 0x01)
|
||||
(some? min-h) (bit-or 0x02)
|
||||
(some? max-w) (bit-or 0x04)
|
||||
(some? min-w) (bit-or 0x08)
|
||||
is-absolute (bit-or 0x10))]
|
||||
(.setFloat32 dview offset margin-top true)
|
||||
(.setFloat32 dview (+ offset 4) margin-right true)
|
||||
(.setFloat32 dview (+ offset 8) margin-bottom true)
|
||||
(.setFloat32 dview (+ offset 12) margin-left true)
|
||||
(.setUint8 dview (+ offset 16) (d/nilv h-sizing 0))
|
||||
(.setUint8 dview (+ offset 17) (d/nilv v-sizing 0))
|
||||
(.setUint8 dview (+ offset 18) flags)
|
||||
(.setUint8 dview (+ offset 19) (d/nilv align-self 0))
|
||||
(.setFloat32 dview (+ offset 20) (d/nilv max-h 0) true)
|
||||
(.setFloat32 dview (+ offset 24) (d/nilv min-h 0) true)
|
||||
(.setFloat32 dview (+ offset 28) (d/nilv max-w 0) true)
|
||||
(.setFloat32 dview (+ offset 32) (d/nilv min-w 0) true)
|
||||
(.setInt32 dview (+ offset 36) (d/nilv z-index 0) true)
|
||||
(+ offset 40)))
|
||||
|
||||
(defn- write-fills-section!
|
||||
"Write fills in the same layout as `_set_shape_fills`:
|
||||
[u8 n][u8;3 pad][n × FILL-U8-SIZE]. Returns next offset."
|
||||
[dview offset fills]
|
||||
(let [fills (types.fills/coerce (or fills []))
|
||||
byte-size (types.fills/get-byte-size fills)
|
||||
;; write-to expects a Uint32Array heap + u32 element offset
|
||||
heap-u32 (js/Uint32Array. (.-buffer dview))
|
||||
u32-off (quot offset 4)]
|
||||
(types.fills/write-to fills heap-u32 u32-off)
|
||||
(+ offset byte-size)))
|
||||
|
||||
(defn- write-stroke-fill!
|
||||
[dview offset stroke]
|
||||
(let [opacity (or (:stroke-opacity stroke) 1.0)
|
||||
color (:stroke-color stroke)
|
||||
gradient (:stroke-color-gradient stroke)
|
||||
image (:stroke-image stroke)]
|
||||
(cond
|
||||
(some? gradient)
|
||||
(types.fills.impl/write-gradient-fill offset dview opacity gradient)
|
||||
|
||||
(some? image)
|
||||
(types.fills.impl/write-image-fill offset dview opacity image)
|
||||
|
||||
(some? color)
|
||||
(types.fills.impl/write-solid-fill offset dview opacity color)
|
||||
|
||||
:else
|
||||
(types.fills.impl/write-solid-fill offset dview 0.0 "#000000"))))
|
||||
|
||||
(defn- write-stroke!
|
||||
[dview offset stroke]
|
||||
(let [width (or (:stroke-width stroke) 1.0)
|
||||
style (-> stroke :stroke-style sr/translate-stroke-style)
|
||||
align (case (:stroke-alignment stroke)
|
||||
:inner STROKE-ALIGN-INNER
|
||||
:outer STROKE-ALIGN-OUTER
|
||||
STROKE-ALIGN-CENTER)
|
||||
cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap)
|
||||
cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap)
|
||||
dash (or (:stroke-dash stroke) -1)
|
||||
gap (or (:stroke-gap stroke) -1)
|
||||
per-side? (boolean (:stroke-per-side stroke))
|
||||
top (or (:stroke-width-top stroke) width)
|
||||
right (or (:stroke-width-right stroke) width)
|
||||
bottom (or (:stroke-width-bottom stroke) width)
|
||||
left (or (:stroke-width-left stroke) width)
|
||||
has-sides? (and per-side? (not= top right bottom left))]
|
||||
(.setFloat32 dview offset width true)
|
||||
(.setUint8 dview (+ offset 4) style)
|
||||
(.setUint8 dview (+ offset 5) align)
|
||||
(.setUint8 dview (+ offset 6) (d/nilv cap-start 0))
|
||||
(.setUint8 dview (+ offset 7) (d/nilv cap-end 0))
|
||||
(.setFloat32 dview (+ offset 8) dash true)
|
||||
(.setFloat32 dview (+ offset 12) gap true)
|
||||
(.setUint8 dview (+ offset 16) (if has-sides? 1 0))
|
||||
(.setFloat32 dview (+ offset 20) top true)
|
||||
(.setFloat32 dview (+ offset 24) right true)
|
||||
(.setFloat32 dview (+ offset 28) bottom true)
|
||||
(.setFloat32 dview (+ offset 32) left true)
|
||||
(write-stroke-fill! dview (+ offset STROKE-HEADER-U8-SIZE) stroke)
|
||||
(+ offset STROKE-HEADER-U8-SIZE types.fills.impl/FILL-U8-SIZE)))
|
||||
|
||||
(defn- visible-strokes
|
||||
[shape]
|
||||
(let [type (dm/get-prop shape :type)]
|
||||
(if (= type :group)
|
||||
[]
|
||||
(into [] (remove :hidden) (or (get shape :strokes) [])))))
|
||||
|
||||
(defn- write-strokes-section!
|
||||
[dview offset strokes]
|
||||
(.setUint32 dview offset (count strokes) true)
|
||||
(reduce (fn [o s] (write-stroke! dview o s))
|
||||
(+ offset 4)
|
||||
strokes))
|
||||
|
||||
(defn write-shape-payload!
|
||||
"Serialize one shape's structural payload into `dview` starting at `offset`
|
||||
(payload only — no length prefix). Returns the offset after the payload.
|
||||
|
||||
Options:
|
||||
- `:include-layout?` — when true, emit FLEX + LAYOUT-ITEM (workspace cold load).
|
||||
- `:include-fills-strokes?` — when true, emit FILLS + STROKES sections."
|
||||
[dview offset shape {:keys [include-layout? include-fills-strokes?]
|
||||
:or {include-layout? false
|
||||
include-fills-strokes? false}}]
|
||||
(let [shape-type (dm/get-prop shape :type)
|
||||
children (into [] (filter uuid?) (get shape :shapes))
|
||||
blur (get shape :blur)
|
||||
bg-blur (get shape :background-blur)
|
||||
shadows (or (get shape :shadow) [])
|
||||
masked? (and (= shape-type :group) (boolean (get shape :masked-group)))
|
||||
bool-type (when (= shape-type :bool) (get shape :bool-type))
|
||||
grow-type (when (= shape-type :text) (get shape :grow-type))
|
||||
flex? (and include-layout? (ctl/flex-layout? shape))
|
||||
layout-item? include-layout?
|
||||
strokes (when include-fills-strokes? (visible-strokes shape))
|
||||
|
||||
mask (cond-> 0
|
||||
true (bit-or SECTION-CHILDREN)
|
||||
(some? blur) (bit-or SECTION-BLUR-LAYER)
|
||||
(some? bg-blur) (bit-or SECTION-BLUR-BG)
|
||||
(seq shadows) (bit-or SECTION-SHADOWS)
|
||||
(= shape-type :group) (bit-or SECTION-MASKED)
|
||||
(some? bool-type) (bit-or SECTION-BOOL-TYPE)
|
||||
(some? grow-type) (bit-or SECTION-GROW-TYPE)
|
||||
flex? (bit-or SECTION-FLEX)
|
||||
layout-item? (bit-or SECTION-LAYOUT-ITEM)
|
||||
include-fills-strokes? (bit-or SECTION-FILLS)
|
||||
include-fills-strokes? (bit-or SECTION-STROKES))
|
||||
|
||||
offset (write-base-props! dview offset shape)
|
||||
_ (.setUint32 dview offset mask true)
|
||||
offset (+ offset 4)
|
||||
|
||||
offset (let [o offset]
|
||||
(.setUint32 dview o (count children) true)
|
||||
(reduce (fn [o id] (write-uuid! dview o id))
|
||||
(+ o 4)
|
||||
children))
|
||||
|
||||
offset (cond-> offset
|
||||
(some? blur)
|
||||
(as-> o (write-blur! dview o blur)))
|
||||
|
||||
offset (cond-> offset
|
||||
(some? bg-blur)
|
||||
(as-> o (write-blur! dview o bg-blur)))
|
||||
|
||||
offset (cond-> offset
|
||||
(seq shadows)
|
||||
(as-> o
|
||||
(do
|
||||
(.setUint32 dview o (count shadows) true)
|
||||
(reduce (fn [o s] (write-shadow! dview o s))
|
||||
(+ o 4)
|
||||
shadows))))
|
||||
|
||||
offset (cond-> offset
|
||||
(= shape-type :group)
|
||||
(as-> o
|
||||
(do (.setUint8 dview o (if masked? 1 0))
|
||||
(+ o 4))))
|
||||
|
||||
offset (cond-> offset
|
||||
(some? bool-type)
|
||||
(as-> o
|
||||
(do (.setUint8 dview o (sr/translate-bool-type bool-type))
|
||||
(+ o 4))))
|
||||
|
||||
offset (cond-> offset
|
||||
(some? grow-type)
|
||||
(as-> o
|
||||
(do (.setUint8 dview o (sr/translate-grow-type grow-type))
|
||||
(+ o 4))))
|
||||
|
||||
;; FLEX before LAYOUT-ITEM (Rust clears layout on flex)
|
||||
offset (cond-> offset
|
||||
flex?
|
||||
(as-> o (write-flex! dview o shape)))
|
||||
|
||||
offset (cond-> offset
|
||||
layout-item?
|
||||
(as-> o (write-layout-item! dview o shape)))
|
||||
|
||||
offset (cond-> offset
|
||||
include-fills-strokes?
|
||||
(as-> o (write-fills-section! dview o (get shape :fills))))
|
||||
|
||||
offset (cond-> offset
|
||||
include-fills-strokes?
|
||||
(as-> o (write-strokes-section! dview o strokes)))]
|
||||
offset))
|
||||
|
||||
(defn- payload-byte-size
|
||||
[shape {:keys [include-layout? include-fills-strokes?]
|
||||
:or {include-layout? false include-fills-strokes? false}}]
|
||||
(let [children (into [] (filter uuid?) (get shape :shapes))
|
||||
shadows (or (get shape :shadow) [])
|
||||
shape-type (dm/get-prop shape :type)
|
||||
blur (get shape :blur)
|
||||
bg-blur (get shape :background-blur)
|
||||
flex? (and include-layout? (ctl/flex-layout? shape))
|
||||
fills-size (if include-fills-strokes?
|
||||
(types.fills/get-byte-size (types.fills/coerce (or (get shape :fills) [])))
|
||||
0)
|
||||
strokes (when include-fills-strokes? (visible-strokes shape))
|
||||
strokes-size (if include-fills-strokes?
|
||||
(+ 4 (* (count strokes)
|
||||
(+ STROKE-HEADER-U8-SIZE types.fills.impl/FILL-U8-SIZE)))
|
||||
0)]
|
||||
(+ BASE-PROPS-SIZE
|
||||
4 ;; mask
|
||||
(+ 4 (* 16 (count children)))
|
||||
(if (some? blur) 8 0)
|
||||
(if (some? bg-blur) 8 0)
|
||||
(if (seq shadows) (+ 4 (* 24 (count shadows))) 0)
|
||||
(if (= shape-type :group) 4 0)
|
||||
(if (and (= shape-type :bool) (some? (get shape :bool-type))) 4 0)
|
||||
(if (and (= shape-type :text) (some? (get shape :grow-type))) 4 0)
|
||||
(if flex? 32 0)
|
||||
(if include-layout? 40 0)
|
||||
fills-size
|
||||
strokes-size)))
|
||||
|
||||
(defn- encode-shape-record
|
||||
"Returns a Uint8Array: [u32 payload_len][payload]."
|
||||
[shape opts]
|
||||
(let [capacity (+ 4 (payload-byte-size shape opts))
|
||||
buffer (js/ArrayBuffer. capacity)
|
||||
dview (js/DataView. buffer)
|
||||
end (write-shape-payload! dview 4 shape opts)
|
||||
payload-len (- end 4)]
|
||||
(assert (= end capacity)
|
||||
(str "upload record size mismatch: wrote " end " expected " capacity))
|
||||
(.setUint32 dview 0 payload-len true)
|
||||
(js/Uint8Array. buffer 0 end)))
|
||||
|
||||
(defn flush-shapes-batch!
|
||||
"Upload `shapes` as one `_set_shapes_batch` call.
|
||||
`opts` passed to each record writer (`:include-layout?`,
|
||||
`:include-fills-strokes?`)."
|
||||
[shapes opts]
|
||||
(when (and (wasm/live?) (seq shapes))
|
||||
(let [records (mapv #(encode-shape-record % opts) shapes)
|
||||
total (reduce (fn [acc ^js u8] (+ acc (.-byteLength u8))) 4 records)
|
||||
offset (mem/alloc total)
|
||||
heap (mem/get-heap-u8)
|
||||
dview (js/DataView. (.-buffer heap))]
|
||||
(.setUint32 dview offset (count records) true)
|
||||
(reduce (fn [o ^js u8]
|
||||
(.set heap u8 o)
|
||||
(+ o (.-byteLength u8)))
|
||||
(+ offset 4)
|
||||
records)
|
||||
(h/call wasm/internal-module "_set_shapes_batch")
|
||||
nil)))
|
||||
|
||||
(defn set-shape-upload!
|
||||
"Single-shape structural upload (enlarged blob, one FFI)."
|
||||
([shape]
|
||||
(set-shape-upload! shape {:include-layout? false}))
|
||||
([shape opts]
|
||||
(flush-shapes-batch! [shape] opts)))
|
||||
@ -12,43 +12,29 @@
|
||||
- the workspace `app.render-wasm.api/set-object` (browser), and
|
||||
- the headless exporter `app.wasm.serialize/set-shape!` (Node).
|
||||
|
||||
It applies only the properties that need no host-specific resources or driver:
|
||||
base props, children, blur, background blur, shadows, svg attrs, group mask,
|
||||
bool type, path/bool geometry and text grow type. The parts that DO differ by
|
||||
host are handled by each caller AFTER this runs:
|
||||
- fills / strokes (image bytes are fetched + uploaded differently),
|
||||
Structural attrs (base, children, blur, shadows, masked, bool, grow) go
|
||||
through the enlarged `_set_shapes_batch` upload. Path geometry stays on the
|
||||
chunked path FFI. Host-specific parts remain in each caller AFTER this runs:
|
||||
- fills / strokes image bytes (records may already be in cold-load batch),
|
||||
- text content (fonts),
|
||||
- svg-raw markup (browser renders it via React),
|
||||
- layout (grid/flex — workspace only).
|
||||
- svg-raw markup (browser React),
|
||||
- layout (grid/flex — workspace cold-load batches flex+item via upload;
|
||||
incremental edits still use `set-shape-layout` / `set-layout-data`).
|
||||
|
||||
The incremental workspace edit path (`set-wasm-attr!`) is unaffected; it keeps
|
||||
dispatching per changed key through the same underlying `props` setters."
|
||||
(:require
|
||||
[app.common.render-wasm.api.props :as props]
|
||||
[app.common.render-wasm.api.shapes :as shapes]))
|
||||
[app.common.render-wasm.api.upload :as upload]))
|
||||
|
||||
(defn serialize-shape!
|
||||
"Applies every host-independent WASM property of `shape`. `set-shape-base-props`
|
||||
runs first because it selects the current shape (`use_shape`) the rest mutate."
|
||||
"Applies every host-independent WASM property of `shape`."
|
||||
[shape]
|
||||
(let [type (get shape :type)]
|
||||
(shapes/set-shape-base-props shape)
|
||||
(props/set-shape-children (get shape :shapes))
|
||||
(props/set-shape-blur (get shape :blur))
|
||||
(props/set-shape-background-blur (get shape :background-blur))
|
||||
(props/set-shape-shadows (get shape :shadow))
|
||||
(upload/set-shape-upload! shape {:include-layout? false})
|
||||
|
||||
(when (some? (get shape :svg-attrs))
|
||||
(props/set-shape-svg-attrs (get shape :svg-attrs)))
|
||||
|
||||
(when (= type :group)
|
||||
(props/set-masked (boolean (get shape :masked-group))))
|
||||
|
||||
(when (= type :bool)
|
||||
(props/set-shape-bool-type (get shape :bool-type)))
|
||||
|
||||
(when (and (contains? #{:path :bool} type) (some? (get shape :content)))
|
||||
(props/set-shape-path-content (get shape :content)))
|
||||
|
||||
(when (= type :text)
|
||||
(props/set-shape-grow-type (get shape :grow-type)))))
|
||||
(props/set-shape-path-content (get shape :content)))))
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
[app.common.render-wasm.api.props :as props]
|
||||
[app.common.render-wasm.api.upload :as upload]
|
||||
[app.common.render-wasm.helpers :as h]
|
||||
[app.common.render-wasm.mem :as mem]
|
||||
[app.common.render-wasm.mem.heap32 :as mem.h32]
|
||||
@ -1030,37 +1031,56 @@
|
||||
(map #(process-fill-image shape-id % thumbnail?))))))
|
||||
|
||||
(defn set-shape-fills
|
||||
[shape-id fills thumbnail?]
|
||||
;; Record write is shared with the headless exporter; the image fetch below is
|
||||
;; browser-only (WebGL textures).
|
||||
(when-let [fills (props/write-shape-fills! fills)]
|
||||
(keep (fn [id]
|
||||
(let [buffer (uuid/get-u32 id)
|
||||
cached-image? (h/call wasm/internal-module "_is_image_cached"
|
||||
(aget buffer 0)
|
||||
(aget buffer 1)
|
||||
(aget buffer 2)
|
||||
(aget buffer 3)
|
||||
thumbnail?)]
|
||||
(when (zero? cached-image?)
|
||||
(fetch-image shape-id id thumbnail?))))
|
||||
(types.fills/get-image-ids fills))))
|
||||
"Writes fill records (unless `write?` is false) and returns pending image
|
||||
fetches. When fills were already uploaded in `_set_shapes_batch`, pass
|
||||
`write?` false so only image fetches remain."
|
||||
([shape-id fills thumbnail?]
|
||||
(set-shape-fills shape-id fills thumbnail? true))
|
||||
([shape-id fills thumbnail? write?]
|
||||
(let [fills (if write?
|
||||
(props/write-shape-fills! fills)
|
||||
(when (seq fills)
|
||||
(types.fills/coerce fills)))]
|
||||
(when fills
|
||||
(keep (fn [id]
|
||||
(let [buffer (uuid/get-u32 id)
|
||||
cached-image? (h/call wasm/internal-module "_is_image_cached"
|
||||
(aget buffer 0)
|
||||
(aget buffer 1)
|
||||
(aget buffer 2)
|
||||
(aget buffer 3)
|
||||
thumbnail?)]
|
||||
(when (zero? cached-image?)
|
||||
(fetch-image shape-id id thumbnail?))))
|
||||
(types.fills/get-image-ids fills))))))
|
||||
|
||||
(defn- stroke-image-ids
|
||||
[strokes]
|
||||
(into []
|
||||
(comp (remove :hidden)
|
||||
(keep #(get-in % [:stroke-image :id])))
|
||||
(or strokes [])))
|
||||
|
||||
(defn set-shape-strokes
|
||||
[shape-id strokes thumbnail?]
|
||||
;; Record write is shared with the headless exporter; the image fetch below is
|
||||
;; browser-only (WebGL textures).
|
||||
(keep (fn [image-id]
|
||||
(let [buffer (uuid/get-u32 image-id)
|
||||
cached-image? (h/call wasm/internal-module "_is_image_cached"
|
||||
(aget buffer 0)
|
||||
(aget buffer 1)
|
||||
(aget buffer 2)
|
||||
(aget buffer 3)
|
||||
thumbnail?)]
|
||||
(when (zero? cached-image?)
|
||||
(fetch-image shape-id image-id thumbnail?))))
|
||||
(props/write-shape-strokes! strokes)))
|
||||
"Writes stroke records (unless `write?` is false) and returns pending image
|
||||
fetches for stroke image fills."
|
||||
([shape-id strokes thumbnail?]
|
||||
(set-shape-strokes shape-id strokes thumbnail? true))
|
||||
([shape-id strokes thumbnail? write?]
|
||||
(let [image-ids (if write?
|
||||
(props/write-shape-strokes! strokes)
|
||||
(stroke-image-ids strokes))]
|
||||
(keep (fn [image-id]
|
||||
(let [buffer (uuid/get-u32 image-id)
|
||||
cached-image? (h/call wasm/internal-module "_is_image_cached"
|
||||
(aget buffer 0)
|
||||
(aget buffer 1)
|
||||
(aget buffer 2)
|
||||
(aget buffer 3)
|
||||
thumbnail?)]
|
||||
(when (zero? cached-image?)
|
||||
(fetch-image shape-id image-id thumbnail?))))
|
||||
image-ids))))
|
||||
|
||||
(defn set-shape-svg-attrs
|
||||
[attrs]
|
||||
@ -1545,47 +1565,70 @@
|
||||
[content]
|
||||
(or content (tc/v2-default-text-content)))
|
||||
|
||||
(defn- set-object-host-attrs
|
||||
"Host-specific attrs after structural upload (text/svg-raw/grid; optionally
|
||||
fills/strokes). When `skip-layout?` is true, flex+layout-item were already in
|
||||
the batch; only grid tracks/cells are applied here. When
|
||||
`skip-fills-strokes?` is true, fill/stroke records were already in the batch;
|
||||
only image fetches remain.
|
||||
|
||||
Always `use-shape` first: after a multi-shape batch the WASM current shape is
|
||||
the last record in the chunk, not this shape."
|
||||
[shape skip-layout? & {:keys [skip-fills-strokes?] :or {skip-fills-strokes? false}}]
|
||||
(let [id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group) [] (get shape :strokes))
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
content))
|
||||
write-fills-strokes? (not skip-fills-strokes?)
|
||||
needs-current? (or write-fills-strokes?
|
||||
(= type :text)
|
||||
(and (some? content) (= type :svg-raw))
|
||||
(if skip-layout?
|
||||
(ctl/grid-layout? shape)
|
||||
true))]
|
||||
|
||||
(when needs-current?
|
||||
(use-shape id))
|
||||
|
||||
(when (and (some? content) (= type :svg-raw))
|
||||
(set-shape-svg-raw-content (get-static-markup shape)))
|
||||
|
||||
(if skip-layout?
|
||||
(when (ctl/grid-layout? shape)
|
||||
(set-grid-layout shape))
|
||||
(do (set-shape-layout shape)
|
||||
(set-layout-data shape)))
|
||||
|
||||
(let [is-text? (= type :text)
|
||||
text-content-pending (when is-text? (set-shape-text-content id content))
|
||||
pending-thumbnails (into [] (concat
|
||||
text-content-pending
|
||||
(when is-text? (set-shape-text-images id content true))
|
||||
(set-shape-fills id fills true write-fills-strokes?)
|
||||
(set-shape-strokes id strokes true write-fills-strokes?)))
|
||||
pending-full (into [] (concat
|
||||
(when is-text? (set-shape-text-images id content false))
|
||||
(set-shape-fills id fills false write-fills-strokes?)
|
||||
(set-shape-strokes id strokes false write-fills-strokes?)))]
|
||||
{:thumbnails pending-thumbnails
|
||||
:full pending-full
|
||||
:font-pending-ids (if (some :callback text-content-pending) [id] [])})))
|
||||
|
||||
(defn set-object
|
||||
[shape]
|
||||
(if-not (and shape (wasm/live?))
|
||||
{:thumbnails [] :full [] :font-pending-ids []}
|
||||
(do
|
||||
(perf/begin-measure "set-object")
|
||||
(let [shape (svg-filters/apply-svg-derived shape)
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
content))]
|
||||
|
||||
(let [shape (svg-filters/apply-svg-derived shape)]
|
||||
(serialize-shape/serialize-shape! shape)
|
||||
|
||||
;; Browser-only: svg-raw markup (needs React) + workspace layout.
|
||||
(when (and (some? content) (= type :svg-raw))
|
||||
(set-shape-svg-raw-content (get-static-markup shape)))
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
(let [is-text? (= type :text)
|
||||
text-content-pending (when is-text? (set-shape-text-content id content))
|
||||
pending-thumbnails (into [] (concat
|
||||
text-content-pending
|
||||
(when is-text? (set-shape-text-images id content true))
|
||||
(set-shape-fills id fills true)
|
||||
(set-shape-strokes id strokes true)))
|
||||
pending-full (into [] (concat
|
||||
(when is-text? (set-shape-text-images id content false))
|
||||
(set-shape-fills id fills false)
|
||||
(set-shape-strokes id strokes false)))]
|
||||
(let [result (set-object-host-attrs shape false)]
|
||||
(perf/end-measure "set-object")
|
||||
{:thumbnails pending-thumbnails
|
||||
:full pending-full
|
||||
:font-pending-ids (if (some :callback text-content-pending) [id] [])})))))
|
||||
|
||||
result)))))
|
||||
(defn- update-text-layouts
|
||||
"Synchronously update text layouts for all shapes and send rect updates
|
||||
to the worker index."
|
||||
@ -1697,30 +1740,54 @@
|
||||
:font-pending-ids (persistent! font-acc)}))]
|
||||
(process-pending shapes thumbnails full font-pending-ids noop-fn)))
|
||||
|
||||
(def ^:private ^:const BATCH_MAX_SHAPES 512)
|
||||
|
||||
(defn- process-shapes-chunk
|
||||
"Process shapes starting at `start-index` until the time budget is exhausted.
|
||||
"Process up to `BATCH_MAX_SHAPES` shapes starting at `start-index`.
|
||||
|
||||
Structural attrs are uploaded in one `_set_shapes_batch` FFI per chunk;
|
||||
host-specific attrs (fills/strokes/text/grid/path) stay per-shape.
|
||||
|
||||
Returns {:thumbnails [...] :full [...] :font-pending-ids [...] :next-index n}"
|
||||
[shapes start-index thumbnails-acc full-acc font-pending-acc]
|
||||
(let [total (count shapes)
|
||||
deadline (+ (js/performance.now) CHUNK_TIME_BUDGET_MS)]
|
||||
(loop [index start-index
|
||||
(let [total (count shapes)
|
||||
end-index (min total (+ start-index BATCH_MAX_SHAPES))
|
||||
chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes))
|
||||
start-index end-index))
|
||||
prepared (mapv svg-filters/apply-svg-derived chunk)]
|
||||
|
||||
;; One multi-shape structural upload (base+children+blur+shadows+flex+item+fills+strokes).
|
||||
(when (seq prepared)
|
||||
(upload/flush-shapes-batch! prepared {:include-layout? true
|
||||
:include-fills-strokes? true}))
|
||||
|
||||
;; Path + svg-attrs still need the legacy per-shape path (variable/large).
|
||||
(doseq [shape prepared]
|
||||
(let [id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)]
|
||||
(when (or (some? (get shape :svg-attrs))
|
||||
(and (contains? #{:path :bool} type) (some? (get shape :content))))
|
||||
(use-shape id)
|
||||
(when (some? (get shape :svg-attrs))
|
||||
(props/set-shape-svg-attrs (get shape :svg-attrs)))
|
||||
(when (and (contains? #{:path :bool} type) (some? (get shape :content)))
|
||||
(props/set-shape-path-content (get shape :content))))))
|
||||
|
||||
(loop [xs prepared
|
||||
t-acc (transient thumbnails-acc)
|
||||
f-acc (transient full-acc)
|
||||
fp-acc (transient font-pending-acc)]
|
||||
(if (and (< index total)
|
||||
;; Check performance.now every 8 shapes to reduce overhead
|
||||
(or (pos? (bit-and (- index start-index) 7))
|
||||
(<= (js/performance.now) deadline)))
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full font-pending-ids]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(if-let [shape (first xs)]
|
||||
(let [{:keys [thumbnails full font-pending-ids]}
|
||||
(set-object-host-attrs shape true :skip-fills-strokes? true)]
|
||||
(recur (next xs)
|
||||
(reduce conj! t-acc thumbnails)
|
||||
(reduce conj! f-acc full)
|
||||
(reduce conj! fp-acc font-pending-ids)))
|
||||
{:thumbnails (persistent! t-acc)
|
||||
:full (persistent! f-acc)
|
||||
:font-pending-ids (persistent! fp-acc)
|
||||
:next-index index}))))
|
||||
:next-index end-index}))))
|
||||
|
||||
(defn- set-objects-async
|
||||
"Asynchronously process shapes in time-budgeted chunks, yielding to the
|
||||
@ -1847,32 +1914,50 @@
|
||||
(defn- set-objects-sync
|
||||
"Synchronously process all shapes (for small shape counts)."
|
||||
[shapes render-callback on-shapes-ready]
|
||||
(let [total-shapes (count shapes)
|
||||
{:keys [thumbnails full font-pending-ids]}
|
||||
(loop [index 0 thumbnails-acc (transient []) full-acc (transient []) font-acc (transient [])]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full font-pending-ids]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(reduce conj! thumbnails-acc thumbnails)
|
||||
(reduce conj! full-acc full)
|
||||
(reduce conj! font-acc font-pending-ids)))
|
||||
{:thumbnails (persistent! thumbnails-acc)
|
||||
:full (persistent! full-acc)
|
||||
:font-pending-ids (persistent! font-acc)}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(when on-shapes-ready (on-shapes-ready))
|
||||
(when (wasm/live?)
|
||||
;; 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")
|
||||
(reset! view-interaction-active? false)
|
||||
(process-pending shapes thumbnails full font-pending-ids
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(request-render "set-objects-sync-complete"))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||
(let [prepared (mapv svg-filters/apply-svg-derived shapes)]
|
||||
(when (seq prepared)
|
||||
(upload/flush-shapes-batch! prepared {:include-layout? true
|
||||
:include-fills-strokes? true}))
|
||||
(doseq [shape prepared]
|
||||
(let [id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)]
|
||||
(when (or (some? (get shape :svg-attrs))
|
||||
(and (contains? #{:path :bool} type) (some? (get shape :content))))
|
||||
(use-shape id)
|
||||
(when (some? (get shape :svg-attrs))
|
||||
(props/set-shape-svg-attrs (get shape :svg-attrs)))
|
||||
(when (and (contains? #{:path :bool} type) (some? (get shape :content)))
|
||||
(props/set-shape-path-content (get shape :content))))))
|
||||
(let [total-shapes (count prepared)
|
||||
{:keys [thumbnails full font-pending-ids]}
|
||||
(loop [index 0
|
||||
thumbnails-acc (transient [])
|
||||
full-acc (transient [])
|
||||
font-acc (transient [])]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth prepared index)
|
||||
{:keys [thumbnails full font-pending-ids]}
|
||||
(set-object-host-attrs shape true :skip-fills-strokes? true)]
|
||||
(recur (inc index)
|
||||
(reduce conj! thumbnails-acc thumbnails)
|
||||
(reduce conj! full-acc full)
|
||||
(reduce conj! font-acc font-pending-ids)))
|
||||
{:thumbnails (persistent! thumbnails-acc)
|
||||
:full (persistent! full-acc)
|
||||
:font-pending-ids (persistent! font-acc)}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(when on-shapes-ready (on-shapes-ready))
|
||||
(when (wasm/live?)
|
||||
;; 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")
|
||||
(reset! view-interaction-active? false)
|
||||
(process-pending shapes thumbnails full font-pending-ids
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(request-render "set-objects-sync-complete"))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))))
|
||||
|
||||
(defn- shapes-in-tree-order
|
||||
"Returns shapes sorted in tree order (parents before children).
|
||||
|
||||
@ -203,6 +203,7 @@ pub extern "C" fn init_headless(_width: i32, _height: i32) -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clean_up() -> Result<()> {
|
||||
crate::performance::page_trace_reset("clean_up");
|
||||
// Cancel the current animation frame if it exists so
|
||||
// it won't try to render without context
|
||||
let render_state = get_render_state();
|
||||
|
||||
@ -274,6 +274,7 @@ pub extern "C" fn render_preview() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn begin_loading() -> Result<()> {
|
||||
performance::page_trace("begin_loading");
|
||||
with_state!(state, {
|
||||
state.loading = true;
|
||||
});
|
||||
@ -288,6 +289,7 @@ pub extern "C" fn end_loading() -> Result<()> {
|
||||
with_state!(state, {
|
||||
state.loading = false;
|
||||
});
|
||||
performance::page_trace_end_loading();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -307,6 +309,7 @@ pub extern "C" fn render_loading_overlay() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn reset_canvas() -> Result<()> {
|
||||
performance::page_trace("reset_canvas");
|
||||
get_render_state().reset_canvas();
|
||||
Ok(())
|
||||
}
|
||||
@ -321,10 +324,12 @@ pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) -> Result<()> {
|
||||
performance::page_trace("set_view:begin");
|
||||
performance::begin_measure!("set_view");
|
||||
let render_state = get_render_state();
|
||||
render_state.set_view(zoom, x, y);
|
||||
performance::end_measure!("set_view");
|
||||
performance::page_trace("set_view:end");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -352,6 +357,7 @@ pub extern "C" fn set_view_start() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_end() -> Result<()> {
|
||||
performance::page_trace("set_view_end:begin");
|
||||
with_state!(state, {
|
||||
performance::begin_measure!("set_view_end");
|
||||
let render_state = get_render_state();
|
||||
@ -382,6 +388,7 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
}
|
||||
performance::end_measure!("set_view_end");
|
||||
});
|
||||
performance::page_trace("set_view_end:end");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -444,15 +451,23 @@ pub extern "C" fn set_focus_mode() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn init_shapes_pool(capacity: usize) -> Result<()> {
|
||||
// First open has no preceding clean_up; page switch already reset there.
|
||||
if !performance::page_trace_active() {
|
||||
performance::page_trace_reset(&format!("init_shapes_pool capacity={capacity}"));
|
||||
} else {
|
||||
performance::page_trace(&format!("init_shapes_pool capacity={capacity}"));
|
||||
}
|
||||
with_state!(state, {
|
||||
state.init_shapes_pool(capacity);
|
||||
});
|
||||
performance::page_trace("init_shapes_pool:done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||
performance::page_trace_use_shape();
|
||||
with_state!(state, {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
state.use_shape(id);
|
||||
@ -552,33 +567,8 @@ pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) -> Result<()>
|
||||
}
|
||||
|
||||
fn set_children_set(entries: Vec<Uuid>) -> Result<()> {
|
||||
let mut deleted = Vec::new();
|
||||
let mut parent_id = None;
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
parent_id = Some(shape.id);
|
||||
(_, deleted) = shape.compute_children_differences(&entries);
|
||||
shape.children = entries.clone();
|
||||
|
||||
for id in entries {
|
||||
state.touch_shape(id);
|
||||
if let Some(children_shape) = state.shapes.get_mut(&id) {
|
||||
children_shape.set_deleted(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
with_state!(state, {
|
||||
let Some(parent_id) = parent_id else {
|
||||
return Err(Error::RecoverableError(
|
||||
"set_children_set: Parent ID not found".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
for id in deleted {
|
||||
state.delete_shape_children(parent_id, id);
|
||||
state.touch_shape(id);
|
||||
}
|
||||
state.set_current_shape_children(entries)?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_time() -> i32 {
|
||||
crate::get_now!() as i32
|
||||
@ -9,6 +11,62 @@ pub fn get_time() -> i32 {
|
||||
now.elapsed().as_millis() as i32
|
||||
}
|
||||
|
||||
/// Always-on page-load / page-switch tracing (temporary profiling).
|
||||
/// Prints to stdout (browser console under Emscripten). No feature gates.
|
||||
/// Active only between `page_trace_reset` and first tiles-complete to avoid
|
||||
/// flooding the console on pan/zoom.
|
||||
static PAGE_TRACE_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
static PAGE_TRACE_EPOCH: AtomicI32 = AtomicI32::new(0);
|
||||
static PAGE_TRACE_LAST: AtomicI32 = AtomicI32::new(0);
|
||||
static PAGE_TRACE_USE_SHAPE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub fn page_trace_active() -> bool {
|
||||
PAGE_TRACE_ACTIVE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Start a new page-load/switch session. Resets epoch and counters.
|
||||
pub fn page_trace_reset(label: &str) {
|
||||
let now = get_time();
|
||||
PAGE_TRACE_ACTIVE.store(true, Ordering::Relaxed);
|
||||
PAGE_TRACE_EPOCH.store(now, Ordering::Relaxed);
|
||||
PAGE_TRACE_LAST.store(now, Ordering::Relaxed);
|
||||
PAGE_TRACE_USE_SHAPE.store(0, Ordering::Relaxed);
|
||||
println!("[wasm-page] === {label} === t={now}ms");
|
||||
}
|
||||
|
||||
/// Log a milestone with delta since previous mark and since session start.
|
||||
pub fn page_trace(label: &str) {
|
||||
if !PAGE_TRACE_ACTIVE.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let now = get_time();
|
||||
let epoch = PAGE_TRACE_EPOCH.load(Ordering::Relaxed);
|
||||
let last = PAGE_TRACE_LAST.load(Ordering::Relaxed);
|
||||
let since_epoch = if epoch == 0 { 0 } else { now - epoch };
|
||||
let since_last = if last == 0 { 0 } else { now - last };
|
||||
PAGE_TRACE_LAST.store(now, Ordering::Relaxed);
|
||||
println!("[wasm-page] {label} +{since_last}ms (session +{since_epoch}ms) t={now}ms");
|
||||
}
|
||||
|
||||
/// Count a `use_shape` during the active page-load session.
|
||||
pub fn page_trace_use_shape() {
|
||||
if PAGE_TRACE_ACTIVE.load(Ordering::Relaxed) {
|
||||
PAGE_TRACE_USE_SHAPE.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log end of bulk loading with shape count.
|
||||
pub fn page_trace_end_loading() {
|
||||
let n = PAGE_TRACE_USE_SHAPE.load(Ordering::Relaxed);
|
||||
page_trace(&format!("end_loading shapes={n}"));
|
||||
}
|
||||
|
||||
/// Final milestone of the page session (first tiles-complete), then silence.
|
||||
pub fn page_trace_done(label: &str) {
|
||||
page_trace(label);
|
||||
PAGE_TRACE_ACTIVE.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Log a message to the browser console (only when profile-macros feature is enabled)
|
||||
#[macro_export]
|
||||
macro_rules! console_log {
|
||||
|
||||
@ -2332,6 +2332,7 @@ impl RenderState {
|
||||
timestamp: i32,
|
||||
sync_render: bool,
|
||||
) -> Result<FrameType> {
|
||||
performance::page_trace("start_render_loop:begin");
|
||||
self.clear(tree);
|
||||
|
||||
let _start = performance::begin_timed_log!("start_render_loop");
|
||||
@ -2536,6 +2537,7 @@ impl RenderState {
|
||||
// HiDPI viewports and is not needed until the next drag.
|
||||
self.present_frame(tree);
|
||||
self.viewport_presented = true;
|
||||
performance::page_trace_done("render:ViewportReady (tiles-complete)");
|
||||
wapi::notify_tiles_render_complete!();
|
||||
Self::drain_partial_gpu_soft();
|
||||
}
|
||||
@ -2547,11 +2549,13 @@ impl RenderState {
|
||||
self.rebuild_backbuffer_crop_cache(tree);
|
||||
}
|
||||
self.present_frame(tree);
|
||||
performance::page_trace_done("render:Full (tiles-complete)");
|
||||
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.
|
||||
performance::page_trace("render:Full (interest fill done)");
|
||||
self.rebuild_backbuffer_crop_cache(tree);
|
||||
}
|
||||
performance::end_measure!("render");
|
||||
@ -4277,6 +4281,7 @@ impl RenderState {
|
||||
/// until the post-gesture full render replaces them.
|
||||
pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) {
|
||||
let zoom_changed = self.zoom_changed();
|
||||
performance::page_trace("rebuild_tile_index:begin");
|
||||
performance::begin_measure!("rebuild_tile_index");
|
||||
let mut nodes = Vec::<Uuid>::with_capacity(64);
|
||||
nodes.push(Uuid::nil());
|
||||
@ -4297,6 +4302,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
performance::end_measure!("rebuild_tile_index");
|
||||
performance::page_trace("rebuild_tile_index:end");
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
||||
|
||||
@ -340,6 +340,36 @@ impl State {
|
||||
self.shapes.set_modifiers(modifiers);
|
||||
}
|
||||
|
||||
/// Replace the current shape's children list (same semantics as `_set_children`).
|
||||
pub fn set_current_shape_children(&mut self, entries: Vec<Uuid>) -> Result<()> {
|
||||
let mut deleted = Vec::new();
|
||||
let parent_id = {
|
||||
let Some(shape) = self.current_shape_mut() else {
|
||||
return Err(Error::RecoverableError(
|
||||
"set_current_shape_children: no current shape".to_string(),
|
||||
));
|
||||
};
|
||||
let id = shape.id;
|
||||
(_, deleted) = shape.compute_children_differences(&entries);
|
||||
shape.children = entries.clone();
|
||||
id
|
||||
};
|
||||
|
||||
for id in &entries {
|
||||
self.touch_shape(*id);
|
||||
if let Some(children_shape) = self.shapes.get_mut(id) {
|
||||
children_shape.set_deleted(false);
|
||||
}
|
||||
}
|
||||
|
||||
for id in deleted {
|
||||
self.delete_shape_children(parent_id, id);
|
||||
self.touch_shape(id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn touch_current(&mut self) {
|
||||
// `mark_touched` only drives incremental on-screen tile invalidation;
|
||||
// the headless export path has no render state, so skip it there.
|
||||
|
||||
@ -82,12 +82,15 @@ impl ShapesPoolImpl {
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, capacity: usize) {
|
||||
performance::page_trace("shapes_pool_initialize:begin");
|
||||
performance::begin_measure!("shapes_pool_initialize");
|
||||
self.counter = 0;
|
||||
self.uuid_to_idx = HashMap::with_capacity(capacity);
|
||||
|
||||
let additional = capacity as i32 - self.shapes.len() as i32;
|
||||
if additional <= 0 {
|
||||
performance::end_measure!("shapes_pool_initialize");
|
||||
performance::page_trace("shapes_pool_initialize:done (reuse)");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -99,6 +102,7 @@ impl ShapesPoolImpl {
|
||||
self.shapes
|
||||
.extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional as usize));
|
||||
performance::end_measure!("shapes_pool_initialize");
|
||||
performance::page_trace("shapes_pool_initialize:done");
|
||||
}
|
||||
|
||||
pub fn add_shape(&mut self, id: Uuid) -> &mut Shape {
|
||||
|
||||
@ -8,7 +8,7 @@ mod gradient;
|
||||
mod image;
|
||||
mod solid;
|
||||
|
||||
const RAW_FILL_DATA_SIZE: usize = std::mem::size_of::<RawFillData>();
|
||||
pub const RAW_FILL_DATA_SIZE: usize = std::mem::size_of::<RawFillData>();
|
||||
|
||||
#[repr(C, u8, align(4))]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, ToJs)]
|
||||
|
||||
@ -4,9 +4,14 @@ use macros::ToJs;
|
||||
|
||||
mod align;
|
||||
pub mod constraints;
|
||||
mod flex;
|
||||
pub mod flex;
|
||||
mod grid;
|
||||
|
||||
pub use align::{
|
||||
RawAlignContent, RawAlignItems, RawAlignSelf, RawJustifyContent, RawJustifyItems,
|
||||
};
|
||||
pub use flex::{RawFlexDirection, RawWrapType};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy, ToJs)]
|
||||
#[repr(u8)]
|
||||
#[allow(dead_code)]
|
||||
|
||||
@ -12,10 +12,10 @@ use macros::wasm_error;
|
||||
|
||||
use super::RawShapeType;
|
||||
|
||||
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
|
||||
const FLAG_HIDDEN: u8 = 0b0000_0010;
|
||||
pub const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
|
||||
pub const FLAG_HIDDEN: u8 = 0b0000_0010;
|
||||
|
||||
const RAW_BASE_PROPS_SIZE: usize = std::mem::size_of::<RawBasePropsData>();
|
||||
pub const RAW_BASE_PROPS_SIZE: usize = std::mem::size_of::<RawBasePropsData>();
|
||||
|
||||
/// Binary layout for batched shape base properties.
|
||||
///
|
||||
@ -26,70 +26,70 @@ const RAW_BASE_PROPS_SIZE: usize = std::mem::size_of::<RawBasePropsData>();
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RawBasePropsData {
|
||||
// UUID id (16 bytes)
|
||||
id_a: u32,
|
||||
id_b: u32,
|
||||
id_c: u32,
|
||||
id_d: u32,
|
||||
pub id_a: u32,
|
||||
pub id_b: u32,
|
||||
pub id_c: u32,
|
||||
pub id_d: u32,
|
||||
// UUID parent_id (16 bytes)
|
||||
parent_a: u32,
|
||||
parent_b: u32,
|
||||
parent_c: u32,
|
||||
parent_d: u32,
|
||||
pub parent_a: u32,
|
||||
pub parent_b: u32,
|
||||
pub parent_c: u32,
|
||||
pub parent_d: u32,
|
||||
// Single-byte fields
|
||||
shape_type: u8,
|
||||
flags: u8,
|
||||
blend_mode: u8,
|
||||
constraint_h: u8,
|
||||
constraint_v: u8,
|
||||
padding: [u8; 3],
|
||||
pub shape_type: u8,
|
||||
pub flags: u8,
|
||||
pub blend_mode: u8,
|
||||
pub constraint_h: u8,
|
||||
pub constraint_v: u8,
|
||||
pub padding: [u8; 3],
|
||||
// f32 fields
|
||||
opacity: f32,
|
||||
rotation: f32,
|
||||
pub opacity: f32,
|
||||
pub rotation: f32,
|
||||
// Transform matrix (a, b, c, d, e, f)
|
||||
transform_a: f32,
|
||||
transform_b: f32,
|
||||
transform_c: f32,
|
||||
transform_d: f32,
|
||||
transform_e: f32,
|
||||
transform_f: f32,
|
||||
pub transform_a: f32,
|
||||
pub transform_b: f32,
|
||||
pub transform_c: f32,
|
||||
pub transform_d: f32,
|
||||
pub transform_e: f32,
|
||||
pub transform_f: f32,
|
||||
// Selrect (x1, y1, x2, y2)
|
||||
selrect_x1: f32,
|
||||
selrect_y1: f32,
|
||||
selrect_x2: f32,
|
||||
selrect_y2: f32,
|
||||
pub selrect_x1: f32,
|
||||
pub selrect_y1: f32,
|
||||
pub selrect_x2: f32,
|
||||
pub selrect_y2: f32,
|
||||
// Corners (r1, r2, r3, r4)
|
||||
corner_r1: f32,
|
||||
corner_r2: f32,
|
||||
corner_r3: f32,
|
||||
corner_r4: f32,
|
||||
pub corner_r1: f32,
|
||||
pub corner_r2: f32,
|
||||
pub corner_r3: f32,
|
||||
pub corner_r4: f32,
|
||||
}
|
||||
|
||||
impl RawBasePropsData {
|
||||
fn id(&self) -> Uuid {
|
||||
pub fn id(&self) -> Uuid {
|
||||
uuid_from_u32_quartet(self.id_a, self.id_b, self.id_c, self.id_d)
|
||||
}
|
||||
|
||||
fn parent_id(&self) -> Uuid {
|
||||
pub fn parent_id(&self) -> Uuid {
|
||||
uuid_from_u32_quartet(self.parent_a, self.parent_b, self.parent_c, self.parent_d)
|
||||
}
|
||||
|
||||
fn clip_content(&self) -> bool {
|
||||
pub fn clip_content(&self) -> bool {
|
||||
(self.flags & FLAG_CLIP_CONTENT) != 0
|
||||
}
|
||||
|
||||
fn hidden(&self) -> bool {
|
||||
pub fn hidden(&self) -> bool {
|
||||
(self.flags & FLAG_HIDDEN) != 0
|
||||
}
|
||||
|
||||
fn blend_mode(&self) -> BlendMode {
|
||||
pub fn blend_mode(&self) -> BlendMode {
|
||||
RawBlendMode::from(self.blend_mode).into()
|
||||
}
|
||||
|
||||
fn constraint_h(&self) -> Option<ConstraintH> {
|
||||
pub fn constraint_h(&self) -> Option<ConstraintH> {
|
||||
RawConstraintH::from(self.constraint_h).into()
|
||||
}
|
||||
|
||||
fn constraint_v(&self) -> Option<ConstraintV> {
|
||||
pub fn constraint_v(&self) -> Option<ConstraintV> {
|
||||
RawConstraintV::from(self.constraint_v).into()
|
||||
}
|
||||
}
|
||||
@ -100,21 +100,8 @@ impl From<[u8; RAW_BASE_PROPS_SIZE]> for RawBasePropsData {
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_base_props() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
if bytes.len() < RAW_BASE_PROPS_SIZE {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// FIXME: this should just be a try_from
|
||||
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE]
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for base props".to_string()))?;
|
||||
let raw = RawBasePropsData::from(data);
|
||||
|
||||
/// Apply base props from a parsed record (selects the shape and sets core attrs).
|
||||
pub fn apply_base_props(raw: &RawBasePropsData) -> Result<()> {
|
||||
let id = raw.id();
|
||||
let parent_id = raw.parent_id();
|
||||
let shape_type = RawShapeType::from(raw.shape_type);
|
||||
@ -153,6 +140,24 @@ pub extern "C" fn set_shape_base_props() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_base_props() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
if bytes.len() < RAW_BASE_PROPS_SIZE {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// FIXME: this should just be a try_from
|
||||
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE]
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for base props".to_string()))?;
|
||||
let raw = RawBasePropsData::from(data);
|
||||
apply_base_props(&raw)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
mod base_props;
|
||||
mod upload_batch;
|
||||
|
||||
use macros::ToJs;
|
||||
|
||||
use crate::shapes::{Bool, Frame, Group, Path, Rect, SVGRaw, TextContent, Type};
|
||||
use crate::shapes::{Bool, Frame, Group, Path, Rect, Shape, SVGRaw, TextContent, Type};
|
||||
use crate::with_current_shape_mut;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, ToJs)]
|
||||
|
||||
480
render-wasm/src/wasm/shapes/upload_batch.rs
Normal file
480
render-wasm/src/wasm/shapes/upload_batch.rs
Normal file
@ -0,0 +1,480 @@
|
||||
//! Multi-shape / enlarged cold-upload batch protocol.
|
||||
//!
|
||||
//! Buffer layout:
|
||||
//! ```text
|
||||
//! [u32 shape_count]
|
||||
//! repeat shape_count times:
|
||||
//! [u32 payload_len] // bytes after this u32
|
||||
//! [104 base props] // RawBasePropsData
|
||||
//! [u32 section_mask]
|
||||
//! optional sections (presence via mask; order is FIXED, not bit-numeric):
|
||||
//! CHILDREN: [u32 n][n × 16 uuid]
|
||||
//! BLUR_LAYER: [u8 hidden][u8;3 pad][f32 value]
|
||||
//! BLUR_BG: same
|
||||
//! SHADOWS: [u32 n][n × 24]
|
||||
//! MASKED: [u8 value][u8;3 pad]
|
||||
//! BOOL_TYPE: [u8 value][u8;3 pad]
|
||||
//! GROW_TYPE: [u8 value][u8;3 pad]
|
||||
//! FLEX: 32 bytes (clears container layout, then sets flex)
|
||||
//! LAYOUT_ITEM: 40 bytes (must follow FLEX so clear_layout does not wipe it)
|
||||
//! FILLS: [u8 n][u8;3][n × RawFillData] (same as set_shape_fills)
|
||||
//! STROKES: [u32 n][n × (36-byte header + RawFillData)]
|
||||
//! ```
|
||||
//!
|
||||
//! Text, path geometry, and grid tracks/cells stay on the legacy
|
||||
//! per-shape FFI path after the batch flush.
|
||||
|
||||
use skia_safe as skia;
|
||||
|
||||
use crate::mem;
|
||||
use crate::performance;
|
||||
use crate::shapes::{
|
||||
Blur, BlurType, BoolType, GrowType, Shadow, ShadowStyle, Shape, Stroke, Type,
|
||||
};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::wasm::fills::{read_fills_from_bytes, RawFillData, RAW_FILL_DATA_SIZE};
|
||||
use crate::wasm::layouts::{
|
||||
RawAlignContent, RawAlignItems, RawAlignSelf, RawFlexDirection, RawJustifyContent,
|
||||
RawJustifyItems, RawSizing, RawWrapType,
|
||||
};
|
||||
use crate::wasm::shadows::RawShadowStyle;
|
||||
use crate::wasm::shapes::base_props::{apply_base_props, RawBasePropsData, RAW_BASE_PROPS_SIZE};
|
||||
use crate::wasm::strokes::{RawStrokeCap, RawStrokeStyle};
|
||||
use crate::with_current_shape_mut;
|
||||
use crate::with_state;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use macros::wasm_error;
|
||||
|
||||
pub const SECTION_CHILDREN: u32 = 1 << 0;
|
||||
pub const SECTION_BLUR_LAYER: u32 = 1 << 1;
|
||||
pub const SECTION_BLUR_BG: u32 = 1 << 2;
|
||||
pub const SECTION_SHADOWS: u32 = 1 << 3;
|
||||
pub const SECTION_MASKED: u32 = 1 << 4;
|
||||
pub const SECTION_BOOL_TYPE: u32 = 1 << 5;
|
||||
pub const SECTION_GROW_TYPE: u32 = 1 << 6;
|
||||
pub const SECTION_LAYOUT_ITEM: u32 = 1 << 7;
|
||||
pub const SECTION_FLEX: u32 = 1 << 8;
|
||||
pub const SECTION_FILLS: u32 = 1 << 9;
|
||||
pub const SECTION_STROKES: u32 = 1 << 10;
|
||||
|
||||
const STROKE_ALIGN_CENTER: u8 = 0;
|
||||
const STROKE_ALIGN_INNER: u8 = 1;
|
||||
const STROKE_ALIGN_OUTER: u8 = 2;
|
||||
|
||||
fn decode_optional_f32(value: f32) -> Option<f32> {
|
||||
if value.is_finite() && value >= 0.0 {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct Cursor<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, pos: 0 }
|
||||
}
|
||||
|
||||
fn remaining(&self) -> usize {
|
||||
self.data.len().saturating_sub(self.pos)
|
||||
}
|
||||
|
||||
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
|
||||
if self.remaining() < n {
|
||||
return Err(Error::CriticalError(
|
||||
"upload_batch: truncated buffer".to_string(),
|
||||
));
|
||||
}
|
||||
let slice = &self.data[self.pos..self.pos + n];
|
||||
self.pos += n;
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
fn u32(&mut self) -> Result<u32> {
|
||||
let b = self.take(4)?;
|
||||
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
||||
}
|
||||
|
||||
fn u8(&mut self) -> Result<u8> {
|
||||
Ok(self.take(1)?[0])
|
||||
}
|
||||
|
||||
fn f32(&mut self) -> Result<f32> {
|
||||
let b = self.take(4)?;
|
||||
Ok(f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
||||
}
|
||||
|
||||
fn i32(&mut self) -> Result<i32> {
|
||||
let b = self.take(4)?;
|
||||
Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
||||
}
|
||||
|
||||
fn uuid(&mut self) -> Result<Uuid> {
|
||||
let a = self.u32()?;
|
||||
let b = self.u32()?;
|
||||
let c = self.u32()?;
|
||||
let d = self.u32()?;
|
||||
Ok(uuid_from_u32_quartet(a, b, c, d))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_base_props(cur: &mut Cursor<'_>) -> Result<RawBasePropsData> {
|
||||
let bytes = cur.take(RAW_BASE_PROPS_SIZE)?;
|
||||
let arr: [u8; RAW_BASE_PROPS_SIZE] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("upload_batch: bad base props".to_string()))?;
|
||||
Ok(RawBasePropsData::from(arr))
|
||||
}
|
||||
|
||||
fn apply_blur(layer: bool, hidden: bool, value: f32) {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let blur_type = if layer {
|
||||
BlurType::LayerBlur
|
||||
} else {
|
||||
BlurType::BackgroundBlur
|
||||
};
|
||||
let blur = Some(Blur::new(blur_type, hidden, value));
|
||||
if layer {
|
||||
shape.set_blur(blur);
|
||||
} else {
|
||||
shape.set_background_blur(blur);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_blur(layer: bool) {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
if layer {
|
||||
shape.set_blur(None);
|
||||
} else {
|
||||
shape.set_background_blur(None);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn apply_shadows(cur: &mut Cursor<'_>) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.clear_shadows();
|
||||
});
|
||||
let n = cur.u32()? as usize;
|
||||
for _ in 0..n {
|
||||
let rgba = cur.u32()?;
|
||||
let blur = cur.f32()?;
|
||||
let spread = cur.f32()?;
|
||||
let x = cur.f32()?;
|
||||
let y = cur.f32()?;
|
||||
let style = cur.u8()?;
|
||||
let hidden = cur.u8()? != 0;
|
||||
let _pad = cur.take(2)?;
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let color = skia::Color::new(rgba);
|
||||
let style: ShadowStyle = RawShadowStyle::from(style).into();
|
||||
shape.add_shadow(Shadow::new(color, blur, spread, (x, y), style, hidden));
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_layout_item(cur: &mut Cursor<'_>) -> Result<()> {
|
||||
let margin_top = cur.f32()?;
|
||||
let margin_right = cur.f32()?;
|
||||
let margin_bottom = cur.f32()?;
|
||||
let margin_left = cur.f32()?;
|
||||
let h_sizing = cur.u8()?;
|
||||
let v_sizing = cur.u8()?;
|
||||
let flags = cur.u8()?;
|
||||
let align_self = cur.u8()?;
|
||||
let max_h = cur.f32()?;
|
||||
let min_h = cur.f32()?;
|
||||
let max_w = cur.f32()?;
|
||||
let min_w = cur.f32()?;
|
||||
let z_index = cur.i32()?;
|
||||
|
||||
let has_max_h = (flags & 0x01) != 0;
|
||||
let has_min_h = (flags & 0x02) != 0;
|
||||
let has_max_w = (flags & 0x04) != 0;
|
||||
let has_min_w = (flags & 0x08) != 0;
|
||||
let is_absolute = (flags & 0x10) != 0;
|
||||
|
||||
let h_sizing = RawSizing::from(h_sizing);
|
||||
let v_sizing = RawSizing::from(v_sizing);
|
||||
let max_h = has_max_h.then(|| max_h.max(0.01));
|
||||
let min_h = has_min_h.then(|| min_h.clamp(0.01, max_h.unwrap_or(f32::INFINITY)));
|
||||
let max_w = has_max_w.then(|| max_w.max(0.01));
|
||||
let min_w = has_min_w.then(|| min_w.clamp(0.01, max_w.unwrap_or(f32::INFINITY)));
|
||||
let z_index = if z_index != 0 { Some(z_index) } else { None };
|
||||
let align_self = RawAlignSelf::from(align_self).try_into().ok();
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_flex_layout_child_data(
|
||||
margin_top,
|
||||
margin_right,
|
||||
margin_bottom,
|
||||
margin_left,
|
||||
h_sizing.into(),
|
||||
v_sizing.into(),
|
||||
max_h,
|
||||
min_h,
|
||||
max_w,
|
||||
min_w,
|
||||
align_self,
|
||||
is_absolute,
|
||||
z_index,
|
||||
);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_flex(cur: &mut Cursor<'_>) -> Result<()> {
|
||||
let dir = cur.u8()?;
|
||||
let align_items = cur.u8()?;
|
||||
let align_content = cur.u8()?;
|
||||
let justify_items = cur.u8()?;
|
||||
let justify_content = cur.u8()?;
|
||||
let wrap_type = cur.u8()?;
|
||||
let _pad = cur.take(2)?;
|
||||
let row_gap = cur.f32()?;
|
||||
let column_gap = cur.f32()?;
|
||||
let padding_top = cur.f32()?;
|
||||
let padding_right = cur.f32()?;
|
||||
let padding_bottom = cur.f32()?;
|
||||
let padding_left = cur.f32()?;
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.clear_layout();
|
||||
shape.set_flex_layout_data(
|
||||
RawFlexDirection::from(dir).into(),
|
||||
row_gap,
|
||||
column_gap,
|
||||
RawAlignItems::from(align_items).into(),
|
||||
RawAlignContent::from(align_content).into(),
|
||||
RawJustifyItems::from(justify_items).into(),
|
||||
RawJustifyContent::from(justify_content).into(),
|
||||
RawWrapType::from(wrap_type).into(),
|
||||
padding_top,
|
||||
padding_right,
|
||||
padding_bottom,
|
||||
padding_left,
|
||||
);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_shape_payload(payload: &[u8]) -> Result<()> {
|
||||
let mut cur = Cursor::new(payload);
|
||||
let raw = read_base_props(&mut cur)?;
|
||||
let mask = cur.u32()?;
|
||||
apply_base_props(&raw)?;
|
||||
|
||||
if mask & SECTION_CHILDREN != 0 {
|
||||
let n = cur.u32()? as usize;
|
||||
let mut entries = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
entries.push(cur.uuid()?);
|
||||
}
|
||||
with_state!(state, {
|
||||
state.set_current_shape_children(entries)?;
|
||||
});
|
||||
}
|
||||
|
||||
if mask & SECTION_BLUR_LAYER != 0 {
|
||||
let hidden = cur.u8()? != 0;
|
||||
let _ = cur.take(3)?;
|
||||
let value = cur.f32()?;
|
||||
apply_blur(true, hidden, value);
|
||||
} else {
|
||||
clear_blur(true);
|
||||
}
|
||||
|
||||
if mask & SECTION_BLUR_BG != 0 {
|
||||
let hidden = cur.u8()? != 0;
|
||||
let _ = cur.take(3)?;
|
||||
let value = cur.f32()?;
|
||||
apply_blur(false, hidden, value);
|
||||
} else {
|
||||
clear_blur(false);
|
||||
}
|
||||
|
||||
if mask & SECTION_SHADOWS != 0 {
|
||||
apply_shadows(&mut cur)?;
|
||||
} else {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.clear_shadows();
|
||||
});
|
||||
}
|
||||
|
||||
if mask & SECTION_MASKED != 0 {
|
||||
let masked = cur.u8()? != 0;
|
||||
let _ = cur.take(3)?;
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_masked(masked);
|
||||
});
|
||||
}
|
||||
|
||||
if mask & SECTION_BOOL_TYPE != 0 {
|
||||
let raw_bool = cur.u8()?;
|
||||
let _ = cur.take(3)?;
|
||||
let bool_type = match raw_bool {
|
||||
0 => BoolType::Union,
|
||||
1 => BoolType::Difference,
|
||||
2 => BoolType::Intersection,
|
||||
_ => BoolType::Exclusion,
|
||||
};
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_bool_type(bool_type);
|
||||
});
|
||||
}
|
||||
|
||||
if mask & SECTION_GROW_TYPE != 0 {
|
||||
let raw_grow = cur.u8()?;
|
||||
let _ = cur.take(3)?;
|
||||
let grow_type = match raw_grow {
|
||||
1 => GrowType::AutoWidth,
|
||||
2 => GrowType::AutoHeight,
|
||||
_ => GrowType::Fixed,
|
||||
};
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
if let Type::Text(text_content) = &mut shape.shape_type {
|
||||
text_content.set_grow_type(grow_type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FLEX before LAYOUT_ITEM: clear_layout must not wipe the item we just set.
|
||||
// Only clear when this payload owns layout (workspace cold-load). Exporter /
|
||||
// serialize-shape! omit both bits and must not clobber existing layout.
|
||||
if mask & SECTION_FLEX != 0 {
|
||||
apply_flex(&mut cur)?;
|
||||
} else if mask & SECTION_LAYOUT_ITEM != 0 {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.clear_layout();
|
||||
});
|
||||
}
|
||||
|
||||
if mask & SECTION_LAYOUT_ITEM != 0 {
|
||||
apply_layout_item(&mut cur)?;
|
||||
}
|
||||
|
||||
if mask & SECTION_FILLS != 0 {
|
||||
apply_fills(&mut cur)?;
|
||||
}
|
||||
|
||||
if mask & SECTION_STROKES != 0 {
|
||||
apply_strokes(&mut cur)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_fills(cur: &mut Cursor<'_>) -> Result<()> {
|
||||
let header = cur.take(4)?;
|
||||
let n = header[0] as usize;
|
||||
let bytes = if n == 0 {
|
||||
&[][..]
|
||||
} else {
|
||||
cur.take(n * RAW_FILL_DATA_SIZE)?
|
||||
};
|
||||
let fills = read_fills_from_bytes(bytes, n);
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_fills(fills);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_strokes(cur: &mut Cursor<'_>) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.clear_strokes();
|
||||
});
|
||||
|
||||
let n = cur.u32()? as usize;
|
||||
for _ in 0..n {
|
||||
let width = cur.f32()?;
|
||||
let style = cur.u8()?;
|
||||
let align = cur.u8()?;
|
||||
let cap_start = cur.u8()?;
|
||||
let cap_end = cur.u8()?;
|
||||
let dash = cur.f32()?;
|
||||
let gap = cur.f32()?;
|
||||
let has_sides = cur.u8()? != 0;
|
||||
let _pad = cur.take(3)?;
|
||||
let top = cur.f32()?;
|
||||
let right = cur.f32()?;
|
||||
let bottom = cur.f32()?;
|
||||
let left = cur.f32()?;
|
||||
let fill_bytes = cur.take(RAW_FILL_DATA_SIZE)?;
|
||||
let fill = RawFillData::try_from(fill_bytes)
|
||||
.map_err(|e| Error::CriticalError(format!("upload_batch stroke fill: {e}")))?;
|
||||
|
||||
let stroke_style = RawStrokeStyle::from(style);
|
||||
let cap_start = RawStrokeCap::from(cap_start);
|
||||
let cap_end = RawStrokeCap::from(cap_end);
|
||||
let dash = decode_optional_f32(dash);
|
||||
let gap = decode_optional_f32(gap);
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let stroke = match align {
|
||||
STROKE_ALIGN_INNER => Stroke::new_inner_stroke(
|
||||
width,
|
||||
stroke_style.into(),
|
||||
cap_start.try_into().ok(),
|
||||
cap_end.try_into().ok(),
|
||||
dash,
|
||||
gap,
|
||||
),
|
||||
STROKE_ALIGN_OUTER => Stroke::new_outer_stroke(
|
||||
width,
|
||||
stroke_style.into(),
|
||||
cap_start.try_into().ok(),
|
||||
cap_end.try_into().ok(),
|
||||
dash,
|
||||
gap,
|
||||
),
|
||||
STROKE_ALIGN_CENTER | _ => Stroke::new_center_stroke(
|
||||
width,
|
||||
stroke_style.into(),
|
||||
cap_start.try_into().ok(),
|
||||
cap_end.try_into().ok(),
|
||||
dash,
|
||||
gap,
|
||||
),
|
||||
};
|
||||
shape.add_stroke(stroke);
|
||||
if has_sides {
|
||||
let _ = shape.set_last_stroke_widths([top, right, bottom, left]);
|
||||
}
|
||||
let _ = shape.set_stroke_fill(fill.into());
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a multi-shape upload buffer previously written via `_alloc_bytes`.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shapes_batch() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
if bytes.len() < 4 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut cur = Cursor::new(&bytes);
|
||||
let count = cur.u32()? as usize;
|
||||
performance::page_trace(&format!("set_shapes_batch:begin count={count}"));
|
||||
|
||||
for _ in 0..count {
|
||||
let payload_len = cur.u32()? as usize;
|
||||
let payload = cur.take(payload_len)?;
|
||||
apply_shape_payload(payload)?;
|
||||
performance::page_trace_use_shape();
|
||||
}
|
||||
|
||||
performance::page_trace(&format!("set_shapes_batch:end count={count}"));
|
||||
Ok(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user