diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index 40f124f307..f9e7318b3b 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -96,7 +96,7 @@ ;; loading all pages into memory for find the frame set for thumbnail. (defn get-file-data-for-thumbnail - [{:keys [::db/conn] :as cfg} {:keys [data id] :as file}] + [{:keys [::db/conn] :as cfg} {:keys [data id] :as file} strip-frames-with-thumbnails] (letfn [;; function responsible on finding the frame marked to be ;; used as thumbnail; the returned frame always have ;; the :page-id set to the page that it belongs. @@ -173,7 +173,7 @@ ;; Assoc the available thumbnails and prune not visible shapes ;; for avoid transfer unnecessary data. - :always + strip-frames-with-thumbnails (update :objects assoc-thumbnails page-id thumbs))))) (def ^:private @@ -186,7 +186,8 @@ [:map {:title "PartialFile"} [:id ::sm/uuid] [:revn {:min 0} ::sm/int] - [:page [:map-of :keyword ::sm/any]]]) + [:page [:map-of :keyword ::sm/any]] + [:strip-frames-with-thumbnails {:optional true} ::sm/boolean]]) (sv/defmethod ::get-file-data-for-thumbnail "Retrieves the data for generate the thumbnail of the file. Used @@ -195,7 +196,7 @@ ::doc/module :files ::sm/params schema:get-file-data-for-thumbnail ::sm/result schema:partial-file} - [cfg {:keys [::rpc/profile-id file-id] :as params}] + [cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}] (db/run! cfg (fn [{:keys [::db/conn] :as cfg}] (files/check-read-permissions! conn profile-id file-id) @@ -205,14 +206,18 @@ file (bfc/get-file cfg file-id :realize? true - :read-only? true)] + :read-only? true) + + strip-frames-with-thumbnails + (or (nil? strip-frames-with-thumbnails) ;; if not present, default to true + (true? strip-frames-with-thumbnails))] (-> (cfeat/get-team-enabled-features cf/flags team) (cfeat/check-file-features! (:features file))) {:file-id file-id :revn (:revn file) - :page (get-file-data-for-thumbnail cfg file)})))) + :page (get-file-data-for-thumbnail cfg file strip-frames-with-thumbnails)})))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MUTATION COMMANDS diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index f80bdc098a..dab57241a8 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -83,7 +83,7 @@ :source-map-detail-level :all}}} :worker - {:target :browser + {:target :esm :output-dir "resources/public/js/worker/" :asset-path "/js/worker" :devtools {:browser-inject :main diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 2b939a86de..a36f7c7e72 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -67,7 +67,7 @@ :height (:height vbox) :fill color}]) -(defn- calculate-dimensions +(defn calculate-dimensions [objects aspect-ratio] (let [root-objects (ctst/get-root-objects objects)] (if (empty? root-objects) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 1393e48e35..695e9fb44e 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -18,6 +18,7 @@ [app.main.data.notifications :as ntf] [app.main.data.project :as dpj] [app.main.data.team :as dtm] + [app.main.features :as features] [app.main.fonts :as fonts] [app.main.rasterizer :as thr] [app.main.refs :as refs] @@ -46,6 +47,8 @@ (log/set-level! :debug) +(def thumbnail-width 252) + ;; --- Grid Item Thumbnail (defn- persist-thumbnail @@ -56,15 +59,22 @@ (defn render-thumbnail [file-id revn] - (->> (mw/ask! {:cmd :thumbnails/generate-for-file - :revn revn - :file-id file-id}) - (rx/mapcat (fn [{:keys [fonts] :as result}] - (->> (fonts/render-font-styles fonts) - (rx/map (fn [styles] - (assoc result - :styles styles - :width 252)))))))) + (if (features/active-feature? @st/state "render-wasm/v1") + (->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm + :revn revn + :file-id file-id + :width thumbnail-width})) + (->> (mw/ask! {:cmd :thumbnails/generate-for-file + :revn revn + :file-id file-id + :width thumbnail-width}) + (rx/mapcat + (fn [{:keys [fonts] :as result}] + (->> (fonts/render-font-styles fonts) + (rx/map (fn [styles] + (-> result + (assoc :styles styles + :width thumbnail-width)))))))))) (defn- ask-for-thumbnail "Creates some hooks to handle the files thumbnails cache" diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 43317b7fba..7fd6960fad 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -94,6 +94,23 @@ (set! wasm/internal-frame-id nil) (ug/dispatch! (ug/event "penpot:wasm:render")))) +(defn render-sync + [] + (when wasm/context-initialized? + (h/call wasm/internal-module "_render_sync") + (set! wasm/internal-frame-id nil))) + +(defn render-sync-shape + [id] + (when wasm/context-initialized? + (let [buffer (uuid/get-u32 id)] + (h/call wasm/internal-module "_render_sync_shape" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3)) + (set! wasm/internal-frame-id nil)))) + (def set-view-render (fns/debounce (fn [ts] @@ -290,6 +307,13 @@ (aset textures new-id texture) new-id)) +(defn- retrieve-image + [url] + (rx/from + (-> (js/fetch url) + (p/then (fn [^js response] (.blob response))) + (p/then (fn [^js image] (js/createImageBitmap image)))))) + (defn- fetch-image "Loads an image and creates a WebGL texture from it, passing the texture ID to WASM. This avoids decoding the image twice (once in browser, once in WASM)." @@ -297,56 +321,50 @@ (let [url (cf/resolve-file-media {:id image-id} thumbnail?)] {:key url :thumbnail? thumbnail? - :callback #(->> (p/create - (fn [resolve reject] - (let [img (js/Image.) - on-load (fn [] - (resolve img)) - on-error (fn [err] - (reject err))] - (set! (.-crossOrigin img) "anonymous") - (.addEventListener img "load" on-load) - (.addEventListener img "error" on-error) - (set! (.-src img) url)))) - (rx/from) - (rx/map (fn [img] - (when-let [gl (get-webgl-context)] - (let [texture (create-webgl-texture-from-image gl img) - texture-id (get-texture-id-for-gl-object texture) - width (.-width ^js img) - height (.-height ^js img) - ;; Header: 32 bytes (2 UUIDs) + 4 bytes (thumbnail) + 4 bytes (texture ID) + 8 bytes (dimensions) - total-bytes 48 - offset (mem/alloc->offset-32 total-bytes) - heap32 (mem/get-heap-u32)] + :callback + (fn [] + (->> (retrieve-image url) + (rx/map + (fn [img] + (when-let [gl (get-webgl-context)] + (let [texture (create-webgl-texture-from-image gl img) + texture-id (get-texture-id-for-gl-object texture) + width (.-width ^js img) + height (.-height ^js img) + ;; Header: 32 bytes (2 UUIDs) + 4 bytes (thumbnail) + ;; + 4 bytes (texture ID) + 8 bytes (dimensions) + total-bytes 48 + offset (mem/alloc->offset-32 total-bytes) + heap32 (mem/get-heap-u32)] - ;; 1. Set shape id (offset + 0 to offset + 3) - (mem.h32/write-uuid offset heap32 shape-id) + ;; 1. Set shape id (offset + 0 to offset + 3) + (mem.h32/write-uuid offset heap32 shape-id) - ;; 2. Set image id (offset + 4 to offset + 7) - (mem.h32/write-uuid (+ offset 4) heap32 image-id) + ;; 2. Set image id (offset + 4 to offset + 7) + (mem.h32/write-uuid (+ offset 4) heap32 image-id) - ;; 3. Set thumbnail flag as u32 (offset + 8) - (aset heap32 (+ offset 8) (if thumbnail? 1 0)) + ;; 3. Set thumbnail flag as u32 (offset + 8) + (aset heap32 (+ offset 8) (if thumbnail? 1 0)) - ;; 4. Set texture ID (offset + 9) - (aset heap32 (+ offset 9) texture-id) + ;; 4. Set texture ID (offset + 9) + (aset heap32 (+ offset 9) texture-id) - ;; 5. Set width (offset + 10) - (aset heap32 (+ offset 10) width) + ;; 5. Set width (offset + 10) + (aset heap32 (+ offset 10) width) - ;; 6. Set height (offset + 11) - (aset heap32 (+ offset 11) height) + ;; 6. Set height (offset + 11) + (aset heap32 (+ offset 11) height) - (h/call wasm/internal-module "_store_image_from_texture") - true)))) - (rx/catch (fn [cause] - (log/error :hint "Could not fetch image" - :image-id image-id - :thumbnail? thumbnail? - :url url - :cause cause) - (rx/empty))))})) + (h/call wasm/internal-module "_store_image_from_texture") + true)))) + (rx/catch + (fn [cause] + (log/error :hint "Could not fetch image" + :image-id image-id + :thumbnail? thumbnail? + :url url + :cause cause) + (rx/empty)))))})) (defn- get-fill-images [leaf] @@ -961,26 +979,30 @@ :dimensions (get-text-dimensions id)}))))) (defn process-pending - [shapes thumbnails full on-complete] - (let [pending-thumbnails - (d/index-by :key :callback thumbnails) + ([shapes thumbnails full on-complete] + (process-pending shapes thumbnails full nil on-complete)) + ([shapes thumbnails full on-render on-complete] + (let [pending-thumbnails + (d/index-by :key :callback thumbnails) - pending-full - (d/index-by :key :callback full)] + pending-full + (d/index-by :key :callback full)] - (->> (rx/concat - (->> (rx/from (vals pending-thumbnails)) - (rx/merge-map (fn [callback] (callback))) - (rx/reduce conj [])) - (->> (rx/from (vals pending-full)) - (rx/mapcat (fn [callback] (callback))) - (rx/reduce conj []))) - (rx/subs! - (fn [_] - (update-text-layouts shapes) - (request-render "pending-finished")) - noop-fn - on-complete)))) + (->> (rx/concat + (->> (rx/from (vals pending-thumbnails)) + (rx/merge-map (fn [callback] (callback))) + (rx/reduce conj [])) + (->> (rx/from (vals pending-full)) + (rx/mapcat (fn [callback] (callback))) + (rx/reduce conj []))) + (rx/subs! + (fn [_] + (update-text-layouts shapes) + (if on-render + (on-render) + (request-render "pending-finished"))) + noop-fn + on-complete))))) (defn process-object [shape] @@ -988,24 +1010,26 @@ (process-pending [shape] thumbnails full noop-fn))) (defn set-objects - [objects] - (perf/begin-measure "set-objects") - (let [shapes (into [] (vals objects)) - total-shapes (count shapes) - ;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]} - {:keys [thumbnails full]} - (loop [index 0 thumbnails-acc [] full-acc []] - (if (< index total-shapes) - (let [shape (nth shapes index) - {:keys [thumbnails full]} (set-object objects shape)] - (recur (inc index) - (into thumbnails-acc thumbnails) - (into full-acc full))) - {:thumbnails thumbnails-acc :full full-acc}))] - (perf/end-measure "set-objects") - (process-pending shapes thumbnails full - (fn [] - (ug/dispatch! (ug/event "penpot:wasm:set-objects")))))) + ([objects] + (set-objects objects nil)) + ([objects render-callback] + (perf/begin-measure "set-objects") + (let [shapes (into [] (vals objects)) + total-shapes (count shapes) + ;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]} + {:keys [thumbnails full]} + (loop [index 0 thumbnails-acc [] full-acc []] + (if (< index total-shapes) + (let [shape (nth shapes index) + {:keys [thumbnails full]} (set-object objects shape)] + (recur (inc index) + (into thumbnails-acc thumbnails) + (into full-acc full))) + {:thumbnails thumbnails-acc :full full-acc}))] + (perf/end-measure "set-objects") + (process-pending shapes thumbnails full render-callback + (fn [] + (ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))) (defn clear-focus-mode [] @@ -1132,14 +1156,16 @@ (request-render "set-modifiers"))))) (defn initialize-viewport - [base-objects zoom vbox background] - (let [rgba (sr-clr/hex->u32argb background 1) - shapes (into [] (vals base-objects)) - total-shapes (count shapes)] - (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" total-shapes) - (set-objects base-objects))) + ([base-objects zoom vbox background] + (initialize-viewport base-objects zoom vbox background nil)) + ([base-objects zoom vbox background callback] + (let [rgba (sr-clr/hex->u32argb background 1) + shapes (into [] (vals base-objects)) + total-shapes (count shapes)] + (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" total-shapes) + (set-objects base-objects callback)))) (def ^:private default-context-options #js {:antialias false @@ -1160,8 +1186,10 @@ (defn set-canvas-size [canvas] - (set! (.-width canvas) (* dpr (.-clientWidth ^js canvas))) - (set! (.-height canvas) (* dpr (.-clientHeight ^js canvas)))) + (let [width (or (.-clientWidth ^js canvas) (.-width ^js canvas)) + height (or (.-clientHeight ^js canvas) (.-height ^js canvas))] + (set! (.-width canvas) (* dpr width)) + (set! (.-height canvas) (* dpr height)))) (defn- get-browser [] @@ -1274,51 +1302,59 @@ (mem/free) content))) + +(defn init-wasm-module + [module] + (let [default-fn (unchecked-get module "default") + serializers + #js + {:blur-type (unchecked-get module "RawBlurType") + :blend-mode (unchecked-get module "RawBlendMode") + :bool-type (unchecked-get module "RawBoolType") + :font-style (unchecked-get module "RawFontStyle") + :flex-direction (unchecked-get module "RawFlexDirection") + :grid-direction (unchecked-get module "RawGridDirection") + :grow-type (unchecked-get module "RawGrowType") + :align-items (unchecked-get module "RawAlignItems") + :align-self (unchecked-get module "RawAlignSelf") + :align-content (unchecked-get module "RawAlignContent") + :justify-items (unchecked-get module "RawJustifyItems") + :justify-content (unchecked-get module "RawJustifyContent") + :justify-self (unchecked-get module "RawJustifySelf") + :wrap-type (unchecked-get module "RawWrapType") + :grid-track-type (unchecked-get module "RawGridTrackType") + :shadow-style (unchecked-get module "RawShadowStyle") + :stroke-style (unchecked-get module "RawStrokeStyle") + :stroke-cap (unchecked-get module "RawStrokeCap") + :shape-type (unchecked-get module "RawShapeType") + :constraint-h (unchecked-get module "RawConstraintH") + :constraint-v (unchecked-get module "RawConstraintV") + :sizing (unchecked-get module "RawSizing") + :vertical-align (unchecked-get module "RawVerticalAlign") + :fill-data (unchecked-get module "RawFillData") + :text-align (unchecked-get module "RawTextAlign") + :text-direction (unchecked-get module "RawTextDirection") + :text-decoration (unchecked-get module "RawTextDecoration") + :text-transform (unchecked-get module "RawTextTransform") + :segment-data (unchecked-get module "RawSegmentData") + :stroke-linecap (unchecked-get module "RawStrokeLineCap") + :stroke-linejoin (unchecked-get module "RawStrokeLineJoin") + :fill-rule (unchecked-get module "RawFillRule")}] + (set! wasm/serializers serializers) + (default-fn))) + (defonce module (delay (if (exists? js/dynamicImport) (let [uri (cf/resolve-static-asset "js/render_wasm.js")] (->> (js/dynamicImport (str uri)) - (p/mcat (fn [module] - (let [default (unchecked-get module "default") - serializers #js{:blur-type (unchecked-get module "RawBlurType") - :blend-mode (unchecked-get module "RawBlendMode") - :bool-type (unchecked-get module "RawBoolType") - :font-style (unchecked-get module "RawFontStyle") - :flex-direction (unchecked-get module "RawFlexDirection") - :grid-direction (unchecked-get module "RawGridDirection") - :grow-type (unchecked-get module "RawGrowType") - :align-items (unchecked-get module "RawAlignItems") - :align-self (unchecked-get module "RawAlignSelf") - :align-content (unchecked-get module "RawAlignContent") - :justify-items (unchecked-get module "RawJustifyItems") - :justify-content (unchecked-get module "RawJustifyContent") - :justify-self (unchecked-get module "RawJustifySelf") - :wrap-type (unchecked-get module "RawWrapType") - :grid-track-type (unchecked-get module "RawGridTrackType") - :shadow-style (unchecked-get module "RawShadowStyle") - :stroke-style (unchecked-get module "RawStrokeStyle") - :stroke-cap (unchecked-get module "RawStrokeCap") - :shape-type (unchecked-get module "RawShapeType") - :constraint-h (unchecked-get module "RawConstraintH") - :constraint-v (unchecked-get module "RawConstraintV") - :sizing (unchecked-get module "RawSizing") - :vertical-align (unchecked-get module "RawVerticalAlign") - :fill-data (unchecked-get module "RawFillData") - :text-align (unchecked-get module "RawTextAlign") - :text-direction (unchecked-get module "RawTextDirection") - :text-decoration (unchecked-get module "RawTextDecoration") - :text-transform (unchecked-get module "RawTextTransform") - :segment-data (unchecked-get module "RawSegmentData") - :stroke-linecap (unchecked-get module "RawStrokeLineCap") - :stroke-linejoin (unchecked-get module "RawStrokeLineJoin") - :fill-rule (unchecked-get module "RawFillRule")}] - (set! wasm/serializers serializers) - (default)))) - (p/fmap (fn [default] - (set! wasm/internal-module default) - true)) - (p/merr (fn [cause] - (js/console.error cause) - (p/resolved false))))) + (p/mcat init-wasm-module) + (p/fmap + (fn [default] + (set! wasm/internal-module default) + true)) + (p/merr + (fn [cause] + (js/console.error cause) + (p/resolved false))))) (p/resolved false)))) diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs index bf1b44c30b..cf3260ad30 100644 --- a/frontend/src/app/util/worker.cljs +++ b/frontend/src/app/util/worker.cljs @@ -89,7 +89,7 @@ (defn init "Return a initialized webworker instance." [path on-error] - (let [instance (js/Worker. path) + (let [instance (js/Worker. path #js {:type "module"}) bus (rx/subject) worker (Worker. instance (rx/to-observable bus)) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index 1e04ba019b..5871c8c58e 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -7,16 +7,24 @@ (ns app.worker.thumbnails (:require ["react-dom/server" :as rds] + [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] + [app.common.geom.shapes.bounds :as gsb] [app.common.logging :as log] + [app.common.types.color :as cc] [app.common.uri :as u] [app.config :as cf] [app.main.fonts :as fonts] [app.main.render :as render] + [app.render-wasm.api :as wasm.api] + [app.render-wasm.wasm :as wasm] [app.util.http :as http] [app.worker.impl :as impl] [beicon.v2.core :as rx] [okulary.core :as l] - [rumext.v2 :as mf])) + [promesa.core :as p] + [rumext.v2 :as mf] + [shadow.esm :refer (dynamic-import)])) (log/set-level! :trace) @@ -42,11 +50,11 @@ :http-body body}))) (defn- request-data-for-thumbnail - [file-id revn] + [file-id revn strip-frames-with-thumbnails] (let [path "api/main/methods/get-file-data-for-thumbnail" params {:file-id file-id :revn revn - :strip-frames-with-thumbnails true} + :strip-frames-with-thumbnails strip-frames-with-thumbnails} request {:method :get :uri (u/join cf/public-uri path) :credentials "include" @@ -86,5 +94,89 @@ (defmethod impl/handler :thumbnails/generate-for-file [{:keys [file-id revn] :as message} _] - (->> (request-data-for-thumbnail file-id revn) + (->> (request-data-for-thumbnail file-id revn true) (rx/map render-thumbnail))) + +(def init-wasm + (delay + (let [uri (cf/resolve-static-asset "js/render_wasm.js")] + (-> (dynamic-import (str uri)) + (p/then #(wasm.api/init-wasm-module %)) + (p/then #(set! wasm/internal-module %)))))) + +(mf/defc svg-wrapper + [{:keys [data-uri background width height]}] + [:svg {:version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + + :style {:width "100%" + :height "100%" + :background background} + :fill "none" + :viewBox (dm/str "0 0 " width " " height)} + [:image {:xlinkHref data-uri + :width width + :height height}]]) + +(defn blob->uri + [blob] + (.readAsDataURL (js/FileReaderSync.) blob)) + +(def thumbnail-aspect-ratio (/ 2 3)) + +(defmethod impl/handler :thumbnails/generate-for-file-wasm + [{:keys [file-id revn width] :as message} _] + + (->> (rx/from @init-wasm) + (rx/mapcat #(request-data-for-thumbnail file-id revn false)) + (rx/mapcat + (fn [{:keys [page] :as file}] + (rx/create + (fn [subs] + (try + (let [background-color (or (:background page) cc/canvas) + height (* width thumbnail-aspect-ratio) + canvas (js/OffscreenCanvas. width height) + init? (wasm.api/init-canvas-context canvas)] + (if init? + (let [objects (:objects page) + frame (some->> page :thumbnail-frame-id (get objects)) + vbox (if frame + (-> (gsb/get-object-bounds objects frame) + (grc/fix-aspect-ratio thumbnail-aspect-ratio)) + (render/calculate-dimensions objects thumbnail-aspect-ratio)) + zoom (/ width (:width vbox))] + + (wasm.api/initialize-viewport + objects zoom vbox background-color + (fn [] + (if frame + (wasm.api/render-sync-shape (:id frame)) + (wasm.api/render-sync)) + + (-> (.convertToBlob canvas) + (p/then + (fn [blob] + (let [data + (rds/renderToStaticMarkup + (mf/element + svg-wrapper + #js {:data-uri (blob->uri blob) + :width width + :height height + :background background-color}))] + (rx/push! subs {:data data :file-id file-id :revn revn})))) + (p/catch #(do (.error js/console %) + (rx/error! subs %))) + (p/finally #(rx/end! subs)))))) + + (do (rx/error! subs "Error loading webgl context") + (rx/end! subs))) + + nil) + + (catch :default err + (.error js/console err) + (rx/error! subs err) + (rx/end! subs))))))))) diff --git a/render-wasm/src/js/wapi.js b/render-wasm/src/js/wapi.js index e732fdbe6d..4af5c0bf89 100644 --- a/render-wasm/src/js/wapi.js +++ b/render-wasm/src/js/wapi.js @@ -1,8 +1,16 @@ addToLibrary({ wapi_requestAnimationFrame: function wapi_requestAnimationFrame() { - return window.requestAnimationFrame(Module._process_animation_frame); + if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { + setTimeout(Module._process_animation_frame); + } else { + return window.requestAnimationFrame(Module._process_animation_frame); + } }, wapi_cancelAnimationFrame: function wapi_cancelAnimationFrame(frameId) { - return window.cancelAnimationFrame(frameId); + if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { + clearTimeout(frameId); + } else { + return window.cancelAnimationFrame(frameId); + } } }); diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index d78b15482b..18d2616988 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -149,6 +149,27 @@ pub extern "C" fn render(_: i32) { }); } +#[no_mangle] +pub extern "C" fn render_sync() { + with_state_mut!(state, { + state.rebuild_tiles(); + state + .render_sync(performance::get_time()) + .expect("Error rendering"); + }); +} + +#[no_mangle] +pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) { + with_state_mut!(state, { + let id = uuid_from_u32_quartet(a, b, c, d); + state.rebuild_tiles_from(Some(&id)); + state + .render_sync_shape(&id, performance::get_time()) + .expect("Error rendering"); + }); +} + #[no_mangle] pub extern "C" fn render_from_cache(_: i32) { with_state_mut!(state, { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 22b58b098c..c458ea3a37 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -867,7 +867,13 @@ impl RenderState { } } - pub fn start_render_loop(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> { + pub fn start_render_loop( + &mut self, + base_object: Option<&Uuid>, + tree: ShapesPoolRef, + timestamp: i32, + sync_render: bool, + ) -> Result<(), String> { let scale = self.get_scale(); self.tile_viewbox.update(self.viewbox, scale); @@ -917,20 +923,27 @@ impl RenderState { self.current_tile = None; self.render_in_progress = true; self.apply_drawing_to_render_canvas(None); - self.process_animation_frame(tree, timestamp)?; + + if sync_render { + self.render_shape_tree_sync(base_object, tree, timestamp)?; + } else { + self.process_animation_frame(base_object, tree, timestamp)?; + } + performance::end_measure!("start_render_loop"); Ok(()) } pub fn process_animation_frame( &mut self, + base_object: Option<&Uuid>, tree: ShapesPoolRef, timestamp: i32, ) -> Result<(), String> { performance::begin_measure!("process_animation_frame"); if self.render_in_progress { if tree.len() != 0 { - self.render_shape_tree_partial(tree, timestamp)?; + self.render_shape_tree_partial(base_object, tree, timestamp, true)?; } else { println!("Empty tree"); } @@ -947,6 +960,22 @@ impl RenderState { Ok(()) } + pub fn render_shape_tree_sync( + &mut self, + base_object: Option<&Uuid>, + tree: ShapesPoolRef, + timestamp: i32, + ) -> Result<(), String> { + if tree.len() != 0 { + self.render_shape_tree_partial(base_object, tree, timestamp, false)?; + } else { + println!("Empty tree"); + } + self.flush_and_submit(); + + Ok(()) + } + #[inline] pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool { iteration % NODE_BATCH_THRESHOLD == 0 @@ -1215,6 +1244,7 @@ impl RenderState { &mut self, tree: ShapesPoolRef, timestamp: i32, + allow_stop: bool, ) -> Result<(bool, bool), String> { let mut iteration = 0; let mut is_empty = true; @@ -1495,7 +1525,7 @@ impl RenderState { } // We try to avoid doing too many calls to get_time - if self.should_stop_rendering(iteration, timestamp) { + if allow_stop && self.should_stop_rendering(iteration, timestamp) { return Ok((is_empty, true)); } iteration += 1; @@ -1505,8 +1535,10 @@ impl RenderState { pub fn render_shape_tree_partial( &mut self, + base_object: Option<&Uuid>, tree: ShapesPoolRef, timestamp: i32, + allow_stop: bool, ) -> Result<(), String> { let mut should_stop = false; while !should_stop { @@ -1532,7 +1564,7 @@ impl RenderState { } else { performance::begin_measure!("render_shape_tree::uncached"); let (is_empty, early_return) = - self.render_shape_tree_partial_uncached(tree, timestamp)?; + self.render_shape_tree_partial_uncached(tree, timestamp, allow_stop)?; if early_return { return Ok(()); @@ -1564,10 +1596,16 @@ impl RenderState { .canvas(SurfaceId::Current) .clear(self.background_color); - let Some(root) = tree.get(&Uuid::nil()) else { - return Err(String::from("Root shape not found")); + let root_ids = { + if let Some(shape_id) = base_object { + vec![*shape_id] + } else { + let Some(root) = tree.get(&Uuid::nil()) else { + return Err(String::from("Root shape not found")); + }; + root.children_ids(false) + } }; - let root_ids = root.children_ids(false); // If we finish processing every node rendering is complete // let's check if there are more pending nodes @@ -1711,13 +1749,19 @@ impl RenderState { performance::end_measure!("rebuild_tiles_shallow"); } - pub fn rebuild_tiles(&mut self, tree: ShapesPoolRef) { + pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) { performance::begin_measure!("rebuild_tiles"); self.tiles.invalidate(); let mut all_tiles = HashSet::::new(); - let mut nodes = vec![Uuid::nil()]; + let mut nodes = { + if let Some(base_id) = base_id { + vec![*base_id] + } else { + vec![Uuid::nil()] + } + }; while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { @@ -1737,7 +1781,6 @@ impl RenderState { for tile in all_tiles { self.remove_cached_tile(tile); } - performance::end_measure!("rebuild_tiles"); } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index dc984c7b52..4257ab6da8 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -63,15 +63,27 @@ impl<'a> State<'a> { self.render_state.render_from_cache(&self.shapes); } + pub fn render_sync(&mut self, timestamp: i32) -> Result<(), String> { + self.render_state + .start_render_loop(None, &self.shapes, timestamp, true)?; + Ok(()) + } + + pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<(), String> { + self.render_state + .start_render_loop(Some(id), &self.shapes, timestamp, true)?; + Ok(()) + } + pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { self.render_state - .start_render_loop(&self.shapes, timestamp)?; + .start_render_loop(None, &self.shapes, timestamp, false)?; Ok(()) } pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> { self.render_state - .process_animation_frame(&self.shapes, timestamp)?; + .process_animation_frame(None, &self.shapes, timestamp)?; Ok(()) } @@ -162,7 +174,11 @@ impl<'a> State<'a> { } pub fn rebuild_tiles(&mut self) { - self.render_state.rebuild_tiles(&self.shapes); + self.render_state.rebuild_tiles_from(&self.shapes, None); + } + + pub fn rebuild_tiles_from(&mut self, base_id: Option<&Uuid>) { + self.render_state.rebuild_tiles_from(&self.shapes, base_id); } pub fn rebuild_touched_tiles(&mut self) {