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