From 7a8824b826e0e706b62f4bc7479f790985396368 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 9 Mar 2026 17:20:50 +0100 Subject: [PATCH] :sparkles: Add support for export with wasm engine --- exporter/src/app/handlers/export_frames.cljs | 7 +- exporter/src/app/handlers/export_shapes.cljs | 11 +- exporter/src/app/renderer.cljs | 4 +- exporter/src/app/renderer/bitmap.cljs | 8 +- .../src/app/main/data/exports/assets.cljs | 72 +++-- frontend/src/app/main/data/exports/wasm.cljs | 28 ++ frontend/src/app/main/render.cljs | 50 +++- .../sidebar/options/shapes/frame.cljs | 2 +- frontend/src/app/plugins/shape.cljs | 73 +++-- frontend/src/app/render.cljs | 55 ++-- frontend/src/app/render_wasm/api.cljs | 132 +++++---- .../src/app/render_wasm/deserializers.cljs | 4 + render-wasm/src/main.rs | 25 ++ render-wasm/src/render.rs | 257 ++++++++++++------ render-wasm/src/render/gpu_state.rs | 35 +++ render-wasm/src/render/surfaces.rs | 89 +++++- render-wasm/src/state.rs | 10 + 17 files changed, 627 insertions(+), 235 deletions(-) create mode 100644 frontend/src/app/main/data/exports/wasm.cljs diff --git a/exporter/src/app/handlers/export_frames.cljs b/exporter/src/app/handlers/export_frames.cljs index 3148a3fc7d..c51a80bc94 100644 --- a/exporter/src/app/handlers/export_frames.cljs +++ b/exporter/src/app/handlers/export_frames.cljs @@ -26,6 +26,7 @@ (s/def ::file-id ::us/uuid) (s/def ::page-id ::us/uuid) (s/def ::object-id ::us/uuid) +(s/def ::is-wasm ::us/boolean) (s/def ::export (s/keys :req-un [::file-id ::page-id ::object-id ::name])) @@ -35,7 +36,7 @@ (s/def ::params (s/keys :req-un [::exports] - :opt-un [::name])) + :opt-un [::name ::is-wasm])) (defn handler [{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}] @@ -47,7 +48,7 @@ (handle-export exchange (assoc params :exports exports)))) (defn handle-export - [{:keys [:request/auth-token] :as exchange} {:keys [exports name profile-id] :as params}] + [{:keys [:request/auth-token] :as exchange} {:keys [exports name profile-id is-wasm] :as params}] (let [topic (str profile-id) file-id (-> exports first :file-id) @@ -94,7 +95,7 @@ procs (->> (seq exports) - (map #(rd/render % on-object)))] + (map #(rd/render (assoc % :is-wasm is-wasm) on-object)))] (->> (p/all procs) (p/fmap (fn [] @result-cache)) diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index 49913fd011..901a2f2bb2 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -37,6 +37,7 @@ (s/def ::suffix ::us/string) (s/def ::type ::us/keyword) (s/def ::wait ::us/boolean) +(s/def ::is-wasm ::us/boolean) (s/def ::export (s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name] @@ -47,7 +48,7 @@ (s/def ::params (s/keys :req-un [::exports ::profile-id] - :opt-un [::wait ::name ::skip-children ::force-multiple])) + :opt-un [::wait ::name ::skip-children ::force-multiple ::is-wasm])) (defn handler [{:keys [:request/auth-token] :as exchange} {:keys [exports force-multiple] :as params}] @@ -61,9 +62,9 @@ (handle-multiple-export exchange (assoc params :exports exports))))) (defn- handle-single-export - [{:keys [:request/auth-token] :as exchange} {:keys [export name skip-children] :as params}] + [{:keys [:request/auth-token] :as exchange} {:keys [export name skip-children is-wasm] :as params}] (let [resource (rsc/create (:type export) (or name (:name export))) - export (assoc export :skip-children skip-children)] + export (assoc export :skip-children skip-children :is-wasm is-wasm)] (->> (rd/render export (fn [{:keys [path] :as object}] @@ -80,7 +81,7 @@ (p/rejected cause)))))) (defn- handle-multiple-export - [{:keys [:request/auth-token] :as exchange} {:keys [exports wait profile-id name] :as params}] + [{:keys [:request/auth-token] :as exchange} {:keys [exports wait profile-id name is-wasm] :as params}] (let [resource (rsc/create :zip (or name (-> exports first :name))) total (count exports) topic (str profile-id) @@ -111,7 +112,7 @@ (rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_"))) proc (->> exports - (map (fn [export] (rd/render export append))) + (map (fn [export] (rd/render (assoc export :is-wasm is-wasm) append))) (p/all) (p/mcat (fn [_] (rsc/close-zip zip))) (p/fmap (constantly resource)) diff --git a/exporter/src/app/renderer.cljs b/exporter/src/app/renderer.cljs index 4a7cf8af73..4ca8826ed2 100644 --- a/exporter/src/app/renderer.cljs +++ b/exporter/src/app/renderer.cljs @@ -22,6 +22,7 @@ (s/def ::scale ::us/number) (s/def ::token ::us/string) (s/def ::filename ::us/string) +(s/def ::is-wasm ::us/boolean) (s/def ::object (s/keys :req-un [::id ::name ::suffix ::filename] @@ -31,7 +32,8 @@ (s/coll-of ::object :min-count 1)) (s/def ::render-params - (s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects])) + (s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects] + :opt-un [::is-wasm])) (defn render [{:keys [type] :as params} on-object] diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index b1df4a7447..00c67ba508 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -17,7 +17,7 @@ [promesa.core :as p])) (defn render - [{:keys [file-id page-id share-id token scale type objects skip-children] :as params} on-object] + [{:keys [file-id page-id share-id token scale type objects skip-children is-wasm] :as params} on-object] (letfn [(prepare-options [uri] #js {:screen #js {:width bw/default-viewport-width :height bw/default-viewport-height} @@ -25,7 +25,7 @@ :height bw/default-viewport-height} :locale "en-US" :storageState #js {:cookies (bw/create-cookies uri {:token token})} - :deviceScaleFactor scale + :deviceScaleFactor (if is-wasm 1 scale) ;; wasm won't use deviceScaleFactor :userAgent bw/default-user-agent}) (render-object [page {:keys [id] :as object}] @@ -58,7 +58,9 @@ :share-id share-id :object-id (mapv :id objects) :route "objects" - :skip-children skip-children} + :skip-children skip-children + :wasm (when is-wasm "true") + :scale scale} uri (-> (cf/get :public-uri) (assoc :path "/render.html") (assoc :query (u/map->query-string params)))] diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs index f2c8315a90..8ab85b5228 100644 --- a/frontend/src/app/main/data/exports/assets.cljs +++ b/frontend/src/app/main/data/exports/assets.cljs @@ -8,10 +8,13 @@ (:require [app.common.time :as ct] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.event :as ev] + [app.main.data.exports.wasm :as wasm.exports] [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] [app.main.data.persistence :as dwp] + [app.main.features :as features] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] @@ -152,35 +155,46 @@ (defn request-simple-export [{:keys [export]}] - (ptk/reify ::request-simple-export - ptk/UpdateEvent - (update [_ state] - (update state :export assoc :in-progress true :id uuid/zero)) + (if (and (contains? cf/flags :wasm-export) + (contains? #{:jpeg :webp :png} (:type export))) + (ptk/reify ::request-simple-export-wasm + ptk/EffectEvent + (effect [_ _ _] + (wasm.exports/export-image export))) - ptk/WatchEvent - (watch [_ state _] - (let [profile-id (:profile-id state) - params {:exports [export] - :profile-id profile-id - :cmd :export-shapes - :wait true}] - (rx/concat - (rx/of ::dwp/force-persist) + (ptk/reify ::request-simple-export + ptk/UpdateEvent + (update [_ state] + (update state :export assoc :in-progress true :id uuid/zero)) - ;; Wait the persist to be succesfull - (->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) - (rx/filter #(or (nil? %) (= :saved %))) - (rx/first) - (rx/timeout 400 (rx/empty))) + ptk/WatchEvent + (watch [_ state _] + (let [profile-id (:profile-id state) + params {:exports [export] + :profile-id profile-id + :cmd :export-shapes + :wait true + :is-wasm + (and + (features/active-feature? state "render-wasm/v1") + (contains? cf/flags :wasm-export))}] + (rx/concat + (rx/of ::dwp/force-persist) - (->> (rp/cmd! :export params) - (rx/map (fn [{:keys [filename mtype uri]}] - (dom/trigger-download-uri filename mtype uri) - (clear-export-state uuid/zero))) - (rx/catch (fn [cause] - (rx/concat - (rx/of (clear-export-state uuid/zero)) - (rx/throw cause)))))))))) + ;; Wait the persist to be succesfull + (->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) + (rx/filter #(or (nil? %) (= :saved %))) + (rx/first) + (rx/timeout 400 (rx/empty))) + + (->> (rp/cmd! :export params) + (rx/map (fn [{:keys [filename mtype uri]}] + (dom/trigger-download-uri filename mtype uri) + (clear-export-state uuid/zero))) + (rx/catch (fn [cause] + (rx/concat + (rx/of (clear-export-state uuid/zero)) + (rx/throw cause))))))))))) (defn request-multiple-export [{:keys [exports cmd] @@ -195,7 +209,11 @@ params {:exports exports :cmd cmd :profile-id profile-id - :force-multiple true} + :force-multiple true + :is-wasm + (and + (features/active-feature? state "render-wasm/v1") + (contains? cf/flags :wasm-export))} progress-stream (->> (ws/get-rcv-stream ws-conn) diff --git a/frontend/src/app/main/data/exports/wasm.cljs b/frontend/src/app/main/data/exports/wasm.cljs new file mode 100644 index 0000000000..e0feb03132 --- /dev/null +++ b/frontend/src/app/main/data/exports/wasm.cljs @@ -0,0 +1,28 @@ +;; 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 INC + +(ns app.main.data.exports.wasm + (:require + [app.common.media :refer [format->mtype]] + [app.render-wasm.api :as wasm.api] + [app.util.dom :as dom] + [app.util.webapi :as wapi])) + +(defn export-image-uri + [{:keys [type scale object-id]}] + (let [bytes (wasm.api/render-shape-pixels object-id scale) + mtype (format->mtype type) + blob (wapi/create-blob bytes mtype)] + (wapi/create-uri blob))) + +(defn export-image + [{:keys [type suffix name] :as params}] + (let [url (export-image-uri params) + mtype (format->mtype type) + filename (str name (or suffix ""))] + (dom/trigger-download-uri filename mtype url) + (wapi/revoke-uri url) + nil)) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index ca3478130e..29f7571625 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -45,6 +45,7 @@ [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.text :as text] [app.main.ui.shapes.text.fontfaces :as ff] + [app.render-wasm.api :as wasm.api] [app.util.dom :as dom] [app.util.http :as http] [app.util.strings :as ust] @@ -53,6 +54,7 @@ [beicon.v2.core :as rx] [clojure.set :as set] [cuerdas.core :as str] + [promesa.core :as p] [rumext.v2 :as mf])) (def ^:const viewbox-decimal-precision 3) @@ -171,6 +173,8 @@ ;; Don't wrap svg elements inside a otherwise some can break [:> svg-raw-wrapper {:shape shape :frame frame}])))))) +(set! wasm.api/shape-wrapper-factory shape-wrapper-factory) + (defn format-viewbox "Format a viewbox given a rectangle" [{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}] @@ -196,7 +200,7 @@ ;; Replace the previous object with the new one objects (assoc objects object-id object) - vector (-> (gpt/point (:x object) (:y object)) + vector (-> (gpt/point (-> object :selrect :x) (-> object :selrect :y)) (gpt/negate)) mod-ids (cons object-id (cfh/get-children-ids objects object-id)) @@ -480,6 +484,50 @@ [:& ff/fontfaces-style {:fonts fonts}] [:& shape-wrapper {:shape object}]]]])) +(defn render-to-canvas + [objects canvas bounds scale object-id] + (try + (when (wasm.api/init-canvas-context canvas) + (wasm.api/initialize-viewport + objects scale bounds "#000000" 0 + (fn [] + (wasm.api/render-sync-shape object-id) + (dom/set-attribute! canvas "id" (dm/str "screenshot-" object-id))))) + (catch :default e + (js/console.error "Error initializing canvas context:" e) + false))) + +(mf/defc object-wasm + {::mf/wrap [mf/memo]} + [{:keys [objects object-id skip-children scale] :as props}] + (let [object (get objects object-id) + object (cond-> object + (:hide-fill-on-export object) + (assoc :fills []) + + skip-children + (assoc :shapes [])) + + {:keys [width height] :as bounds} + (gsb/get-object-bounds objects object {:ignore-margin? false}) + + scale (or scale 1) + canvas-ref (mf/use-ref nil)] + + (mf/use-effect + (fn [] + (let [canvas (mf/ref-val canvas-ref)] + (->> @wasm.api/module + (p/fmap + (fn [ready?] + (when ready? + (render-to-canvas objects canvas bounds scale object-id)))))))) + + [:canvas {:ref canvas-ref + :width (* scale width) + :height (* scale height) + :style {:background "transparent"}}])) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SPRITES (DEBUG) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index 731eba6478..d2051a051e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -161,7 +161,7 @@ [:& blur-menu {:ids ids :values (select-keys shape [:blur])}] [:& frame-grid {:shape shape}] - [:> exports-menu* {:type type + [:> exports-menu* {:type shape-type :ids ids :shapes shapes :values (select-keys shape exports-attrs) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index e3dcd65ec2..d97e10f9f8 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -32,6 +32,8 @@ [app.common.types.shape.shadow :as ctss] [app.common.types.text :as txt] [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.data.exports.wasm :as wasm.exports] [app.main.data.plugins :as dp] [app.main.data.workspace :as dw] [app.main.data.workspace.groups :as dwg] @@ -1200,30 +1202,53 @@ (u/not-valid plugin-id :export value) :else - (let [shape (u/locate-shape file-id page-id id) - payload - {:cmd :export-shapes - :profile-id (:profile-id @st/state) - :wait true - :exports [{:file-id file-id - :page-id page-id - :object-id id - :name (:name shape) - :type (:type value :png) - :suffix (:suffix value "") - :scale (:scale value 1)}]}] - (js/Promise. - (fn [resolve reject] - (->> (rp/cmd! :export payload) - (rx/mapcat (fn [{:keys [uri]}] - (->> (http/send! {:method :get - :uri uri - :response-type :blob - :omit-default-headers true}) - (rx/map :body)))) - (rx/mapcat #(.arrayBuffer %)) - (rx/map #(js/Uint8Array. %)) - (rx/subs! resolve reject)))))))) + (if (and (contains? cf/flags :wasm-export) + (contains? #{:jpeg :webp :png} (:type value :png))) + ;; New export with wasm + (let [uri (wasm.exports/export-image-uri + {:file-id file-id + :page-id page-id + :object-id id + :type (:type value :png) + :scale (:scale value 1)})] + (js/Promise. + (fn [resolve reject] + (->> (http/send! + {:method :get + :uri uri + :response-type :blob + :omit-default-headers true}) + (rx/map :body) + (rx/mapcat #(.arrayBuffer %)) + (rx/map #(js/Uint8Array. %)) + (rx/subs! resolve reject))))) + + + ;; Old export through exporter + (let [shape (u/locate-shape file-id page-id id) + payload + {:cmd :export-shapes + :profile-id (:profile-id @st/state) + :wait true + :exports [{:file-id file-id + :page-id page-id + :object-id id + :name (:name shape) + :type (:type value :png) + :suffix (:suffix value "") + :scale (:scale value 1)}]}] + (js/Promise. + (fn [resolve reject] + (->> (rp/cmd! :export payload) + (rx/mapcat (fn [{:keys [uri]}] + (->> (http/send! {:method :get + :uri uri + :response-type :blob + :omit-default-headers true}) + (rx/map :body)))) + (rx/mapcat #(.arrayBuffer %)) + (rx/map #(js/Uint8Array. %)) + (rx/subs! resolve reject))))))))) ;; Interactions :addInteraction diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 642cd0a11c..e134d5d95b 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -63,7 +63,7 @@ (mf/defc object-svg {::mf/wrap-props false} - [{:keys [object-id embed skip-children]}] + [{:keys [object-id embed skip-children wasm scale]}] (let [objects (mf/deref ref:objects)] ;; Set the globa CSS to assign the page size, needed for PDF @@ -77,27 +77,44 @@ (mth/ceil height) "px")})))) (when objects - [:& (mf/provider ctx/is-render?) {:value true} - [:& render/object-svg - {:objects objects - :object-id object-id - :embed embed - :skip-children skip-children}]]))) + (if wasm + [:& render/object-wasm + {:objects objects + :object-id object-id + :embed embed + :scale scale + :skip-children skip-children}] -(mf/defc objects-svg - {::mf/wrap-props false} - [{:keys [object-ids embed skip-children]}] - (when-let [objects (mf/deref ref:objects)] - (for [object-id object-ids] - (let [objects (render/adapt-objects-for-shape objects object-id)] [:& (mf/provider ctx/is-render?) {:value true} [:& render/object-svg {:objects objects - :key (str object-id) :object-id object-id :embed embed :skip-children skip-children}]])))) +(mf/defc objects-svg + {::mf/wrap-props false} + [{:keys [object-ids embed skip-children wasm scale]}] + (when-let [objects (mf/deref ref:objects)] + (for [object-id object-ids] + (let [objects (render/adapt-objects-for-shape objects object-id)] + (if wasm + [:& render/object-wasm + {:objects objects + :key (str object-id) + :object-id object-id + :embed embed + :scale scale + :skip-children skip-children}] + + [:& (mf/provider ctx/is-render?) {:value true} + [:& render/object-svg + {:objects objects + :key (str object-id) + :object-id object-id + :embed embed + :skip-children skip-children}]]))))) + (defn- fetch-objects-bundle [& {:keys [file-id page-id share-id object-id] :as options}] (ptk/reify ::fetch-objects-bundle @@ -136,7 +153,7 @@ (defn- render-objects [params] (try - (let [{:keys [file-id page-id embed share-id object-id skip-children] :as params} + (let [{:keys [file-id page-id embed share-id object-id skip-children wasm scale] :as params} (coerce-render-objects-params params)] (st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id)) (if (uuid? object-id) @@ -147,7 +164,9 @@ :share-id share-id :object-id object-id :embed embed - :skip-children skip-children}]) + :skip-children skip-children + :wasm wasm + :scale scale}]) (mf/html [:& objects-svg @@ -156,7 +175,9 @@ :share-id share-id :object-ids (into #{} object-id) :embed embed - :skip-children skip-children}]))) + :skip-children skip-children + :wasm wasm + :scale scale}]))) (catch :default cause (when-let [explain (-> cause ex-data ::sm/explain)] (js/console.log "Unexpected error") diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index fb80c7be45..60b06f596e 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -23,7 +23,6 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.main.refs :as refs] - [app.main.render :as render] [app.main.store :as st] [app.main.ui.shapes.text] [app.main.worker :as mw] @@ -110,6 +109,9 @@ (def noop-fn (constantly nil)) +;; +(def shape-wrapper-factory nil) + (defn- yield-to-browser "Returns a promise that resolves after yielding to the browser's event loop. Uses requestAnimationFrame for smooth visual updates during loading." @@ -125,7 +127,7 @@ (let [objects (mf/deref refs/workspace-page-objects) shape-wrapper (mf/with-memo [shape] - (render/shape-wrapper-factory objects))] + (shape-wrapper-factory objects))] [:svg {:version "1.1" :xmlns "http://www.w3.org/2000/svg" @@ -1010,62 +1012,62 @@ (defn set-object [shape] (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) + (when shape + (let [shape (svg-filters/apply-svg-derived shape) + id (dm/get-prop shape :id) + type (dm/get-prop shape :type) - masked (get shape :masked-group) + masked (get shape :masked-group) - fills (get shape :fills) - strokes (if (= type :group) - [] (get shape :strokes)) - children (get shape :shapes) - content (let [content (get shape :content)] - (if (= type :text) - (ensure-text-content content) - content)) - bool-type (get shape :bool-type) - grow-type (get shape :grow-type) - blur (get shape :blur) - svg-attrs (get shape :svg-attrs) - shadows (get shape :shadow)] + fills (get shape :fills) + strokes (if (= type :group) + [] (get shape :strokes)) + children (get shape :shapes) + content (let [content (get shape :content)] + (if (= type :text) + (ensure-text-content content) + content)) + bool-type (get shape :bool-type) + grow-type (get shape :grow-type) + blur (get shape :blur) + svg-attrs (get shape :svg-attrs) + shadows (get shape :shadow)] - (shapes/set-shape-base-props shape) + (shapes/set-shape-base-props shape) - ;; Remaining properties that need separate calls (variable-length or conditional) - (set-shape-children children) - (set-shape-blur blur) - (when (= type :group) - (set-masked (boolean masked))) - (when (= type :bool) - (set-shape-bool-type bool-type)) - (when (and (some? content) - (or (= type :path) - (= type :bool))) - (set-shape-path-content content)) - (when (some? svg-attrs) - (set-shape-svg-attrs svg-attrs)) - (when (and (some? content) (= type :svg-raw)) - (set-shape-svg-raw-content (get-static-markup shape))) - (set-shape-shadows shadows) - (when (= type :text) - (set-shape-grow-type grow-type)) + ;; Remaining properties that need separate calls (variable-length or conditional) + (set-shape-children children) + (set-shape-blur blur) + (when (= type :group) + (set-masked (boolean masked))) + (when (= type :bool) + (set-shape-bool-type bool-type)) + (when (and (some? content) + (or (= type :path) + (= type :bool))) + (set-shape-path-content content)) + (when (some? svg-attrs) + (set-shape-svg-attrs svg-attrs)) + (when (and (some? content) (= type :svg-raw)) + (set-shape-svg-raw-content (get-static-markup shape))) + (set-shape-shadows shadows) + (when (= type :text) + (set-shape-grow-type grow-type)) - (set-shape-layout shape) - (set-layout-data shape) - - (let [pending_thumbnails (into [] (concat - (set-shape-text-content id content) - (set-shape-text-images id content true) - (set-shape-fills id fills true) - (set-shape-strokes id strokes true))) - pending_full (into [] (concat - (set-shape-text-images id content false) - (set-shape-fills id fills false) - (set-shape-strokes id strokes false)))] - (perf/end-measure "set-object") - {:thumbnails pending_thumbnails - :full pending_full}))) + (set-shape-layout shape) + (set-layout-data shape) + (let [pending_thumbnails (into [] (concat + (set-shape-text-content id content) + (set-shape-text-images id content true) + (set-shape-fills id fills true) + (set-shape-strokes id strokes true))) + pending_full (into [] (concat + (set-shape-text-images id content false) + (set-shape-fills id fills false) + (set-shape-strokes id strokes false)))] + (perf/end-measure "set-object") + {:thumbnails pending_thumbnails + :full pending_full})))) (defn update-text-layouts [shapes] @@ -1375,9 +1377,11 @@ (defn initialize-viewport ([base-objects zoom vbox background] - (initialize-viewport base-objects zoom vbox background nil)) + (initialize-viewport base-objects zoom vbox background 1 nil)) ([base-objects zoom vbox background callback] - (let [rgba (sr-clr/hex->u32argb background 1) + (initialize-viewport base-objects zoom vbox background 1 callback)) + ([base-objects zoom vbox background background-opacity callback] + (let [rgba (sr-clr/hex->u32argb background background-opacity) shapes (into [] (vals base-objects)) total-shapes (count shapes)] (h/call wasm/internal-module "_set_canvas_background" rgba) @@ -1654,6 +1658,24 @@ (let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")] (run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur))) +(defn render-shape-pixels + [shape-id scale] + (let [buffer (uuid/get-u32 shape-id) + + offset + (h/call wasm/internal-module "_render_shape_pixels" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3) + scale) + + heap (mem/get-heap-u8) + heapu32 (mem/get-heap-u32) + length (aget heapu32 (mem/->offset-32 offset)) + result (dr/read-image-bytes heap (+ offset 12) length)] + (mem/free) + result)) (defn init-wasm-module [module] diff --git a/frontend/src/app/render_wasm/deserializers.cljs b/frontend/src/app/render_wasm/deserializers.cljs index 09376033d1..1b0788a127 100644 --- a/frontend/src/app/render_wasm/deserializers.cljs +++ b/frontend/src/app/render_wasm/deserializers.cljs @@ -45,6 +45,10 @@ :center (gpt/point cx cy) :transform (gmt/matrix a b c d e f)})) +(defn read-image-bytes + [heap offset length] + (.slice ^js heap offset (+ offset length))) + (defn read-position-data-entry [heapu32 heapf32 offset] (let [paragraph (aget heapu32 (+ offset 0)) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 0366009ca7..cff52ad821 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -848,6 +848,31 @@ pub extern "C" fn end_temp_objects() -> Result<()> { Ok(()) } +#[no_mangle] +#[wasm_error] +pub extern "C" fn render_shape_pixels( + a: u32, + b: u32, + c: u32, + d: u32, + scale: f32, +) -> Result<*mut u8> { + let id = uuid_from_u32_quartet(a, b, c, d); + + with_state_mut!(state, { + let (data, width, height) = + state.render_shape_pixels(&id, scale, performance::get_time())?; + + let len = data.len() as u32; + let mut buf = Vec::with_capacity(4 + data.len()); + buf.extend_from_slice(&len.to_le_bytes()); + buf.extend_from_slice(&width.to_le_bytes()); + buf.extend_from_slice(&height.to_le_bytes()); + buf.extend_from_slice(&data); + Ok(mem::write_bytes(buf)) + }) +} + fn main() { #[cfg(target_arch = "wasm32")] init_gl!(); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ed117bcb60..9eaaee0e85 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -18,6 +18,7 @@ use std::borrow::Cow; use std::collections::HashSet; use gpu_state::GpuState; + use options::RenderOptions; pub use surfaces::{SurfaceId, Surfaces}; @@ -45,6 +46,7 @@ const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0; type ClipStack = Vec<(Rect, Option, Matrix)>; +#[derive(Debug)] pub struct NodeRenderState { pub id: Uuid, // We use this bool to keep that we've traversed all the children inside this node. @@ -305,6 +307,7 @@ pub(crate) struct RenderState { pub ignore_nested_blurs: bool, /// Preview render mode - when true, uses simplified rendering for progressive loading pub preview_mode: bool, + pub export_context: Option<(Rect, f32)>, } pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { @@ -378,6 +381,7 @@ impl RenderState { touched_ids: HashSet::default(), ignore_nested_blurs: false, preview_mode: false, + export_context: None, }) } @@ -645,7 +649,7 @@ impl RenderState { Ok(()) } - pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) { + pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) { performance::begin_measure!("apply_drawing_to_render_canvas"); let paint = skia::Paint::default(); @@ -653,12 +657,12 @@ impl RenderState { // Only draw surfaces that have content (dirty flag optimization) if self.surfaces.is_dirty(SurfaceId::TextDropShadows) { self.surfaces - .draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint)); + .draw_into(SurfaceId::TextDropShadows, target, Some(&paint)); } if self.surfaces.is_dirty(SurfaceId::Fills) { self.surfaces - .draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint)); + .draw_into(SurfaceId::Fills, target, Some(&paint)); } let mut render_overlay_below_strokes = false; @@ -668,17 +672,17 @@ impl RenderState { if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) { self.surfaces - .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); + .draw_into(SurfaceId::InnerShadows, target, Some(&paint)); } if self.surfaces.is_dirty(SurfaceId::Strokes) { self.surfaces - .draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint)); + .draw_into(SurfaceId::Strokes, target, Some(&paint)); } if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) { self.surfaces - .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); + .draw_into(SurfaceId::InnerShadows, target, Some(&paint)); } // Build mask of dirty surfaces that need clearing @@ -751,6 +755,7 @@ impl RenderState { offset: Option<(f32, f32)>, parent_shadows: Option>, outset: Option, + target_surface: SurfaceId, ) -> Result<()> { let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 @@ -795,28 +800,23 @@ impl RenderState { && !shape .svg_attrs .as_ref() - .is_some_and(|attrs| attrs.fill_none); + .is_some_and(|attrs| attrs.fill_none) + && target_surface != SurfaceId::Export; if can_render_directly { let scale = self.get_scale(); let translation = self .surfaces .get_render_context_translation(self.render_area, scale); - self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { + + self.surfaces.apply_mut(target_surface as u32, |s| { let canvas = s.canvas(); canvas.save(); canvas.scale((scale, scale)); canvas.translate(translation); }); - fills::render( - self, - shape, - &shape.fills, - antialias, - SurfaceId::Current, - None, - )?; + fills::render(self, shape, &shape.fills, antialias, target_surface, None)?; // Pass strokes in natural order; stroke merging handles top-most ordering internally. let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect(); @@ -824,12 +824,12 @@ impl RenderState { self, shape, &visible_strokes, - Some(SurfaceId::Current), + Some(target_surface), antialias, outset, )?; - self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { + self.surfaces.apply_mut(target_surface as u32, |s| { s.canvas().restore(); }); @@ -1289,7 +1289,7 @@ impl RenderState { } if apply_to_current_surface { - self.apply_drawing_to_render_canvas(Some(&shape)); + self.apply_drawing_to_render_canvas(Some(&shape), target_surface); } // Only restore if we saved (optimization for simple shapes) @@ -1461,7 +1461,7 @@ impl RenderState { self.current_tile = None; self.render_in_progress = true; - self.apply_drawing_to_render_canvas(None); + self.apply_drawing_to_render_canvas(None, SurfaceId::Current); if sync_render { self.render_shape_tree_sync(base_object, tree, timestamp)?; @@ -1512,6 +1512,56 @@ impl RenderState { Ok(()) } + pub fn render_shape_pixels( + &mut self, + id: &Uuid, + tree: ShapesPoolRef, + scale: f32, + timestamp: i32, + ) -> Result<(Vec, i32, i32)> { + let target_surface = SurfaceId::Export; + + self.surfaces + .canvas(target_surface) + .clear(skia::Color::TRANSPARENT); + + if tree.len() != 0 { + let shape = tree.get(id).unwrap(); + let mut extrect = shape.extrect(tree, scale); + self.export_context = Some((extrect, scale)); + let margins = self.surfaces.margins; + extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale)); + + self.surfaces.resize_export_surface(scale, extrect); + self.surfaces.update_render_context(extrect, scale); + + self.pending_nodes.push(NodeRenderState { + id: *id, + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: false, + flattened: false, + }); + self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?; + } + + self.surfaces + .flush_and_submit(&mut self.gpu_state, target_surface); + + let image = self.surfaces.snapshot(target_surface); + let data = image + .encode( + &mut self.gpu_state.context, + skia::EncodedImageFormat::PNG, + 100, + ) + .expect("PNG encode failed"); + let skia::ISize { width, height } = image.dimensions(); + + Ok((data.as_bytes().to_vec(), width, height)) + } + #[inline] pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool { iteration % NODE_BATCH_THRESHOLD == 0 @@ -1519,7 +1569,7 @@ impl RenderState { } #[inline] - pub fn render_shape_enter(&mut self, element: &Shape, mask: bool) { + pub fn render_shape_enter(&mut self, element: &Shape, mask: bool, target_surface: SurfaceId) { // Masked groups needs two rendering passes, the first one rendering // the content and the second one rendering the mask so we need to do // an extra save_layer to keep all the masked group separate from @@ -1533,9 +1583,7 @@ impl RenderState { if group.masked { let paint = skia::Paint::default(); let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); - self.surfaces - .canvas(SurfaceId::Current) - .save_layer(&layer_rec); + self.surfaces.canvas(target_surface).save_layer(&layer_rec); } } @@ -1550,9 +1598,7 @@ impl RenderState { let mut mask_paint = skia::Paint::default(); mask_paint.set_blend_mode(skia::BlendMode::DstIn); let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint); - self.surfaces - .canvas(SurfaceId::Current) - .save_layer(&mask_rec); + self.surfaces.canvas(target_surface).save_layer(&mask_rec); } // Only create save_layer if actually needed @@ -1579,9 +1625,7 @@ impl RenderState { } let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); - self.surfaces - .canvas(SurfaceId::Current) - .save_layer(&layer_rec); + self.surfaces.canvas(target_surface).save_layer(&layer_rec); } self.focus_mode.enter(&element.id); @@ -1593,6 +1637,7 @@ impl RenderState { element: &Shape, visited_mask: bool, clip_bounds: Option, + target_surface: SurfaceId, ) -> Result<()> { if visited_mask { // Because masked groups needs two rendering passes (first drawing @@ -1600,7 +1645,7 @@ impl RenderState { // extra restore. if let Type::Group(group) = element.shape_type { if group.masked { - self.surfaces.canvas(SurfaceId::Current).restore(); + self.surfaces.canvas(target_surface).restore(); } } } else { @@ -1664,6 +1709,7 @@ impl RenderState { None, None, None, + target_surface, )?; } @@ -1672,7 +1718,7 @@ impl RenderState { let needs_layer = element.needs_layer(); if needs_layer { - self.surfaces.canvas(SurfaceId::Current).restore(); + self.surfaces.canvas(target_surface).restore(); } self.focus_mode.exit(&element.id); @@ -1758,8 +1804,8 @@ impl RenderState { shadow: &Shadow, clip_bounds: Option, scale: f32, - translation: (f32, f32), extra_layer_blur: Option, + target_surface: SurfaceId, ) -> Result<()> { let mut transformed_shadow: Cow = Cow::Borrowed(shadow); transformed_shadow.to_mut().offset = (0.0, 0.0); @@ -1822,7 +1868,8 @@ impl RenderState { // Account for the shadow offset so the temporary surface fully contains the shifted blur. bounds.offset(world_offset); // Early cull if the shadow bounds are outside the render area. - if !bounds.intersects(self.render_area_with_margins) { + if !bounds.intersects(self.render_area_with_margins) && target_surface != SurfaceId::Export + { return Ok(()); } @@ -1830,8 +1877,8 @@ impl RenderState { if scale > 1.0 && shadow.blur <= 0.0 { let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); drop_canvas.save(); - drop_canvas.scale((scale, scale)); - drop_canvas.translate(translation); + //drop_canvas.scale((scale, scale)); + //drop_canvas.translate(translation); self.with_nested_blurs_suppressed(|state| { state.render_shape( @@ -1845,6 +1892,7 @@ impl RenderState { Some(shadow.offset), None, Some(shadow.spread), + target_surface, ) })?; @@ -1872,8 +1920,8 @@ impl RenderState { if use_low_zoom_path { let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); drop_canvas.save_layer(&layer_rec); - drop_canvas.scale((scale, scale)); - drop_canvas.translate(translation); + //drop_canvas.scale((scale, scale)); + //drop_canvas.translate(translation); self.with_nested_blurs_suppressed(|state| { state.render_shape( @@ -1887,6 +1935,7 @@ impl RenderState { Some(shadow.offset), // Offset is geometric None, Some(shadow.spread), + target_surface, ) })?; @@ -1928,6 +1977,7 @@ impl RenderState { Some(shadow.offset), // Offset is geometric None, Some(shadow.spread), + target_surface, ) })?; @@ -1939,8 +1989,8 @@ impl RenderState { if let Some((mut surface, filter_scale)) = filter_result { let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); drop_canvas.save(); - drop_canvas.scale((scale, scale)); - drop_canvas.translate(translation); + //drop_canvas.scale((scale, scale)); + //drop_canvas.translate(translation); let mut drop_paint = skia::Paint::default(); drop_paint.set_image_filter(blur_filter.clone()); @@ -1969,6 +2019,7 @@ impl RenderState { } drop_canvas.restore(); } + Ok(()) } @@ -1984,6 +2035,7 @@ impl RenderState { scale: f32, translation: (f32, f32), node_render_state: &NodeRenderState, + target_surface: SurfaceId, ) -> Result<()> { let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale)); let inherited_layer_blur = match element.shape_type { @@ -2004,8 +2056,8 @@ impl RenderState { shadow, clip_bounds.clone(), scale, - translation, None, + target_surface, )?; if !matches!(element.shape_type, Type::Bool(_)) { @@ -2033,8 +2085,8 @@ impl RenderState { shadow, nested_clip_bounds, scale, - translation, inherited_layer_blur, + target_surface, )?; } else { let paint = skia::Paint::default(); @@ -2071,6 +2123,7 @@ impl RenderState { None, Some(vec![new_shadow_paint.clone()]), None, + target_surface, ) })?; self.surfaces.canvas(SurfaceId::DropShadows).restore(); @@ -2084,48 +2137,75 @@ impl RenderState { self.surfaces .canvas(SurfaceId::DropShadows) .draw_paint(&paint); + self.surfaces.canvas(SurfaceId::DropShadows).restore(); } if let Some(clips) = clip_bounds.as_ref() { let antialias = element.should_use_antialias(scale); - self.surfaces.canvas(SurfaceId::Current).save(); + self.surfaces.canvas(target_surface).save(); for (bounds, corners, transform) in clips.iter() { - let mut total_matrix = Matrix::new_identity(); - total_matrix.pre_scale((scale, scale), None); - total_matrix.pre_translate((translation.0, translation.1)); - total_matrix.pre_concat(transform); + if target_surface == SurfaceId::Export { + let Some((export_rect, export_scale)) = self.export_context else { + continue; + }; - self.surfaces - .canvas(SurfaceId::Current) - .concat(&total_matrix); + let mut total_matrix = Matrix::new_identity(); - if let Some(corners) = corners { - let rrect = RRect::new_rect_radii(*bounds, corners); - self.surfaces.canvas(SurfaceId::Current).clip_rrect( - rrect, - skia::ClipOp::Intersect, - antialias, - ); + total_matrix.pre_scale((export_scale, export_scale), None); + total_matrix.pre_translate((-export_rect.x(), -export_rect.y())); + + total_matrix.pre_concat(transform); + + let canvas = self.surfaces.canvas(target_surface); + canvas.concat(&total_matrix); + + let bounds = *bounds; + if let Some(corners) = corners { + let rrect = RRect::new_rect_radii(bounds, corners); + canvas.clip_rrect(rrect, skia::ClipOp::Intersect, antialias); + } else { + canvas.clip_rect(bounds, skia::ClipOp::Intersect, antialias); + } + self.surfaces + .canvas(target_surface) + .concat(&total_matrix.invert().unwrap_or_default()); } else { - self.surfaces.canvas(SurfaceId::Current).clip_rect( - *bounds, - skia::ClipOp::Intersect, - antialias, - ); - } + let mut total_matrix = Matrix::new_identity(); + total_matrix.pre_scale((scale, scale), None); + total_matrix.pre_translate((translation.0, translation.1)); + total_matrix.pre_concat(transform); - self.surfaces - .canvas(SurfaceId::Current) - .concat(&total_matrix.invert().unwrap_or_default()); + self.surfaces.canvas(target_surface).concat(&total_matrix); + + if let Some(corners) = corners { + let rrect = RRect::new_rect_radii(*bounds, corners); + self.surfaces.canvas(target_surface).clip_rrect( + rrect, + skia::ClipOp::Intersect, + antialias, + ); + } else { + self.surfaces.canvas(target_surface).clip_rect( + *bounds, + skia::ClipOp::Intersect, + antialias, + ); + } + + self.surfaces + .canvas(target_surface) + .concat(&total_matrix.invert().unwrap_or_default()); + } } self.surfaces - .draw_into(SurfaceId::DropShadows, SurfaceId::Current, None); - self.surfaces.canvas(SurfaceId::Current).restore(); + .draw_into(SurfaceId::DropShadows, target_surface, None); + self.surfaces.canvas(target_surface).restore(); } else { self.surfaces - .draw_into(SurfaceId::DropShadows, SurfaceId::Current, None); + .draw_into(SurfaceId::DropShadows, target_surface, None); } + self.surfaces .canvas(SurfaceId::DropShadows) .clear(skia::Color::TRANSPARENT); @@ -2137,10 +2217,16 @@ impl RenderState { tree: ShapesPoolRef, timestamp: i32, allow_stop: bool, + export: bool, ) -> Result<(bool, bool)> { let mut iteration = 0; let mut is_empty = true; + let mut target_surface = SurfaceId::Current; + if export { + target_surface = SurfaceId::Export; + } + while let Some(node_render_state) = self.pending_nodes.pop() { let node_id = node_render_state.id; let visited_children = node_render_state.visited_children; @@ -2165,7 +2251,7 @@ impl RenderState { if visited_children { if !node_render_state.flattened { - self.render_shape_exit(element, visited_mask, clip_bounds)?; + self.render_shape_exit(element, visited_mask, clip_bounds, target_surface)?; } continue; } @@ -2188,16 +2274,17 @@ impl RenderState { let has_effects = transformed_element.has_effects_that_extend_bounds(); - let is_visible = if is_container || has_effects { - let element_extrect = - extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale)); - element_extrect.intersects(self.render_area_with_margins) - && !transformed_element.visually_insignificant(scale, tree) - } else { - let selrect = transformed_element.selrect(); - selrect.intersects(self.render_area_with_margins) - && !transformed_element.visually_insignificant(scale, tree) - }; + let is_visible = export + || if is_container || has_effects { + let element_extrect = + extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale)); + element_extrect.intersects(self.render_area_with_margins) + && !transformed_element.visually_insignificant(scale, tree) + } else { + let selrect = transformed_element.selrect(); + selrect.intersects(self.render_area_with_margins) + && !transformed_element.visually_insignificant(scale, tree) + }; if self.options.is_debug_visible() { let shape_extrect_bounds = self.get_shape_extrect_bounds(element, tree); @@ -2231,6 +2318,7 @@ impl RenderState { let translation = self .surfaces .get_render_context_translation(self.render_area, scale); + self.render_element_drop_shadows_and_composite( element, tree, @@ -2239,6 +2327,7 @@ impl RenderState { scale, translation, &node_render_state, + target_surface, )?; } @@ -2254,7 +2343,7 @@ impl RenderState { self.render_background_blur(element); } - self.render_shape_enter(element, mask); + self.render_shape_enter(element, mask, target_surface); } if !node_render_state.is_root() && self.focus_mode.is_active() { @@ -2281,6 +2370,7 @@ impl RenderState { scale, translation, &node_render_state, + target_surface, )?; } @@ -2295,13 +2385,14 @@ impl RenderState { None, None, None, + target_surface, )?; self.surfaces .canvas(SurfaceId::DropShadows) .clear(skia::Color::TRANSPARENT); } else if visited_children { - self.apply_drawing_to_render_canvas(Some(element)); + self.apply_drawing_to_render_canvas(Some(element), target_surface); } // Skip nested state updates for flattened containers @@ -2433,7 +2524,7 @@ impl RenderState { let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile); let can_stop = allow_stop && !tile_is_visible; let (is_empty, early_return) = - self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?; + self.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?; if early_return { return Ok(()); diff --git a/render-wasm/src/render/gpu_state.rs b/render-wasm/src/render/gpu_state.rs index 910c5beda2..aa62b83817 100644 --- a/render-wasm/src/render/gpu_state.rs +++ b/render-wasm/src/render/gpu_state.rs @@ -123,4 +123,39 @@ impl GpuState { Ok(surface) } + + #[allow(dead_code)] + pub fn create_surface_from_texture( + &mut self, + width: i32, + height: i32, + texture_id: u32, + ) -> skia::Surface { + let texture_info = TextureInfo { + target: gl::TEXTURE_2D, + id: texture_id, + format: gl::RGBA8, + protected: skia::gpu::Protected::No, + }; + + let backend_texture = unsafe { + gpu::backend_textures::make_gl( + (width, height), + gpu::Mipmapped::No, + texture_info, + String::from("export_texture"), + ) + }; + + gpu::surfaces::wrap_backend_texture( + &mut self.context, + &backend_texture, + gpu::SurfaceOrigin::BottomLeft, + None, + skia::ColorType::RGBA8888, + None, + None, + ) + .unwrap() + } } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 7337409923..2ab32d1a61 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -18,17 +18,18 @@ const TILE_SIZE_MULTIPLIER: i32 = 2; #[repr(u32)] #[derive(Debug, PartialEq, Clone, Copy)] pub enum SurfaceId { - Target = 0b00_0000_0001, - Filter = 0b00_0000_0010, - Cache = 0b00_0000_0100, - Current = 0b00_0000_1000, - Fills = 0b00_0001_0000, - Strokes = 0b00_0010_0000, - DropShadows = 0b00_0100_0000, - InnerShadows = 0b00_1000_0000, - TextDropShadows = 0b01_0000_0000, - UI = 0b10_0000_0000, - Debug = 0b10_0000_0001, + Target = 0b000_0000_0001, + Filter = 0b000_0000_0010, + Cache = 0b000_0000_0100, + Current = 0b000_0000_1000, + Fills = 0b000_0001_0000, + Strokes = 0b000_0010_0000, + DropShadows = 0b000_0100_0000, + InnerShadows = 0b000_1000_0000, + TextDropShadows = 0b001_0000_0000, + Export = 0b010_0000_0000, + UI = 0b100_0000_0000, + Debug = 0b100_0000_0001, } pub struct Surfaces { @@ -53,11 +54,15 @@ pub struct Surfaces { // for drawing debug info. debug: skia::Surface, // for drawing tiles. + export: skia::Surface, + tiles: TileTextureCache, sampling_options: skia::SamplingOptions, - margins: skia::ISize, + pub margins: skia::ISize, // Tracks which surfaces have content (dirty flag bitmask) dirty_surfaces: u32, + + extra_tile_dims: skia::ISize, } #[allow(dead_code)] @@ -79,6 +84,7 @@ impl Surfaces { let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?; let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?; + let drop_shadows = gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims)?; let inner_shadows = @@ -89,6 +95,7 @@ impl Surfaces { gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims)?; let shape_strokes = gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims)?; + let export = gpu_state.create_surface_with_isize("export".to_string(), extra_tile_dims)?; let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?; let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?; @@ -106,10 +113,12 @@ impl Surfaces { shape_strokes, ui, debug, + export, tiles, sampling_options, margins, dirty_surfaces: 0, + extra_tile_dims, }) } @@ -278,6 +287,9 @@ impl Surfaces { if ids & SurfaceId::Debug as u32 != 0 { f(self.get_mut(SurfaceId::Debug)); } + if ids & SurfaceId::Export as u32 != 0 { + f(self.get_mut(SurfaceId::Export)); + } performance::begin_measure!("apply_mut::flags"); } @@ -301,7 +313,8 @@ impl Surfaces { let surface_ids = SurfaceId::Fills as u32 | SurfaceId::Strokes as u32 | SurfaceId::InnerShadows as u32 - | SurfaceId::TextDropShadows as u32; + | SurfaceId::TextDropShadows as u32 + | SurfaceId::DropShadows as u32; // Clear surfaces before updating transformations to remove residual content self.apply_mut(surface_ids, |s| { @@ -313,6 +326,7 @@ impl Surfaces { self.mark_dirty(SurfaceId::Strokes); self.mark_dirty(SurfaceId::InnerShadows); self.mark_dirty(SurfaceId::TextDropShadows); + self.mark_dirty(SurfaceId::DropShadows); // Update transformations self.apply_mut(surface_ids, |s| { @@ -324,7 +338,7 @@ impl Surfaces { } #[inline] - fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface { + pub fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface { match id { SurfaceId::Target => &mut self.target, SurfaceId::Filter => &mut self.filter, @@ -337,6 +351,7 @@ impl Surfaces { SurfaceId::Strokes => &mut self.shape_strokes, SurfaceId::Debug => &mut self.debug, SurfaceId::UI => &mut self.ui, + SurfaceId::Export => &mut self.export, } } @@ -353,6 +368,7 @@ impl Surfaces { SurfaceId::Strokes => &self.shape_strokes, SurfaceId::Debug => &self.debug, SurfaceId::UI => &self.ui, + SurfaceId::Export => &self.export, } } @@ -492,12 +508,14 @@ impl Surfaces { self.canvas(SurfaceId::TextDropShadows).restore_to_count(1); self.canvas(SurfaceId::Strokes).restore_to_count(1); self.canvas(SurfaceId::Current).restore_to_count(1); + self.canvas(SurfaceId::Export).restore_to_count(1); self.apply_mut( SurfaceId::Fills as u32 | SurfaceId::Strokes as u32 | SurfaceId::Current as u32 | SurfaceId::InnerShadows as u32 - | SurfaceId::TextDropShadows as u32, + | SurfaceId::TextDropShadows as u32 + | SurfaceId::Export as u32, |s| { s.canvas().clear(color).reset_matrix(); }, @@ -627,6 +645,47 @@ impl Surfaces { pub fn gc(&mut self) { self.tiles.gc(); } + + pub fn resize_export_surface(&mut self, scale: f32, rect: skia::Rect) { + let target_w = (scale * rect.width()).ceil() as i32; + let target_h = (scale * rect.height()).ceil() as i32; + + let max_w = i32::max(self.extra_tile_dims.width, target_w); + let max_h = i32::max(self.extra_tile_dims.height, target_h); + + if max_w > self.extra_tile_dims.width || max_h > self.extra_tile_dims.height { + self.extra_tile_dims = skia::ISize::new(max_w, max_h); + self.drop_shadows = self + .drop_shadows + .new_surface_with_dimensions((max_w, max_h)) + .unwrap(); + self.inner_shadows = self + .inner_shadows + .new_surface_with_dimensions((max_w, max_h)) + .unwrap(); + self.text_drop_shadows = self + .text_drop_shadows + .new_surface_with_dimensions((max_w, max_h)) + .unwrap(); + self.text_drop_shadows = self + .text_drop_shadows + .new_surface_with_dimensions((max_w, max_h)) + .unwrap(); + self.shape_strokes = self + .shape_strokes + .new_surface_with_dimensions((max_w, max_h)) + .unwrap(); + self.shape_fills = self + .shape_strokes + .new_surface_with_dimensions((max_w, max_h)) + .unwrap(); + } + + self.export = self + .export + .new_surface_with_dimensions((target_w, target_h)) + .unwrap(); + } } pub struct TileTextureCache { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index a976ef331f..9239b38eb4 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -101,6 +101,16 @@ impl State { .start_render_loop(Some(id), &self.shapes, timestamp, true) } + pub fn render_shape_pixels( + &mut self, + id: &Uuid, + scale: f32, + timestamp: i32, + ) -> Result<(Vec, i32, i32)> { + self.render_state + .render_shape_pixels(id, &self.shapes, scale, timestamp) + } + pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> { // If zoom changed (e.g. interrupted zoom render followed by pan), the // tile index may be stale for the new viewport position. Rebuild the