From 10a2c9bc5c5dfe05565f8305fe114365b1403851 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 1 Oct 2026 18:34:54 +0200 Subject: [PATCH] images --- .../common/render_wasm/api/upload_encode.cljc | 24 ++++++++++++--- .../render_wasm/upload_encode_test.cljc | 29 +++++++++++++++++++ frontend/src/app/render_wasm/api.cljs | 21 +++++++++----- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/common/src/app/common/render_wasm/api/upload_encode.cljc b/common/src/app/common/render_wasm/api/upload_encode.cljc index 0f510ca927..53b14809e8 100644 --- a/common/src/app/common/render_wasm/api/upload_encode.cljc +++ b/common/src/app/common/render_wasm/api/upload_encode.cljc @@ -21,8 +21,8 @@ [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid])) -;; v2: JVM UUID layout fixed to LE u32 quartet (matches CLJS/WASM). -(def ^:const PROTOCOL-VERSION "wasm-shapes-batch/v2") +;; v3: fill/stroke image UUIDs use LE u32 quartet on the JVM (v2 only fixed shape ids). +(def ^:const PROTOCOL-VERSION "wasm-shapes-batch/v3") (def ^:const BASE-PROPS-SIZE 104) (def ^:const FLAG-CLIP-CONTENT 0x01) @@ -262,12 +262,28 @@ [fills] (+ 4 (* (count fills) types.fills.impl/FILL-U8-SIZE))) +(defn- write-image-fill-wasm! + "Like `types.fills.impl/write-image-fill`, then rewrite the image id with + `write-uuid!` (JVM `buf/write-uuid` is BE; WASM expects LE u32 quartet)." + [offset dview opacity image] + (let [end (types.fills.impl/write-image-fill offset dview opacity image)] + (write-uuid! dview (+ offset 4) (get image :id)) + end)) + (defn- write-fills-section! [dview offset fills] - (let [fills (types.fills/coerce (or fills [])) + (let [fills* (into [] (take types.fills.impl/MAX-FILLS) (or fills [])) + fills (types.fills/coerce fills*) nbytes (fills-data-byte-size fills) src (.-dbuffer fills)] (copy-u8! dview offset src 0 nbytes) + ;; Coerce encodes image ids with `buf/write-uuid` (BE on JVM). Patch to LE. + (doseq [[i fill] (map-indexed vector fills*) + :let [image (:fill-image fill)] + :when image] + (write-uuid! dview + (+ offset 4 (* i types.fills.impl/FILL-U8-SIZE) 4) + (get image :id))) (+ offset nbytes))) (defn- write-stroke-fill! @@ -281,7 +297,7 @@ (types.fills.impl/write-gradient-fill offset dview opacity gradient) (some? image) - (types.fills.impl/write-image-fill offset dview opacity image) + (write-image-fill-wasm! offset dview opacity image) (some? color) (types.fills.impl/write-solid-fill offset dview opacity color) diff --git a/common/test/common_tests/render_wasm/upload_encode_test.cljc b/common/test/common_tests/render_wasm/upload_encode_test.cljc index 91d05263b7..f9ee2c1b18 100644 --- a/common/test/common_tests/render_wasm/upload_encode_test.cljc +++ b/common/test/common_tests/render_wasm/upload_encode_test.cljc @@ -132,6 +132,35 @@ (t/is (= (nth expected i) (u8-at rec (+ 4 i))) (str "uuid byte " i))))) +(t/deftest encode-fill-image-uuid-matches-wasm-le-u32-quartet + ;; Image fill id must use the same LE layout as shape ids; otherwise WASM + ;; looks up a swapped id and never binds the host-fetched texture. + (let [shape-id #uuid "11111111-1111-1111-1111-111111111111" + image-id #uuid "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8" + shape (-> (sample-rect shape-id uuid/zero) + (assoc :parent-id uuid/zero + :fills [{:fill-image {:id image-id + :width 10 + :height 10 + :keep-aspect-ratio false} + :fill-opacity 1}] + :strokes [])) + rec (encode/encode-shape-record + shape + {:include-layout? false + :include-fills-strokes? true}) + ;; [len][base 104][mask][children count=0][fills count][fill…] + ;; image uuid is at fill_payload + 4. + fill0 (+ 4 104 4 4 4) + expected [0xa4 0xa3 0xa2 0xa1 + 0xc2 0xc1 0xb2 0xb1 + 0xd4 0xd3 0xd2 0xd1 + 0xd8 0xd7 0xd6 0xd5]] + (t/is (= 0x03 (u8-at rec fill0)) "image fill type tag") + (doseq [i (range 16)] + (t/is (= (nth expected i) (u8-at rec (+ fill0 4 i))) + (str "fill-image uuid byte " i))))) + (t/deftest encode-text-shape-type-and-grow (let [id (uuid/next) shape {:id id diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 82ac49ecb9..7ee82acda2 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -622,9 +622,7 @@ "Paint geometry/fills as soon as the structural batch is in WASM, before the slower host-attrs pass (text/path/grid/images). - Leaves `_begin_loading` active afterward so host-attr mutations do not - invalidate tiles until the final `_end_loading` in finalize. Optionally - invokes `on-shapes-ready` so page-transition blur can lift on this frame." + Leaves loading mode OFF so later image stores can invalidate tiles." [on-shapes-ready] (when (wasm/live?) (h/call wasm/internal-module "_end_loading") @@ -632,8 +630,7 @@ ;; Sync render: do not wait for rAF — host-attrs chunks would delay it. (h/call wasm/internal-module "_render_sync") (set! wasm/internal-frame-id nil) - (js/console.info "[wasm-batch] structural paint") - (h/call wasm/internal-module "_begin_loading"))) + (js/console.info "[wasm-batch] structural paint"))) (defonce pending-render (atom false)) (defonce shapes-loading? (atom false)) @@ -796,6 +793,16 @@ (when was-loading (request-render "set-objects:flush")))) +(defn- paint-after-images! + "Sync paint after image bytes are stored. Progressive `request-render` can + keep early-paint tiles that lack textures; `render_sync` rebuilds them." + [] + (when (wasm/live?) + (stop-progressive-render!) + (h/call wasm/internal-module "_render_sync") + (set! wasm/internal-frame-id nil) + (ug/dispatch! (ug/event "penpot:wasm:render")))) + (declare get-text-dimensions) (defn has-shape @@ -1813,7 +1820,7 @@ noop-fn (fn [] (relayout-after-fonts! shapes text-font-state) - (request-render "images-loaded") + (paint-after-images!) (when (fn? on-complete) (on-complete))))) ;; No pending images — complete immediately. (when on-complete (on-complete))))) @@ -2043,7 +2050,7 @@ noop-fn (fn [] (relayout-after-fonts! prepared-shapes text-font-state-acc) - (request-render "images-loaded"))))))))) + (paint-after-images!))))))))) (process-next-client-chunk [index thumbnails-acc full-acc text-font-state-acc] (if (< index total-shapes)