From 65ee2f9081ddd1fa4e9b8eced33b736d4c6154ae Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 12 Nov 2024 11:09:50 +0100 Subject: [PATCH] :tada: Save shape data in rust memory --- common/src/app/common/types/shape/impl.cljc | 110 ++++-------- common/src/app/common/uuid.cljc | 5 + common/src/app/common/uuid_impl.js | 40 ++++- .../sidebar/options/menus/measures.cljs | 36 ++-- .../app/main/ui/workspace/viewport_wasm.cljs | 29 ++- frontend/src/app/render_wasm.cljs | 167 ++++++++---------- render-wasm/Cargo.lock | 45 +++-- render-wasm/Cargo.toml | 3 +- render-wasm/src/main.rs | 150 ++++++++++++---- render-wasm/src/render.rs | 54 ++++-- render-wasm/src/shapes.rs | 93 +++++++--- render-wasm/src/state.rs | 36 ++++ render-wasm/src/utils.rs | 8 + 13 files changed, 479 insertions(+), 297 deletions(-) create mode 100644 render-wasm/src/state.rs create mode 100644 render-wasm/src/utils.rs diff --git a/common/src/app/common/types/shape/impl.cljc b/common/src/app/common/types/shape/impl.cljc index 407ee9b34..dc6f92231 100644 --- a/common/src/app/common/types/shape/impl.cljc +++ b/common/src/app/common/types/shape/impl.cljc @@ -7,34 +7,30 @@ (ns app.common.types.shape.impl (:require #?(:clj [app.common.fressian :as fres]) - #?(:cljs [app.common.data.macros :as dm]) - #?(:cljs [app.common.geom.rect :as grc]) #?(:cljs [cuerdas.core :as str]) [app.common.record :as cr] [app.common.transit :as t] [clojure.core :as c])) -(def enabled-wasm-ready-shape false) - -#?(:cljs - (do - (def ArrayBuffer js/ArrayBuffer) - (def Float32Array js/Float32Array))) +(defonce ^:dynamic *wasm-sync* false) +(defonce enabled-wasm-ready-shape false) +(defonce wasm-create-shape (constantly nil)) +(defonce wasm-use-shape (constantly nil)) +(defonce wasm-set-shape-selrect (constantly nil)) +(defonce wasm-set-shape-transform (constantly nil)) +(defonce wasm-set-shape-rotation (constantly nil)) (cr/defrecord Shape [id name type x y width height rotation selrect points transform transform-inverse parent-id frame-id flip-x flip-y]) -(declare ^:private clone-f32-array) (declare ^:private impl-assoc) (declare ^:private impl-conj) (declare ^:private impl-dissoc) -(declare ^:private read-selrect) -(declare ^:private write-selrect) ;; TODO: implement lazy MapEntry #?(:cljs - (deftype ShapeWithBuffer [buffer delegate] + (deftype ShapeProxy [delegate] Object (toString [coll] (str "{" (str/join ", " (for [[k v] coll] (str k " " v))) "}")) @@ -42,14 +38,9 @@ (equiv [this other] (-equiv this other)) - ;; ICloneable - ;; (-clone [_] - ;; (let [bf32 (clone-float32-array buffer)] - ;; (ShapeWithBuffer. bf32 delegate))) - IWithMeta (-with-meta [_ meta] - (ShapeWithBuffer. buffer (with-meta delegate meta))) + (ShapeProxy. (with-meta delegate meta))) IMeta (-meta [_] (meta delegate)) @@ -68,9 +59,8 @@ ISequential ISeqable - (-seq [coll] - (cons (find coll :selrect) - (seq delegate))) + (-seq [_] + (c/-seq delegate)) ICounted (-count [_] @@ -81,23 +71,18 @@ (-lookup coll k nil)) (-lookup [_ k not-found] - (if (= k :selrect) - (read-selrect buffer) - (c/-lookup delegate k not-found))) + (c/-lookup delegate k not-found)) IFind (-find [_ k] - (if (= k :selrect) - (c/MapEntry. k (read-selrect buffer) nil) ; Replace with lazy MapEntry - (c/-find delegate k))) + (c/-find delegate k)) IAssociative (-assoc [coll k v] (impl-assoc coll k v)) (-contains-key? [_ k] - (or (= k :selrect) - (contains? delegate k))) + (contains? delegate k)) IMap (-dissoc [coll k] @@ -118,64 +103,34 @@ [o] #?(:clj (instance? Shape o) :cljs (or (instance? Shape o) - (instance? ShapeWithBuffer o)))) + (instance? ShapeProxy o)))) ;; --- SHAPE IMPL -#?(:cljs - (defn- clone-f32-array - [^Float32Array src] - (let [copy (new Float32Array (.-length src))] - (.set copy src) - copy))) - -#?(:cljs - (defn- write-selrect - "Write the selrect into the buffer" - [data selrect] - (assert (instance? Float32Array data) "expected instance of float32array") - - (aset data 0 (dm/get-prop selrect :x1)) - (aset data 1 (dm/get-prop selrect :y1)) - (aset data 2 (dm/get-prop selrect :x2)) - (aset data 3 (dm/get-prop selrect :y2)))) - -#?(:cljs - (defn- read-selrect - "Read selrect from internal buffer" - [^Float32Array buffer] - (let [x1 (aget buffer 0) - y1 (aget buffer 1) - x2 (aget buffer 2) - y2 (aget buffer 3)] - (grc/make-rect x1 y1 - (- x2 x1) - (- y2 y1))))) - #?(:cljs (defn- impl-assoc [coll k v] - (if (= k :selrect) - (let [buffer (clone-f32-array (.-buffer coll))] - (write-selrect buffer v) - (ShapeWithBuffer. buffer (.-delegate ^ShapeWithBuffer coll))) - - (let [delegate (.-delegate ^ShapeWithBuffer coll) - delegate' (assoc delegate k v)] - (if (identical? delegate' delegate) - coll - (let [buffer (clone-f32-array (.-buffer coll))] - (ShapeWithBuffer. buffer delegate'))))))) + (when *wasm-sync* + (wasm-use-shape (:id coll)) + (case k + :selrect (wasm-set-shape-selrect v) + :rotation (wasm-set-shape-rotation v) + :transform (wasm-set-shape-transform v) + nil)) + (let [delegate (.-delegate ^ShapeProxy coll) + delegate' (assoc delegate k v)] + (if (identical? delegate' delegate) + coll + (ShapeProxy. delegate'))))) #?(:cljs (defn- impl-dissoc [coll k] - (let [delegate (.-delegate ^ShapeWithBuffer coll) + (let [delegate (.-delegate ^ShapeProxy coll) delegate' (dissoc delegate k)] (if (identical? delegate delegate') coll - (let [buffer (clone-f32-array (.-buffer coll))] - (ShapeWithBuffer. buffer delegate')))))) + (ShapeProxy. delegate'))))) #?(:cljs (defn- impl-conj @@ -196,10 +151,7 @@ [attrs] #?(:cljs (if enabled-wasm-ready-shape - (let [selrect (:selrect attrs) - buffer (new Float32Array 4)] - (write-selrect buffer selrect) - (ShapeWithBuffer. buffer (dissoc attrs :selrect))) + (ShapeProxy. attrs) (map->Shape attrs)) :clj (map->Shape attrs))) @@ -215,7 +167,7 @@ #?(:cljs (t/add-handlers! {:id "shape" - :class ShapeWithBuffer + :class ShapeProxy :wfn #(into {} %) :rfn create-shape})) diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index 2086a0a5b..975e6afec 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -76,6 +76,11 @@ [id] (impl/short-v8 (dm/str id)))) +#?(:cljs + (defn uuid->u32 + [id] + (impl/get-u32 (dm/str id)))) + #?(:clj (defn hash-int [id] diff --git a/common/src/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js index 8746d6ab2..38ad8203f 100644 --- a/common/src/app/common/uuid_impl.js +++ b/common/src/app/common/uuid_impl.js @@ -122,7 +122,6 @@ goog.scope(function() { }; })(); - self.v4 = (function () { const arr = new Uint8Array(16); @@ -211,7 +210,6 @@ goog.scope(function() { return factory; })(); - self.short_v8 = function(uuid) { const buff = encoding.hexToBuffer(uuid); const short = new Uint8Array(buff, 4); @@ -222,5 +220,41 @@ goog.scope(function() { const most = mostSigBits.toString("16").padStart(16, "0"); const least = leastSigBits.toString("16").padStart(16, "0"); return `${most.substring(0, 8)}-${most.substring(8, 12)}-${most.substring(12)}-${least.substring(0, 4)}-${least.substring(4)}`; - } + }; + + self.get_u32 = (function() { + const UUID_BYTE_SIZE = 16; + const ab = new ArrayBuffer(UUID_BYTE_SIZE); + const u32buffer = new Uint32Array(ab); + const HYPHEN = '-'.charCodeAt(0); + const A = 'a'.charCodeAt(0); + const A_SUB = A - 10; + const ZERO = '0'.charCodeAt(0); + const MAX_DIGIT = 8; + const HALF_BITS = 4; + return function(uuid) { + let digit = 0; + let numDigit = 0; + let u32index = 0; + let u32 = 0; + for (let i = 0; i < uuid.length; i++) { + const charCode = uuid.charCodeAt(i); + if (charCode === HYPHEN) continue; + if (charCode >= A) { + digit = charCode - A_SUB; + } else { + digit = charCode - ZERO; + } + numDigit++; + const bitPos = (MAX_DIGIT - numDigit) * HALF_BITS; + u32 |= (digit << bitPos); + if (numDigit === MAX_DIGIT) { + u32buffer[u32index++] = u32; + u32 = 0; + numDigit = 0; + } + } + return u32buffer; + } + })(); }); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index df5516f88..fb9dc299b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.logic.shapes :as cls] + [app.common.types.shape.impl :as shape.impl] [app.common.types.shape.layout :as ctl] [app.common.types.shape.radius :as ctsr] [app.common.types.tokens-lib :as ctob] @@ -248,19 +249,20 @@ (fn [value attr] (let [token-value (wtc/maybe-resolve-token-value value) undo-id (js/Symbol)] - (if-not design-tokens? - (st/emit! (udw/trigger-bounding-box-cloaking ids) - (udw/update-dimensions ids attr (or token-value value))) - (st/emit! (udw/trigger-bounding-box-cloaking ids) - (dwu/start-undo-transaction undo-id) - (dwsh/update-shapes ids - (if token-value - #(assoc-in % [:applied-tokens attr] (:id value)) - #(d/dissoc-in % [:applied-tokens attr])) - {:reg-objects? true - :attrs [:applied-tokens]}) - (udw/update-dimensions ids attr (or token-value value)) - (dwu/commit-undo-transaction undo-id)))))) + (binding [shape.impl/*wasm-sync* true] + (if-not design-tokens? + (st/emit! (udw/trigger-bounding-box-cloaking ids) + (udw/update-dimensions ids attr (or token-value value))) + (st/emit! (udw/trigger-bounding-box-cloaking ids) + (dwu/start-undo-transaction undo-id) + (dwsh/update-shapes ids + (if token-value + #(assoc-in % [:applied-tokens attr] (:id value)) + #(d/dissoc-in % [:applied-tokens attr])) + {:reg-objects? true + :attrs [:applied-tokens]}) + (udw/update-dimensions ids attr (or token-value value)) + (dwu/commit-undo-transaction undo-id))))))) on-proportion-lock-change (mf/use-fn @@ -283,7 +285,8 @@ (mf/deps ids) (fn [value attr] (st/emit! (udw/trigger-bounding-box-cloaking ids)) - (doall (map #(do-position-change %1 %2 value attr) shapes frames)))) + (binding [shape.impl/*wasm-sync* true] + (doall (map #(do-position-change %1 %2 value attr) shapes frames))))) ;; ROTATION @@ -291,8 +294,9 @@ (mf/use-fn (mf/deps ids) (fn [value] - (st/emit! (udw/trigger-bounding-box-cloaking ids) - (udw/increase-rotation ids value)))) + (binding [shape.impl/*wasm-sync* true] + (st/emit! (udw/trigger-bounding-box-cloaking ids) + (udw/increase-rotation ids value))))) ;; RADIUS diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index f5c6963cf..c13cd657f 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -13,6 +13,7 @@ [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] [app.common.types.shape-tree :as ctt] + [app.common.types.shape.impl :as shape.impl] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.modifiers :as dwm] [app.main.features :as features] @@ -111,10 +112,9 @@ modifiers (mf/deref refs/workspace-modifiers) text-modifiers (mf/deref refs/workspace-text-modifier) - render-context-lost? (mf/deref refs/render-context-lost?) - objects-modified (mf/with-memo [base-objects text-modifiers modifiers] - (apply-modifiers-to-selected selected base-objects text-modifiers modifiers)) + (binding [shape.impl/*wasm-sync* true] + (apply-modifiers-to-selected selected base-objects text-modifiers modifiers))) selected-shapes (keep (d/getf objects-modified) selected) @@ -177,8 +177,6 @@ mode-inspect? (= options-mode :inspect) - on-render-restore-context #(.reload js/location) - on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect z?) on-context-menu (actions/on-context-menu hover hover-ids read-only?) on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id drawing-path? base-objects edition drawing-tool z? read-only?) @@ -281,18 +279,18 @@ (p/fmap (fn [ready?] (when ready? (reset! canvas-init? true) - (render.wasm/setup-canvas canvas))))) + (render.wasm/assign-canvas canvas))))) (fn [] - (render.wasm/dispose-canvas canvas)))) + (render.wasm/clear-canvas)))) - (mf/with-effect [objects-modified canvas-init?] + (mf/with-effect [base-objects modifiers canvas-init?] (when @canvas-init? - (render.wasm/set-objects objects-modified) + ;; FIXME: review this to not call it but still do the first draw + ;; (render.wasm/set-objects base-objects modifiers) (render.wasm/draw-objects zoom vbox))) (mf/with-effect [vbox canvas-init?] - (let [frame-id (when @canvas-init? (do - (render.wasm/draw-objects zoom vbox)))] + (let [frame-id (when @canvas-init? (render.wasm/draw-objects zoom vbox))] (partial render.wasm/cancel-draw frame-id))) (hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?) @@ -639,11 +637,4 @@ {:objects base-objects :zoom zoom :vbox vbox - :bottom-padding (when palete-size (+ palete-size 8))}]]]] - - (when render-context-lost? - [:div {:id "context-lost" :class (stl/css :context-lost)} - [:h1 "GL Error Screen"] - [:button - {:on-click on-render-restore-context} - "Restore context"]])])) + :bottom-padding (when palete-size (+ palete-size 8))}]]]]])) diff --git a/frontend/src/app/render_wasm.cljs b/frontend/src/app/render_wasm.cljs index 20c769d93..424d3db8d 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -9,12 +9,10 @@ (:require [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] - [app.common.types.shape.impl] + [app.common.types.shape.impl :as ctsi] + [app.common.uuid :as uuid] [app.config :as cf] - [app.main.data.render-wasm :as drw] - [app.main.store :as st] - [app.util.debug :as dbg] - [app.util.dom :as dom] + [app.util.object :as obj] [promesa.core :as p])) (def enabled? @@ -23,7 +21,6 @@ (set! app.common.types.shape.impl/enabled-wasm-ready-shape enabled?) (defonce internal-module #js {}) -(defonce internal-gpu-state #js {}) ;; TODO: remove the `take` once we have the dynamic data structure in Rust (def xform @@ -31,49 +28,70 @@ (remove cfh/root?) (take 2048))) -;; Size in number of f32 values that represents the shape selrect ( -(def rect-size 4) +(defn create-shape + [id] + (let [buffer (uuid/uuid->u32 id)] + (._create_shape ^js internal-module (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))) + +(defn use-shape + [id] + (let [buffer (uuid/uuid->u32 id)] + (._use_shape ^js internal-module (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))) + +(defn set-shape-selrect + [selrect] + (let [x1 (:x1 selrect) + y1 (:y1 selrect) + x2 (:x2 selrect) + y2 (:y2 selrect)] + (._set_shape_selrect ^js internal-module x1 y1 x2 y2))) + +(defn set-shape-transform + [transform] + (let [a (:a transform) + b (:b transform) + c (:c transform) + d (:d transform) + e (:e transform) + f (:f transform)] + (._set_shape_transform ^js internal-module a b c d e f))) + +(defn set-shape-rotation + [rotation] + (._set_shape_rotation ^js internal-module rotation)) + +(defn set-shape-x + [x] + (._set_shape_x ^js internal-module x)) + +(defn set-shape-y + [y] + (._set_shape_y ^js internal-module y)) (defn set-objects [objects] - ;; FIXME: maybe change the name of `_shapes_buffer` (?) - (let [get-shapes-buffer-ptr - (unchecked-get internal-module "_shapes_buffer") - - heap - (unchecked-get internal-module "HEAPF32") - - shapes - (into [] xform (vals objects)) - - total-shapes - (count shapes) - - heap-offset - (get-shapes-buffer-ptr) - - heap-size - (* rect-size total-shapes) - - mem - (js/Float32Array. (.-buffer heap) - heap-offset - heap-size)] - + (let [shapes (into [] xform (vals objects)) + total-shapes (count shapes)] (loop [index 0] (when (< index total-shapes) - (let [shape (nth shapes index)] - (.set ^js mem (.-buffer shape) (* index rect-size)) + (let [shape (nth shapes index) + id (dm/get-prop shape :id) + selrect (dm/get-prop shape :selrect) + rotation (dm/get-prop shape :rotation) + transform (dm/get-prop shape :transform)] + (use-shape id) + (set-shape-selrect selrect) + (set-shape-rotation rotation) + (set-shape-transform transform) (recur (inc index))))))) (defn draw-objects [zoom vbox] - (let [draw-all-shapes (unchecked-get internal-module "_draw_all_shapes")] - (js/requestAnimationFrame - (fn [] - (let [pan-x (- (dm/get-prop vbox :x)) - pan-y (- (dm/get-prop vbox :y))] - (draw-all-shapes internal-gpu-state zoom pan-x pan-y)))))) + (js/requestAnimationFrame + (fn [] + (let [pan-x (- (dm/get-prop vbox :x)) + pan-y (- (dm/get-prop vbox :y))] + (._draw_all_shapes ^js internal-module zoom pan-x pan-y))))) (defn cancel-draw [frame-id] @@ -86,67 +104,28 @@ :stencil true :alpha true}) -(defn init-skia - [canvas] - (let [init-fn (unchecked-get internal-module "_init") - state (init-fn (.-width ^js canvas) - (.-height ^js canvas))] - (set! internal-gpu-state state))) - -;; NOTE: This function can be called externally -;; by the button in the context lost component (shown -;; in viewport-wasm) or called internally by -;; on-webgl-context -(defn restore-canvas - [canvas] - (st/emit! (drw/context-restored)) - ;; We need to reinitialize skia when the - ;; context is restored. - (init-skia canvas)) - -;; Handles both events: webglcontextlost and -;; webglcontextrestored -(defn on-webgl-context - [event] - (dom/prevent-default event) - (if (= (.-type event) "webglcontextlost") - (st/emit! (drw/context-lost)) - (restore-canvas (dom/get-target event)))) - -(defn dispose-canvas - [canvas] +(defn clear-canvas + [] ;; TODO: perform corresponding cleaning - (.removeEventListener canvas "webglcontextlost" on-webgl-context) - (.removeEventListener canvas "webglcontextrestored" on-webgl-context)) + ) -(defn init-debug-webgl-context-state - [context] - (let [context-extension (.getExtension ^js context "WEBGL_lose_context") - info-extension (.getExtension ^js context "WEBGL_debug_renderer_info")] - (set! (.-penpotGL js/window) #js {:context context-extension - :renderer info-extension}) - (js/console.log "WEBGL_lose_context" context-extension) - (js/console.log "WEBGL_debug_renderer_info" info-extension))) - -(defn setup-canvas +(defn assign-canvas [canvas] (let [gl (unchecked-get internal-module "GL") + init-fn (unchecked-get internal-module "_init") + context (.getContext ^js canvas "webgl2" canvas-options) ;; Register the context with emscripten - handle (.registerContext ^js gl context #js {"majorVersion" 2}) - _ (.makeContextCurrent ^js gl handle)] - - (when (dbg/enabled? :gl-context) - (init-debug-webgl-context-state context)) - - (.addEventListener canvas "webglcontextlost" on-webgl-context) - (.addEventListener canvas "webglcontextrestored" on-webgl-context) - + handle (.registerContext ^js gl context #js {"majorVersion" 2})] + (.makeContextCurrent ^js gl handle) + ;; Initialize Skia + (init-fn (.-width ^js canvas) + (.-height ^js canvas)) (set! (.-width canvas) (.-clientWidth ^js canvas)) (set! (.-height canvas) (.-clientHeight ^js canvas)) - (init-skia canvas))) + (obj/set! js/window "shape_list" (fn [] ((unchecked-get internal-module "_shape_list")))))) (defonce module (->> (js/dynamicImport "/js/render_wasm.js") @@ -159,3 +138,9 @@ (p/merr (fn [cause] (js/console.error cause) (p/resolved false))))) + +(set! app.common.types.shape.impl/wasm-create-shape create-shape) +(set! app.common.types.shape.impl/wasm-use-shape use-shape) +(set! app.common.types.shape.impl/wasm-set-shape-selrect set-shape-selrect) +(set! app.common.types.shape.impl/wasm-set-shape-transform set-shape-transform) +(set! app.common.types.shape.impl/wasm-set-shape-rotation set-shape-rotation) diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index 7595770cd..ecd7772a4 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -96,22 +96,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "emscripten-functions" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c026cc030b24957ca45d9555f9fa241d6b3a01d725cd98a25924de249b840a" -dependencies = [ - "cc", - "emscripten-functions-sys", -] - -[[package]] -name = "emscripten-functions-sys" -version = "4.1.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65715a5f07b03636d7cd5508a45d1b62486840cb7d91a66564a73f1d7aa70b79" - [[package]] name = "equivalent" version = "1.0.1" @@ -150,6 +134,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gl" version = "0.14.0" @@ -386,10 +381,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" name = "render" version = "0.1.0" dependencies = [ - "emscripten-functions", - "emscripten-functions-sys", "gl", "skia-safe", + "uuid", ] [[package]] @@ -554,6 +548,21 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "which" version = "4.4.2" diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index a0de796ea..d2449bb5b 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -11,10 +11,9 @@ name = "render_wasm" path = "src/main.rs" [dependencies] -emscripten-functions = "0.2.3" -emscripten-functions-sys = "4.1.67" gl = "0.14.0" skia-safe = { version = "0.78.2", features = ["gl"] } +uuid = { version = "1.11.0", features = ["v4"] } [profile.release] opt-level = "s" diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 408d95fe9..423b6f8fb 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,83 +1,161 @@ pub mod render; pub mod shapes; +pub mod state; +pub mod utils; + +use std::collections::HashMap; use skia_safe as skia; +use uuid::Uuid; -use render::State; +use crate::shapes::Shape; +use crate::state::State; +use crate::utils::uuid_from_u32_quartet; + +static mut STATE: Option> = None; /// This is called from JS after the WebGL context has been created. #[no_mangle] -pub extern "C" fn init(width: i32, height: i32) -> Box { - let mut gpu_state = render::create_gpu_state(); - let surface = render::create_surface(&mut gpu_state, width, height); - - let state = State::new(gpu_state, surface); - - Box::new(state) +pub extern "C" fn init(width: i32, height: i32) { + let state_box = Box::new(State::with_capacity(width, height, 2048)); + unsafe { + STATE = Some(state_box); + } } /// This is called from JS when the window is resized. /// # Safety #[no_mangle] -pub unsafe extern "C" fn resize_surface(state: *mut State, width: i32, height: i32) { - let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); - let surface = render::create_surface(&mut state.gpu_state, width, height); +pub unsafe extern "C" fn resize_surface(width: i32, height: i32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let surface = render::create_surface(&mut state.render_state.gpu_state, width, height); state.set_surface(surface); } /// Draws a rect at the specified coordinates with the give ncolor /// # Safety #[no_mangle] -pub unsafe extern "C" fn draw_rect(state: *mut State, x1: f32, y1: f32, x2: f32, y2: f32) { - let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); +pub unsafe extern "C" fn draw_rect(x1: f32, y1: f32, x2: f32, y2: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let r = skia::Rect::new(x1, y1, x2, y2); - render::render_rect(&mut state.surface, r, skia::Color::RED); + render::render_rect(&mut state.render_state.surface, r, skia::Color::RED); } #[no_mangle] -pub unsafe extern "C" fn draw_all_shapes(state: *mut State, zoom: f32, pan_x: f32, pan_y: f32) { - let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); +pub unsafe extern "C" fn draw_all_shapes(zoom: f32, pan_x: f32, pan_y: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - reset_canvas(state); - scale(state, zoom, zoom); - translate(state, pan_x, pan_y); + reset_canvas(); + scale(zoom, zoom); + translate(pan_x, pan_y); - shapes::draw_all(state); + render::render_all(state); - flush(state); + flush(); } #[no_mangle] -pub unsafe extern "C" fn flush(state: *mut State) { - let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); +pub unsafe extern "C" fn flush() { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); state + .render_state .gpu_state .context - .flush_and_submit_surface(&mut state.surface, None); + .flush_and_submit_surface(&mut state.render_state.surface, None); } #[no_mangle] -pub unsafe extern "C" fn translate(state: *mut State, dx: f32, dy: f32) { - (*state).surface.canvas().translate((dx, dy)); +pub unsafe extern "C" fn translate(dx: f32, dy: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + state.render_state.surface.canvas().translate((dx, dy)); } #[no_mangle] -pub unsafe extern "C" fn scale(state: *mut State, sx: f32, sy: f32) { - (*state).surface.canvas().scale((sx, sy)); +pub unsafe extern "C" fn scale(sx: f32, sy: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + state.render_state.surface.canvas().scale((sx, sy)); } #[no_mangle] -pub unsafe extern "C" fn reset_canvas(state: *mut State) { - let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); - state.surface.canvas().clear(skia_safe::Color::TRANSPARENT); - state.surface.canvas().reset_matrix(); - flush(state); +pub extern "C" fn reset_canvas() { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + state + .render_state + .surface + .canvas() + .clear(skia_safe::Color::TRANSPARENT); + state.render_state.surface.canvas().reset_matrix(); +} + +pub fn get_or_create_shape<'a>(shapes: &'a mut HashMap, id: Uuid) -> &'a mut Shape { + if !shapes.contains_key(&id) { + let new_shape = Shape::new(id); + shapes.insert(id, new_shape); + } + + shapes.get_mut(&id).unwrap() } #[no_mangle] -pub unsafe extern "C" fn shapes_buffer() -> *mut shapes::Shape { - let ptr = shapes::SHAPES_BUFFER.as_mut_ptr(); - return ptr; +pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let id = uuid_from_u32_quartet(a, b, c, d); + state.current_id = Some(id); + let shapes = &mut state.shapes; + state.current_shape = Some(get_or_create_shape(shapes, id)); +} + +#[no_mangle] +pub unsafe extern "C" fn set_shape_selrect(x1: f32, y1: f32, x2: f32, y2: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + + if let Some(shape) = state.current_shape.as_deref_mut() { + shape.selrect.x1 = x1; + shape.selrect.y1 = y1; + shape.selrect.x2 = x2; + shape.selrect.y2 = y2; + } +} + +#[no_mangle] +pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape.as_deref_mut() { + shape.rotation = rotation; + } +} + +#[no_mangle] +pub unsafe extern "C" fn set_shape_x(x: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape.as_deref_mut() { + let width = shape.selrect.x2 - shape.selrect.x1; + shape.selrect.x1 = x; + shape.selrect.x2 = x + width; + } +} + +#[no_mangle] +pub unsafe extern "C" fn set_shape_y(y: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape.as_deref_mut() { + let height = shape.selrect.y2 - shape.selrect.y1; + shape.selrect.y1 = y; + shape.selrect.y2 = y + height; + } +} + +#[no_mangle] +pub unsafe extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape.as_deref_mut() { + shape.transform.a = a; + shape.transform.b = b; + shape.transform.c = c; + shape.transform.d = d; + shape.transform.e = e; + shape.transform.f = f; + } } fn main() { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 8046d0776..02a93ccf7 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,33 +1,29 @@ use skia_safe as skia; use skia_safe::gpu::{self, gl::FramebufferInfo, DirectContext}; +use crate::state::State; + extern "C" { pub fn emscripten_GetProcAddress( name: *const ::std::os::raw::c_char, ) -> *const ::std::os::raw::c_void; } -pub struct GpuState { +pub(crate) struct GpuState { pub context: DirectContext, framebuffer_info: FramebufferInfo, } -/// This struct holds the state of the Rust application between JS calls. -/// -/// It is created by [init] and passed to the other exported functions. Note that rust-skia data -/// structures are not thread safe, so a state must not be shared between different Web Workers. -pub struct State { +pub(crate) struct RenderState { pub gpu_state: GpuState, pub surface: skia::Surface, } -impl State { - pub fn new(gpu_state: GpuState, surface: skia::Surface) -> Self { - State { gpu_state, surface } - } - - pub fn set_surface(&mut self, surface: skia::Surface) { - self.surface = surface; +impl RenderState { + pub fn new(width: i32, height: i32) -> RenderState { + let mut gpu_state = create_gpu_state(); + let surface = create_surface(&mut gpu_state, width, height); + RenderState { gpu_state, surface } } } @@ -84,3 +80,35 @@ pub(crate) fn render_rect(surface: &mut skia::Surface, rect: skia::Rect, color: paint.set_anti_alias(true); surface.canvas().draw_rect(rect, &paint); } + +pub(crate) fn render_all(state: &mut State) { + for shape in state.shapes.values() { + let r = skia::Rect::new( + shape.selrect.x1, + shape.selrect.y1, + shape.selrect.x2, + shape.selrect.y2, + ); + + state.render_state.surface.canvas().save(); + + // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc + let mut matrix = skia::Matrix::new_identity(); + let (translate_x, translate_y) = shape.translation(); + let (scale_x, scale_y) = shape.scale(); + let (skew_x, skew_y) = shape.skew(); + + matrix.set_all(scale_x, skew_x, translate_x, skew_y, scale_y, translate_y, 0., 0., 1.); + + let mut center = r.center(); + matrix.post_translate(center); + center.negate(); + matrix.pre_translate(center); + + state.render_state.surface.canvas().concat(&matrix); + + render_rect(&mut state.render_state.surface, r, skia::Color::RED); + + state.render_state.surface.canvas().restore(); + } +} diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 891267620..8015aaf1b 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1,31 +1,84 @@ -use crate::render::{render_rect, State}; -use skia_safe as skia; +use uuid::Uuid; #[derive(Debug, Clone, Copy)] -pub struct Selrect { +pub enum Kind { + None, + Text, + Path, + SVGRaw, + Image, + Circle, + Rect, + Bool, + Group, + Frame, +} + +pub struct Point { + pub x: f32, + pub y: f32, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Rect { pub x1: f32, pub y1: f32, pub x2: f32, pub y2: f32, } -pub type Shape = Selrect; // temp +#[derive(Debug, Clone, Copy)] +pub struct Matrix { + pub a: f32, + pub b: f32, + pub c: f32, + pub d: f32, + pub e: f32, + pub f: f32, +} -pub static mut SHAPES_BUFFER: [Shape; 2048] = [Selrect { - x1: 0.0, - y1: 0.0, - x2: 0.0, - y2: 0.0, -}; 2048]; - -pub(crate) fn draw_all(state: &mut State) { - let shapes; - unsafe { - shapes = SHAPES_BUFFER.iter(); - } - - for shape in shapes { - let r = skia::Rect::new(shape.x1, shape.y1, shape.x2, shape.y2); - render_rect(&mut state.surface, r, skia::Color::RED); +impl Matrix { + pub fn identity() -> Self { + Self { + a: 1., + b: 0., + c: 0., + d: 1., + e: 0., + f: 0., + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Shape { + pub id: Uuid, + pub kind: Kind, + pub selrect: Rect, + pub transform: Matrix, + pub rotation: f32, +} + +impl Shape { + pub fn new(id: Uuid) -> Self { + Self { + id, + kind: Kind::Rect, + selrect: Rect::default(), + transform: Matrix::identity(), + rotation: 0., + } + } + + pub fn translation(&self) -> (f32, f32) { + (self.transform.e, self.transform.f) + } + + pub fn scale(&self) -> (f32, f32) { + (self.transform.a, self.transform.d) + } + + pub fn skew(&self) -> (f32, f32) { + (self.transform.c, self.transform.b) } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs new file mode 100644 index 000000000..3fdb427d7 --- /dev/null +++ b/render-wasm/src/state.rs @@ -0,0 +1,36 @@ +use skia_safe as skia; +use std::collections::HashMap; +use std::vec::Vec; +use uuid::Uuid; + +use crate::render::RenderState; +use crate::shapes::Shape; + +/// This struct holds the state of the Rust application between JS calls. +/// +/// It is created by [init] and passed to the other exported functions. +/// Note that rust-skia data structures are not thread safe, so a state +/// must not be shared between different Web Workers. +pub(crate) struct State<'a> { + pub render_state: RenderState, + pub current_id: Option, + pub current_shape: Option<&'a mut Shape>, + pub shapes: HashMap, + pub display_list: Vec, +} + +impl<'a> State<'a> { + pub fn with_capacity(width: i32, height: i32, capacity: usize) -> Self { + State { + render_state: RenderState::new(width, height), + current_id: None, + current_shape: None, + shapes: HashMap::with_capacity(capacity), + display_list: Vec::with_capacity(capacity), + } + } + + pub fn set_surface(&mut self, surface: skia::Surface) { + self.render_state.surface = surface; + } +} diff --git a/render-wasm/src/utils.rs b/render-wasm/src/utils.rs new file mode 100644 index 000000000..8c5b39419 --- /dev/null +++ b/render-wasm/src/utils.rs @@ -0,0 +1,8 @@ +use uuid::Uuid; + +pub fn uuid_from_u32_quartet(a: u32, b: u32, c: u32, d: u32) -> Uuid +{ + let hi: u64 = ((a as u64) << 32) | b as u64; + let lo: u64 = ((c as u64) << 32) | d as u64; + Uuid::from_u64_pair(hi, lo) +}