Skip to main content

cranpose_ui_graphics/
render_effect.rs

1//! Render effects that can be applied to graphics layers.
2//!
3//! Matches the Jetpack Compose `RenderEffect` API with extensions for custom
4//! WGSL shaders (`RuntimeShader`).
5
6use std::sync::{Arc, Mutex, OnceLock, Weak};
7
8use crate::{LayerShape, Rect};
9
10const RUNTIME_SHADER_INLINE_UNIFORMS: usize = 16;
11
12/// Edge treatment for blur effects at the boundary of the blurred region.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
14pub enum TileMode {
15    /// Clamp to the edge pixel color.
16    #[default]
17    Clamp,
18    /// Repeat the gradient/effect from start to end.
19    Repeated,
20    /// Mirror the gradient/effect every other repetition.
21    Mirror,
22    /// Treat pixels outside the boundary as transparent.
23    Decal,
24}
25
26/// Controls blur behavior outside source bounds.
27///
28/// This mirrors Compose's `BlurredEdgeTreatment`:
29/// - bounded treatment (`shape != None`) clips blur output and uses `TileMode::Clamp`
30/// - unbounded treatment (`shape == None`) does not clip and uses `TileMode::Decal`
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct BlurredEdgeTreatment {
33    shape: Option<LayerShape>,
34}
35
36impl BlurredEdgeTreatment {
37    /// Bounded treatment that clips to a rectangle.
38    pub const RECTANGLE: Self = Self {
39        shape: Some(LayerShape::Rectangle),
40    };
41
42    /// Unbounded treatment that does not clip blurred output.
43    pub const UNBOUNDED: Self = Self { shape: None };
44
45    /// Bounded treatment with a specific clip shape.
46    pub const fn with_shape(shape: LayerShape) -> Self {
47        Self { shape: Some(shape) }
48    }
49
50    pub fn shape(self) -> Option<LayerShape> {
51        self.shape
52    }
53
54    pub fn clip(self) -> bool {
55        self.shape.is_some()
56    }
57
58    pub fn tile_mode(self) -> TileMode {
59        if self.clip() {
60            TileMode::Clamp
61        } else {
62            TileMode::Decal
63        }
64    }
65}
66
67impl Default for BlurredEdgeTreatment {
68    fn default() -> Self {
69        Self::RECTANGLE
70    }
71}
72
73/// The vertex stage and bindings every runtime shader starts from: a
74/// fullscreen triangle whose `uv` spans the input, the input texture and
75/// sampler at group 0, and the 64 uniform vectors at group 1. A shader
76/// source is this prelude followed by an `effect_fs` fragment stage.
77pub const RUNTIME_SHADER_PRELUDE_WGSL: &str = concat!(
78    include_str!("../shaders/fullscreen_quad_vs.wgsl"),
79    include_str!("../shaders/runtime_shader_bindings.wgsl"),
80);
81
82/// A custom WGSL shader effect, analogous to Android's `RuntimeShader`.
83///
84/// The shader source must be a complete WGSL module that declares:
85/// ```wgsl
86/// @group(0) @binding(0) var input_texture: texture_2d<f32>;
87/// @group(0) @binding(1) var input_sampler: sampler;
88/// @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
89/// ```
90///
91/// Float uniforms are packed linearly into the `u` array. Access them in WGSL
92/// as `u[index / 4][index % 4]` for individual floats, or `u[index / 4].xy`
93/// for vec2, etc. User uniforms may use indices `0..224`; slots `224..256`
94/// are reserved for renderer metadata:
95///
96/// | slots     | content                                                     |
97/// |-----------|-------------------------------------------------------------|
98/// | 224..236  | substrate regions `(x, y, w, h)` in input texels, the third at 224, the second at 228, the first at 232; zero = none |
99/// | 236..240  | source region `(x, y, w, h)` in input texels; zero = whole  |
100/// | 240..244  | composite mask rect `(x, y, w, h)` in region pixels; zero = none |
101/// | 244..248  | composite mask corner radii (top-left, top-right, bottom-left, bottom-right) |
102/// | 248..252  | effect rect `(x, y, w, h)` in region pixels                 |
103/// | 252..254  | logical size the input represents; zero = its texel size   |
104/// | 254       | composite alpha                                             |
105///
106/// A shader that reads the source region, mask and alpha slots declares it
107/// with [`set_batched_source`](Self::set_batched_source); one that reads a
108/// low-frequency copy of its source declares each with
109/// [`set_substrates`](Self::set_substrates) and samples it through its
110/// substrate region, held to that region's texel centers, so one tap
111/// stands for a neighbourhood the shader would otherwise walk tap by tap.
112/// The renderer then
113/// packs its input edge to edge beside other effects' inputs in one texture
114/// and draws it straight into the final pass with its clip applied. Such a
115/// shader holds every sample coordinate to its region's texel centers: the
116/// texels beside the region belong to other effects, or to no one. Every
117/// other shader is given the whole texture as its input and `uv` spans it.
118///
119/// RuntimeShader pipelines operate on premultiplied-alpha textures. Custom
120/// shaders should preserve premultiplied output semantics.
121#[derive(Clone, Debug)]
122pub struct RuntimeShader {
123    source: Arc<str>,
124    source_hash: u64,
125    uniforms: RuntimeShaderUniforms,
126    overrides: Vec<(&'static str, f64)>,
127    input_padding: f32,
128    output_padding: f32,
129    batched_source: bool,
130    substrates: Vec<SubstrateSpec>,
131    draw_split: Option<&'static str>,
132    domains: Option<Box<ShaderDomains>>,
133}
134
135#[derive(Clone, Copy, Debug, Default, PartialEq)]
136struct ShaderDomains {
137    output_support: Option<Rect>,
138    sample_domain: Option<Rect>,
139}
140
141fn finite_rect(rect: Option<Rect>) -> Option<Rect> {
142    rect.filter(|rect| {
143        rect.x.is_finite()
144            && rect.y.is_finite()
145            && rect.width.is_finite()
146            && rect.height.is_finite()
147    })
148}
149
150/// The most substrates one shader may declare.
151pub const MAX_SUBSTRATES: usize = 3;
152
153/// A low-frequency copy of a shader's source the renderer packs beside it
154/// and hands the shader through a reserved substrate region slot.
155#[derive(Clone, Copy, Debug, PartialEq)]
156pub enum SubstrateSpec {
157    /// The source averaged in blocks of `block` x `block` texels, one
158    /// substrate texel per block.
159    Average { block: u32 },
160    /// The source blurred by a Gaussian of `radius_px` device pixels, kept
161    /// at the blur's scratch resolution.
162    Blur { radius_px: f32 },
163}
164
165impl SubstrateSpec {
166    fn hash_bits<H: std::hash::Hasher>(&self, state: &mut H) {
167        use std::hash::Hash;
168        match self {
169            Self::Average { block } => {
170                0u8.hash(state);
171                block.hash(state);
172            }
173            Self::Blur { radius_px } => {
174                1u8.hash(state);
175                radius_px.to_bits().hash(state);
176            }
177        }
178    }
179}
180
181#[derive(Clone, Debug, PartialEq)]
182struct RuntimeShaderUniforms {
183    len: usize,
184    inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
185    heap: Option<Vec<f32>>,
186}
187
188impl RuntimeShaderUniforms {
189    fn new() -> Self {
190        Self {
191            len: 0,
192            inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
193            heap: None,
194        }
195    }
196
197    fn as_slice(&self) -> &[f32] {
198        if let Some(heap) = &self.heap {
199            heap.as_slice()
200        } else {
201            &self.inline[..self.len]
202        }
203    }
204
205    fn len(&self) -> usize {
206        self.as_slice().len()
207    }
208
209    fn ensure_len(&mut self, min_len: usize) {
210        if let Some(heap) = &mut self.heap {
211            if heap.len() < min_len {
212                heap.resize(min_len, 0.0);
213            }
214            return;
215        }
216
217        if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
218            self.len = self.len.max(min_len);
219            return;
220        }
221
222        let mut heap = Vec::with_capacity(min_len);
223        heap.extend_from_slice(&self.inline[..self.len]);
224        heap.resize(min_len, 0.0);
225        self.heap = Some(heap);
226    }
227
228    fn set(&mut self, index: usize, value: f32) {
229        if let Some(heap) = &mut self.heap {
230            heap[index] = value;
231        } else {
232            self.inline[index] = value;
233        }
234    }
235
236    #[cfg(test)]
237    fn is_inline(&self) -> bool {
238        self.heap.is_none()
239    }
240}
241
242/// Error returned when a shader uniform write targets renderer-owned storage.
243#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
244pub enum RuntimeShaderUniformError {
245    #[error(
246        "uniform range starting at {index} with width {width} exceeds user uniform range 0..{max_user_uniforms}; slots {reserved_start}..{max_uniforms} are reserved for renderer data"
247    )]
248    OutOfUserRange {
249        index: usize,
250        width: usize,
251        max_user_uniforms: usize,
252        reserved_start: usize,
253        max_uniforms: usize,
254    },
255}
256
257impl RuntimeShader {
258    /// Total uniform storage size in floats (64 vec4s = 256 floats).
259    ///
260    /// The final slots are reserved for renderer-managed data.
261    pub const MAX_UNIFORMS: usize = 256;
262    /// First renderer-reserved uniform slot.
263    pub const RESERVED_UNIFORM_START: usize = 224;
264    /// Reserved slots of the substrate regions `(x, y, w, h)` in input
265    /// texels, in declaration order.
266    pub const SUBSTRATE_REGION_UNIFORMS: [usize; MAX_SUBSTRATES] = [232, 228, 224];
267    /// Reserved slot of the source region `(x, y, w, h)` in input texels.
268    pub const SOURCE_REGION_UNIFORM: usize = 236;
269    /// Reserved slot of the composite mask rect `(x, y, w, h)` in region pixels.
270    pub const MASK_RECT_UNIFORM: usize = 240;
271    /// Reserved slot of the composite mask corner radii.
272    pub const MASK_RADII_UNIFORM: usize = 244;
273    /// Reserved slot of the effect rect `(x, y, w, h)` in region pixels.
274    pub const EFFECT_RECT_UNIFORM: usize = 248;
275    /// Reserved slot of the logical size the input represents.
276    pub const LOGICAL_SIZE_UNIFORM: usize = 252;
277    /// Reserved slot of the composite alpha.
278    pub const ALPHA_UNIFORM: usize = 254;
279    /// Maximum user-addressable uniform count.
280    pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
281
282    /// Create a new RuntimeShader from WGSL source code.
283    #[track_caller]
284    pub fn new(wgsl_source: &str) -> Self {
285        let (source, source_hash) =
286            cached_shader_source(std::panic::Location::caller(), wgsl_source);
287        Self::with_source(source, source_hash)
288    }
289
290    /// Create a RuntimeShader from shared WGSL source code.
291    ///
292    /// This avoids repeatedly copying large shader modules for animated effects
293    /// that rebuild only their uniform payload every frame.
294    pub fn from_shared_source(source: Arc<str>) -> Self {
295        let source_hash = cached_shared_shader_source_hash(&source);
296        Self::with_source(source, source_hash)
297    }
298
299    fn with_source(source: Arc<str>, source_hash: u64) -> Self {
300        Self {
301            source,
302            source_hash,
303            uniforms: RuntimeShaderUniforms::new(),
304            overrides: Vec::new(),
305            input_padding: 0.0,
306            output_padding: 0.0,
307            batched_source: false,
308            substrates: Vec::new(),
309            draw_split: None,
310            domains: None,
311        }
312    }
313
314    /// Fixes a pipeline-overridable constant (`override NAME: T = ...;` in
315    /// the WGSL) for every pipeline compiled from this shader. The value is
316    /// converted to the constant's declared scalar type the way WebGPU does
317    /// (a `bool` is `value != 0`). Each distinct override set compiles its
318    /// own pipeline; renderers use this to fold a material's inactive
319    /// features away without changing the shader text.
320    pub fn set_override(&mut self, name: &'static str, value: f64) {
321        match self
322            .overrides
323            .binary_search_by(|(existing, _)| existing.cmp(&name))
324        {
325            Ok(index) => self.overrides[index].1 = value,
326            Err(index) => self.overrides.insert(index, (name, value)),
327        }
328    }
329
330    /// Removes a pipeline override by name, returning whether one was present.
331    pub fn clear_override(&mut self, name: &str) -> bool {
332        let Ok(index) = self
333            .overrides
334            .binary_search_by(|(existing, _)| (*existing).cmp(name))
335        else {
336            return false;
337        };
338        self.overrides.remove(index);
339        true
340    }
341
342    /// The pipeline-overridable constants fixed by [`Self::set_override`],
343    /// ordered by name.
344    pub fn overrides(&self) -> &[(&'static str, f64)] {
345        &self.overrides
346    }
347
348    /// Hash of the fixed override set; zero when no override is fixed.
349    pub fn overrides_hash(&self) -> u64 {
350        if self.overrides.is_empty() {
351            return 0;
352        }
353        let mut bytes = Vec::new();
354        for (name, value) in &self.overrides {
355            bytes.extend_from_slice(name.as_bytes());
356            bytes.push(0);
357            bytes.extend_from_slice(&value.to_bits().to_le_bytes());
358        }
359        hash_shader_bytes(&bytes)
360    }
361
362    /// Declares how far the shader may sample outside its effect rect, in
363    /// logical pixels. Backdrop rendering uses this to capture enough input
364    /// around refractive and displacement shaders.
365    pub fn set_input_padding(&mut self, padding: f32) {
366        self.input_padding = if padding.is_finite() {
367            padding.max(0.0)
368        } else {
369            0.0
370        };
371    }
372
373    /// Returns the declared input padding in logical pixels.
374    pub fn input_padding(&self) -> f32 {
375        self.input_padding
376    }
377
378    /// Declares how far the shader WRITES outside its effect rect, in logical
379    /// pixels. Backdrop compositing widens its scissor by this amount so
380    /// SDF-driven coverage (rim glow, wobble, glued neighbor shapes) can
381    /// extend past the node bounds instead of being clipped to them.
382    pub fn set_output_padding(&mut self, padding: f32) {
383        self.output_padding = if padding.is_finite() {
384            padding.max(0.0)
385        } else {
386            0.0
387        };
388    }
389
390    /// Returns the declared output padding in logical pixels.
391    pub fn output_padding(&self) -> f32 {
392        self.output_padding
393    }
394
395    /// Declares the rect outside which the shader writes nothing: every
396    /// pixel its coverage can make nonzero at its current uniforms, the
397    /// output padding's reach included, in logical pixels with the origin
398    /// at the effect rect's top-left. A renderer composites only the part
399    /// of the effect rect inside it; the capture it reads stays whole, so a
400    /// node that carries headroom around a smaller material pays the
401    /// composite for the material alone. It says nothing about sampling:
402    /// see [`Self::set_sample_domain`]. `None`, the default, means the
403    /// whole effect rect and its output padding. A rect with a non-finite
404    /// side clears the declaration.
405    pub fn set_output_support(&mut self, support: Option<Rect>) {
406        self.set_domains(ShaderDomains {
407            output_support: finite_rect(support),
408            sample_domain: self.sample_domain(),
409        });
410    }
411
412    /// The declared output support, when the shader gave one.
413    pub fn output_support(&self) -> Option<Rect> {
414        self.domains
415            .as_ref()
416            .and_then(|domains| domains.output_support)
417    }
418
419    fn set_domains(&mut self, domains: ShaderDomains) {
420        self.domains = (domains != ShaderDomains::default()).then(|| Box::new(domains));
421    }
422
423    /// Declares the rect outside which the shader never samples its input,
424    /// in logical pixels with the origin at the effect rect's top-left. A
425    /// renderer may leave the input outside it unresolved: a blur feeding
426    /// this shader need only write the domain. The default, `None`, is the
427    /// whole effect rect and its input padding, which the input padding
428    /// contract already promises; an output support says nothing about
429    /// sampling, so a shader that shades a small region but reads a far
430    /// one keeps the default. A rect with a non-finite side clears it.
431    pub fn set_sample_domain(&mut self, domain: Option<Rect>) {
432        self.set_domains(ShaderDomains {
433            output_support: self.output_support(),
434            sample_domain: finite_rect(domain),
435        });
436    }
437
438    /// The declared sample domain, when the shader gave one.
439    pub fn sample_domain(&self) -> Option<Rect> {
440        self.domains
441            .as_ref()
442            .and_then(|domains| domains.sample_domain)
443    }
444
445    /// Set a single float uniform at the given index.
446    ///
447    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float`]
448    /// when the caller needs to handle invalid uniform writes explicitly.
449    pub fn set_float(&mut self, index: usize, value: f32) {
450        let _ = self.try_set_float(index, value);
451    }
452
453    /// Set a single float uniform at the given index.
454    pub fn try_set_float(
455        &mut self,
456        index: usize,
457        value: f32,
458    ) -> Result<(), RuntimeShaderUniformError> {
459        self.try_ensure_capacity(index, 1)?;
460        self.uniforms.set(index, value);
461        Ok(())
462    }
463
464    /// Set a vec2 uniform at the given index (consumes indices `[index, index+1]`).
465    ///
466    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float2`]
467    /// when the caller needs to handle invalid uniform writes explicitly.
468    pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
469        let _ = self.try_set_float2(index, x, y);
470    }
471
472    /// Set a vec2 uniform at the given index (consumes indices `[index, index+1]`).
473    pub fn try_set_float2(
474        &mut self,
475        index: usize,
476        x: f32,
477        y: f32,
478    ) -> Result<(), RuntimeShaderUniformError> {
479        self.try_ensure_capacity(index, 2)?;
480        self.uniforms.set(index, x);
481        self.uniforms.set(index + 1, y);
482        Ok(())
483    }
484
485    /// Set a vec4 uniform at the given index (consumes indices `[index..index+4]`).
486    ///
487    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float4`]
488    /// when the caller needs to handle invalid uniform writes explicitly.
489    pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
490        let _ = self.try_set_float4(index, x, y, z, w);
491    }
492
493    /// Set a vec4 uniform at the given index (consumes indices `[index..index+4]`).
494    pub fn try_set_float4(
495        &mut self,
496        index: usize,
497        x: f32,
498        y: f32,
499        z: f32,
500        w: f32,
501    ) -> Result<(), RuntimeShaderUniformError> {
502        self.try_ensure_capacity(index, 4)?;
503        self.uniforms.set(index, x);
504        self.uniforms.set(index + 1, y);
505        self.uniforms.set(index + 2, z);
506        self.uniforms.set(index + 3, w);
507        Ok(())
508    }
509
510    /// Declares that the shader reads the reserved source region, mask and
511    /// alpha slots and samples only within its region's texel centers, so the
512    /// renderer may hand it an input region packed edge to edge beside others
513    /// and draw it straight into the final pass with its clip applied.
514    pub fn set_batched_source(&mut self, batched: bool) {
515        self.batched_source = batched;
516    }
517
518    /// Whether the shader reads the reserved source region, mask and alpha
519    /// slots.
520    pub fn batched_source(&self) -> bool {
521        self.batched_source
522    }
523
524    /// Declares the low-frequency copies of its source the shader reads
525    /// through the reserved substrate region slots, in slot order. Only a
526    /// batched shader packed with its stage is handed them; a shader
527    /// without finds the slots zero and samples the source itself.
528    ///
529    /// # Panics
530    ///
531    /// When more than [`MAX_SUBSTRATES`] are declared.
532    pub fn set_substrates(&mut self, substrates: Vec<SubstrateSpec>) {
533        assert!(
534            substrates.len() <= MAX_SUBSTRATES,
535            "a runtime shader declares at most {MAX_SUBSTRATES} substrates"
536        );
537        self.substrates = substrates;
538    }
539
540    /// The substrates the shader declared, in slot order.
541    pub fn substrates(&self) -> &[SubstrateSpec] {
542        &self.substrates
543    }
544
545    /// Hashes the declared substrates and the draw split into `state`.
546    pub fn hash_substrates<H: std::hash::Hasher>(&self, state: &mut H) {
547        use std::hash::Hash;
548        self.substrates.len().hash(state);
549        for substrate in &self.substrates {
550            substrate.hash_bits(state);
551        }
552        self.draw_split.hash(state);
553    }
554
555    /// Declares an `override NAME: i32` the renderer sets to 1 and 2 to draw
556    /// the shader twice in the final pass, once for its interior and once
557    /// for its rim, each pipeline compiled without the other's work and
558    /// discarding the other's fragments before its fetches. Nothing else
559    /// about the draw changes: the two draws partition the pixels the one
560    /// draw shaded and land on the same bits.
561    pub fn set_draw_split(&mut self, override_name: Option<&'static str>) {
562        self.draw_split = override_name;
563    }
564
565    /// The override selecting the interior or the rim draw, when declared.
566    pub fn draw_split(&self) -> Option<&'static str> {
567        self.draw_split
568    }
569
570    /// Get the WGSL source code.
571    pub fn source(&self) -> &str {
572        &self.source
573    }
574
575    /// Get the uniform data as a float slice (for uploading to GPU).
576    pub fn uniforms(&self) -> &[f32] {
577        self.uniforms.as_slice()
578    }
579
580    /// Get the uniform data padded to full 256-float array (for GPU uniform buffer).
581    pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
582        let mut padded = [0.0f32; Self::MAX_UNIFORMS];
583        let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
584        padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
585        padded
586    }
587
588    /// Compute a hash of the shader source for pipeline caching.
589    pub fn source_hash(&self) -> u64 {
590        self.source_hash
591    }
592
593    fn try_ensure_capacity(
594        &mut self,
595        index: usize,
596        width: usize,
597    ) -> Result<(), RuntimeShaderUniformError> {
598        let min_len = index
599            .checked_add(width)
600            .ok_or_else(|| Self::uniform_range_error(index, width))?;
601        if min_len > Self::MAX_USER_UNIFORMS {
602            return Err(Self::uniform_range_error(index, width));
603        }
604        self.uniforms.ensure_len(min_len);
605        Ok(())
606    }
607
608    fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
609        RuntimeShaderUniformError::OutOfUserRange {
610            index,
611            width,
612            max_user_uniforms: Self::MAX_USER_UNIFORMS,
613            reserved_start: Self::RESERVED_UNIFORM_START,
614            max_uniforms: Self::MAX_UNIFORMS,
615        }
616    }
617}
618
619impl PartialEq for RuntimeShader {
620    fn eq(&self, other: &Self) -> bool {
621        self.source_hash == other.source_hash
622            && (Arc::ptr_eq(&self.source, &other.source)
623                || self.source.as_ref() == other.source.as_ref())
624            && self.uniforms == other.uniforms
625            && self.overrides.len() == other.overrides.len()
626            && self
627                .overrides
628                .iter()
629                .zip(&other.overrides)
630                .all(|(a, b)| a.0 == b.0 && a.1.to_bits() == b.1.to_bits())
631            && self.input_padding.to_bits() == other.input_padding.to_bits()
632            && self.output_padding.to_bits() == other.output_padding.to_bits()
633            && self.batched_source == other.batched_source
634            && self.substrates == other.substrates
635            && self.draw_split == other.draw_split
636            && self.domains == other.domains
637    }
638}
639
640fn hash_shader_source(source: &str) -> u64 {
641    hash_shader_bytes(source.as_bytes())
642}
643
644fn hash_shader_bytes(bytes: &[u8]) -> u64 {
645    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
646    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
647
648    bytes.iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
649        (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
650    })
651}
652
653#[derive(Clone, Copy, Debug, PartialEq, Eq)]
654struct ShaderSourceCallsite {
655    file: &'static str,
656    line: u32,
657    column: u32,
658}
659
660struct CachedShaderSource {
661    callsite: ShaderSourceCallsite,
662    source_hash: u64,
663    source: Arc<str>,
664}
665
666struct CachedSharedShaderSourceHash {
667    byte_ptr: usize,
668    len: usize,
669    source_hash: u64,
670    source: Weak<str>,
671}
672
673fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
674    static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
675    let byte_ptr = source.as_ptr() as usize;
676    let len = source.len();
677    let mut cache = CACHE
678        .get_or_init(|| Mutex::new(Vec::new()))
679        .lock()
680        .unwrap_or_else(|poisoned| poisoned.into_inner());
681
682    cache.retain(|entry| entry.source.strong_count() > 0);
683    if let Some(entry) = cache.iter().find(|entry| {
684        entry.byte_ptr == byte_ptr
685            && entry.len == len
686            && entry
687                .source
688                .upgrade()
689                .is_some_and(|cached| Arc::ptr_eq(&cached, source))
690    }) {
691        return entry.source_hash;
692    }
693
694    let source_hash = hash_shader_source(source);
695    cache.push(CachedSharedShaderSourceHash {
696        byte_ptr,
697        len,
698        source_hash,
699        source: Arc::downgrade(source),
700    });
701    source_hash
702}
703
704fn cached_shader_source(
705    location: &'static std::panic::Location<'static>,
706    source: &str,
707) -> (Arc<str>, u64) {
708    static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
709    let callsite = ShaderSourceCallsite {
710        file: location.file(),
711        line: location.line(),
712        column: location.column(),
713    };
714    let mut cache = CACHE
715        .get_or_init(|| Mutex::new(Vec::new()))
716        .lock()
717        .unwrap_or_else(|poisoned| poisoned.into_inner());
718
719    if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
720        if entry.source.as_ref() == source {
721            return (entry.source.clone(), entry.source_hash);
722        }
723        let source_hash = hash_shader_source(source);
724        entry.source_hash = source_hash;
725        entry.source = Arc::<str>::from(source);
726        return (entry.source.clone(), entry.source_hash);
727    }
728
729    let source_hash = hash_shader_source(source);
730    let shared = Arc::<str>::from(source);
731    cache.push(CachedShaderSource {
732        callsite,
733        source_hash,
734        source: shared.clone(),
735    });
736    (shared, source_hash)
737}
738
739/// A render effect applied to a graphics layer's rendered content.
740///
741/// Matches Jetpack Compose's `RenderEffect` sealed class hierarchy,
742/// extended with `Shader` for custom WGSL effects.
743#[derive(Clone, Debug, PartialEq)]
744pub enum RenderEffect {
745    /// Gaussian blur applied to the layer's rendered content.
746    Blur {
747        radius_x: f32,
748        radius_y: f32,
749        edge_treatment: TileMode,
750    },
751    /// Offset the rendered content by a fixed amount.
752    Offset { offset_x: f32, offset_y: f32 },
753    /// Apply a custom WGSL shader effect.
754    Shader { shader: RuntimeShader },
755    /// Chain two effects: apply `first`, then apply `second` to the result.
756    Chain {
757        first: Box<RenderEffect>,
758        second: Box<RenderEffect>,
759    },
760}
761
762impl RenderEffect {
763    /// Create a blur effect with equal radius in both directions.
764    pub fn blur(radius: f32) -> Self {
765        Self::blur_with_edge_treatment(radius, TileMode::default())
766    }
767
768    /// Create a blur effect with equal radius in both directions and explicit
769    /// edge treatment semantics.
770    pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
771        Self::Blur {
772            radius_x: radius,
773            radius_y: radius,
774            edge_treatment,
775        }
776    }
777
778    /// Create a blur effect with separate horizontal and vertical radii.
779    pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
780        Self::Blur {
781            radius_x,
782            radius_y,
783            edge_treatment,
784        }
785    }
786
787    /// Create an offset effect.
788    pub fn offset(offset_x: f32, offset_y: f32) -> Self {
789        Self::Offset { offset_x, offset_y }
790    }
791
792    /// Create a custom shader effect from a RuntimeShader.
793    pub fn runtime_shader(shader: RuntimeShader) -> Self {
794        Self::Shader { shader }
795    }
796
797    /// Chain this effect with another: `self` is applied first, then `other`.
798    pub fn then(self, other: RenderEffect) -> Self {
799        Self::Chain {
800            first: Box::new(self),
801            second: Box::new(other),
802        }
803    }
804
805    /// Returns `true` if this effect or any chained sub-effect is a
806    /// `RuntimeShader`. Animated shaders produce different output every frame,
807    /// so layer surface caching is counterproductive for them.
808    pub fn contains_runtime_shader(&self) -> bool {
809        match self {
810            RenderEffect::Shader { .. } => true,
811            RenderEffect::Chain { first, second } => {
812                first.contains_runtime_shader() || second.contains_runtime_shader()
813            }
814            _ => false,
815        }
816    }
817
818    /// Maximum logical-pixel input padding required by this effect.
819    pub fn input_padding(&self) -> f32 {
820        match self {
821            RenderEffect::Blur {
822                radius_x, radius_y, ..
823            } => radius_x.abs().max(radius_y.abs()),
824            RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
825            RenderEffect::Shader { shader } => shader.input_padding(),
826            RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
827        }
828    }
829
830    /// Maximum logical-pixel distance this effect WRITES outside its rect.
831    /// Only runtime shaders may declare one (SDF coverage past node bounds);
832    /// blur/offset stay confined to their tight rect.
833    pub fn output_padding(&self) -> f32 {
834        match self {
835            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
836            RenderEffect::Shader { shader } => shader.output_padding(),
837            RenderEffect::Chain { first, second } => {
838                first.output_padding() + second.output_padding()
839            }
840        }
841    }
842
843    /// The rect outside which this effect writes nothing, in its logical
844    /// space with the origin at its rect's top-left, when the stage that
845    /// produces its output declared one; blur and offset write their whole
846    /// rect and declare none.
847    pub fn output_support(&self) -> Option<Rect> {
848        match self {
849            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
850            RenderEffect::Shader { shader } => shader.output_support(),
851            RenderEffect::Chain { second, .. } => second.output_support(),
852        }
853    }
854
855    /// The rect outside which the stage that produces this effect's output
856    /// never samples what it is given, when it declared one; the whole
857    /// input otherwise. A blur samples everything it writes and more.
858    pub fn sample_domain(&self) -> Option<Rect> {
859        match self {
860            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
861            RenderEffect::Shader { shader } => shader.sample_domain(),
862            RenderEffect::Chain { second, .. } => second.sample_domain(),
863        }
864    }
865}
866
867#[cfg(test)]
868mod tests {
869    #[test]
870    fn overrides_stay_sorted_and_replace_by_name() {
871        let mut shader = super::RuntimeShader::new("// overrides");
872        shader.set_override("ZETA", 1.0);
873        shader.set_override("ALPHA", 0.0);
874        shader.set_override("ZETA", 2.0);
875        assert_eq!(shader.overrides(), &[("ALPHA", 0.0), ("ZETA", 2.0)]);
876    }
877
878    #[test]
879    fn clear_override_removes_present_name_and_preserves_remaining_set() {
880        let mut shader = super::RuntimeShader::new("");
881        shader.set_override("ZETA", 1.0);
882        shader.set_override("ALPHA", 2.0);
883        let mut expected = super::RuntimeShader::new("");
884        expected.set_override("ALPHA", 2.0);
885        assert!(shader.clear_override("ZETA"));
886        assert!(!shader.clear_override("MISSING"));
887        assert_eq!(shader.overrides(), &[("ALPHA", 2.0)]);
888        assert_eq!(shader.overrides_hash(), expected.overrides_hash());
889        assert!(shader.clear_override("ALPHA"));
890        assert!(shader.overrides().is_empty());
891        assert_eq!(shader.overrides_hash(), 0);
892    }
893
894    #[test]
895    fn overrides_distinguish_otherwise_equal_shaders() {
896        let plain = super::RuntimeShader::new("// overrides-eq");
897        let mut raised = plain.clone();
898        raised.set_override("FLAG", 1.0);
899        assert_eq!(plain.overrides_hash(), 0);
900        assert_ne!(plain.overrides_hash(), raised.overrides_hash());
901        assert_ne!(plain, raised);
902        let mut lowered = raised.clone();
903        lowered.set_override("FLAG", 0.0);
904        assert_ne!(raised.overrides_hash(), lowered.overrides_hash());
905        assert_ne!(raised, lowered);
906    }
907
908    use super::*;
909    use crate::RoundedCornerShape;
910
911    #[test]
912    fn runtime_shader_set_uniforms() {
913        let mut shader = RuntimeShader::new("// test");
914        shader.set_float(0, 1.0);
915        shader.set_float2(2, 3.0, 4.0);
916        shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
917
918        assert_eq!(shader.uniforms()[0], 1.0);
919        assert_eq!(shader.uniforms()[1], 0.0);
920        assert_eq!(shader.uniforms()[2], 3.0);
921        assert_eq!(shader.uniforms()[3], 4.0);
922        assert_eq!(shader.uniforms()[4], 5.0);
923        assert_eq!(shader.uniforms()[5], 6.0);
924        assert_eq!(shader.uniforms()[6], 7.0);
925        assert_eq!(shader.uniforms()[7], 8.0);
926    }
927
928    #[test]
929    fn runtime_shader_padded() {
930        let mut shader = RuntimeShader::new("// test");
931        shader.set_float(0, 42.0);
932        let padded = shader.uniforms_padded();
933        assert_eq!(padded[0], 42.0);
934        assert_eq!(padded[1], 0.0);
935        assert_eq!(padded[255], 0.0);
936    }
937
938    #[test]
939    fn blur_and_offset_declare_input_padding() {
940        assert_eq!(
941            RenderEffect::blur_xy(6.0, 12.0, TileMode::Clamp).input_padding(),
942            12.0
943        );
944        assert_eq!(RenderEffect::offset(-8.0, 3.0).input_padding(), 8.0);
945    }
946
947    #[test]
948    fn chained_effect_padding_accumulates_sampling_ranges() {
949        let mut shader = RuntimeShader::new("// test");
950        shader.set_input_padding(9.0);
951        let effect = RenderEffect::blur_xy(4.0, 6.0, TileMode::Clamp)
952            .then(RenderEffect::runtime_shader(shader))
953            .then(RenderEffect::offset(2.0, -5.0));
954
955        assert_eq!(effect.input_padding(), 20.0);
956    }
957
958    #[test]
959    fn runtime_shader_keeps_common_uniform_payload_inline() {
960        let mut shader = RuntimeShader::new("// test");
961        shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
962        shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
963        shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
964        shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
965
966        assert!(shader.uniforms.is_inline());
967        assert_eq!(shader.uniforms().len(), 16);
968
969        shader.set_float(16, 17.0);
970        assert!(!shader.uniforms.is_inline());
971        assert_eq!(shader.uniforms()[16], 17.0);
972    }
973
974    #[test]
975    fn runtime_shader_try_set_reports_reserved_uniform_slots() {
976        let mut shader = RuntimeShader::new("// test");
977
978        let err = shader
979            .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
980            .unwrap_err();
981        assert_eq!(
982            err,
983            RuntimeShaderUniformError::OutOfUserRange {
984                index: RuntimeShader::RESERVED_UNIFORM_START,
985                width: 1,
986                max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
987                reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
988                max_uniforms: RuntimeShader::MAX_UNIFORMS,
989            }
990        );
991        assert!(shader.uniforms().is_empty());
992
993        let err = shader
994            .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
995            .unwrap_err();
996        assert_eq!(
997            err,
998            RuntimeShaderUniformError::OutOfUserRange {
999                index: RuntimeShader::MAX_USER_UNIFORMS - 3,
1000                width: 4,
1001                max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
1002                reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
1003                max_uniforms: RuntimeShader::MAX_UNIFORMS,
1004            }
1005        );
1006    }
1007
1008    #[test]
1009    fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
1010        let mut shader = RuntimeShader::new("// test");
1011        shader.set_float(0, 7.0);
1012
1013        shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
1014        shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
1015
1016        assert_eq!(shader.uniforms(), &[7.0]);
1017    }
1018
1019    #[test]
1020    fn render_effect_chaining() {
1021        let blur = RenderEffect::blur(10.0);
1022        let offset = RenderEffect::offset(5.0, 5.0);
1023        let chained = blur.then(offset);
1024        match chained {
1025            RenderEffect::Chain { first, second } => {
1026                assert!(matches!(*first, RenderEffect::Blur { .. }));
1027                assert!(matches!(*second, RenderEffect::Offset { .. }));
1028            }
1029            _ => panic!("expected Chain"),
1030        }
1031    }
1032
1033    #[test]
1034    fn blur_convenience() {
1035        let effect = RenderEffect::blur(15.0);
1036        match effect {
1037            RenderEffect::Blur {
1038                radius_x,
1039                radius_y,
1040                edge_treatment,
1041            } => {
1042                assert_eq!(radius_x, 15.0);
1043                assert_eq!(radius_y, 15.0);
1044                assert_eq!(edge_treatment, TileMode::Clamp);
1045            }
1046            _ => panic!("expected Blur"),
1047        }
1048    }
1049
1050    #[test]
1051    fn blur_with_edge_treatment_uses_explicit_mode() {
1052        let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
1053        match effect {
1054            RenderEffect::Blur {
1055                radius_x,
1056                radius_y,
1057                edge_treatment,
1058            } => {
1059                assert_eq!(radius_x, 6.0);
1060                assert_eq!(radius_y, 6.0);
1061                assert_eq!(edge_treatment, TileMode::Decal);
1062            }
1063            _ => panic!("expected Blur"),
1064        }
1065    }
1066
1067    #[test]
1068    fn source_hash_consistent() {
1069        let s1 = RuntimeShader::new("fn main() {}");
1070        let s2 = RuntimeShader::new("fn main() {}");
1071        assert_eq!(s1.source_hash(), s2.source_hash());
1072    }
1073
1074    #[test]
1075    fn runtime_shader_from_shared_source_reuses_shared_source() {
1076        let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
1077        let s1 = RuntimeShader::from_shared_source(source.clone());
1078        let s2 = RuntimeShader::from_shared_source(source);
1079
1080        assert!(Arc::ptr_eq(&s1.source, &s2.source));
1081        assert_eq!(s1.source_hash(), s2.source_hash());
1082    }
1083
1084    fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
1085        RuntimeShader::new(source)
1086    }
1087
1088    fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
1089        RuntimeShader::new(source)
1090    }
1091
1092    #[test]
1093    fn runtime_shader_new_reuses_same_callsite_source() {
1094        let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
1095        let s1 = runtime_shader_from_reuse_callsite(source);
1096        let s2 = runtime_shader_from_reuse_callsite(source);
1097
1098        assert!(Arc::ptr_eq(&s1.source, &s2.source));
1099        assert_eq!(s1.source_hash(), s2.source_hash());
1100    }
1101
1102    #[test]
1103    fn runtime_shader_new_replaces_changed_callsite_source() {
1104        let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
1105        let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
1106
1107        assert!(!Arc::ptr_eq(&s1.source, &s2.source));
1108        assert_ne!(s1.source_hash(), s2.source_hash());
1109        assert_eq!(s2.source(), "fn b() {}");
1110    }
1111
1112    #[test]
1113    fn runtime_shader_source_storage_has_no_process_global_interner() {
1114        let source = include_str!("render_effect.rs");
1115        let blocked_static = ["static ", "INTERNER"].concat();
1116        let blocked_type = ["ShaderSource", "Interner"].concat();
1117
1118        assert!(
1119            !source.contains(&blocked_static) && !source.contains(&blocked_type),
1120            "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
1121        );
1122    }
1123
1124    #[test]
1125    fn blur_xy_preserves_tile_mode() {
1126        let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
1127        match effect {
1128            RenderEffect::Blur {
1129                radius_x,
1130                radius_y,
1131                edge_treatment,
1132            } => {
1133                assert_eq!(radius_x, 3.0);
1134                assert_eq!(radius_y, 7.0);
1135                assert_eq!(edge_treatment, TileMode::Clamp);
1136            }
1137            _ => panic!("expected Blur"),
1138        }
1139    }
1140
1141    #[test]
1142    fn offset_constructor_sets_components() {
1143        let effect = RenderEffect::offset(11.0, -5.0);
1144        match effect {
1145            RenderEffect::Offset { offset_x, offset_y } => {
1146                assert_eq!(offset_x, 11.0);
1147                assert_eq!(offset_y, -5.0);
1148            }
1149            _ => panic!("expected Offset"),
1150        }
1151    }
1152
1153    #[test]
1154    fn runtime_shader_equality_is_source_value_based() {
1155        let mut s1 = RuntimeShader::new("fn main() {}");
1156        let mut s2 = RuntimeShader::new("fn main() {}");
1157        s1.set_float(0, 1.0);
1158        s2.set_float(0, 1.0);
1159        assert_eq!(s1, s2);
1160    }
1161
1162    #[test]
1163    fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
1164        let treatment = BlurredEdgeTreatment::default();
1165        assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
1166        assert!(treatment.clip());
1167        assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1168    }
1169
1170    #[test]
1171    fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
1172        let treatment = BlurredEdgeTreatment::UNBOUNDED;
1173        assert_eq!(treatment.shape(), None);
1174        assert!(!treatment.clip());
1175        assert_eq!(treatment.tile_mode(), TileMode::Decal);
1176    }
1177
1178    #[test]
1179    fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
1180        let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
1181        let treatment = BlurredEdgeTreatment::with_shape(rounded);
1182        assert_eq!(treatment.shape(), Some(rounded));
1183        assert!(treatment.clip());
1184        assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1185    }
1186
1187    #[test]
1188    fn an_effect_chains_output_support_is_the_support_of_the_stage_that_writes_its_output() {
1189        let mut shader = RuntimeShader::new("fn glass_fs() {}");
1190        assert_eq!(shader.output_support(), None);
1191        let support = Rect {
1192            x: 4.0,
1193            y: 6.0,
1194            width: 30.0,
1195            height: 12.0,
1196        };
1197        shader.set_output_support(Some(support));
1198        assert_eq!(shader.output_support(), Some(support));
1199        let effect = RenderEffect::blur(3.0).then(RenderEffect::Shader {
1200            shader: shader.clone(),
1201        });
1202        assert_eq!(effect.output_support(), Some(support));
1203        let effect = RenderEffect::Shader {
1204            shader: shader.clone(),
1205        }
1206        .then(RenderEffect::blur(3.0));
1207        assert_eq!(effect.output_support(), None);
1208        assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1209    }
1210
1211    #[test]
1212    fn a_sample_domain_is_the_writers_and_a_blur_declares_none() {
1213        let mut shader = RuntimeShader::new("fn glass_fs() {}");
1214        let domain = Rect {
1215            x: -2.0,
1216            y: -2.0,
1217            width: 20.0,
1218            height: 12.0,
1219        };
1220        let plain = shader.clone();
1221        shader.set_sample_domain(Some(domain));
1222        assert_ne!(shader, plain);
1223        assert_eq!(shader.sample_domain(), Some(domain));
1224        let effect = RenderEffect::blur(3.0).then(RenderEffect::Shader {
1225            shader: shader.clone(),
1226        });
1227        assert_eq!(effect.sample_domain(), Some(domain));
1228        assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1229        assert_eq!(RenderEffect::blur(3.0).sample_domain(), None);
1230        shader.set_sample_domain(Some(Rect {
1231            x: f32::INFINITY,
1232            ..domain
1233        }));
1234        assert_eq!(shader.sample_domain(), None);
1235    }
1236
1237    #[test]
1238    fn a_non_finite_output_support_clears_the_declaration_and_a_support_tells_shaders_apart() {
1239        let mut shader = RuntimeShader::new("fn glass_fs() {}");
1240        let plain = shader.clone();
1241        shader.set_output_support(Some(Rect {
1242            x: 0.0,
1243            y: 0.0,
1244            width: 10.0,
1245            height: 10.0,
1246        }));
1247        assert_ne!(shader, plain);
1248        shader.set_output_support(Some(Rect {
1249            x: 0.0,
1250            y: 0.0,
1251            width: f32::NAN,
1252            height: 10.0,
1253        }));
1254        assert_eq!(shader.output_support(), None);
1255        assert_eq!(shader, plain);
1256    }
1257}