mirror of
https://github.com/penpot/penpot.git
synced 2026-05-29 11:52:25 +00:00
🔧 Batch blur and shadow effects into single WASM call
This commit is contained in:
parent
c372b1f668
commit
0873a2fbc4
@ -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)))
|
||||||
|
|||||||
198
render-wasm/src/wasm/shapes/effect_props.rs
Normal file
198
render-wasm/src/wasm/shapes/effect_props.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
mod base_props;
|
mod base_props;
|
||||||
|
mod effect_props;
|
||||||
|
|
||||||
use macros::ToJs;
|
use macros::ToJs;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user