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