🎉 Save shape data in rust memory

This commit is contained in:
Alejandro Alonso 2024-11-12 11:09:50 +01:00
parent 48909dc3c4
commit 65ee2f9081
13 changed files with 479 additions and 297 deletions

View file

@ -7,34 +7,30 @@
(ns app.common.types.shape.impl (ns app.common.types.shape.impl
(:require (:require
#?(:clj [app.common.fressian :as fres]) #?(: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]) #?(:cljs [cuerdas.core :as str])
[app.common.record :as cr] [app.common.record :as cr]
[app.common.transit :as t] [app.common.transit :as t]
[clojure.core :as c])) [clojure.core :as c]))
(def enabled-wasm-ready-shape false) (defonce ^:dynamic *wasm-sync* false)
(defonce enabled-wasm-ready-shape false)
#?(:cljs (defonce wasm-create-shape (constantly nil))
(do (defonce wasm-use-shape (constantly nil))
(def ArrayBuffer js/ArrayBuffer) (defonce wasm-set-shape-selrect (constantly nil))
(def Float32Array js/Float32Array))) (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 (cr/defrecord Shape [id name type x y width height rotation selrect points
transform transform-inverse parent-id frame-id flip-x flip-y]) transform transform-inverse parent-id frame-id flip-x flip-y])
(declare ^:private clone-f32-array)
(declare ^:private impl-assoc) (declare ^:private impl-assoc)
(declare ^:private impl-conj) (declare ^:private impl-conj)
(declare ^:private impl-dissoc) (declare ^:private impl-dissoc)
(declare ^:private read-selrect)
(declare ^:private write-selrect)
;; TODO: implement lazy MapEntry ;; TODO: implement lazy MapEntry
#?(:cljs #?(:cljs
(deftype ShapeWithBuffer [buffer delegate] (deftype ShapeProxy [delegate]
Object Object
(toString [coll] (toString [coll]
(str "{" (str/join ", " (for [[k v] coll] (str k " " v))) "}")) (str "{" (str/join ", " (for [[k v] coll] (str k " " v))) "}"))
@ -42,14 +38,9 @@
(equiv [this other] (equiv [this other]
(-equiv this other)) (-equiv this other))
;; ICloneable
;; (-clone [_]
;; (let [bf32 (clone-float32-array buffer)]
;; (ShapeWithBuffer. bf32 delegate)))
IWithMeta IWithMeta
(-with-meta [_ meta] (-with-meta [_ meta]
(ShapeWithBuffer. buffer (with-meta delegate meta))) (ShapeProxy. (with-meta delegate meta)))
IMeta IMeta
(-meta [_] (meta delegate)) (-meta [_] (meta delegate))
@ -68,9 +59,8 @@
ISequential ISequential
ISeqable ISeqable
(-seq [coll] (-seq [_]
(cons (find coll :selrect) (c/-seq delegate))
(seq delegate)))
ICounted ICounted
(-count [_] (-count [_]
@ -81,23 +71,18 @@
(-lookup coll k nil)) (-lookup coll k nil))
(-lookup [_ k not-found] (-lookup [_ k not-found]
(if (= k :selrect) (c/-lookup delegate k not-found))
(read-selrect buffer)
(c/-lookup delegate k not-found)))
IFind IFind
(-find [_ k] (-find [_ k]
(if (= k :selrect) (c/-find delegate k))
(c/MapEntry. k (read-selrect buffer) nil) ; Replace with lazy MapEntry
(c/-find delegate k)))
IAssociative IAssociative
(-assoc [coll k v] (-assoc [coll k v]
(impl-assoc coll k v)) (impl-assoc coll k v))
(-contains-key? [_ k] (-contains-key? [_ k]
(or (= k :selrect) (contains? delegate k))
(contains? delegate k)))
IMap IMap
(-dissoc [coll k] (-dissoc [coll k]
@ -118,64 +103,34 @@
[o] [o]
#?(:clj (instance? Shape o) #?(:clj (instance? Shape o)
:cljs (or (instance? Shape o) :cljs (or (instance? Shape o)
(instance? ShapeWithBuffer o)))) (instance? ShapeProxy o))))
;; --- SHAPE IMPL ;; --- 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 #?(:cljs
(defn- impl-assoc (defn- impl-assoc
[coll k v] [coll k v]
(if (= k :selrect) (when *wasm-sync*
(let [buffer (clone-f32-array (.-buffer coll))] (wasm-use-shape (:id coll))
(write-selrect buffer v) (case k
(ShapeWithBuffer. buffer (.-delegate ^ShapeWithBuffer coll))) :selrect (wasm-set-shape-selrect v)
:rotation (wasm-set-shape-rotation v)
(let [delegate (.-delegate ^ShapeWithBuffer coll) :transform (wasm-set-shape-transform v)
delegate' (assoc delegate k v)] nil))
(if (identical? delegate' delegate) (let [delegate (.-delegate ^ShapeProxy coll)
coll delegate' (assoc delegate k v)]
(let [buffer (clone-f32-array (.-buffer coll))] (if (identical? delegate' delegate)
(ShapeWithBuffer. buffer delegate'))))))) coll
(ShapeProxy. delegate')))))
#?(:cljs #?(:cljs
(defn- impl-dissoc (defn- impl-dissoc
[coll k] [coll k]
(let [delegate (.-delegate ^ShapeWithBuffer coll) (let [delegate (.-delegate ^ShapeProxy coll)
delegate' (dissoc delegate k)] delegate' (dissoc delegate k)]
(if (identical? delegate delegate') (if (identical? delegate delegate')
coll coll
(let [buffer (clone-f32-array (.-buffer coll))] (ShapeProxy. delegate')))))
(ShapeWithBuffer. buffer delegate'))))))
#?(:cljs #?(:cljs
(defn- impl-conj (defn- impl-conj
@ -196,10 +151,7 @@
[attrs] [attrs]
#?(:cljs #?(:cljs
(if enabled-wasm-ready-shape (if enabled-wasm-ready-shape
(let [selrect (:selrect attrs) (ShapeProxy. attrs)
buffer (new Float32Array 4)]
(write-selrect buffer selrect)
(ShapeWithBuffer. buffer (dissoc attrs :selrect)))
(map->Shape attrs)) (map->Shape attrs))
:clj (map->Shape attrs))) :clj (map->Shape attrs)))
@ -215,7 +167,7 @@
#?(:cljs #?(:cljs
(t/add-handlers! (t/add-handlers!
{:id "shape" {:id "shape"
:class ShapeWithBuffer :class ShapeProxy
:wfn #(into {} %) :wfn #(into {} %)
:rfn create-shape})) :rfn create-shape}))

View file

@ -76,6 +76,11 @@
[id] [id]
(impl/short-v8 (dm/str id)))) (impl/short-v8 (dm/str id))))
#?(:cljs
(defn uuid->u32
[id]
(impl/get-u32 (dm/str id))))
#?(:clj #?(:clj
(defn hash-int (defn hash-int
[id] [id]

View file

@ -122,7 +122,6 @@ goog.scope(function() {
}; };
})(); })();
self.v4 = (function () { self.v4 = (function () {
const arr = new Uint8Array(16); const arr = new Uint8Array(16);
@ -211,7 +210,6 @@ goog.scope(function() {
return factory; return factory;
})(); })();
self.short_v8 = function(uuid) { self.short_v8 = function(uuid) {
const buff = encoding.hexToBuffer(uuid); const buff = encoding.hexToBuffer(uuid);
const short = new Uint8Array(buff, 4); const short = new Uint8Array(buff, 4);
@ -222,5 +220,41 @@ goog.scope(function() {
const most = mostSigBits.toString("16").padStart(16, "0"); const most = mostSigBits.toString("16").padStart(16, "0");
const least = leastSigBits.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)}`; 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;
}
})();
}); });

View file

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.logic.shapes :as cls] [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.layout :as ctl]
[app.common.types.shape.radius :as ctsr] [app.common.types.shape.radius :as ctsr]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
@ -248,19 +249,20 @@
(fn [value attr] (fn [value attr]
(let [token-value (wtc/maybe-resolve-token-value value) (let [token-value (wtc/maybe-resolve-token-value value)
undo-id (js/Symbol)] undo-id (js/Symbol)]
(if-not design-tokens? (binding [shape.impl/*wasm-sync* true]
(st/emit! (udw/trigger-bounding-box-cloaking ids) (if-not design-tokens?
(udw/update-dimensions ids attr (or token-value value))) (st/emit! (udw/trigger-bounding-box-cloaking ids)
(st/emit! (udw/trigger-bounding-box-cloaking ids) (udw/update-dimensions ids attr (or token-value value)))
(dwu/start-undo-transaction undo-id) (st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwsh/update-shapes ids (dwu/start-undo-transaction undo-id)
(if token-value (dwsh/update-shapes ids
#(assoc-in % [:applied-tokens attr] (:id value)) (if token-value
#(d/dissoc-in % [:applied-tokens attr])) #(assoc-in % [:applied-tokens attr] (:id value))
{:reg-objects? true #(d/dissoc-in % [:applied-tokens attr]))
:attrs [:applied-tokens]}) {:reg-objects? true
(udw/update-dimensions ids attr (or token-value value)) :attrs [:applied-tokens]})
(dwu/commit-undo-transaction undo-id)))))) (udw/update-dimensions ids attr (or token-value value))
(dwu/commit-undo-transaction undo-id)))))))
on-proportion-lock-change on-proportion-lock-change
(mf/use-fn (mf/use-fn
@ -283,7 +285,8 @@
(mf/deps ids) (mf/deps ids)
(fn [value attr] (fn [value attr]
(st/emit! (udw/trigger-bounding-box-cloaking ids)) (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 ;; ROTATION
@ -291,8 +294,9 @@
(mf/use-fn (mf/use-fn
(mf/deps ids) (mf/deps ids)
(fn [value] (fn [value]
(st/emit! (udw/trigger-bounding-box-cloaking ids) (binding [shape.impl/*wasm-sync* true]
(udw/increase-rotation ids value)))) (st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/increase-rotation ids value)))))
;; RADIUS ;; RADIUS

View file

@ -13,6 +13,7 @@
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.types.shape-tree :as ctt] [app.common.types.shape-tree :as ctt]
[app.common.types.shape.impl :as shape.impl]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.modifiers :as dwm]
[app.main.features :as features] [app.main.features :as features]
@ -111,10 +112,9 @@
modifiers (mf/deref refs/workspace-modifiers) modifiers (mf/deref refs/workspace-modifiers)
text-modifiers (mf/deref refs/workspace-text-modifier) 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] 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) selected-shapes (keep (d/getf objects-modified) selected)
@ -177,8 +177,6 @@
mode-inspect? (= options-mode :inspect) 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-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-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?) 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?] (p/fmap (fn [ready?]
(when ready? (when ready?
(reset! canvas-init? true) (reset! canvas-init? true)
(render.wasm/setup-canvas canvas))))) (render.wasm/assign-canvas canvas)))))
(fn [] (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? (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))) (render.wasm/draw-objects zoom vbox)))
(mf/with-effect [vbox canvas-init?] (mf/with-effect [vbox canvas-init?]
(let [frame-id (when @canvas-init? (do (let [frame-id (when @canvas-init? (render.wasm/draw-objects zoom vbox))]
(render.wasm/draw-objects zoom vbox)))]
(partial render.wasm/cancel-draw frame-id))) (partial render.wasm/cancel-draw frame-id)))
(hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?) (hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?)
@ -639,11 +637,4 @@
{:objects base-objects {:objects base-objects
:zoom zoom :zoom zoom
:vbox vbox :vbox vbox
:bottom-padding (when palete-size (+ palete-size 8))}]]]] :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"]])]))

View file

@ -9,12 +9,10 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.helpers :as cfh] [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.config :as cf]
[app.main.data.render-wasm :as drw] [app.util.object :as obj]
[app.main.store :as st]
[app.util.debug :as dbg]
[app.util.dom :as dom]
[promesa.core :as p])) [promesa.core :as p]))
(def enabled? (def enabled?
@ -23,7 +21,6 @@
(set! app.common.types.shape.impl/enabled-wasm-ready-shape enabled?) (set! app.common.types.shape.impl/enabled-wasm-ready-shape enabled?)
(defonce internal-module #js {}) (defonce internal-module #js {})
(defonce internal-gpu-state #js {})
;; TODO: remove the `take` once we have the dynamic data structure in Rust ;; TODO: remove the `take` once we have the dynamic data structure in Rust
(def xform (def xform
@ -31,49 +28,70 @@
(remove cfh/root?) (remove cfh/root?)
(take 2048))) (take 2048)))
;; Size in number of f32 values that represents the shape selrect ( (defn create-shape
(def rect-size 4) [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 (defn set-objects
[objects] [objects]
;; FIXME: maybe change the name of `_shapes_buffer` (?) (let [shapes (into [] xform (vals objects))
(let [get-shapes-buffer-ptr total-shapes (count shapes)]
(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)]
(loop [index 0] (loop [index 0]
(when (< index total-shapes) (when (< index total-shapes)
(let [shape (nth shapes index)] (let [shape (nth shapes index)
(.set ^js mem (.-buffer shape) (* index rect-size)) 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))))))) (recur (inc index)))))))
(defn draw-objects (defn draw-objects
[zoom vbox] [zoom vbox]
(let [draw-all-shapes (unchecked-get internal-module "_draw_all_shapes")] (js/requestAnimationFrame
(js/requestAnimationFrame (fn []
(fn [] (let [pan-x (- (dm/get-prop vbox :x))
(let [pan-x (- (dm/get-prop vbox :x)) pan-y (- (dm/get-prop vbox :y))]
pan-y (- (dm/get-prop vbox :y))] (._draw_all_shapes ^js internal-module zoom pan-x pan-y)))))
(draw-all-shapes internal-gpu-state zoom pan-x pan-y))))))
(defn cancel-draw (defn cancel-draw
[frame-id] [frame-id]
@ -86,67 +104,28 @@
:stencil true :stencil true
:alpha true}) :alpha true})
(defn init-skia (defn clear-canvas
[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]
;; TODO: perform corresponding cleaning ;; TODO: perform corresponding cleaning
(.removeEventListener canvas "webglcontextlost" on-webgl-context) )
(.removeEventListener canvas "webglcontextrestored" on-webgl-context))
(defn init-debug-webgl-context-state (defn assign-canvas
[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
[canvas] [canvas]
(let [gl (unchecked-get internal-module "GL") (let [gl (unchecked-get internal-module "GL")
init-fn (unchecked-get internal-module "_init")
context (.getContext ^js canvas "webgl2" canvas-options) context (.getContext ^js canvas "webgl2" canvas-options)
;; Register the context with emscripten ;; Register the context with emscripten
handle (.registerContext ^js gl context #js {"majorVersion" 2}) handle (.registerContext ^js gl context #js {"majorVersion" 2})]
_ (.makeContextCurrent ^js gl handle)] (.makeContextCurrent ^js gl handle)
;; Initialize Skia
(when (dbg/enabled? :gl-context) (init-fn (.-width ^js canvas)
(init-debug-webgl-context-state context)) (.-height ^js canvas))
(.addEventListener canvas "webglcontextlost" on-webgl-context)
(.addEventListener canvas "webglcontextrestored" on-webgl-context)
(set! (.-width canvas) (.-clientWidth ^js canvas)) (set! (.-width canvas) (.-clientWidth ^js canvas))
(set! (.-height canvas) (.-clientHeight ^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 (defonce module
(->> (js/dynamicImport "/js/render_wasm.js") (->> (js/dynamicImport "/js/render_wasm.js")
@ -159,3 +138,9 @@
(p/merr (fn [cause] (p/merr (fn [cause]
(js/console.error cause) (js/console.error cause)
(p/resolved false))))) (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)

45
render-wasm/Cargo.lock generated
View file

@ -96,22 +96,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -150,6 +134,17 @@ dependencies = [
"miniz_oxide", "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]] [[package]]
name = "gl" name = "gl"
version = "0.14.0" version = "0.14.0"
@ -386,10 +381,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
name = "render" name = "render"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"emscripten-functions",
"emscripten-functions-sys",
"gl", "gl",
"skia-safe", "skia-safe",
"uuid",
] ]
[[package]] [[package]]
@ -554,6 +548,21 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 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]] [[package]]
name = "which" name = "which"
version = "4.4.2" version = "4.4.2"

View file

@ -11,10 +11,9 @@ name = "render_wasm"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
emscripten-functions = "0.2.3"
emscripten-functions-sys = "4.1.67"
gl = "0.14.0" gl = "0.14.0"
skia-safe = { version = "0.78.2", features = ["gl"] } skia-safe = { version = "0.78.2", features = ["gl"] }
uuid = { version = "1.11.0", features = ["v4"] }
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"

View file

@ -1,83 +1,161 @@
pub mod render; pub mod render;
pub mod shapes; pub mod shapes;
pub mod state;
pub mod utils;
use std::collections::HashMap;
use skia_safe as skia; 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<Box<State>> = None;
/// This is called from JS after the WebGL context has been created. /// This is called from JS after the WebGL context has been created.
#[no_mangle] #[no_mangle]
pub extern "C" fn init(width: i32, height: i32) -> Box<render::State> { pub extern "C" fn init(width: i32, height: i32) {
let mut gpu_state = render::create_gpu_state(); let state_box = Box::new(State::with_capacity(width, height, 2048));
let surface = render::create_surface(&mut gpu_state, width, height); unsafe {
STATE = Some(state_box);
let state = State::new(gpu_state, surface); }
Box::new(state)
} }
/// This is called from JS when the window is resized. /// This is called from JS when the window is resized.
/// # Safety /// # Safety
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn resize_surface(state: *mut State, width: i32, height: i32) { pub unsafe extern "C" fn resize_surface(width: i32, height: i32) {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
let surface = render::create_surface(&mut state.gpu_state, width, height); let surface = render::create_surface(&mut state.render_state.gpu_state, width, height);
state.set_surface(surface); state.set_surface(surface);
} }
/// Draws a rect at the specified coordinates with the give ncolor /// Draws a rect at the specified coordinates with the give ncolor
/// # Safety /// # Safety
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn draw_rect(state: *mut State, x1: f32, y1: f32, x2: f32, y2: f32) { 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 state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
let r = skia::Rect::new(x1, y1, x2, y2); 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] #[no_mangle]
pub unsafe extern "C" fn draw_all_shapes(state: *mut State, zoom: f32, pan_x: f32, pan_y: f32) { 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"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
reset_canvas(state); reset_canvas();
scale(state, zoom, zoom); scale(zoom, zoom);
translate(state, pan_x, pan_y); translate(pan_x, pan_y);
shapes::draw_all(state); render::render_all(state);
flush(state); flush();
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn flush(state: *mut State) { pub unsafe extern "C" fn flush() {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
state state
.render_state
.gpu_state .gpu_state
.context .context
.flush_and_submit_surface(&mut state.surface, None); .flush_and_submit_surface(&mut state.render_state.surface, None);
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn translate(state: *mut State, dx: f32, dy: f32) { pub unsafe extern "C" fn translate(dx: f32, dy: f32) {
(*state).surface.canvas().translate((dx, dy)); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
state.render_state.surface.canvas().translate((dx, dy));
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn scale(state: *mut State, sx: f32, sy: f32) { pub unsafe extern "C" fn scale(sx: f32, sy: f32) {
(*state).surface.canvas().scale((sx, sy)); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
state.render_state.surface.canvas().scale((sx, sy));
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn reset_canvas(state: *mut State) { pub extern "C" fn reset_canvas() {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
state.surface.canvas().clear(skia_safe::Color::TRANSPARENT); state
state.surface.canvas().reset_matrix(); .render_state
flush(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<Uuid, Shape>, 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] #[no_mangle]
pub unsafe extern "C" fn shapes_buffer() -> *mut shapes::Shape { pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) {
let ptr = shapes::SHAPES_BUFFER.as_mut_ptr(); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
return ptr; 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() { fn main() {

View file

@ -1,33 +1,29 @@
use skia_safe as skia; use skia_safe as skia;
use skia_safe::gpu::{self, gl::FramebufferInfo, DirectContext}; use skia_safe::gpu::{self, gl::FramebufferInfo, DirectContext};
use crate::state::State;
extern "C" { extern "C" {
pub fn emscripten_GetProcAddress( pub fn emscripten_GetProcAddress(
name: *const ::std::os::raw::c_char, name: *const ::std::os::raw::c_char,
) -> *const ::std::os::raw::c_void; ) -> *const ::std::os::raw::c_void;
} }
pub struct GpuState { pub(crate) struct GpuState {
pub context: DirectContext, pub context: DirectContext,
framebuffer_info: FramebufferInfo, framebuffer_info: FramebufferInfo,
} }
/// This struct holds the state of the Rust application between JS calls. pub(crate) struct RenderState {
///
/// 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 gpu_state: GpuState, pub gpu_state: GpuState,
pub surface: skia::Surface, pub surface: skia::Surface,
} }
impl State { impl RenderState {
pub fn new(gpu_state: GpuState, surface: skia::Surface) -> Self { pub fn new(width: i32, height: i32) -> RenderState {
State { gpu_state, surface } let mut gpu_state = create_gpu_state();
} let surface = create_surface(&mut gpu_state, width, height);
RenderState { gpu_state, surface }
pub fn set_surface(&mut self, surface: skia::Surface) {
self.surface = surface;
} }
} }
@ -84,3 +80,35 @@ pub(crate) fn render_rect(surface: &mut skia::Surface, rect: skia::Rect, color:
paint.set_anti_alias(true); paint.set_anti_alias(true);
surface.canvas().draw_rect(rect, &paint); 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();
}
}

View file

@ -1,31 +1,84 @@
use crate::render::{render_rect, State}; use uuid::Uuid;
use skia_safe as skia;
#[derive(Debug, Clone, Copy)] #[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 x1: f32,
pub y1: f32, pub y1: f32,
pub x2: f32, pub x2: f32,
pub y2: 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 { impl Matrix {
x1: 0.0, pub fn identity() -> Self {
y1: 0.0, Self {
x2: 0.0, a: 1.,
y2: 0.0, b: 0.,
}; 2048]; c: 0.,
d: 1.,
pub(crate) fn draw_all(state: &mut State) { e: 0.,
let shapes; f: 0.,
unsafe { }
shapes = SHAPES_BUFFER.iter(); }
} }
for shape in shapes { #[derive(Debug, Clone, Copy)]
let r = skia::Rect::new(shape.x1, shape.y1, shape.x2, shape.y2); pub struct Shape {
render_rect(&mut state.surface, r, skia::Color::RED); 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)
} }
} }

36
render-wasm/src/state.rs Normal file
View file

@ -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<Uuid>,
pub current_shape: Option<&'a mut Shape>,
pub shapes: HashMap<Uuid, Shape>,
pub display_list: Vec<Uuid>,
}
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;
}
}

8
render-wasm/src/utils.rs Normal file
View file

@ -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)
}