From 83387701a03e2ec46b3a417ae4e700cf25f92c25 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 21 Jan 2026 14:45:13 +0100 Subject: [PATCH] :wrench: Add batched shape base properties serialization for improved WASM performance --- frontend/src/app/render_wasm/api.cljs | 33 +-- frontend/src/app/render_wasm/api/shapes.cljs | 199 ++++++++++++++++++ render-wasm/src/wasm/layouts.rs | 2 +- render-wasm/src/wasm/shapes/base_props.rs | 180 ++++++++++++++++ .../src/wasm/{shapes.rs => shapes/mod.rs} | 2 + 5 files changed, 389 insertions(+), 27 deletions(-) create mode 100644 frontend/src/app/render_wasm/api/shapes.cljs create mode 100644 render-wasm/src/wasm/shapes/base_props.rs rename render-wasm/src/wasm/{shapes.rs => shapes/mod.rs} (98%) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 24e054cfc0..7e33fdc1ca 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -28,6 +28,7 @@ [app.main.ui.shapes.text] [app.main.worker :as mw] [app.render-wasm.api.fonts :as f] + [app.render-wasm.api.shapes :as shapes] [app.render-wasm.api.texts :as t] [app.render-wasm.api.webgl :as webgl] [app.render-wasm.deserializers :as dr] @@ -895,24 +896,12 @@ id (dm/get-prop shape :id) type (dm/get-prop shape :type) - parent-id (get shape :parent-id) masked (get shape :masked-group) - selrect (get shape :selrect) - constraint-h (get shape :constraints-h) - constraint-v (get shape :constraints-v) - clip-content (if (= type :frame) - (not (get shape :show-content)) - false) - rotation (get shape :rotation) - transform (get shape :transform) fills (get shape :fills) strokes (if (= type :group) [] (get shape :strokes)) children (get shape :shapes) - blend-mode (get shape :blend-mode) - opacity (get shape :opacity) - hidden (get shape :hidden) content (let [content (get shape :content)] (if (= type :text) (ensure-text-content content) @@ -921,22 +910,15 @@ grow-type (get shape :grow-type) blur (get shape :blur) svg-attrs (get shape :svg-attrs) - shadows (get shape :shadow) - corners (map #(get shape %) [:r1 :r2 :r3 :r4])] + shadows (get shape :shadow)] - (use-shape id) - (set-parent-id parent-id) - (set-shape-type type) - (set-shape-clip-content clip-content) - (set-shape-constraints constraint-h constraint-v) + ;; Batched call: sets id, parent_id, type, clip_content, hidden, blend_mode, + ;; constraints, opacity, rotation, transform, selrect, and corners in one WASM call. + ;; This replaces 12+ individual WASM calls with a single batched operation. + (shapes/set-shape-base-props shape) - (set-shape-rotation rotation) - (set-shape-transform transform) - (set-shape-blend-mode blend-mode) - (set-shape-opacity opacity) - (set-shape-hidden hidden) + ;; Remaining properties that need separate calls (variable-length or conditional) (set-shape-children children) - (set-shape-corners corners) (set-shape-blur blur) (when (= type :group) (set-masked (boolean masked))) @@ -956,7 +938,6 @@ (set-shape-layout shape) (set-layout-data shape) - (set-shape-selrect selrect) (let [pending_thumbnails (into [] (concat (set-shape-text-content id content) diff --git a/frontend/src/app/render_wasm/api/shapes.cljs b/frontend/src/app/render_wasm/api/shapes.cljs new file mode 100644 index 0000000000..3ab680dac4 --- /dev/null +++ b/frontend/src/app/render_wasm/api/shapes.cljs @@ -0,0 +1,199 @@ +;; 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.render-wasm.api.shapes + "Batched shape property serialization for improved WASM performance. + + This module provides a single WASM call to set all base shape properties, + replacing multiple individual calls (use_shape, set_parent, set_shape_type, + etc.) with one batched operation." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.uuid :as uuid] + [app.render-wasm.helpers :as h] + [app.render-wasm.mem :as mem] + [app.render-wasm.serializers :as sr] + [app.render-wasm.wasm :as wasm])) + +;; Binary layout constants matching Rust implementation: +;; +;; | Offset | Size | Field | Type | +;; |--------|------|--------------|-----------------------------------| +;; | 0 | 16 | id | UUID (4 × u32 LE) | +;; | 16 | 16 | parent_id | UUID (4 × u32 LE) | +;; | 32 | 1 | shape_type | u8 | +;; | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) | +;; | 34 | 1 | blend_mode | u8 | +;; | 35 | 1 | constraint_h | u8 (0xFF = None) | +;; | 36 | 1 | constraint_v | u8 (0xFF = None) | +;; | 37 | 3 | padding | - | +;; | 40 | 4 | opacity | f32 LE | +;; | 44 | 4 | rotation | f32 LE | +;; | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) | +;; | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) | +;; | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) | +;; |--------|------|--------------|-----------------------------------| +;; | Total | 104 | | | + +(def ^:const BASE-PROPS-SIZE 104) +(def ^:const FLAG-CLIP-CONTENT 0x01) +(def ^:const FLAG-HIDDEN 0x02) +(def ^:const CONSTRAINT-NONE 0xFF) + +(defn- write-uuid-to-heap + "Write a UUID to the heap at the given byte offset using DataView." + [dview offset id] + (let [buffer (uuid/get-u32 id)] + (.setUint32 dview offset (aget buffer 0) true) + (.setUint32 dview (+ offset 4) (aget buffer 1) true) + (.setUint32 dview (+ offset 8) (aget buffer 2) true) + (.setUint32 dview (+ offset 12) (aget buffer 3) true))) + +(defn- serialize-transform + "Extract transform matrix values, defaulting to identity matrix." + [transform] + (if (some? transform) + [(dm/get-prop transform :a) + (dm/get-prop transform :b) + (dm/get-prop transform :c) + (dm/get-prop transform :d) + (dm/get-prop transform :e) + (dm/get-prop transform :f)] + [1.0 0.0 0.0 1.0 0.0 0.0])) ; identity matrix + +(defn- serialize-selrect + "Extract selrect values." + [selrect] + (if (some? selrect) + [(dm/get-prop selrect :x1) + (dm/get-prop selrect :y1) + (dm/get-prop selrect :x2) + (dm/get-prop selrect :y2)] + [0.0 0.0 0.0 0.0])) + +(defn set-shape-base-props + "Set all base shape properties in a single WASM call. + + This replaces the following individual calls: + - use-shape + - set-parent-id + - set-shape-type + - set-shape-clip-content + - set-shape-rotation + - set-shape-transform + - set-shape-blend-mode + - set-shape-opacity + - set-shape-hidden + - set-shape-selrect + - set-shape-corners + - set-shape-constraints (clear + h + v) + + Returns nil." + [shape] + (when wasm/context-initialized? + (let [;; Extract all properties from shape + id (dm/get-prop shape :id) + parent-id (get shape :parent-id) + shape-type (dm/get-prop shape :type) + + ;; Boolean flags + clip-content (if (= shape-type :frame) + (not (get shape :show-content)) + false) + hidden (get shape :hidden false) + + ;; Compute flags byte + flags (cond-> 0 + clip-content (bit-or FLAG-CLIP-CONTENT) + hidden (bit-or FLAG-HIDDEN)) + + ;; Enum values + blend-mode (sr/translate-blend-mode (get shape :blend-mode)) + constraint-h (let [c (get shape :constraints-h)] + (if (some? c) + (sr/translate-constraint-h c) + CONSTRAINT-NONE)) + constraint-v (let [c (get shape :constraints-v)] + (if (some? c) + (sr/translate-constraint-v c) + CONSTRAINT-NONE)) + + ;; Float values + opacity (d/nilv (get shape :opacity) 1.0) + rotation (d/nilv (get shape :rotation) 0.0) + + ;; Transform matrix + [ta tb tc td te tf] (serialize-transform (get shape :transform)) + + ;; Selrect + selrect (get shape :selrect) + [sx1 sy1 sx2 sy2] (serialize-selrect selrect) + + ;; Corners + r1 (d/nilv (get shape :r1) 0.0) + r2 (d/nilv (get shape :r2) 0.0) + r3 (d/nilv (get shape :r3) 0.0) + r4 (d/nilv (get shape :r4) 0.0) + + ;; Allocate buffer and get DataView + offset (mem/alloc BASE-PROPS-SIZE) + heap (mem/get-heap-u8) + dview (js/DataView. (.-buffer heap))] + + ;; Write id (offset 0, 16 bytes) + (write-uuid-to-heap dview offset id) + + ;; Write parent_id (offset 16, 16 bytes) + (write-uuid-to-heap dview (+ offset 16) (d/nilv parent-id uuid/zero)) + + ;; Write shape_type (offset 32, 1 byte) + (.setUint8 dview (+ offset 32) (sr/translate-shape-type shape-type)) + + ;; Write flags (offset 33, 1 byte) + (.setUint8 dview (+ offset 33) flags) + + ;; Write blend_mode (offset 34, 1 byte) + (.setUint8 dview (+ offset 34) blend-mode) + + ;; Write constraint_h (offset 35, 1 byte) + (.setUint8 dview (+ offset 35) constraint-h) + + ;; Write constraint_v (offset 36, 1 byte) + (.setUint8 dview (+ offset 36) constraint-v) + + ;; Padding at offset 37-39 (already zero from alloc) + + ;; Write opacity (offset 40, f32) + (.setFloat32 dview (+ offset 40) opacity true) + + ;; Write rotation (offset 44, f32) + (.setFloat32 dview (+ offset 44) rotation true) + + ;; Write transform matrix (offset 48, 6 × f32) + (.setFloat32 dview (+ offset 48) ta true) + (.setFloat32 dview (+ offset 52) tb true) + (.setFloat32 dview (+ offset 56) tc true) + (.setFloat32 dview (+ offset 60) td true) + (.setFloat32 dview (+ offset 64) te true) + (.setFloat32 dview (+ offset 68) tf true) + + ;; Write selrect (offset 72, 4 × f32) + (.setFloat32 dview (+ offset 72) sx1 true) + (.setFloat32 dview (+ offset 76) sy1 true) + (.setFloat32 dview (+ offset 80) sx2 true) + (.setFloat32 dview (+ offset 84) sy2 true) + + ;; Write corners (offset 88, 4 × f32) + (.setFloat32 dview (+ offset 88) r1 true) + (.setFloat32 dview (+ offset 92) r2 true) + (.setFloat32 dview (+ offset 96) r3 true) + (.setFloat32 dview (+ offset 100) r4 true) + + ;; Call WASM function + (h/call wasm/internal-module "_set_shape_base_props") + + nil))) diff --git a/render-wasm/src/wasm/layouts.rs b/render-wasm/src/wasm/layouts.rs index 903edaa0e6..9f77a21429 100644 --- a/render-wasm/src/wasm/layouts.rs +++ b/render-wasm/src/wasm/layouts.rs @@ -3,7 +3,7 @@ use crate::{with_current_shape_mut, STATE}; use macros::ToJs; mod align; -mod constraints; +pub mod constraints; mod flex; mod grid; diff --git a/render-wasm/src/wasm/shapes/base_props.rs b/render-wasm/src/wasm/shapes/base_props.rs new file mode 100644 index 0000000000..4846d9b320 --- /dev/null +++ b/render-wasm/src/wasm/shapes/base_props.rs @@ -0,0 +1,180 @@ +use crate::mem; +use crate::shapes::{BlendMode, ConstraintH, ConstraintV}; +use crate::utils::uuid_from_u32_quartet; +use crate::uuid::Uuid; +use crate::wasm::blend::RawBlendMode; +use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV}; +use crate::{with_state_mut, STATE}; + +use super::RawShapeType; + +/// Binary layout for batched shape base properties: +/// +/// | Offset | Size | Field | Type | +/// |--------|------|--------------|-----------------------------------| +/// | 0 | 16 | id | UUID (4 × u32 LE) | +/// | 16 | 16 | parent_id | UUID (4 × u32 LE) | +/// | 32 | 1 | shape_type | u8 | +/// | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) | +/// | 34 | 1 | blend_mode | u8 | +/// | 35 | 1 | constraint_h | u8 (0xFF = None) | +/// | 36 | 1 | constraint_v | u8 (0xFF = None) | +/// | 37 | 3 | padding | - | +/// | 40 | 4 | opacity | f32 LE | +/// | 44 | 4 | rotation | f32 LE | +/// | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) | +/// | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) | +/// | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) | +/// |--------|------|--------------|-----------------------------------| +/// | Total | 104 | | | + +pub const BASE_PROPS_SIZE: usize = 104; + +const FLAG_CLIP_CONTENT: u8 = 0b0000_0001; +const FLAG_HIDDEN: u8 = 0b0000_0010; +const CONSTRAINT_NONE: u8 = 0xFF; + +/// Reads a f32 from a byte slice at the given offset (little-endian) +#[inline] +fn read_f32_le(bytes: &[u8], offset: usize) -> f32 { + f32::from_le_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]) +} + +/// Reads a u32 from a byte slice at the given offset (little-endian) +#[inline] +fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]) +} + +/// Parses UUID from bytes at given offset +#[inline] +fn read_uuid(bytes: &[u8], offset: usize) -> Uuid { + uuid_from_u32_quartet( + read_u32_le(bytes, offset), + read_u32_le(bytes, offset + 4), + read_u32_le(bytes, offset + 8), + read_u32_le(bytes, offset + 12), + ) +} + +/// Sets base shape properties from a pre-allocated buffer in a single WASM call. +/// +/// This replaces multiple individual WASM calls (use_shape, set_parent, set_shape_type, +/// set_shape_clip_content, set_shape_rotation, set_shape_transform, set_shape_blend_mode, +/// set_shape_opacity, set_shape_hidden, set_shape_selrect, set_shape_corners, +/// set_shape_constraints) with a single batched call. +#[no_mangle] +pub extern "C" fn set_shape_base_props() { + let bytes = mem::bytes(); + + if bytes.len() < BASE_PROPS_SIZE { + return; + } + + // Parse all fields from the buffer + let id = read_uuid(&bytes, 0); + let parent_id = read_uuid(&bytes, 16); + let shape_type = bytes[32]; + let flags = bytes[33]; + let blend_mode = bytes[34]; + let constraint_h = bytes[35]; + let constraint_v = bytes[36]; + // bytes[37..40] are padding + + let opacity = read_f32_le(&bytes, 40); + let rotation = read_f32_le(&bytes, 44); + + // Transform matrix (a, b, c, d, e, f) + let transform_a = read_f32_le(&bytes, 48); + let transform_b = read_f32_le(&bytes, 52); + let transform_c = read_f32_le(&bytes, 56); + let transform_d = read_f32_le(&bytes, 60); + let transform_e = read_f32_le(&bytes, 64); + let transform_f = read_f32_le(&bytes, 68); + + // Selrect (x1, y1, x2, y2) + let selrect_x1 = read_f32_le(&bytes, 72); + let selrect_y1 = read_f32_le(&bytes, 76); + let selrect_x2 = read_f32_le(&bytes, 80); + let selrect_y2 = read_f32_le(&bytes, 84); + + // Corners (r1, r2, r3, r4) + let corner_r1 = read_f32_le(&bytes, 88); + let corner_r2 = read_f32_le(&bytes, 92); + let corner_r3 = read_f32_le(&bytes, 96); + let corner_r4 = read_f32_le(&bytes, 100); + + // Decode flags + let clip_content = (flags & FLAG_CLIP_CONTENT) != 0; + let hidden = (flags & FLAG_HIDDEN) != 0; + + // Convert raw enum values + let shape_type_enum = RawShapeType::from(shape_type); + let blend_mode_enum: BlendMode = RawBlendMode::from(blend_mode).into(); + + let constraint_h_opt: Option = if constraint_h == CONSTRAINT_NONE { + None + } else { + Some(RawConstraintH::from(constraint_h).into()) + }; + + let constraint_v_opt: Option = if constraint_v == CONSTRAINT_NONE { + None + } else { + Some(RawConstraintV::from(constraint_v).into()) + }; + + with_state_mut!(state, { + // Select/create the shape + state.use_shape(id); + + // Set parent relationship + state.set_parent_for_current_shape(parent_id); + + // Mark shape as touched + state.touch_current(); + + // Apply all properties to the current shape + if let Some(shape) = state.current_shape_mut() { + // Type + shape.set_shape_type(shape_type_enum.into()); + + // Boolean flags + shape.set_clip(clip_content); + shape.set_hidden(hidden); + + // Blend mode and opacity + shape.set_blend_mode(blend_mode_enum); + shape.set_opacity(opacity); + + // Constraints + shape.set_constraint_h(constraint_h_opt); + shape.set_constraint_v(constraint_v_opt); + + // Transform + shape.set_rotation(rotation); + shape.set_transform( + transform_a, + transform_b, + transform_c, + transform_d, + transform_e, + transform_f, + ); + + // Geometry + shape.set_selrect(selrect_x1, selrect_y1, selrect_x2, selrect_y2); + shape.set_corners((corner_r1, corner_r2, corner_r3, corner_r4)); + } + }); +} diff --git a/render-wasm/src/wasm/shapes.rs b/render-wasm/src/wasm/shapes/mod.rs similarity index 98% rename from render-wasm/src/wasm/shapes.rs rename to render-wasm/src/wasm/shapes/mod.rs index c2ff3b4931..3f32e824c3 100644 --- a/render-wasm/src/wasm/shapes.rs +++ b/render-wasm/src/wasm/shapes/mod.rs @@ -1,3 +1,5 @@ +mod base_props; + use macros::ToJs; use crate::shapes::{Bool, Frame, Group, Path, Rect, SVGRaw, TextContent, Type};