🔧 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.helpers :as h]
[app.render-wasm.mem :as mem] [app.render-wasm.mem :as mem]
[app.render-wasm.serializers :as sr] [app.render-wasm.serializers :as sr]
[app.render-wasm.serializers.color :as sr-clr]
[app.render-wasm.wasm :as wasm])) [app.render-wasm.wasm :as wasm]))
;; Binary layout constants matching Rust implementation: ;; Binary layout constants matching Rust implementation:
@ -76,7 +77,7 @@
[0.0 0.0 0.0 0.0])) [0.0 0.0 0.0 0.0]))
(defn set-shape-base-props (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: This replaces the following individual calls:
- use-shape - use-shape
@ -91,9 +92,11 @@
- set-shape-selrect - set-shape-selrect
- set-shape-corners - set-shape-corners
- set-shape-constraints (clear + h + v) - set-shape-constraints (clear + h + v)
- set-shape-children (when include-children? is true)
Returns nil." Returns nil."
[shape] ([shape] (set-shape-base-props shape false))
([shape include-children?]
(when wasm/context-initialized? (when wasm/context-initialized?
(let [id (dm/get-prop shape :id) (let [id (dm/get-prop shape :id)
parent-id (get shape :parent-id) parent-id (get shape :parent-id)
@ -134,8 +137,18 @@
r3 (d/nilv (get shape :r3) 0.0) r3 (d/nilv (get shape :r3) 0.0)
r4 (d/nilv (get shape :r4) 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 ;; Allocate buffer and get DataView
offset (mem/alloc BASE-PROPS-SIZE) offset (mem/alloc total-size)
heap (mem/get-heap-u8) heap (mem/get-heap-u8)
dview (js/DataView. (.-buffer heap))] dview (js/DataView. (.-buffer heap))]
@ -188,6 +201,97 @@
(.setFloat32 dview (+ offset 96) r3 true) (.setFloat32 dview (+ offset 96) r3 true)
(.setFloat32 dview (+ offset 100) r4 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") (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 [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))]
;; 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)))
;; Write shadow count
(.setUint32 dview (+ offset 8) shadow-count true)
;; 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))) 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 base_props;
mod effect_props;
use macros::ToJs; use macros::ToJs;