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 arrayvec::ArrayVec;
9
10use crate::{LayerShape, Rect};
11
12const RUNTIME_SHADER_INLINE_UNIFORMS: usize = 16;
13
14/// Edge treatment for blur effects at the boundary of the blurred region.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
16pub enum TileMode {
17    /// Clamp to the edge pixel color.
18    #[default]
19    Clamp,
20    /// Repeat the gradient/effect from start to end.
21    Repeated,
22    /// Mirror the gradient/effect every other repetition.
23    Mirror,
24    /// Treat pixels outside the boundary as transparent.
25    Decal,
26}
27
28/// Controls blur behavior outside source bounds.
29///
30/// This mirrors Compose's `BlurredEdgeTreatment`:
31/// - bounded treatment (`shape != None`) clips blur output and uses `TileMode::Clamp`
32/// - unbounded treatment (`shape == None`) does not clip and uses `TileMode::Decal`
33#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct BlurredEdgeTreatment {
35    shape: Option<LayerShape>,
36}
37
38impl BlurredEdgeTreatment {
39    /// Bounded treatment that clips to a rectangle.
40    pub const RECTANGLE: Self = Self {
41        shape: Some(LayerShape::Rectangle),
42    };
43
44    /// Unbounded treatment that does not clip blurred output.
45    pub const UNBOUNDED: Self = Self { shape: None };
46
47    /// Bounded treatment with a specific clip shape.
48    pub const fn with_shape(shape: LayerShape) -> Self {
49        Self { shape: Some(shape) }
50    }
51
52    pub fn shape(self) -> Option<LayerShape> {
53        self.shape
54    }
55
56    pub fn clip(self) -> bool {
57        self.shape.is_some()
58    }
59
60    pub fn tile_mode(self) -> TileMode {
61        if self.clip() {
62            TileMode::Clamp
63        } else {
64            TileMode::Decal
65        }
66    }
67}
68
69impl Default for BlurredEdgeTreatment {
70    fn default() -> Self {
71        Self::RECTANGLE
72    }
73}
74
75/// The vertex stage and bindings every runtime shader starts from: a
76/// fullscreen triangle whose `uv` spans the input, the input texture and
77/// sampler at group 0, and the 64 uniform vectors at group 1. A shader
78/// source is this prelude followed by an `effect_fs` fragment stage.
79pub const RUNTIME_SHADER_PRELUDE_WGSL: &str = concat!(
80    include_str!("../shaders/fullscreen_quad_vs.wgsl"),
81    include_str!("../shaders/runtime_shader_bindings.wgsl"),
82);
83
84/// A custom WGSL shader effect, analogous to Android's `RuntimeShader`.
85///
86/// The shader source must be a complete WGSL module that declares:
87/// ```wgsl
88/// @group(0) @binding(0) var input_texture: texture_2d<f32>;
89/// @group(0) @binding(1) var input_sampler: sampler;
90/// @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
91/// ```
92///
93/// Float uniforms are packed linearly into the `u` array. Access them in WGSL
94/// as `u[index / 4][index % 4]` for individual floats, or `u[index / 4].xy`
95/// for vec2, etc. User uniforms may use indices `0..224`; slots `224..256`
96/// are reserved for renderer metadata:
97///
98/// | slots     | content                                                     |
99/// |-----------|-------------------------------------------------------------|
100/// | 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 |
101/// | 236..240  | source region `(x, y, w, h)` in input texels; zero = whole  |
102/// | 240..244  | composite mask rect `(x, y, w, h)` in region pixels; zero = none |
103/// | 244..248  | composite mask corner radii (top-left, top-right, bottom-left, bottom-right) |
104/// | 248..252  | effect rect `(x, y, w, h)` in region pixels                 |
105/// | 252..254  | logical size the input represents; zero = its texel size   |
106/// | 254       | composite alpha                                             |
107///
108/// A shader that reads the source region, mask and alpha slots declares it
109/// with [`set_batched_source`](Self::set_batched_source); one that reads a
110/// low-frequency copy of its source declares each with
111/// [`set_substrates`](Self::set_substrates) and samples it through its
112/// substrate region, held to that region's texel centers, so one tap
113/// stands for a neighbourhood the shader would otherwise walk tap by tap.
114/// The renderer then
115/// packs its input edge to edge beside other effects' inputs in one texture
116/// and draws it straight into the final pass with its clip applied. Such a
117/// shader holds every sample coordinate to its region's texel centers: the
118/// texels beside the region belong to other effects, or to no one. Every
119/// other shader is given the whole texture as its input and `uv` spans it.
120///
121/// RuntimeShader pipelines operate on premultiplied-alpha textures. Custom
122/// shaders should preserve premultiplied output semantics.
123#[derive(Clone, Debug)]
124pub struct RuntimeShader {
125    source: Arc<str>,
126    source_hash: u64,
127    uniforms: RuntimeShaderUniforms,
128    specialization: Option<Arc<ShaderSpecialization>>,
129    input_padding: f32,
130    output_padding: f32,
131    batched_source: bool,
132    domains: Option<Box<ShaderDomains>>,
133}
134
135#[derive(Clone, Debug, Default)]
136struct ShaderSpecialization {
137    overrides: Vec<(&'static str, f64)>,
138    overrides_hash: OnceLock<u64>,
139    substrates: ArrayVec<SubstrateSpec, MAX_SUBSTRATES>,
140    draw_split: Option<&'static str>,
141}
142
143pub(crate) struct ShaderSpecializationCache<K, const N: usize> {
144    entries: ArrayVec<CachedShaderSpecialization<K>, N>,
145}
146
147struct CachedShaderSpecialization<K> {
148    source: Option<Arc<ShaderSpecialization>>,
149    key: K,
150    result: Option<Arc<ShaderSpecialization>>,
151}
152
153impl<K: PartialEq, const N: usize> ShaderSpecializationCache<K, N> {
154    pub(crate) const fn new() -> Self {
155        assert!(N > 0);
156        Self {
157            entries: ArrayVec::new_const(),
158        }
159    }
160
161    pub(crate) fn apply(
162        &mut self,
163        shader: &mut RuntimeShader,
164        key: K,
165        specialize: impl FnOnce(&mut RuntimeShader, &K),
166    ) {
167        let hit = self.entries.iter().rposition(|entry| {
168            entry.key == key
169                && match (&entry.source, &shader.specialization) {
170                    (Some(source), Some(current)) => Arc::ptr_eq(source, current),
171                    (None, None) => true,
172                    _ => false,
173                }
174        });
175        if let Some(index) = hit {
176            let entry = self.entries.remove(index);
177            shader.specialization.clone_from(&entry.result);
178            self.entries.push(entry);
179            return;
180        }
181        if shader
182            .specialization
183            .as_ref()
184            .is_some_and(|source| Arc::strong_count(source) == 1)
185        {
186            specialize(shader, &key);
187            return;
188        }
189        let source = shader.specialization.clone();
190        specialize(shader, &key);
191        if self.entries.is_full() {
192            self.entries.remove(0);
193        }
194        self.entries.push(CachedShaderSpecialization {
195            source,
196            key,
197            result: shader.specialization.clone(),
198        });
199    }
200}
201
202static DEFAULT_SHADER_SPECIALIZATION: ShaderSpecialization = ShaderSpecialization {
203    overrides: Vec::new(),
204    overrides_hash: OnceLock::new(),
205    substrates: ArrayVec::new_const(),
206    draw_split: None,
207};
208
209#[derive(Clone, Copy, Debug, Default, PartialEq)]
210struct ShaderDomains {
211    output_support: Option<Rect>,
212    sample_domain: Option<Rect>,
213}
214
215fn finite_rect(rect: Option<Rect>) -> Option<Rect> {
216    rect.filter(|rect| {
217        rect.x.is_finite()
218            && rect.y.is_finite()
219            && rect.width.is_finite()
220            && rect.height.is_finite()
221    })
222}
223
224/// The most substrates one shader may declare.
225pub const MAX_SUBSTRATES: usize = 3;
226
227/// A low-frequency copy of a shader's source the renderer packs beside it
228/// and hands the shader through a reserved substrate region slot.
229#[derive(Clone, Copy, Debug, PartialEq)]
230pub enum SubstrateSpec {
231    /// The componentwise source mean over the layer's bounds, stored in one texel.
232    /// Filter padding is excluded; bounds are clipped to the capture and rounded
233    /// outward to texels. The renderer averages rows and then columns in its
234    /// render-target format. A capture outside the layer uses its complete source.
235    Mean,
236    /// The source averaged in blocks of `block` x `block` texels, one
237    /// substrate texel per block.
238    Average { block: u32 },
239    /// The source blurred by a Gaussian of `radius_px` device pixels, kept
240    /// at the blur's scratch resolution.
241    Blur { radius_px: f32 },
242}
243
244impl SubstrateSpec {
245    fn same_bits(&self, other: &Self) -> bool {
246        match (self, other) {
247            (Self::Mean, Self::Mean) => true,
248            (Self::Average { block: a }, Self::Average { block: b }) => a == b,
249            (Self::Blur { radius_px: a }, Self::Blur { radius_px: b }) => {
250                a.to_bits() == b.to_bits()
251            }
252            _ => false,
253        }
254    }
255
256    fn hash_bits<H: std::hash::Hasher>(&self, state: &mut H) {
257        use std::hash::Hash;
258        match self {
259            Self::Mean => 2u8.hash(state),
260            Self::Average { block } => {
261                0u8.hash(state);
262                block.hash(state);
263            }
264            Self::Blur { radius_px } => {
265                1u8.hash(state);
266                radius_px.to_bits().hash(state);
267            }
268        }
269    }
270}
271
272#[derive(Clone, Debug, PartialEq)]
273struct RuntimeShaderUniforms {
274    len: usize,
275    inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
276    heap: Option<Vec<f32>>,
277}
278
279impl RuntimeShaderUniforms {
280    fn new() -> Self {
281        Self {
282            len: 0,
283            inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
284            heap: None,
285        }
286    }
287
288    fn as_slice(&self) -> &[f32] {
289        if let Some(heap) = &self.heap {
290            heap.as_slice()
291        } else {
292            &self.inline[..self.len]
293        }
294    }
295
296    fn len(&self) -> usize {
297        self.as_slice().len()
298    }
299
300    fn ensure_len(&mut self, min_len: usize) {
301        if let Some(heap) = &mut self.heap {
302            if heap.len() < min_len {
303                heap.resize(min_len, 0.0);
304            }
305            return;
306        }
307
308        if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
309            self.len = self.len.max(min_len);
310            return;
311        }
312
313        let mut heap = Vec::with_capacity(min_len);
314        heap.extend_from_slice(&self.inline[..self.len]);
315        heap.resize(min_len, 0.0);
316        self.heap = Some(heap);
317    }
318
319    fn set(&mut self, index: usize, value: f32) {
320        if let Some(heap) = &mut self.heap {
321            heap[index] = value;
322        } else {
323            self.inline[index] = value;
324        }
325    }
326
327    #[cfg(test)]
328    fn is_inline(&self) -> bool {
329        self.heap.is_none()
330    }
331}
332
333/// Error returned when a shader uniform write targets renderer-owned storage.
334#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
335pub enum RuntimeShaderUniformError {
336    #[error(
337        "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"
338    )]
339    OutOfUserRange {
340        index: usize,
341        width: usize,
342        max_user_uniforms: usize,
343        reserved_start: usize,
344        max_uniforms: usize,
345    },
346}
347
348impl RuntimeShader {
349    /// Total uniform storage size in floats (64 vec4s = 256 floats).
350    ///
351    /// The final slots are reserved for renderer-managed data.
352    pub const MAX_UNIFORMS: usize = 256;
353    /// First renderer-reserved uniform slot.
354    pub const RESERVED_UNIFORM_START: usize = 224;
355    /// Reserved slots of the substrate regions `(x, y, w, h)` in input
356    /// texels, in declaration order.
357    pub const SUBSTRATE_REGION_UNIFORMS: [usize; MAX_SUBSTRATES] = [232, 228, 224];
358    /// Reserved slot of the source region `(x, y, w, h)` in input texels.
359    pub const SOURCE_REGION_UNIFORM: usize = 236;
360    /// Reserved slot of the composite mask rect `(x, y, w, h)` in region pixels.
361    pub const MASK_RECT_UNIFORM: usize = 240;
362    /// Reserved slot of the composite mask corner radii.
363    pub const MASK_RADII_UNIFORM: usize = 244;
364    /// Reserved slot of the effect rect `(x, y, w, h)` in region pixels.
365    pub const EFFECT_RECT_UNIFORM: usize = 248;
366    /// Reserved slot of the logical size the input represents.
367    pub const LOGICAL_SIZE_UNIFORM: usize = 252;
368    /// Reserved slot of the composite alpha.
369    pub const ALPHA_UNIFORM: usize = 254;
370    /// Maximum user-addressable uniform count.
371    pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
372
373    /// Create a new RuntimeShader from WGSL source code.
374    #[track_caller]
375    pub fn new(wgsl_source: &str) -> Self {
376        let (source, source_hash) =
377            cached_shader_source(std::panic::Location::caller(), wgsl_source);
378        Self::with_source(source, source_hash)
379    }
380
381    /// Create a RuntimeShader from shared WGSL source code.
382    ///
383    /// This avoids repeatedly copying large shader modules for animated effects
384    /// that rebuild only their uniform payload every frame.
385    pub fn from_shared_source(source: Arc<str>) -> Self {
386        let source_hash = cached_shared_shader_source_hash(&source);
387        Self::with_source(source, source_hash)
388    }
389
390    fn with_source(source: Arc<str>, source_hash: u64) -> Self {
391        Self {
392            source,
393            source_hash,
394            uniforms: RuntimeShaderUniforms::new(),
395            specialization: None,
396            input_padding: 0.0,
397            output_padding: 0.0,
398            batched_source: false,
399            domains: None,
400        }
401    }
402
403    fn specialization(&self) -> &ShaderSpecialization {
404        self.specialization
405            .as_deref()
406            .unwrap_or(&DEFAULT_SHADER_SPECIALIZATION)
407    }
408
409    fn specialization_mut(&mut self) -> &mut ShaderSpecialization {
410        Arc::make_mut(self.specialization.get_or_insert_with(Arc::default))
411    }
412
413    /// Fixes a pipeline-overridable constant (`override NAME: T = ...;` in
414    /// the WGSL) for every pipeline compiled from this shader. The value is
415    /// converted to the constant's declared scalar type the way WebGPU does
416    /// (a `bool` is `value != 0`). Each distinct override set compiles its
417    /// own pipeline; renderers use this to fold a material's inactive
418    /// features away without changing the shader text.
419    pub fn set_override(&mut self, name: &'static str, value: f64) {
420        let position = self
421            .overrides()
422            .binary_search_by(|(existing, _)| existing.cmp(&name));
423        if position.is_ok_and(|index| self.overrides()[index].1.to_bits() == value.to_bits()) {
424            return;
425        }
426        let specialization = self.specialization_mut();
427        specialization.overrides_hash.take();
428        let overrides = &mut specialization.overrides;
429        match position {
430            Ok(index) => overrides[index].1 = value,
431            Err(index) => overrides.insert(index, (name, value)),
432        }
433    }
434
435    /// Removes a pipeline override by name, returning whether one was present.
436    pub fn clear_override(&mut self, name: &str) -> bool {
437        let Ok(index) = self
438            .overrides()
439            .binary_search_by(|(existing, _)| (*existing).cmp(name))
440        else {
441            return false;
442        };
443        let specialization = self.specialization_mut();
444        specialization.overrides_hash.take();
445        specialization.overrides.remove(index);
446        true
447    }
448
449    /// The pipeline-overridable constants fixed by [`Self::set_override`],
450    /// ordered by name.
451    pub fn overrides(&self) -> &[(&'static str, f64)] {
452        &self.specialization().overrides
453    }
454
455    /// Hash of the fixed override set; zero when no override is fixed.
456    pub fn overrides_hash(&self) -> u64 {
457        let specialization = self.specialization();
458        if specialization.overrides.is_empty() {
459            return 0;
460        }
461        *specialization.overrides_hash.get_or_init(|| {
462            #[cfg(test)]
463            OVERRIDE_HASH_COMPUTATIONS.with(|count| count.set(count.get() + 1));
464            hash_shader_bytes(specialization.overrides.iter().flat_map(|(name, value)| {
465                name.bytes().chain([0]).chain(value.to_bits().to_le_bytes())
466            }))
467        })
468    }
469
470    /// Declares how far the shader may sample outside its effect rect, in
471    /// logical pixels. Backdrop rendering uses this to capture enough input
472    /// around refractive and displacement shaders.
473    pub fn set_input_padding(&mut self, padding: f32) {
474        self.input_padding = if padding.is_finite() {
475            padding.max(0.0)
476        } else {
477            0.0
478        };
479    }
480
481    /// Returns the declared input padding in logical pixels.
482    pub fn input_padding(&self) -> f32 {
483        self.input_padding
484    }
485
486    /// Declares how far the shader WRITES outside its effect rect, in logical
487    /// pixels. Backdrop compositing widens its scissor by this amount so
488    /// SDF-driven coverage (rim glow, wobble, glued neighbor shapes) can
489    /// extend past the node bounds instead of being clipped to them.
490    pub fn set_output_padding(&mut self, padding: f32) {
491        self.output_padding = if padding.is_finite() {
492            padding.max(0.0)
493        } else {
494            0.0
495        };
496    }
497
498    /// Returns the declared output padding in logical pixels.
499    pub fn output_padding(&self) -> f32 {
500        self.output_padding
501    }
502
503    /// Declares the rect outside which the shader writes nothing: every
504    /// pixel its coverage can make nonzero at its current uniforms, the
505    /// output padding's reach included, in logical pixels with the origin
506    /// at the effect rect's top-left. A renderer composites only the part
507    /// of the effect rect inside it; the capture it reads stays whole, so a
508    /// node that carries headroom around a smaller material pays the
509    /// composite for the material alone. It says nothing about sampling:
510    /// see [`Self::set_sample_domain`]. `None`, the default, means the
511    /// whole effect rect and its output padding. A rect with a non-finite
512    /// side clears the declaration.
513    pub fn set_output_support(&mut self, support: Option<Rect>) {
514        self.set_domains(ShaderDomains {
515            output_support: finite_rect(support),
516            sample_domain: self.sample_domain(),
517        });
518    }
519
520    /// The declared output support, when the shader gave one.
521    pub fn output_support(&self) -> Option<Rect> {
522        self.domains
523            .as_ref()
524            .and_then(|domains| domains.output_support)
525    }
526
527    fn set_domains(&mut self, domains: ShaderDomains) {
528        self.domains = (domains != ShaderDomains::default()).then(|| Box::new(domains));
529    }
530
531    /// Declares the rect outside which the shader never samples its input,
532    /// in logical pixels with the origin at the effect rect's top-left. A
533    /// renderer may leave the input outside it unresolved: a blur feeding
534    /// this shader need only write the domain. The default, `None`, is the
535    /// whole effect rect and its input padding, which the input padding
536    /// contract already promises; an output support says nothing about
537    /// sampling, so a shader that shades a small region but reads a far
538    /// one keeps the default. A rect with a non-finite side clears it.
539    pub fn set_sample_domain(&mut self, domain: Option<Rect>) {
540        self.set_domains(ShaderDomains {
541            output_support: self.output_support(),
542            sample_domain: finite_rect(domain),
543        });
544    }
545
546    /// The declared sample domain, when the shader gave one.
547    pub fn sample_domain(&self) -> Option<Rect> {
548        self.domains
549            .as_ref()
550            .and_then(|domains| domains.sample_domain)
551    }
552
553    /// Set a single float uniform at the given index.
554    ///
555    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float`]
556    /// when the caller needs to handle invalid uniform writes explicitly.
557    pub fn set_float(&mut self, index: usize, value: f32) {
558        let _ = self.try_set_float(index, value);
559    }
560
561    /// Set a single float uniform at the given index.
562    pub fn try_set_float(
563        &mut self,
564        index: usize,
565        value: f32,
566    ) -> Result<(), RuntimeShaderUniformError> {
567        self.try_ensure_capacity(index, 1)?;
568        self.uniforms.set(index, value);
569        Ok(())
570    }
571
572    /// Set a vec2 uniform at the given index (consumes indices `[index, index+1]`).
573    ///
574    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float2`]
575    /// when the caller needs to handle invalid uniform writes explicitly.
576    pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
577        let _ = self.try_set_float2(index, x, y);
578    }
579
580    /// Set a vec2 uniform at the given index (consumes indices `[index, index+1]`).
581    pub fn try_set_float2(
582        &mut self,
583        index: usize,
584        x: f32,
585        y: f32,
586    ) -> Result<(), RuntimeShaderUniformError> {
587        self.try_ensure_capacity(index, 2)?;
588        self.uniforms.set(index, x);
589        self.uniforms.set(index + 1, y);
590        Ok(())
591    }
592
593    /// Set a vec4 uniform at the given index (consumes indices `[index..index+4]`).
594    ///
595    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float4`]
596    /// when the caller needs to handle invalid uniform writes explicitly.
597    pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
598        let _ = self.try_set_float4(index, x, y, z, w);
599    }
600
601    /// Set a vec4 uniform at the given index (consumes indices `[index..index+4]`).
602    pub fn try_set_float4(
603        &mut self,
604        index: usize,
605        x: f32,
606        y: f32,
607        z: f32,
608        w: f32,
609    ) -> Result<(), RuntimeShaderUniformError> {
610        self.try_ensure_capacity(index, 4)?;
611        self.uniforms.set(index, x);
612        self.uniforms.set(index + 1, y);
613        self.uniforms.set(index + 2, z);
614        self.uniforms.set(index + 3, w);
615        Ok(())
616    }
617
618    /// Declares that the shader reads the reserved source region, mask and
619    /// alpha slots and samples only within its region's texel centers, so the
620    /// renderer may hand it an input region packed edge to edge beside others
621    /// and draw it straight into the final pass with its clip applied.
622    pub fn set_batched_source(&mut self, batched: bool) {
623        self.batched_source = batched;
624    }
625
626    /// Whether the shader reads the reserved source region, mask and alpha
627    /// slots.
628    pub fn batched_source(&self) -> bool {
629        self.batched_source
630    }
631
632    /// Declares the low-frequency copies of its source the shader reads
633    /// through the reserved substrate region slots, in slot order. Only a
634    /// batched shader packed with its stage is handed them; a shader
635    /// without finds the slots zero and samples the source itself.
636    ///
637    /// # Panics
638    ///
639    /// When more than [`MAX_SUBSTRATES`] are declared.
640    pub fn set_substrates(&mut self, substrates: &[SubstrateSpec]) {
641        assert!(
642            substrates.len() <= MAX_SUBSTRATES,
643            "a runtime shader declares at most {MAX_SUBSTRATES} substrates"
644        );
645        if self.substrates().len() == substrates.len()
646            && self
647                .substrates()
648                .iter()
649                .zip(substrates)
650                .all(|(existing, incoming)| existing.same_bits(incoming))
651        {
652            return;
653        }
654        self.specialization_mut().substrates = substrates.iter().copied().collect();
655    }
656
657    /// The substrates the shader declared, in slot order.
658    pub fn substrates(&self) -> &[SubstrateSpec] {
659        &self.specialization().substrates
660    }
661
662    /// Hashes the declared substrates and the draw split into `state`.
663    pub fn hash_substrates<H: std::hash::Hasher>(&self, state: &mut H) {
664        use std::hash::Hash;
665        self.substrates().len().hash(state);
666        for substrate in self.substrates() {
667            substrate.hash_bits(state);
668        }
669        self.draw_split().hash(state);
670    }
671
672    /// Declares an `override NAME: i32` the renderer sets to 1 and 2 to draw
673    /// the shader twice in the final pass, once for its interior and once
674    /// for its rim, each pipeline compiled without the other's work and
675    /// discarding the other's fragments before its fetches. Nothing else
676    /// about the draw changes: the two draws partition the pixels the one
677    /// draw shaded and land on the same bits.
678    pub fn set_draw_split(&mut self, override_name: Option<&'static str>) {
679        if self.draw_split() == override_name {
680            return;
681        }
682        self.specialization_mut().draw_split = override_name;
683    }
684
685    /// The override selecting the interior or the rim draw, when declared.
686    pub fn draw_split(&self) -> Option<&'static str> {
687        self.specialization().draw_split
688    }
689
690    /// Get the WGSL source code.
691    pub fn source(&self) -> &str {
692        &self.source
693    }
694
695    /// Get the uniform data as a float slice (for uploading to GPU).
696    pub fn uniforms(&self) -> &[f32] {
697        self.uniforms.as_slice()
698    }
699
700    /// Get the uniform data padded to full 256-float array (for GPU uniform buffer).
701    pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
702        let mut padded = [0.0f32; Self::MAX_UNIFORMS];
703        let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
704        padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
705        padded
706    }
707
708    /// Compute a hash of the shader source for pipeline caching.
709    pub fn source_hash(&self) -> u64 {
710        self.source_hash
711    }
712
713    fn try_ensure_capacity(
714        &mut self,
715        index: usize,
716        width: usize,
717    ) -> Result<(), RuntimeShaderUniformError> {
718        let min_len = index
719            .checked_add(width)
720            .ok_or_else(|| Self::uniform_range_error(index, width))?;
721        if min_len > Self::MAX_USER_UNIFORMS {
722            return Err(Self::uniform_range_error(index, width));
723        }
724        self.uniforms.ensure_len(min_len);
725        Ok(())
726    }
727
728    fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
729        RuntimeShaderUniformError::OutOfUserRange {
730            index,
731            width,
732            max_user_uniforms: Self::MAX_USER_UNIFORMS,
733            reserved_start: Self::RESERVED_UNIFORM_START,
734            max_uniforms: Self::MAX_UNIFORMS,
735        }
736    }
737}
738
739#[cfg(test)]
740thread_local! {
741    static OVERRIDE_HASH_COMPUTATIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
742}
743
744impl PartialEq for RuntimeShader {
745    fn eq(&self, other: &Self) -> bool {
746        self.source_hash == other.source_hash
747            && (Arc::ptr_eq(&self.source, &other.source)
748                || self.source.as_ref() == other.source.as_ref())
749            && self.uniforms == other.uniforms
750            && self.overrides().len() == other.overrides().len()
751            && self
752                .overrides()
753                .iter()
754                .zip(other.overrides())
755                .all(|(a, b)| a.0 == b.0 && a.1.to_bits() == b.1.to_bits())
756            && self.input_padding.to_bits() == other.input_padding.to_bits()
757            && self.output_padding.to_bits() == other.output_padding.to_bits()
758            && self.batched_source == other.batched_source
759            && self.substrates() == other.substrates()
760            && self.draw_split() == other.draw_split()
761            && self.domains == other.domains
762    }
763}
764
765fn hash_shader_source(source: &str) -> u64 {
766    hash_shader_bytes(source.bytes())
767}
768
769fn hash_shader_bytes(bytes: impl IntoIterator<Item = u8>) -> u64 {
770    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
771    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
772
773    bytes.into_iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
774        (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME)
775    })
776}
777
778#[derive(Clone, Copy, Debug, PartialEq, Eq)]
779struct ShaderSourceCallsite {
780    file: &'static str,
781    line: u32,
782    column: u32,
783}
784
785struct CachedShaderSource {
786    callsite: ShaderSourceCallsite,
787    source_hash: u64,
788    source: Arc<str>,
789}
790
791struct CachedSharedShaderSourceHash {
792    byte_ptr: usize,
793    len: usize,
794    source_hash: u64,
795    source: Weak<str>,
796}
797
798fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
799    static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
800    let byte_ptr = source.as_ptr() as usize;
801    let len = source.len();
802    let mut cache = CACHE
803        .get_or_init(|| Mutex::new(Vec::new()))
804        .lock()
805        .unwrap_or_else(|poisoned| poisoned.into_inner());
806
807    cache.retain(|entry| entry.source.strong_count() > 0);
808    if let Some(entry) = cache.iter().find(|entry| {
809        entry.byte_ptr == byte_ptr
810            && entry.len == len
811            && entry
812                .source
813                .upgrade()
814                .is_some_and(|cached| Arc::ptr_eq(&cached, source))
815    }) {
816        return entry.source_hash;
817    }
818
819    let source_hash = hash_shader_source(source);
820    cache.push(CachedSharedShaderSourceHash {
821        byte_ptr,
822        len,
823        source_hash,
824        source: Arc::downgrade(source),
825    });
826    source_hash
827}
828
829fn cached_shader_source(
830    location: &'static std::panic::Location<'static>,
831    source: &str,
832) -> (Arc<str>, u64) {
833    static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
834    let callsite = ShaderSourceCallsite {
835        file: location.file(),
836        line: location.line(),
837        column: location.column(),
838    };
839    let mut cache = CACHE
840        .get_or_init(|| Mutex::new(Vec::new()))
841        .lock()
842        .unwrap_or_else(|poisoned| poisoned.into_inner());
843
844    if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
845        if entry.source.as_ref() == source {
846            return (entry.source.clone(), entry.source_hash);
847        }
848        let source_hash = hash_shader_source(source);
849        entry.source_hash = source_hash;
850        entry.source = Arc::<str>::from(source);
851        return (entry.source.clone(), entry.source_hash);
852    }
853
854    let source_hash = hash_shader_source(source);
855    let shared = Arc::<str>::from(source);
856    cache.push(CachedShaderSource {
857        callsite,
858        source_hash,
859        source: shared.clone(),
860    });
861    (shared, source_hash)
862}
863
864/// A render effect applied to a graphics layer's rendered content.
865///
866/// Matches Jetpack Compose's `RenderEffect` sealed class hierarchy,
867/// extended with `Shader` for custom WGSL effects.
868#[derive(Clone, Debug, PartialEq)]
869pub enum RenderEffect {
870    /// Gaussian blur applied to the layer's rendered content.
871    Blur {
872        radius_x: f32,
873        radius_y: f32,
874        edge_treatment: TileMode,
875    },
876    /// Offset the rendered content by a fixed amount.
877    Offset { offset_x: f32, offset_y: f32 },
878    /// Apply a custom WGSL shader effect.
879    Shader {
880        /// Shared shader configuration; use [`Arc::make_mut`] to edit a cloned effect independently.
881        shader: Arc<RuntimeShader>,
882    },
883    /// Chain two effects: apply `first`, then apply `second` to the result.
884    ///
885    /// Child effects are shared; use [`Arc::make_mut`] to edit a cloned chain independently.
886    Chain {
887        first: Arc<RenderEffect>,
888        second: Arc<RenderEffect>,
889    },
890}
891
892impl RenderEffect {
893    /// Create a blur effect with equal radius in both directions.
894    pub fn blur(radius: f32) -> Self {
895        Self::blur_with_edge_treatment(radius, TileMode::default())
896    }
897
898    /// Create a blur effect with equal radius in both directions and explicit
899    /// edge treatment semantics.
900    pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
901        Self::Blur {
902            radius_x: radius,
903            radius_y: radius,
904            edge_treatment,
905        }
906    }
907
908    /// Create a blur effect with separate horizontal and vertical radii.
909    pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
910        Self::Blur {
911            radius_x,
912            radius_y,
913            edge_treatment,
914        }
915    }
916
917    /// Create an offset effect.
918    pub fn offset(offset_x: f32, offset_y: f32) -> Self {
919        Self::Offset { offset_x, offset_y }
920    }
921
922    /// Create a custom shader effect from a RuntimeShader.
923    pub fn runtime_shader(shader: RuntimeShader) -> Self {
924        Self::Shader {
925            shader: Arc::new(shader),
926        }
927    }
928
929    /// Chain this effect with another: `self` is applied first, then `other`.
930    pub fn then(self, other: RenderEffect) -> Self {
931        Self::Chain {
932            first: Arc::new(self),
933            second: Arc::new(other),
934        }
935    }
936
937    /// Returns `true` if this effect or any chained sub-effect is a
938    /// `RuntimeShader`. Animated shaders produce different output every frame,
939    /// so layer surface caching is counterproductive for them.
940    pub fn contains_runtime_shader(&self) -> bool {
941        match self {
942            RenderEffect::Shader { .. } => true,
943            RenderEffect::Chain { first, second } => {
944                first.contains_runtime_shader() || second.contains_runtime_shader()
945            }
946            _ => false,
947        }
948    }
949
950    /// Maximum logical-pixel input padding required by this effect.
951    pub fn input_padding(&self) -> f32 {
952        match self {
953            RenderEffect::Blur {
954                radius_x, radius_y, ..
955            } => radius_x.abs().max(radius_y.abs()),
956            RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
957            RenderEffect::Shader { shader } => shader.input_padding(),
958            RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
959        }
960    }
961
962    /// Maximum logical-pixel distance this effect WRITES outside its rect.
963    /// Only runtime shaders may declare one (SDF coverage past node bounds);
964    /// blur/offset stay confined to their tight rect.
965    pub fn output_padding(&self) -> f32 {
966        match self {
967            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
968            RenderEffect::Shader { shader } => shader.output_padding(),
969            RenderEffect::Chain { first, second } => {
970                first.output_padding() + second.output_padding()
971            }
972        }
973    }
974
975    /// The rect outside which this effect writes nothing, in its logical
976    /// space with the origin at its rect's top-left, when the stage that
977    /// produces its output declared one; blur and offset write their whole
978    /// rect and declare none.
979    pub fn output_support(&self) -> Option<Rect> {
980        match self {
981            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
982            RenderEffect::Shader { shader } => shader.output_support(),
983            RenderEffect::Chain { second, .. } => second.output_support(),
984        }
985    }
986
987    /// The rect outside which the stage that produces this effect's output
988    /// never samples what it is given, when it declared one; the whole
989    /// input otherwise. A blur samples everything it writes and more.
990    pub fn sample_domain(&self) -> Option<Rect> {
991        match self {
992            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
993            RenderEffect::Shader { shader } => shader.sample_domain(),
994            RenderEffect::Chain { second, .. } => second.sample_domain(),
995        }
996    }
997}
998
999#[cfg(test)]
1000mod tests {
1001    #[test]
1002    fn overrides_stay_sorted_and_replace_by_name() {
1003        let mut shader = super::RuntimeShader::new("// overrides");
1004        shader.set_override("ZETA", 1.0);
1005        shader.set_override("ALPHA", 0.0);
1006        shader.set_override("ZETA", 2.0);
1007        assert_eq!(shader.overrides(), &[("ALPHA", 0.0), ("ZETA", 2.0)]);
1008    }
1009
1010    #[test]
1011    fn clear_override_removes_present_name_and_preserves_remaining_set() {
1012        let mut shader = super::RuntimeShader::new("");
1013        shader.set_override("ZETA", 1.0);
1014        shader.set_override("ALPHA", 2.0);
1015        let mut expected = super::RuntimeShader::new("");
1016        expected.set_override("ALPHA", 2.0);
1017        assert!(shader.clear_override("ZETA"));
1018        assert!(!shader.clear_override("MISSING"));
1019        assert_eq!(shader.overrides(), &[("ALPHA", 2.0)]);
1020        assert_eq!(shader.overrides_hash(), expected.overrides_hash());
1021        assert!(shader.clear_override("ALPHA"));
1022        assert!(shader.overrides().is_empty());
1023        assert_eq!(shader.overrides_hash(), 0);
1024    }
1025
1026    #[test]
1027    fn overrides_distinguish_otherwise_equal_shaders() {
1028        let plain = super::RuntimeShader::new("// overrides-eq");
1029        let mut raised = plain.clone();
1030        raised.set_override("FLAG", 1.0);
1031        assert_eq!(plain.overrides_hash(), 0);
1032        assert_ne!(plain.overrides_hash(), raised.overrides_hash());
1033        assert_ne!(plain, raised);
1034        let mut lowered = raised.clone();
1035        lowered.set_override("FLAG", 0.0);
1036        assert_ne!(raised.overrides_hash(), lowered.overrides_hash());
1037        assert_ne!(raised, lowered);
1038    }
1039
1040    #[test]
1041    fn unchanged_override_lookups_share_one_hash_computation() {
1042        let mut shader = RuntimeShader::new("");
1043        shader.set_override("FLAG", 1.0);
1044        shader.set_override("SCALE", 0.5);
1045        OVERRIDE_HASH_COMPUTATIONS.with(|count| count.set(0));
1046        let expected = shader.overrides_hash();
1047        for _ in 0..24 {
1048            let mut copy = shader.clone();
1049            copy.set_float(0, 7.0);
1050            copy.set_override("FLAG", 1.0);
1051            assert!(!copy.clear_override("ABSENT"));
1052            assert_eq!(copy.overrides_hash(), expected);
1053        }
1054        assert_eq!(OVERRIDE_HASH_COMPUTATIONS.with(std::cell::Cell::get), 1);
1055    }
1056
1057    #[test]
1058    fn override_hash_tracks_clone_mutations_and_float_bits() {
1059        fn independent_hash(shader: &RuntimeShader) -> u64 {
1060            let bytes: Vec<u8> = shader
1061                .overrides()
1062                .iter()
1063                .flat_map(|(name, value)| {
1064                    name.bytes().chain([0]).chain(value.to_bits().to_le_bytes())
1065                })
1066                .collect();
1067            if bytes.is_empty() {
1068                0
1069            } else {
1070                hash_shader_bytes(bytes)
1071            }
1072        }
1073
1074        let mut original = RuntimeShader::new("");
1075        original.set_override("VALUE", 0.0);
1076        let first = original.overrides_hash();
1077        assert_eq!(first, independent_hash(&original));
1078        for value in [
1079            -0.0,
1080            0.5,
1081            f64::INFINITY,
1082            f64::from_bits(0x7ff8_0000_0000_0001),
1083        ] {
1084            let mut changed = original.clone();
1085            changed.set_override("VALUE", value);
1086            assert_eq!(changed.overrides_hash(), independent_hash(&changed));
1087            assert_ne!(changed.overrides_hash(), first);
1088            changed.set_override("ADDED", 1.0);
1089            assert_eq!(changed.overrides_hash(), independent_hash(&changed));
1090            assert!(changed.clear_override("VALUE"));
1091            assert_eq!(changed.overrides_hash(), independent_hash(&changed));
1092            assert!(changed.clear_override("ADDED"));
1093            assert_eq!(changed.overrides_hash(), 0);
1094            assert_eq!(original.overrides_hash(), first);
1095        }
1096    }
1097
1098    #[test]
1099    fn shader_clones_share_declarations_and_isolate_mutation() {
1100        let mut shader = RuntimeShader::new("fn effect_fs() {}");
1101        shader.set_override("FLAG", 1.0);
1102        shader.set_substrates(&[SubstrateSpec::Average { block: 4 }]);
1103        shader.set_draw_split(Some("SPLIT"));
1104        let support = Rect {
1105            x: 1.0,
1106            y: 2.0,
1107            width: 30.0,
1108            height: 40.0,
1109        };
1110        shader.set_output_support(Some(support));
1111        let mut cloned = shader.clone();
1112        assert_eq!(cloned.overrides().as_ptr(), shader.overrides().as_ptr());
1113        assert_eq!(cloned.substrates().as_ptr(), shader.substrates().as_ptr());
1114        cloned.set_float(0, 2.0);
1115        assert!(shader.uniforms().is_empty());
1116        assert_eq!(cloned.overrides().as_ptr(), shader.overrides().as_ptr());
1117        cloned.set_override("FLAG", 2.0);
1118        assert_eq!(cloned.substrates(), shader.substrates());
1119        assert_eq!(cloned.draw_split(), shader.draw_split());
1120        cloned.set_substrates(&[SubstrateSpec::Average { block: 8 }]);
1121        cloned.set_draw_split(None);
1122        cloned.set_output_support(None);
1123        assert_eq!(shader.overrides(), &[("FLAG", 1.0)]);
1124        assert_eq!(shader.substrates(), &[SubstrateSpec::Average { block: 4 }]);
1125        assert_eq!(shader.draw_split(), Some("SPLIT"));
1126        assert_eq!(shader.output_support(), Some(support));
1127        assert_ne!(cloned, shader);
1128    }
1129
1130    use super::*;
1131    use crate::RoundedCornerShape;
1132
1133    #[test]
1134    fn cloned_effect_chains_keep_order_and_isolate_nested_edits() {
1135        let original = RenderEffect::offset(2.0, 7.0)
1136            .then(RenderEffect::blur(3.0))
1137            .then(RenderEffect::offset(-4.0, 1.0));
1138        let mut edited = original.clone();
1139        assert_eq!(edited, original);
1140        let RenderEffect::Chain {
1141            first: original_first,
1142            second: original_second,
1143        } = &original
1144        else {
1145            panic!("chain effect")
1146        };
1147        let RenderEffect::Chain {
1148            first: edited_first,
1149            second: edited_second,
1150        } = &mut edited
1151        else {
1152            panic!("chain effect")
1153        };
1154        assert!(Arc::ptr_eq(original_first, edited_first));
1155        assert!(Arc::ptr_eq(original_second, edited_second));
1156        assert_eq!(original_second.as_ref(), &RenderEffect::offset(-4.0, 1.0));
1157        let RenderEffect::Chain { first, second } = Arc::make_mut(edited_first) else {
1158            panic!("nested chain")
1159        };
1160        assert_eq!(first.as_ref(), &RenderEffect::offset(2.0, 7.0));
1161        assert_eq!(second.as_ref(), &RenderEffect::blur(3.0));
1162        *Arc::make_mut(first) = RenderEffect::offset(12.0, 17.0);
1163        *Arc::make_mut(edited_second) = RenderEffect::blur(11.0);
1164        assert_eq!(
1165            original,
1166            RenderEffect::offset(2.0, 7.0)
1167                .then(RenderEffect::blur(3.0))
1168                .then(RenderEffect::offset(-4.0, 1.0))
1169        );
1170        assert_eq!(
1171            edited,
1172            RenderEffect::offset(12.0, 17.0)
1173                .then(RenderEffect::blur(3.0))
1174                .then(RenderEffect::blur(11.0))
1175        );
1176    }
1177
1178    #[test]
1179    fn cloned_shader_effects_preserve_configuration_and_isolate_edits() {
1180        let mut shader = RuntimeShader::new("fn effect_fs() {}");
1181        shader.set_float(20, 3.0);
1182        shader.set_override("FEATURE", -0.0);
1183        shader.set_input_padding(7.0);
1184        shader.set_substrates(&[SubstrateSpec::Blur { radius_px: 12.0 }]);
1185        shader.set_draw_split(Some("SPLIT"));
1186        let original = RenderEffect::runtime_shader(shader.clone());
1187        let mut edited = original.clone();
1188        assert_eq!(edited, original);
1189        let RenderEffect::Shader {
1190            shader: original_shader,
1191        } = &original
1192        else {
1193            panic!("shader effect")
1194        };
1195        assert_eq!(original_shader.as_ref(), &shader);
1196        let RenderEffect::Shader {
1197            shader: edited_shader,
1198        } = &mut edited
1199        else {
1200            panic!("shader effect")
1201        };
1202        assert!(Arc::ptr_eq(original_shader, edited_shader));
1203        let changed = Arc::make_mut(edited_shader);
1204        changed.set_float(20, 9.0);
1205        changed.set_override("FEATURE", 1.0);
1206        changed.set_substrates(&[]);
1207        changed.set_draw_split(None);
1208        assert_eq!(original_shader.as_ref(), &shader);
1209        assert_eq!(edited_shader.uniforms()[20], 9.0);
1210        assert_eq!(edited_shader.overrides(), &[("FEATURE", 1.0)]);
1211        assert!(edited_shader.substrates().is_empty());
1212        assert_eq!(edited_shader.draw_split(), None);
1213        assert_ne!(original, edited);
1214    }
1215
1216    #[test]
1217    fn runtime_shader_set_uniforms() {
1218        let mut shader = RuntimeShader::new("// test");
1219        shader.set_float(0, 1.0);
1220        shader.set_float2(2, 3.0, 4.0);
1221        shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
1222
1223        assert_eq!(shader.uniforms()[0], 1.0);
1224        assert_eq!(shader.uniforms()[1], 0.0);
1225        assert_eq!(shader.uniforms()[2], 3.0);
1226        assert_eq!(shader.uniforms()[3], 4.0);
1227        assert_eq!(shader.uniforms()[4], 5.0);
1228        assert_eq!(shader.uniforms()[5], 6.0);
1229        assert_eq!(shader.uniforms()[6], 7.0);
1230        assert_eq!(shader.uniforms()[7], 8.0);
1231    }
1232
1233    #[test]
1234    fn runtime_shader_padded() {
1235        let mut shader = RuntimeShader::new("// test");
1236        shader.set_float(0, 42.0);
1237        let padded = shader.uniforms_padded();
1238        assert_eq!(padded[0], 42.0);
1239        assert_eq!(padded[1], 0.0);
1240        assert_eq!(padded[255], 0.0);
1241    }
1242
1243    #[test]
1244    fn blur_and_offset_declare_input_padding() {
1245        assert_eq!(
1246            RenderEffect::blur_xy(6.0, 12.0, TileMode::Clamp).input_padding(),
1247            12.0
1248        );
1249        assert_eq!(RenderEffect::offset(-8.0, 3.0).input_padding(), 8.0);
1250    }
1251
1252    #[test]
1253    fn chained_effect_padding_accumulates_sampling_ranges() {
1254        let mut shader = RuntimeShader::new("// test");
1255        shader.set_input_padding(9.0);
1256        let effect = RenderEffect::blur_xy(4.0, 6.0, TileMode::Clamp)
1257            .then(RenderEffect::runtime_shader(shader))
1258            .then(RenderEffect::offset(2.0, -5.0));
1259
1260        assert_eq!(effect.input_padding(), 20.0);
1261    }
1262
1263    #[test]
1264    fn runtime_shader_keeps_common_uniform_payload_inline() {
1265        let mut shader = RuntimeShader::new("// test");
1266        shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
1267        shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
1268        shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
1269        shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
1270
1271        assert!(shader.uniforms.is_inline());
1272        assert_eq!(shader.uniforms().len(), 16);
1273
1274        shader.set_float(16, 17.0);
1275        assert!(!shader.uniforms.is_inline());
1276        assert_eq!(shader.uniforms()[16], 17.0);
1277    }
1278
1279    #[test]
1280    fn runtime_shader_try_set_reports_reserved_uniform_slots() {
1281        let mut shader = RuntimeShader::new("// test");
1282
1283        let err = shader
1284            .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
1285            .unwrap_err();
1286        assert_eq!(
1287            err,
1288            RuntimeShaderUniformError::OutOfUserRange {
1289                index: RuntimeShader::RESERVED_UNIFORM_START,
1290                width: 1,
1291                max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
1292                reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
1293                max_uniforms: RuntimeShader::MAX_UNIFORMS,
1294            }
1295        );
1296        assert!(shader.uniforms().is_empty());
1297
1298        let err = shader
1299            .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
1300            .unwrap_err();
1301        assert_eq!(
1302            err,
1303            RuntimeShaderUniformError::OutOfUserRange {
1304                index: RuntimeShader::MAX_USER_UNIFORMS - 3,
1305                width: 4,
1306                max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
1307                reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
1308                max_uniforms: RuntimeShader::MAX_UNIFORMS,
1309            }
1310        );
1311    }
1312
1313    #[test]
1314    fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
1315        let mut shader = RuntimeShader::new("// test");
1316        shader.set_float(0, 7.0);
1317
1318        shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
1319        shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
1320
1321        assert_eq!(shader.uniforms(), &[7.0]);
1322    }
1323
1324    #[test]
1325    fn render_effect_chaining() {
1326        let blur = RenderEffect::blur(10.0);
1327        let offset = RenderEffect::offset(5.0, 5.0);
1328        let chained = blur.then(offset);
1329        match chained {
1330            RenderEffect::Chain { first, second } => {
1331                assert!(matches!(*first, RenderEffect::Blur { .. }));
1332                assert!(matches!(*second, RenderEffect::Offset { .. }));
1333            }
1334            _ => panic!("expected Chain"),
1335        }
1336    }
1337
1338    #[test]
1339    fn blur_convenience() {
1340        let effect = RenderEffect::blur(15.0);
1341        match effect {
1342            RenderEffect::Blur {
1343                radius_x,
1344                radius_y,
1345                edge_treatment,
1346            } => {
1347                assert_eq!(radius_x, 15.0);
1348                assert_eq!(radius_y, 15.0);
1349                assert_eq!(edge_treatment, TileMode::Clamp);
1350            }
1351            _ => panic!("expected Blur"),
1352        }
1353    }
1354
1355    #[test]
1356    fn blur_with_edge_treatment_uses_explicit_mode() {
1357        let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
1358        match effect {
1359            RenderEffect::Blur {
1360                radius_x,
1361                radius_y,
1362                edge_treatment,
1363            } => {
1364                assert_eq!(radius_x, 6.0);
1365                assert_eq!(radius_y, 6.0);
1366                assert_eq!(edge_treatment, TileMode::Decal);
1367            }
1368            _ => panic!("expected Blur"),
1369        }
1370    }
1371
1372    #[test]
1373    fn source_hash_consistent() {
1374        let s1 = RuntimeShader::new("fn main() {}");
1375        let s2 = RuntimeShader::new("fn main() {}");
1376        assert_eq!(s1.source_hash(), s2.source_hash());
1377    }
1378
1379    #[test]
1380    fn runtime_shader_from_shared_source_reuses_shared_source() {
1381        let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
1382        let s1 = RuntimeShader::from_shared_source(source.clone());
1383        let s2 = RuntimeShader::from_shared_source(source);
1384
1385        assert!(Arc::ptr_eq(&s1.source, &s2.source));
1386        assert_eq!(s1.source_hash(), s2.source_hash());
1387    }
1388
1389    fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
1390        RuntimeShader::new(source)
1391    }
1392
1393    fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
1394        RuntimeShader::new(source)
1395    }
1396
1397    #[test]
1398    fn runtime_shader_new_reuses_same_callsite_source() {
1399        let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
1400        let s1 = runtime_shader_from_reuse_callsite(source);
1401        let s2 = runtime_shader_from_reuse_callsite(source);
1402
1403        assert!(Arc::ptr_eq(&s1.source, &s2.source));
1404        assert_eq!(s1.source_hash(), s2.source_hash());
1405    }
1406
1407    #[test]
1408    fn runtime_shader_new_replaces_changed_callsite_source() {
1409        let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
1410        let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
1411
1412        assert!(!Arc::ptr_eq(&s1.source, &s2.source));
1413        assert_ne!(s1.source_hash(), s2.source_hash());
1414        assert_eq!(s2.source(), "fn b() {}");
1415    }
1416
1417    #[test]
1418    fn runtime_shader_source_storage_has_no_process_global_interner() {
1419        let source = include_str!("render_effect.rs");
1420        let blocked_static = ["static ", "INTERNER"].concat();
1421        let blocked_type = ["ShaderSource", "Interner"].concat();
1422
1423        assert!(
1424            !source.contains(&blocked_static) && !source.contains(&blocked_type),
1425            "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
1426        );
1427    }
1428
1429    #[test]
1430    fn blur_xy_preserves_tile_mode() {
1431        let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
1432        match effect {
1433            RenderEffect::Blur {
1434                radius_x,
1435                radius_y,
1436                edge_treatment,
1437            } => {
1438                assert_eq!(radius_x, 3.0);
1439                assert_eq!(radius_y, 7.0);
1440                assert_eq!(edge_treatment, TileMode::Clamp);
1441            }
1442            _ => panic!("expected Blur"),
1443        }
1444    }
1445
1446    #[test]
1447    fn offset_constructor_sets_components() {
1448        let effect = RenderEffect::offset(11.0, -5.0);
1449        match effect {
1450            RenderEffect::Offset { offset_x, offset_y } => {
1451                assert_eq!(offset_x, 11.0);
1452                assert_eq!(offset_y, -5.0);
1453            }
1454            _ => panic!("expected Offset"),
1455        }
1456    }
1457
1458    #[test]
1459    fn runtime_shader_equality_is_source_value_based() {
1460        let mut s1 = RuntimeShader::new("fn main() {}");
1461        let mut s2 = RuntimeShader::new("fn main() {}");
1462        s1.set_float(0, 1.0);
1463        s2.set_float(0, 1.0);
1464        assert_eq!(s1, s2);
1465    }
1466
1467    #[test]
1468    fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
1469        let treatment = BlurredEdgeTreatment::default();
1470        assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
1471        assert!(treatment.clip());
1472        assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1473    }
1474
1475    #[test]
1476    fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
1477        let treatment = BlurredEdgeTreatment::UNBOUNDED;
1478        assert_eq!(treatment.shape(), None);
1479        assert!(!treatment.clip());
1480        assert_eq!(treatment.tile_mode(), TileMode::Decal);
1481    }
1482
1483    #[test]
1484    fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
1485        let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
1486        let treatment = BlurredEdgeTreatment::with_shape(rounded);
1487        assert_eq!(treatment.shape(), Some(rounded));
1488        assert!(treatment.clip());
1489        assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1490    }
1491
1492    #[test]
1493    fn an_effect_chains_output_support_is_the_support_of_the_stage_that_writes_its_output() {
1494        let mut shader = RuntimeShader::new("fn glass_fs() {}");
1495        assert_eq!(shader.output_support(), None);
1496        let support = Rect {
1497            x: 4.0,
1498            y: 6.0,
1499            width: 30.0,
1500            height: 12.0,
1501        };
1502        shader.set_output_support(Some(support));
1503        assert_eq!(shader.output_support(), Some(support));
1504        let effect = RenderEffect::blur(3.0).then(RenderEffect::runtime_shader(shader.clone()));
1505        assert_eq!(effect.output_support(), Some(support));
1506        let effect = RenderEffect::runtime_shader(shader.clone()).then(RenderEffect::blur(3.0));
1507        assert_eq!(effect.output_support(), None);
1508        assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1509    }
1510
1511    #[test]
1512    fn a_sample_domain_is_the_writers_and_a_blur_declares_none() {
1513        let mut shader = RuntimeShader::new("fn glass_fs() {}");
1514        let domain = Rect {
1515            x: -2.0,
1516            y: -2.0,
1517            width: 20.0,
1518            height: 12.0,
1519        };
1520        let plain = shader.clone();
1521        shader.set_sample_domain(Some(domain));
1522        assert_ne!(shader, plain);
1523        assert_eq!(shader.sample_domain(), Some(domain));
1524        let effect = RenderEffect::blur(3.0).then(RenderEffect::runtime_shader(shader.clone()));
1525        assert_eq!(effect.sample_domain(), Some(domain));
1526        assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1527        assert_eq!(RenderEffect::blur(3.0).sample_domain(), None);
1528        shader.set_sample_domain(Some(Rect {
1529            x: f32::INFINITY,
1530            ..domain
1531        }));
1532        assert_eq!(shader.sample_domain(), None);
1533    }
1534
1535    #[test]
1536    fn a_non_finite_output_support_clears_the_declaration_and_a_support_tells_shaders_apart() {
1537        let mut shader = RuntimeShader::new("fn glass_fs() {}");
1538        let plain = shader.clone();
1539        shader.set_output_support(Some(Rect {
1540            x: 0.0,
1541            y: 0.0,
1542            width: 10.0,
1543            height: 10.0,
1544        }));
1545        assert_ne!(shader, plain);
1546        shader.set_output_support(Some(Rect {
1547            x: 0.0,
1548            y: 0.0,
1549            width: f32::NAN,
1550            height: 10.0,
1551        }));
1552        assert_eq!(shader.output_support(), None);
1553        assert_eq!(shader, plain);
1554    }
1555    #[test]
1556    fn specialization_cache_preserves_source_identity_and_shader_values() {
1557        let mut cache = ShaderSpecializationCache::<u32, 2>::new();
1558        let mut first = RuntimeShader::new("fn effect_fs() {}");
1559        first.set_override("CALLER", -0.0);
1560        let mut second = first.clone();
1561        second.set_override("CALLER", f64::from_bits(0x7ff8_0000_0000_0001));
1562        let sources = [first, second];
1563        for key in [1, 1, 2, 3, 1] {
1564            for source in &sources {
1565                let mut shader = source.clone();
1566                shader.set_float(0, key as f32);
1567                shader.set_input_padding(key as f32);
1568                cache.apply(&mut shader, key, |shader, &key| {
1569                    shader.set_override("FEATURE", f64::from(key));
1570                    shader.set_draw_split(Some("SPLIT"));
1571                    shader.set_substrates(&[SubstrateSpec::Average { block: key }]);
1572                });
1573                assert_eq!(
1574                    shader.overrides()[0].1.to_bits(),
1575                    source.overrides()[0].1.to_bits()
1576                );
1577                assert_eq!(shader.overrides()[1], ("FEATURE", f64::from(key)));
1578                assert_eq!(
1579                    shader.substrates(),
1580                    &[SubstrateSpec::Average { block: key }]
1581                );
1582                assert_eq!(shader.draw_split(), Some("SPLIT"));
1583                assert_eq!(shader.uniforms(), &[key as f32]);
1584                assert_eq!(shader.input_padding(), key as f32);
1585                assert_eq!(source.overrides().len(), 1);
1586                assert!(source.substrates().is_empty());
1587                assert_eq!(source.draw_split(), None);
1588                let mut repeated = source.clone();
1589                cache.apply(&mut repeated, key, |_, _| {
1590                    panic!("shared specialization missed")
1591                });
1592                assert_eq!(repeated.overrides_hash(), shader.overrides_hash());
1593                assert!(Arc::ptr_eq(
1594                    repeated.specialization.as_ref().unwrap(),
1595                    shader.specialization.as_ref().unwrap(),
1596                ));
1597                assert!(cache.entries.len() <= 2);
1598            }
1599        }
1600    }
1601
1602    #[test]
1603    fn specialization_cache_mutates_unique_state_without_retaining_it() {
1604        let mut cache = ShaderSpecializationCache::<(), 2>::new();
1605        let mut shader = RuntimeShader::new("fn effect_fs() {}");
1606        shader.set_override("VALUE", 1.0);
1607        let allocation = Arc::as_ptr(shader.specialization.as_ref().unwrap());
1608        cache.apply(&mut shader, (), |shader, ()| {
1609            shader.set_override("VALUE", 2.0)
1610        });
1611        assert_eq!(shader.overrides(), &[("VALUE", 2.0)]);
1612        assert_eq!(
1613            Arc::as_ptr(shader.specialization.as_ref().unwrap()),
1614            allocation
1615        );
1616        assert!(cache.entries.is_empty());
1617    }
1618
1619    #[test]
1620    fn unchanged_shader_declarations_keep_their_storage() {
1621        let mut shader = RuntimeShader::new("fn effect_fs() {}");
1622        shader.set_substrates(&[]);
1623        shader.set_draw_split(None);
1624        assert!(!shader.clear_override("MISSING"));
1625        assert!(shader.specialization.is_none());
1626        shader.set_override("FLAG", 1.0);
1627        let mut cloned = shader.clone();
1628        cloned.set_override("FLAG", 1.0);
1629        cloned.set_substrates(&[]);
1630        cloned.set_draw_split(None);
1631        assert!(!cloned.clear_override("MISSING"));
1632        assert_eq!(cloned.overrides().as_ptr(), shader.overrides().as_ptr());
1633    }
1634
1635    #[test]
1636    fn mean_substrates_have_distinct_stable_identity() {
1637        use std::hash::{DefaultHasher, Hasher};
1638        let hash = |spec: SubstrateSpec| {
1639            let mut h = DefaultHasher::new();
1640            spec.hash_bits(&mut h);
1641            h.finish()
1642        };
1643        let mean = SubstrateSpec::Mean;
1644        assert!(mean.same_bits(&mean));
1645        for other in [
1646            SubstrateSpec::Average { block: 4 },
1647            SubstrateSpec::Blur { radius_px: 12.0 },
1648        ] {
1649            assert!(!mean.same_bits(&other));
1650            assert_ne!(hash(mean), hash(other));
1651        }
1652        let mut shader = RuntimeShader::new("fn effect_fs() {}");
1653        shader.set_substrates(&[mean]);
1654        let mut cloned = shader.clone();
1655        cloned.set_substrates(&[SubstrateSpec::Average { block: 4 }]);
1656        assert_eq!(shader.substrates(), &[mean]);
1657        assert_eq!(cloned.substrates(), &[SubstrateSpec::Average { block: 4 }]);
1658    }
1659
1660    #[test]
1661    fn shader_substrates_preserve_order_and_ownership_across_size_changes() {
1662        let declared = [
1663            SubstrateSpec::Blur { radius_px: 12.0 },
1664            SubstrateSpec::Average { block: 4 },
1665            SubstrateSpec::Blur { radius_px: -0.0 },
1666        ];
1667        let mut source = declared;
1668        let mut original = RuntimeShader::new("fn effect_fs() {}");
1669        original.set_substrates(&source);
1670        source[0] = SubstrateSpec::Average { block: 16 };
1671        let mut changed = original.clone();
1672        for replacement in [&source[..1], &source[..2], &source[..0], &source[..]] {
1673            changed.set_substrates(replacement);
1674            assert_eq!(changed.substrates().len(), replacement.len());
1675            assert!(
1676                changed
1677                    .substrates()
1678                    .iter()
1679                    .zip(replacement)
1680                    .all(|(actual, expected)| actual.same_bits(expected))
1681            );
1682            assert_eq!(original.substrates().len(), declared.len());
1683            assert!(
1684                original
1685                    .substrates()
1686                    .iter()
1687                    .zip(&declared)
1688                    .all(|(actual, expected)| actual.same_bits(expected))
1689            );
1690        }
1691    }
1692
1693    #[test]
1694    fn shader_substrate_setters_preserve_float_bits_when_detaching() {
1695        let mut original = RuntimeShader::new("fn effect_fs() {}");
1696        original.set_substrates(&[SubstrateSpec::Blur { radius_px: 0.0 }]);
1697        let mut cloned = original.clone();
1698        cloned.set_substrates(&[SubstrateSpec::Blur { radius_px: 0.0 }]);
1699        assert_eq!(cloned.substrates().as_ptr(), original.substrates().as_ptr());
1700        cloned.set_substrates(&[SubstrateSpec::Blur { radius_px: -0.0 }]);
1701        let [SubstrateSpec::Blur { radius_px }] = cloned.substrates() else {
1702            panic!("one blur substrate");
1703        };
1704        assert_eq!(radius_px.to_bits(), (-0.0_f32).to_bits());
1705        let [SubstrateSpec::Blur { radius_px }] = original.substrates() else {
1706            panic!("original blur substrate");
1707        };
1708        assert_eq!(radius_px.to_bits(), 0.0_f32.to_bits());
1709    }
1710}