From 4623f36042f6a174402167ed6c8d047ee917d5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Fri, 25 Oct 2024 15:00:57 +0200 Subject: [PATCH] :sparkles: Write shapes directly to wasm memory --- common/src/app/common/geom/rect.cljc | 20 +-- common/src/app/common/record.cljc | 44 +++--- common/src/app/common/types/shape.cljc | 2 +- common/src/app/common/types/shape/impl.cljc | 126 +++++++++--------- .../app/main/data/workspace/selection.cljs | 2 +- .../src/app/main/ui/workspace/viewport.cljs | 29 ++-- frontend/src/app/render_wasm.cljs | 51 +++---- render-wasm/src/main.rs | 20 +++ render-wasm/src/shapes.rs | 31 +++++ 9 files changed, 186 insertions(+), 139 deletions(-) create mode 100644 render-wasm/src/shapes.rs diff --git a/common/src/app/common/geom/rect.cljc b/common/src/app/common/geom/rect.cljc index 3308b9256..b18befe6c 100644 --- a/common/src/app/common/geom/rect.cljc +++ b/common/src/app/common/geom/rect.cljc @@ -158,22 +158,22 @@ y (dm/get-prop rect :y) w (dm/get-prop rect :width) h (dm/get-prop rect :height)] - (rc/assoc! rect - :x1 x - :y1 y - :x2 (+ x w) - :y2 (+ y h))) + (assoc rect + :x1 x + :y1 y + :x2 (+ x w) + :y2 (+ y h))) :corners (let [x1 (dm/get-prop rect :x1) y1 (dm/get-prop rect :y1) x2 (dm/get-prop rect :x2) y2 (dm/get-prop rect :y2)] - (rc/assoc! rect - :x (mth/min x1 x2) - :y (mth/min y1 y2) - :width (mth/abs (- x2 x1)) - :height (mth/abs (- y2 y1)))))) + (assoc rect + :x (mth/min x1 x2) + :y (mth/min y1 y2) + :width (mth/abs (- x2 x1)) + :height (mth/abs (- y2 y1)))))) (defn close-rect? [rect1 rect2] diff --git a/common/src/app/common/record.cljc b/common/src/app/common/record.cljc index cbb9fa006..385917a0a 100644 --- a/common/src/app/common/record.cljc +++ b/common/src/app/common/record.cljc @@ -403,30 +403,30 @@ nil))) ~rsym))) -;; (defmacro clone -;; [ssym] -;; (if (:ns &env) -;; `(cljs.core/clone ~ssym) -;; ssym)) +(defmacro clone + [ssym] + (if (:ns &env) + `(cljs.core/clone ~ssym) + ssym)) -;; (defmacro assoc! -;; "A record specific update operation" -;; [ssym & pairs] -;; (if (:ns &env) -;; (let [pairs (partition-all 2 pairs)] -;; `(-> ~ssym -;; ~@(map (fn [[ks vs]] -;; `(cljs.core/-assoc! ~ks ~vs)) -;; pairs))) -;; `(assoc ~ssym ~@pairs))) +(defmacro assoc! + "A record specific update operation" + [ssym & pairs] + (if (:ns &env) + (let [pairs (partition-all 2 pairs)] + `(-> ~ssym + ~@(map (fn [[ks vs]] + `(cljs.core/-assoc! ~ks ~vs)) + pairs))) + `(assoc ~ssym ~@pairs))) -;; (defmacro update! -;; "A record specific update operation" -;; [ssym ksym f & params] -;; (if (:ns &env) -;; (let [ssym (with-meta ssym {:tag 'js})] -;; `(cljs.core/assoc! ~ssym ~ksym (~f (. ~ssym ~(property-symbol ksym)) ~@params))) -;; `(update ~ssym ~ksym ~f ~@params))) +(defmacro update! + "A record specific update operation" + [ssym ksym f & params] + (if (:ns &env) + (let [ssym (with-meta ssym {:tag 'js})] + `(cljs.core/assoc! ~ssym ~ksym (~f (. ~ssym ~(property-symbol ksym)) ~@params))) + `(update ~ssym ~ksym ~f ~@params))) (defmacro define-properties! "Define properties in the prototype with `.defineProperty`" diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 9b13bdad1..707126c98 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -19,12 +19,12 @@ [app.common.schema.generators :as sg] [app.common.transit :as t] [app.common.types.color :as ctc] - [app.common.types.shape.impl :as impl] [app.common.types.grid :as ctg] [app.common.types.plugins :as ctpg] [app.common.types.shape.attrs :refer [default-color]] [app.common.types.shape.blur :as ctsb] [app.common.types.shape.export :as ctse] + [app.common.types.shape.impl :as impl] [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctsl] [app.common.types.shape.path :as ctsp] diff --git a/common/src/app/common/types/shape/impl.cljc b/common/src/app/common/types/shape/impl.cljc index 3dc75c498..331b4e99d 100644 --- a/common/src/app/common/types/shape/impl.cljc +++ b/common/src/app/common/types/shape/impl.cljc @@ -10,18 +10,20 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.record :as cr] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.transit :as t] [app.common.uuid :as uuid] - [app.common.geom.rect :as grc] - [cuerdas.core :as str] [clojure.core :as c] - [clojure.set :as set])) + [clojure.set :as set] + [cuerdas.core :as str])) -(def ArrayBuffer js/ArrayBuffer) -(def Float32Array js/Float32Array) +#?(:cljs + (do + (def ArrayBuffer js/ArrayBuffer) + (def Float32Array js/Float32Array))) (cr/defrecord Shape [id name type x y width height rotation selrect points transform transform-inverse parent-id frame-id flip-x flip-y]) @@ -49,77 +51,77 @@ ;; (let [bf32 (clone-float32-array buffer)] ;; (ShapeWithBuffer. bf32 delegate))) - IWithMeta - (-with-meta [coll meta] - (ShapeWithBuffer. buffer (with-meta delegate meta))) + IWithMeta + (-with-meta [coll meta] + (ShapeWithBuffer. buffer (with-meta delegate meta))) - IMeta - (-meta [coll] (meta delegate)) + IMeta + (-meta [coll] (meta delegate)) - ICollection - (-conj [coll entry] - (impl-conj coll entry)) + ICollection + (-conj [coll entry] + (impl-conj coll entry)) - IEquiv - (-equiv [coll other] - (c/equiv-map coll other)) + IEquiv + (-equiv [coll other] + (c/equiv-map coll other)) - IHash - (-hash [coll] (hash (into {} coll))) + IHash + (-hash [coll] (hash (into {} coll))) - ISequential + ISequential - ISeqable - (-seq [coll] - (cons (find coll :selrect) - (seq delegate))) + ISeqable + (-seq [coll] + (cons (find coll :selrect) + (seq delegate))) - ICounted - (-count [coll] - (+ 1 (count delegate))) + ICounted + (-count [coll] + (+ 1 (count delegate))) - ILookup - (-lookup [coll k] - (-lookup coll k nil)) + ILookup + (-lookup [coll k] + (-lookup coll k nil)) - (-lookup [coll k not-found] - (if (= k :selrect) - (read-selrect buffer) - (c/-lookup delegate k not-found))) + (-lookup [coll k not-found] + (if (= k :selrect) + (read-selrect buffer) + (c/-lookup delegate k not-found))) - IFind - (-find [coll k] - (if (= k :selrect) - (c/MapEntry. k (read-selrect buffer) nil) ; Replace with lazy MapEntry - (c/-find delegate k))) + IFind + (-find [coll k] + (if (= k :selrect) + (c/MapEntry. k (read-selrect buffer) nil) ; Replace with lazy MapEntry + (c/-find delegate k))) - IAssociative - (-assoc [coll k v] - (impl-assoc coll k v)) + IAssociative + (-assoc [coll k v] + (impl-assoc coll k v)) - (-contains-key? [coll k] - (or (= k :selrect) - (contains? delegate k))) + (-contains-key? [coll k] + (or (= k :selrect) + (contains? delegate k))) - IMap - (-dissoc [coll k] - (impl-dissoc coll k)) + IMap + (-dissoc [coll k] + (impl-dissoc coll k)) - IFn - (-invoke [coll k] - (-lookup coll k)) + IFn + (-invoke [coll k] + (-lookup coll k)) - (-invoke [coll k not-found] - (-lookup coll k not-found)) + (-invoke [coll k not-found] + (-lookup coll k not-found)) - IPrintWithWriter - (-pr-writer [coll writer opts] - (-write writer (str "#penpot/shape " (:id delegate)))))) + IPrintWithWriter + (-pr-writer [coll writer opts] + (-write writer (str "#penpot/shape " (:id delegate)))))) (defn shape? [o] (or (instance? Shape o) - (instance? ShapeWithBuffer o))) + #?(:cljs (instance? ShapeWithBuffer o)))) ;; --- SHAPE IMPL @@ -211,12 +213,12 @@ :rfn #?(:cljs create-shape :clj map->Shape)}) -(t/add-handlers! - {:id "shape" - :class ShapeWithBuffer - :wfn #(into {} %) - :rfn #?(:cljs create-shape - :clj map->Shape)}) +#?(:cljs (t/add-handlers! + {:id "shape" + :class ShapeWithBuffer + :wfn #(into {} %) + :rfn #?(:cljs create-shape + :clj map->Shape)})) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 1f29cbcc3..f0210cf53 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -72,7 +72,7 @@ (cr/update! :x2 + (:x delta)) (cr/update! :y2 + (:y delta))) selrect (if ^boolean space? - (-> selrect + (-> (cr/clone selrect) (cr/update! :x1 + (:x delta)) (cr/update! :y1 + (:y delta))) selrect)] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a83e45973..1170068a2 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -134,13 +134,13 @@ hover-top-frame-id (mf/use-state nil) frame-hover (mf/use-state nil) active-frames (mf/use-state #{}) + canvas-init? (mf/use-state false) ;; REFS [viewport-ref on-viewport-ref] (create-viewport-ref) canvas-ref (mf/use-ref nil) - canvas-init (mf/use-ref false) ;; VARS disable-paste (mf/use-var false) @@ -277,19 +277,22 @@ (when ^boolean render.wasm/enabled? (mf/with-effect [] (time (when-let [canvas (mf/ref-val canvas-ref)] - (->> render.wasm/module - (p/fmap (fn [ready?] - (when ready? - (mf/set-ref-val! canvas-init true) - (render.wasm/assign-canvas canvas))))) - (fn [] - (render.wasm/clear-canvas))))) + (->> render.wasm/module + (p/fmap (fn [ready?] + (when ready? + (reset! canvas-init? true) + (render.wasm/assign-canvas canvas))))) + (fn [] + (render.wasm/clear-canvas))))) - (mf/with-effect [vbox objects-modified] - (let [sem (when (mf/ref-val canvas-init) - (render.wasm/draw-objects objects-modified zoom vbox))] - (partial render.wasm/cancel-draw sem))) - ) + (mf/with-effect [objects-modified canvas-init?] + (when @canvas-init? + (render.wasm/set-objects objects-modified) + (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)))] + (partial render.wasm/cancel-draw frame-id)))) (hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?) (hooks/setup-viewport-size vport viewport-ref) diff --git a/frontend/src/app/render_wasm.cljs b/frontend/src/app/render_wasm.cljs index 197d0d9b2..045d6973b 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -8,6 +8,7 @@ "A WASM based render API" (:require [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.config :as cf] [promesa.core :as p])) @@ -17,41 +18,31 @@ (defonce ^:dynamic internal-module #js {}) (defonce ^:dynamic internal-gpu-state #js {}) -(defn draw-objects - [objects zoom vbox] - (let [draw-rect (unchecked-get internal-module "_draw_rect") - translate (unchecked-get internal-module "_translate") - reset-canvas (unchecked-get internal-module "_reset_canvas") - scale (unchecked-get internal-module "_scale") - flush (unchecked-get internal-module "_flush") - gpu-state internal-gpu-state] +(defn set-objects [objects] + (let [shapes-buffer (unchecked-get internal-module "_shapes_buffer") + heap (unchecked-get internal-module "HEAPF32") + ;; size *in bytes* for each shapes::Shape + rect-size 16 + ;; TODO: remove the `take` once we have the dynamic data structure in Rust + supported-shapes (take 2048 (filter #(not (cfh/root? %)) (vals objects))) + mem (js/Float32Array. (.-buffer heap) (shapes-buffer) (* rect-size (count supported-shapes)))] + (run! (fn [[shape index]] + (.set mem (.-buffer shape) (* index rect-size))) + (zipmap supported-shapes (range))))) +(defn draw-objects + [zoom vbox] + (let [draw-all-shapes (unchecked-get internal-module "_draw_all_shapes")] (js/requestAnimationFrame (fn [] - (reset-canvas gpu-state) - (scale gpu-state zoom zoom) - - (let [x (dm/get-prop vbox :x) - y (dm/get-prop vbox :y)] - (translate gpu-state (- x) (- y))) - - (run! (fn [shape] - ;; (js/console.log "render-shape" (.-buffer shape)) - (let [selrect (dm/get-prop shape :selrect) - x1 (dm/get-prop selrect :x1) - y1 (dm/get-prop selrect :y1) - x2 (dm/get-prop selrect :x2) - y2 (dm/get-prop selrect :y2)] - ;; (prn (:id shape) selrect) - (draw-rect gpu-state x1 y1 x2 y2))) - (vals objects)) - - (flush gpu-state))))) + (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)))))) (defn cancel-draw - [sem] - (when (some? sem) - (js/cancelAnimationFrame sem))) + [frame-id] + (when (some? frame-id) + (js/cancelAnimationFrame frame-id))) (def ^:private canvas-options #js {:antialias true diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 3690c6c2f..408d95fe9 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,4 +1,5 @@ pub mod render; +pub mod shapes; use skia_safe as skia; @@ -33,6 +34,19 @@ pub unsafe extern "C" fn draw_rect(state: *mut State, x1: f32, y1: f32, x2: f32, render::render_rect(&mut 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"); + + reset_canvas(state); + scale(state, zoom, zoom); + translate(state, pan_x, pan_y); + + shapes::draw_all(state); + + flush(state); +} + #[no_mangle] pub unsafe extern "C" fn flush(state: *mut State) { let state = unsafe { state.as_mut() }.expect("got an invalid state pointer"); @@ -60,6 +74,12 @@ pub unsafe extern "C" fn reset_canvas(state: *mut State) { flush(state); } +#[no_mangle] +pub unsafe extern "C" fn shapes_buffer() -> *mut shapes::Shape { + let ptr = shapes::SHAPES_BUFFER.as_mut_ptr(); + return ptr; +} + fn main() { render::init_gl(); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs new file mode 100644 index 000000000..891267620 --- /dev/null +++ b/render-wasm/src/shapes.rs @@ -0,0 +1,31 @@ +use crate::render::{render_rect, State}; +use skia_safe as skia; + +#[derive(Debug, Clone, Copy)] +pub struct Selrect { + pub x1: f32, + pub y1: f32, + pub x2: f32, + pub y2: f32, +} + +pub type Shape = Selrect; // temp + +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); + } +}