🔧 Batch blur and shadow effects into single WASM call

This commit is contained in:
Elena Torro
2026-03-09 17:23:57 +01:00
parent c372b1f668
commit 0873a2fbc4
3 changed files with 396 additions and 93 deletions

View File

@@ -17,6 +17,7 @@
[app.render-wasm.helpers :as h]
[app.render-wasm.mem :as mem]
[app.render-wasm.serializers :as sr]
[app.render-wasm.serializers.color :as sr-clr]
[app.render-wasm.wasm :as wasm]))
;; Binary layout constants matching Rust implementation:
@@ -76,7 +77,7 @@
[0.0 0.0 0.0 0.0]))
(defn set-shape-base-props
"Set all base shape properties in a single WASM call.
"Set all base shape properties (and optionally children) in a single WASM call.
This replaces the following individual calls:
- use-shape
@@ -91,103 +92,206 @@
- set-shape-selrect
- set-shape-corners
- set-shape-constraints (clear + h + v)
- set-shape-children (when include-children? is true)
Returns nil."
[shape]
([shape] (set-shape-base-props shape false))
([shape include-children?]
(when wasm/context-initialized?
(let [id (dm/get-prop shape :id)
parent-id (get shape :parent-id)
shape-type (dm/get-prop shape :type)
clip-content (if (= shape-type :frame)
(not (get shape :show-content))
false)
hidden (get shape :hidden false)
flags (cond-> 0
clip-content (bit-or FLAG-CLIP-CONTENT)
hidden (bit-or FLAG-HIDDEN))
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))
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)
;; Children (when batched)
children (when include-children?
(into [] (filter uuid?) (get shape :shapes)))
child-count (if include-children? (count children) 0)
;; Total buffer: 104 base + (4 child_count + 16*N child UUIDs) when batched
total-size (if include-children?
(+ BASE-PROPS-SIZE 4 (* child-count 16))
BASE-PROPS-SIZE)
;; Allocate buffer and get DataView
offset (mem/alloc total-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)
;; Write children (offset 104+) when batched
(when include-children?
(.setUint32 dview (+ offset 104) child-count true)
(loop [i 0
cs (seq children)]
(when cs
(write-uuid-to-heap dview (+ offset 108 (* i 16)) (first cs))
(recur (inc i) (next cs)))))
(h/call wasm/internal-module "_set_shape_base_props")
nil))))
;; Binary layout for batched blur + shadows:
;;
;; Header (12 bytes):
;; | Offset | Size | Field | Type |
;; |--------|------|---------------|------------|
;; | 0 | 1 | blur_present | u8 |
;; | 1 | 1 | blur_type | u8 |
;; | 2 | 1 | blur_hidden | u8 |
;; | 3 | 1 | padding | - |
;; | 4 | 4 | blur_value | f32 LE |
;; | 8 | 4 | shadow_count | u32 LE |
;;
;; Per shadow (24 bytes each):
;; | Offset | Size | Field | Type |
;; |--------|------|----------|------------|
;; | 0 | 4 | color | u32 LE |
;; | 4 | 4 | blur | f32 LE |
;; | 8 | 4 | spread | f32 LE |
;; | 12 | 4 | offset_x | f32 LE |
;; | 16 | 4 | offset_y | f32 LE |
;; | 20 | 1 | style | u8 |
;; | 21 | 1 | hidden | u8 |
;; | 22 | 2 | padding | - |
(def ^:const EFFECTS-HEADER-SIZE 12)
(def ^:const SHADOW-ENTRY-SIZE 24)
(defn set-shape-effects
"Set blur and shadows in a single WASM call.
Replaces:
- set-shape-blur / clear-shape-blur
- clear-shape-shadows + N × add-shape-shadow
Returns nil."
[blur shadows]
(when wasm/context-initialized?
(let [id (dm/get-prop shape :id)
parent-id (get shape :parent-id)
shape-type (dm/get-prop shape :type)
(let [shadow-count (count shadows)
total-size (+ EFFECTS-HEADER-SIZE (* shadow-count SHADOW-ENTRY-SIZE))
offset (mem/alloc total-size)
heap (mem/get-heap-u8)
dview (js/DataView. (.-buffer heap))]
clip-content (if (= shape-type :frame)
(not (get shape :show-content))
false)
hidden (get shape :hidden false)
;; Write blur header
(if (some? blur)
(let [type (-> blur :type sr/translate-blur-type)
hidden (if (:hidden blur) 1 0)
value (:value blur)]
(.setUint8 dview offset 1) ;; blur_present
(.setUint8 dview (+ offset 1) type) ;; blur_type
(.setUint8 dview (+ offset 2) hidden) ;; blur_hidden
(.setFloat32 dview (+ offset 4) value true)) ;; blur_value
(do
(.setUint8 dview offset 0) ;; blur_present = 0
(.setUint8 dview (+ offset 1) 0)
(.setUint8 dview (+ offset 2) 0)
(.setFloat32 dview (+ offset 4) 0.0 true)))
flags (cond-> 0
clip-content (bit-or FLAG-CLIP-CONTENT)
hidden (bit-or FLAG-HIDDEN))
;; Write shadow count
(.setUint32 dview (+ offset 8) shadow-count true)
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))
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)
(h/call wasm/internal-module "_set_shape_base_props")
;; Write shadow entries
(loop [i 0
shadows-seq (seq shadows)]
(when shadows-seq
(let [shadow (first shadows-seq)
entry-offset (+ offset EFFECTS-HEADER-SIZE (* i SHADOW-ENTRY-SIZE))
color (get shadow :color)
rgba (sr-clr/hex->u32argb (get color :color)
(get color :opacity))]
(.setUint32 dview entry-offset rgba true)
(.setFloat32 dview (+ entry-offset 4) (get shadow :blur) true)
(.setFloat32 dview (+ entry-offset 8) (get shadow :spread) true)
(.setFloat32 dview (+ entry-offset 12) (get shadow :offset-x) true)
(.setFloat32 dview (+ entry-offset 16) (get shadow :offset-y) true)
(.setUint8 dview (+ entry-offset 20) (sr/translate-shadow-style (get shadow :style)))
(.setUint8 dview (+ entry-offset 21) (if (get shadow :hidden) 1 0))
(recur (inc i) (next shadows-seq)))))
(h/call wasm/internal-module "_set_shape_effects")
nil)))

View File

@@ -0,0 +1,198 @@
use skia_safe as skia;
use crate::mem;
use crate::shapes::{Blur, Shadow};
use crate::wasm::blurs::RawBlurType;
use crate::wasm::shadows::RawShadowStyle;
use crate::{with_current_shape_mut, STATE};
const RAW_EFFECT_HEADER_SIZE: usize = std::mem::size_of::<RawEffectHeader>();
const RAW_SHADOW_ENTRY_SIZE: usize = std::mem::size_of::<RawShadowEntry>();
/// Binary layout for the effect header (blur + shadow count).
///
/// The struct fields directly mirror the binary protocol — the layout
/// documentation lives in the struct definition itself via `#[repr(C)]`.
#[repr(C)]
#[repr(align(4))]
#[derive(Debug, Clone, Copy)]
pub struct RawEffectHeader {
blur_present: u8,
blur_type: u8,
blur_hidden: u8,
padding: u8,
blur_value: f32,
shadow_count: u32,
}
impl RawEffectHeader {
fn has_blur(&self) -> bool {
self.blur_present != 0
}
fn is_blur_hidden(&self) -> bool {
self.blur_hidden != 0
}
}
impl From<[u8; RAW_EFFECT_HEADER_SIZE]> for RawEffectHeader {
fn from(bytes: [u8; RAW_EFFECT_HEADER_SIZE]) -> Self {
unsafe { std::mem::transmute(bytes) }
}
}
/// Binary layout for a single shadow entry.
#[repr(C)]
#[repr(align(4))]
#[derive(Debug, Clone, Copy)]
pub struct RawShadowEntry {
color: u32,
blur: f32,
spread: f32,
offset_x: f32,
offset_y: f32,
style: u8,
hidden: u8,
padding: [u8; 2],
}
impl RawShadowEntry {
fn is_hidden(&self) -> bool {
self.hidden != 0
}
}
impl From<[u8; RAW_SHADOW_ENTRY_SIZE]> for RawShadowEntry {
fn from(bytes: [u8; RAW_SHADOW_ENTRY_SIZE]) -> Self {
unsafe { std::mem::transmute(bytes) }
}
}
#[no_mangle]
pub extern "C" fn set_shape_effects() {
let bytes = mem::bytes();
if bytes.len() < RAW_EFFECT_HEADER_SIZE {
return;
}
let header_bytes: [u8; RAW_EFFECT_HEADER_SIZE] =
bytes[..RAW_EFFECT_HEADER_SIZE].try_into().unwrap();
let header = RawEffectHeader::from(header_bytes);
with_current_shape_mut!(state, |shape: &mut Shape| {
// Parse blur
if header.has_blur() {
let blur_type = RawBlurType::from(header.blur_type);
shape.set_blur(Some(Blur::new(
blur_type.into(),
header.is_blur_hidden(),
header.blur_value,
)));
} else {
shape.set_blur(None);
}
// Parse shadows
let shadow_count = header.shadow_count as usize;
shape.clear_shadows();
let shadows_data = &bytes[RAW_EFFECT_HEADER_SIZE..];
for i in 0..shadow_count {
let offset = i * RAW_SHADOW_ENTRY_SIZE;
if offset + RAW_SHADOW_ENTRY_SIZE > shadows_data.len() {
break;
}
let entry_bytes: [u8; RAW_SHADOW_ENTRY_SIZE] = shadows_data
[offset..offset + RAW_SHADOW_ENTRY_SIZE]
.try_into()
.unwrap();
let entry = RawShadowEntry::from(entry_bytes);
let shadow = Shadow::new(
skia::Color::new(entry.color),
entry.blur,
entry.spread,
(entry.offset_x, entry.offset_y),
RawShadowStyle::from(entry.style).into(),
entry.is_hidden(),
);
shape.add_shadow(shadow);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raw_effect_header_layout() {
assert_eq!(RAW_EFFECT_HEADER_SIZE, 12);
assert_eq!(std::mem::align_of::<RawEffectHeader>(), 4);
}
#[test]
fn test_raw_shadow_entry_layout() {
assert_eq!(RAW_SHADOW_ENTRY_SIZE, 24);
assert_eq!(std::mem::align_of::<RawShadowEntry>(), 4);
}
#[test]
fn test_header_field_offsets() {
assert_eq!(std::mem::offset_of!(RawEffectHeader, blur_present), 0);
assert_eq!(std::mem::offset_of!(RawEffectHeader, blur_type), 1);
assert_eq!(std::mem::offset_of!(RawEffectHeader, blur_hidden), 2);
assert_eq!(std::mem::offset_of!(RawEffectHeader, blur_value), 4);
assert_eq!(std::mem::offset_of!(RawEffectHeader, shadow_count), 8);
}
#[test]
fn test_shadow_entry_field_offsets() {
assert_eq!(std::mem::offset_of!(RawShadowEntry, color), 0);
assert_eq!(std::mem::offset_of!(RawShadowEntry, blur), 4);
assert_eq!(std::mem::offset_of!(RawShadowEntry, spread), 8);
assert_eq!(std::mem::offset_of!(RawShadowEntry, offset_x), 12);
assert_eq!(std::mem::offset_of!(RawShadowEntry, offset_y), 16);
assert_eq!(std::mem::offset_of!(RawShadowEntry, style), 20);
assert_eq!(std::mem::offset_of!(RawShadowEntry, hidden), 21);
}
#[test]
fn test_header_deserialization() {
let mut bytes = [0u8; RAW_EFFECT_HEADER_SIZE];
bytes[0] = 1; // blur_present
bytes[1] = 1; // blur_type = LayerBlur
bytes[2] = 0; // blur_hidden = false
bytes[4..8].copy_from_slice(&5.0_f32.to_le_bytes()); // blur_value
bytes[8..12].copy_from_slice(&3_u32.to_le_bytes()); // shadow_count
let header = RawEffectHeader::from(bytes);
assert!(header.has_blur());
assert!(!header.is_blur_hidden());
assert_eq!(header.blur_type, 1);
assert_eq!(header.blur_value, 5.0);
assert_eq!(header.shadow_count, 3);
}
#[test]
fn test_shadow_entry_deserialization() {
let mut bytes = [0u8; RAW_SHADOW_ENTRY_SIZE];
bytes[0..4].copy_from_slice(&0xFF0000FF_u32.to_le_bytes()); // color
bytes[4..8].copy_from_slice(&4.0_f32.to_le_bytes()); // blur
bytes[8..12].copy_from_slice(&2.0_f32.to_le_bytes()); // spread
bytes[12..16].copy_from_slice(&10.0_f32.to_le_bytes()); // offset_x
bytes[16..20].copy_from_slice(&20.0_f32.to_le_bytes()); // offset_y
bytes[20] = 0; // style = DropShadow
bytes[21] = 1; // hidden = true
let entry = RawShadowEntry::from(bytes);
assert_eq!(entry.color, 0xFF0000FF);
assert_eq!(entry.blur, 4.0);
assert_eq!(entry.spread, 2.0);
assert_eq!(entry.offset_x, 10.0);
assert_eq!(entry.offset_y, 20.0);
assert_eq!(entry.style, 0);
assert!(entry.is_hidden());
}
}

View File

@@ -1,4 +1,5 @@
mod base_props;
mod effect_props;
use macros::ToJs;