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    position_independent: bool,
133    preserves_transparency: bool,
134    domains: Option<Box<ShaderDomains>>,
135}
136
137#[derive(Clone, Debug, Default)]
138struct ShaderSpecialization {
139    overrides: Vec<(&'static str, f64)>,
140    overrides_hash: OnceLock<u64>,
141    substrates: ArrayVec<SubstrateSpec, MAX_SUBSTRATES>,
142    draw_split: Option<&'static str>,
143    exact: bool,
144}
145
146pub(crate) struct ShaderSpecializationCache<K, const N: usize> {
147    entries: ArrayVec<CachedShaderSpecialization<K>, N>,
148}
149
150struct CachedShaderSpecialization<K> {
151    source: Option<Arc<ShaderSpecialization>>,
152    key: K,
153    result: Option<Arc<ShaderSpecialization>>,
154}
155
156impl<K: PartialEq, const N: usize> ShaderSpecializationCache<K, N> {
157    pub(crate) const fn new() -> Self {
158        assert!(N > 0);
159        Self {
160            entries: ArrayVec::new_const(),
161        }
162    }
163
164    pub(crate) fn apply(
165        &mut self,
166        shader: &mut RuntimeShader,
167        key: K,
168        specialize: impl FnOnce(&mut RuntimeShader, &K),
169    ) {
170        let hit = self.entries.iter().rposition(|entry| {
171            entry.key == key
172                && match (&entry.source, &shader.specialization) {
173                    (Some(source), Some(current)) => Arc::ptr_eq(source, current),
174                    (None, None) => true,
175                    _ => false,
176                }
177        });
178        if let Some(index) = hit {
179            let entry = self.entries.remove(index);
180            shader.specialization.clone_from(&entry.result);
181            self.entries.push(entry);
182            return;
183        }
184        if shader
185            .specialization
186            .as_ref()
187            .is_some_and(|source| Arc::strong_count(source) == 1)
188        {
189            specialize(shader, &key);
190            return;
191        }
192        let source = shader.specialization.clone();
193        specialize(shader, &key);
194        if self.entries.is_full() {
195            self.entries.remove(0);
196        }
197        self.entries.push(CachedShaderSpecialization {
198            source,
199            key,
200            result: shader.specialization.clone(),
201        });
202    }
203}
204
205static DEFAULT_SHADER_SPECIALIZATION: ShaderSpecialization = ShaderSpecialization {
206    overrides: Vec::new(),
207    overrides_hash: OnceLock::new(),
208    substrates: ArrayVec::new_const(),
209    draw_split: None,
210    exact: false,
211};
212
213#[derive(Clone, Copy, Debug, Default, PartialEq)]
214struct ShaderDomains {
215    output_support: Option<Rect>,
216    sample_domain: Option<Rect>,
217}
218
219fn finite_rect(rect: Option<Rect>) -> Option<Rect> {
220    rect.filter(|rect| {
221        rect.x.is_finite()
222            && rect.y.is_finite()
223            && rect.width.is_finite()
224            && rect.height.is_finite()
225    })
226}
227
228/// The most substrates one shader may declare.
229pub const MAX_SUBSTRATES: usize = 3;
230
231/// A low-frequency copy of a shader's source the renderer packs beside it
232/// and hands the shader through a reserved substrate region slot.
233#[derive(Clone, Copy, Debug, PartialEq)]
234pub enum SubstrateSpec {
235    /// The componentwise source mean over the layer's bounds, stored in one texel.
236    /// Filter padding is excluded; bounds are clipped to the capture and rounded
237    /// outward to texels. The renderer averages rows and then columns in its
238    /// render-target format. A capture outside the layer uses its complete source.
239    Mean,
240    /// The source averaged in blocks of `block` x `block` texels, one
241    /// substrate texel per block.
242    Average { block: u32 },
243    /// The source blurred by a Gaussian of `radius_px` device pixels, kept
244    /// at the blur's scratch resolution.
245    Blur { radius_px: f32 },
246}
247
248impl SubstrateSpec {
249    fn same_bits(&self, other: &Self) -> bool {
250        match (self, other) {
251            (Self::Mean, Self::Mean) => true,
252            (Self::Average { block: a }, Self::Average { block: b }) => a == b,
253            (Self::Blur { radius_px: a }, Self::Blur { radius_px: b }) => {
254                a.to_bits() == b.to_bits()
255            }
256            _ => false,
257        }
258    }
259
260    fn hash_bits<H: std::hash::Hasher>(&self, state: &mut H) {
261        use std::hash::Hash;
262        match self {
263            Self::Mean => 2u8.hash(state),
264            Self::Average { block } => {
265                0u8.hash(state);
266                block.hash(state);
267            }
268            Self::Blur { radius_px } => {
269                1u8.hash(state);
270                radius_px.to_bits().hash(state);
271            }
272        }
273    }
274}
275
276#[derive(Clone, Debug, PartialEq)]
277struct RuntimeShaderUniforms {
278    len: usize,
279    inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
280    heap: Option<Vec<f32>>,
281}
282
283impl RuntimeShaderUniforms {
284    fn new() -> Self {
285        Self {
286            len: 0,
287            inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
288            heap: None,
289        }
290    }
291
292    fn as_slice(&self) -> &[f32] {
293        if let Some(heap) = &self.heap {
294            heap.as_slice()
295        } else {
296            &self.inline[..self.len]
297        }
298    }
299
300    fn len(&self) -> usize {
301        self.as_slice().len()
302    }
303
304    fn ensure_len(&mut self, min_len: usize) {
305        if let Some(heap) = &mut self.heap {
306            if heap.len() < min_len {
307                heap.resize(min_len, 0.0);
308            }
309            return;
310        }
311
312        if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
313            self.len = self.len.max(min_len);
314            return;
315        }
316
317        let mut heap = Vec::with_capacity(min_len);
318        heap.extend_from_slice(&self.inline[..self.len]);
319        heap.resize(min_len, 0.0);
320        self.heap = Some(heap);
321    }
322
323    fn set(&mut self, index: usize, value: f32) {
324        if let Some(heap) = &mut self.heap {
325            heap[index] = value;
326        } else {
327            self.inline[index] = value;
328        }
329    }
330
331    #[cfg(test)]
332    fn is_inline(&self) -> bool {
333        self.heap.is_none()
334    }
335}
336
337/// Error returned when a shader uniform write targets renderer-owned storage.
338#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
339pub enum RuntimeShaderUniformError {
340    #[error(
341        "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"
342    )]
343    OutOfUserRange {
344        index: usize,
345        width: usize,
346        max_user_uniforms: usize,
347        reserved_start: usize,
348        max_uniforms: usize,
349    },
350}
351
352impl RuntimeShader {
353    /// Total uniform storage size in floats (64 vec4s = 256 floats).
354    ///
355    /// The final slots are reserved for renderer-managed data.
356    pub const MAX_UNIFORMS: usize = 256;
357    /// First renderer-reserved uniform slot.
358    pub const RESERVED_UNIFORM_START: usize = 224;
359    /// Reserved slots of the substrate regions `(x, y, w, h)` in input
360    /// texels, in declaration order.
361    pub const SUBSTRATE_REGION_UNIFORMS: [usize; MAX_SUBSTRATES] = [232, 228, 224];
362    /// Reserved slot of the source region `(x, y, w, h)` in input texels.
363    pub const SOURCE_REGION_UNIFORM: usize = 236;
364    /// Reserved slot of the composite mask rect `(x, y, w, h)` in region pixels.
365    pub const MASK_RECT_UNIFORM: usize = 240;
366    /// Reserved slot of the composite mask corner radii.
367    pub const MASK_RADII_UNIFORM: usize = 244;
368    /// Reserved slot of the effect rect `(x, y, w, h)` in region pixels.
369    pub const EFFECT_RECT_UNIFORM: usize = 248;
370    /// Reserved slot of the logical size the input represents.
371    pub const LOGICAL_SIZE_UNIFORM: usize = 252;
372    /// Reserved slot of the composite alpha.
373    pub const ALPHA_UNIFORM: usize = 254;
374    /// Maximum user-addressable uniform count.
375    pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
376
377    /// Create a new RuntimeShader from WGSL source code.
378    #[track_caller]
379    pub fn new(wgsl_source: &str) -> Self {
380        let (source, source_hash) =
381            cached_shader_source(std::panic::Location::caller(), wgsl_source);
382        Self::with_source(source, source_hash)
383    }
384
385    /// Create a RuntimeShader from shared WGSL source code.
386    ///
387    /// This avoids repeatedly copying large shader modules for animated effects
388    /// that rebuild only their uniform payload every frame.
389    pub fn from_shared_source(source: Arc<str>) -> Self {
390        let source_hash = cached_shared_shader_source_hash(&source);
391        Self::with_source(source, source_hash)
392    }
393
394    fn with_source(source: Arc<str>, source_hash: u64) -> Self {
395        Self {
396            source,
397            source_hash,
398            uniforms: RuntimeShaderUniforms::new(),
399            specialization: None,
400            input_padding: 0.0,
401            output_padding: 0.0,
402            batched_source: false,
403            position_independent: false,
404            preserves_transparency: false,
405            domains: None,
406        }
407    }
408
409    fn specialization(&self) -> &ShaderSpecialization {
410        self.specialization
411            .as_deref()
412            .unwrap_or(&DEFAULT_SHADER_SPECIALIZATION)
413    }
414
415    fn specialization_mut(&mut self) -> &mut ShaderSpecialization {
416        Arc::make_mut(self.specialization.get_or_insert_with(Arc::default))
417    }
418
419    /// Fixes a pipeline-overridable constant (`override NAME: T = ...;` in
420    /// the WGSL) for every pipeline compiled from this shader. The value is
421    /// converted to the constant's declared scalar type the way WebGPU does
422    /// (a `bool` is `value != 0`). Each distinct override set compiles its
423    /// own pipeline; renderers use this to fold a material's inactive
424    /// features away without changing the shader text.
425    ///
426    /// The pipeline compiles inside the frame that first draws the shader,
427    /// unless the shader declares its specialization exact with
428    /// [`Self::set_specialization_exact`]: then the renderer compiles it in
429    /// the background and draws with the general pipeline meanwhile.
430    /// A requested warm-up without an existing general pipeline finishes
431    /// before its first draw instead of compiling a new stand-in.
432    pub fn set_override(&mut self, name: &'static str, value: f64) {
433        let position = self
434            .overrides()
435            .binary_search_by(|(existing, _)| existing.cmp(&name));
436        if position.is_ok_and(|index| self.overrides()[index].1.to_bits() == value.to_bits()) {
437            return;
438        }
439        let specialization = self.specialization_mut();
440        specialization.overrides_hash.take();
441        let overrides = &mut specialization.overrides;
442        match position {
443            Ok(index) => overrides[index].1 = value,
444            Err(index) => overrides.insert(index, (name, value)),
445        }
446    }
447
448    /// Removes a pipeline override by name, returning whether one was present.
449    pub fn clear_override(&mut self, name: &str) -> bool {
450        let Ok(index) = self
451            .overrides()
452            .binary_search_by(|(existing, _)| (*existing).cmp(name))
453        else {
454            return false;
455        };
456        let specialization = self.specialization_mut();
457        specialization.overrides_hash.take();
458        specialization.overrides.remove(index);
459        true
460    }
461
462    /// The pipeline-overridable constants fixed by [`Self::set_override`],
463    /// ordered by name.
464    pub fn overrides(&self) -> &[(&'static str, f64)] {
465        &self.specialization().overrides
466    }
467
468    /// Hash of the fixed override set; zero when no override is fixed.
469    pub fn overrides_hash(&self) -> u64 {
470        let specialization = self.specialization();
471        if specialization.overrides.is_empty() {
472            return 0;
473        }
474        *specialization.overrides_hash.get_or_init(|| {
475            #[cfg(test)]
476            OVERRIDE_HASH_COMPUTATIONS.with(|count| count.set(count.get() + 1));
477            hash_shader_bytes(specialization.overrides.iter().flat_map(|(name, value)| {
478                name.bytes().chain([0]).chain(value.to_bits().to_le_bytes())
479            }))
480        })
481    }
482
483    /// Declares how far the shader may sample outside its effect rect, in
484    /// logical pixels. Backdrop rendering uses this to capture enough input
485    /// around refractive and displacement shaders.
486    pub fn set_input_padding(&mut self, padding: f32) {
487        self.input_padding = if padding.is_finite() {
488            padding.max(0.0)
489        } else {
490            0.0
491        };
492    }
493
494    /// Returns the declared input padding in logical pixels.
495    pub fn input_padding(&self) -> f32 {
496        self.input_padding
497    }
498
499    /// Declares how far the shader WRITES outside its effect rect, in logical
500    /// pixels. Backdrop compositing widens its scissor by this amount so
501    /// SDF-driven coverage (rim glow, wobble, glued neighbor shapes) can
502    /// extend past the node bounds instead of being clipped to them.
503    pub fn set_output_padding(&mut self, padding: f32) {
504        self.output_padding = if padding.is_finite() {
505            padding.max(0.0)
506        } else {
507            0.0
508        };
509    }
510
511    /// Returns the declared output padding in logical pixels.
512    pub fn output_padding(&self) -> f32 {
513        self.output_padding
514    }
515
516    /// Declares the rect outside which the shader writes nothing: every
517    /// pixel its coverage can make nonzero at its current uniforms, the
518    /// output padding's reach included, in logical pixels with the origin
519    /// at the effect rect's top-left. A renderer composites only the part
520    /// of the effect rect inside it; the capture it reads stays whole, so a
521    /// node that carries headroom around a smaller material pays the
522    /// composite for the material alone. It says nothing about sampling:
523    /// see [`Self::set_sample_domain`]. `None`, the default, means the
524    /// whole effect rect and its output padding. A rect with a non-finite
525    /// side clears the declaration.
526    pub fn set_output_support(&mut self, support: Option<Rect>) {
527        self.set_domains(ShaderDomains {
528            output_support: finite_rect(support),
529            sample_domain: self.sample_domain(),
530        });
531    }
532
533    /// The declared output support, when the shader gave one.
534    pub fn output_support(&self) -> Option<Rect> {
535        self.domains
536            .as_ref()
537            .and_then(|domains| domains.output_support)
538    }
539
540    fn set_domains(&mut self, domains: ShaderDomains) {
541        self.domains = (domains != ShaderDomains::default()).then(|| Box::new(domains));
542    }
543
544    /// Declares the rect outside which the shader never samples its input,
545    /// in logical pixels with the origin at the effect rect's top-left. A
546    /// renderer may leave the input outside it unresolved: a blur feeding
547    /// this shader need only write the domain. The default, `None`, is the
548    /// whole effect rect and its input padding, which the input padding
549    /// contract already promises; an output support says nothing about
550    /// sampling, so a shader that shades a small region but reads a far
551    /// one keeps the default. A rect with a non-finite side clears it.
552    pub fn set_sample_domain(&mut self, domain: Option<Rect>) {
553        self.set_domains(ShaderDomains {
554            output_support: self.output_support(),
555            sample_domain: finite_rect(domain),
556        });
557    }
558
559    /// The declared sample domain, when the shader gave one.
560    pub fn sample_domain(&self) -> Option<Rect> {
561        self.domains
562            .as_ref()
563            .and_then(|domains| domains.sample_domain)
564    }
565
566    /// Set a single float uniform at the given index.
567    ///
568    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float`]
569    /// when the caller needs to handle invalid uniform writes explicitly.
570    pub fn set_float(&mut self, index: usize, value: f32) {
571        let _ = self.try_set_float(index, value);
572    }
573
574    /// Set a single float uniform at the given index.
575    pub fn try_set_float(
576        &mut self,
577        index: usize,
578        value: f32,
579    ) -> Result<(), RuntimeShaderUniformError> {
580        self.try_ensure_capacity(index, 1)?;
581        self.uniforms.set(index, value);
582        Ok(())
583    }
584
585    /// Set a vec2 uniform at the given index (consumes indices `[index, index+1]`).
586    ///
587    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float2`]
588    /// when the caller needs to handle invalid uniform writes explicitly.
589    pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
590        let _ = self.try_set_float2(index, x, y);
591    }
592
593    /// Set a vec2 uniform at the given index (consumes indices `[index, index+1]`).
594    pub fn try_set_float2(
595        &mut self,
596        index: usize,
597        x: f32,
598        y: f32,
599    ) -> Result<(), RuntimeShaderUniformError> {
600        self.try_ensure_capacity(index, 2)?;
601        self.uniforms.set(index, x);
602        self.uniforms.set(index + 1, y);
603        Ok(())
604    }
605
606    /// Set a vec4 uniform at the given index (consumes indices `[index..index+4]`).
607    ///
608    /// Invalid renderer-reserved ranges are ignored. Use [`Self::try_set_float4`]
609    /// when the caller needs to handle invalid uniform writes explicitly.
610    pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
611        let _ = self.try_set_float4(index, x, y, z, w);
612    }
613
614    /// Set a vec4 uniform at the given index (consumes indices `[index..index+4]`).
615    pub fn try_set_float4(
616        &mut self,
617        index: usize,
618        x: f32,
619        y: f32,
620        z: f32,
621        w: f32,
622    ) -> Result<(), RuntimeShaderUniformError> {
623        self.try_ensure_capacity(index, 4)?;
624        self.uniforms.set(index, x);
625        self.uniforms.set(index + 1, y);
626        self.uniforms.set(index + 2, z);
627        self.uniforms.set(index + 3, w);
628        Ok(())
629    }
630
631    /// Declares that the shader reads the reserved source region, mask and
632    /// alpha slots and samples only within its region's texel centers, so the
633    /// renderer may hand it an input region packed edge to edge beside others
634    /// and draw it straight into the final pass with its clip applied.
635    pub fn set_batched_source(&mut self, batched: bool) {
636        self.batched_source = batched;
637    }
638
639    /// Whether the shader reads the reserved source region, mask and alpha
640    /// slots.
641    pub fn batched_source(&self) -> bool {
642        self.batched_source
643    }
644
645    /// Declares that fragment output is independent of `@builtin(position)`.
646    /// The renderer may then apply a layer's effect directly in its parent's
647    /// pass when the source and destination raster grids match. UVs and the
648    /// source metadata keep their meaning; fragment positions belong to the
649    /// render target and can change when a pass is removed. A backend may also
650    /// infer independence from shader validation when this is not declared.
651    pub fn set_position_independent(&mut self, independent: bool) {
652        self.position_independent = independent;
653    }
654
655    /// Whether the caller declares independence from fragment positions.
656    pub fn position_independent(&self) -> bool {
657        self.position_independent
658    }
659
660    /// Declares that the shader returns zero wherever every texel it reads
661    /// is zero. A layer that draws nothing under such a shader composites
662    /// nothing, so the renderer leaves the page as it is instead of shading
663    /// the layer's pixels to prove it.
664    pub fn set_preserves_transparency(&mut self, preserves: bool) {
665        self.preserves_transparency = preserves;
666    }
667
668    /// Whether the shader declared it returns zero over a transparent input.
669    pub fn preserves_transparency(&self) -> bool {
670        self.preserves_transparency
671    }
672
673    /// Declares the low-frequency copies of its source the shader reads
674    /// through the reserved substrate region slots, in slot order. Only a
675    /// batched shader packed with its stage is handed them; a shader
676    /// without finds the slots zero and samples the source itself.
677    ///
678    /// # Panics
679    ///
680    /// When more than [`MAX_SUBSTRATES`] are declared.
681    pub fn set_substrates(&mut self, substrates: &[SubstrateSpec]) {
682        assert!(
683            substrates.len() <= MAX_SUBSTRATES,
684            "a runtime shader declares at most {MAX_SUBSTRATES} substrates"
685        );
686        if self.substrates().len() == substrates.len()
687            && self
688                .substrates()
689                .iter()
690                .zip(substrates)
691                .all(|(existing, incoming)| existing.same_bits(incoming))
692        {
693            return;
694        }
695        self.specialization_mut().substrates = substrates.iter().copied().collect();
696    }
697
698    /// The substrates the shader declared, in slot order.
699    pub fn substrates(&self) -> &[SubstrateSpec] {
700        &self.specialization().substrates
701    }
702
703    /// Hashes the declared substrates and the draw split into `state`.
704    pub fn hash_substrates<H: std::hash::Hasher>(&self, state: &mut H) {
705        use std::hash::Hash;
706        self.substrates().len().hash(state);
707        for substrate in self.substrates() {
708            substrate.hash_bits(state);
709        }
710        self.draw_split().hash(state);
711    }
712
713    /// Declares an `override NAME: i32` the renderer sets to 1 and 2 to draw
714    /// the shader twice in the final pass, once for its interior and once
715    /// for its rim, each pipeline compiled without the other's work and
716    /// discarding the other's fragments before its fetches. Nothing else
717    /// about the draw changes: the two draws partition the pixels the one
718    /// draw shaded and land on the same bits.
719    pub fn set_draw_split(&mut self, override_name: Option<&'static str>) {
720        if self.draw_split() == override_name {
721            return;
722        }
723        self.specialization_mut().draw_split = override_name;
724    }
725
726    /// The override selecting the interior or the rim draw, when declared.
727    pub fn draw_split(&self) -> Option<&'static str> {
728        self.specialization().draw_split
729    }
730
731    /// Declares that every override and the draw split of this shader are
732    /// folds: a specialized pipeline lands on the same bytes as the general
733    /// pipeline, which reads every folded value from its uniform. The
734    /// renderer then compiles specializations in the background and draws
735    /// with the general pipeline until they land. An override that selects
736    /// a different picture, such as a pass switch, must leave this unset;
737    /// its pipeline compiles inside the frame that first draws it.
738    /// An explicitly requested warm-up uses an existing general pipeline
739    /// while pending, or finishes before drawing if none exists.
740    pub fn set_specialization_exact(&mut self, exact: bool) {
741        if self.specialization_exact() == exact {
742            return;
743        }
744        self.specialization_mut().exact = exact;
745    }
746
747    /// Whether the shader declared its specialization exact.
748    pub fn specialization_exact(&self) -> bool {
749        self.specialization().exact
750    }
751
752    /// Get the WGSL source code.
753    pub fn source(&self) -> &str {
754        &self.source
755    }
756
757    /// Get the uniform data as a float slice (for uploading to GPU).
758    pub fn uniforms(&self) -> &[f32] {
759        self.uniforms.as_slice()
760    }
761
762    /// Get the uniform data padded to full 256-float array (for GPU uniform buffer).
763    pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
764        let mut padded = [0.0f32; Self::MAX_UNIFORMS];
765        let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
766        padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
767        padded
768    }
769
770    /// Compute a hash of the shader source for pipeline caching.
771    pub fn source_hash(&self) -> u64 {
772        self.source_hash
773    }
774
775    fn try_ensure_capacity(
776        &mut self,
777        index: usize,
778        width: usize,
779    ) -> Result<(), RuntimeShaderUniformError> {
780        let min_len = index
781            .checked_add(width)
782            .ok_or_else(|| Self::uniform_range_error(index, width))?;
783        if min_len > Self::MAX_USER_UNIFORMS {
784            return Err(Self::uniform_range_error(index, width));
785        }
786        self.uniforms.ensure_len(min_len);
787        Ok(())
788    }
789
790    fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
791        RuntimeShaderUniformError::OutOfUserRange {
792            index,
793            width,
794            max_user_uniforms: Self::MAX_USER_UNIFORMS,
795            reserved_start: Self::RESERVED_UNIFORM_START,
796            max_uniforms: Self::MAX_UNIFORMS,
797        }
798    }
799}
800
801#[cfg(test)]
802thread_local! {
803    static OVERRIDE_HASH_COMPUTATIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
804}
805
806impl PartialEq for RuntimeShader {
807    fn eq(&self, other: &Self) -> bool {
808        self.source_hash == other.source_hash
809            && (Arc::ptr_eq(&self.source, &other.source)
810                || self.source.as_ref() == other.source.as_ref())
811            && self.uniforms == other.uniforms
812            && self.overrides().len() == other.overrides().len()
813            && self
814                .overrides()
815                .iter()
816                .zip(other.overrides())
817                .all(|(a, b)| a.0 == b.0 && a.1.to_bits() == b.1.to_bits())
818            && self.input_padding.to_bits() == other.input_padding.to_bits()
819            && self.output_padding.to_bits() == other.output_padding.to_bits()
820            && self.batched_source == other.batched_source
821            && self.position_independent == other.position_independent
822            && self.preserves_transparency == other.preserves_transparency
823            && self.substrates() == other.substrates()
824            && self.draw_split() == other.draw_split()
825            && self.domains == other.domains
826    }
827}
828
829fn hash_shader_source(source: &str) -> u64 {
830    hash_shader_bytes(source.bytes())
831}
832
833fn hash_shader_bytes(bytes: impl IntoIterator<Item = u8>) -> u64 {
834    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
835    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
836
837    bytes.into_iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
838        (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME)
839    })
840}
841
842#[derive(Clone, Copy, Debug, PartialEq, Eq)]
843struct ShaderSourceCallsite {
844    file: &'static str,
845    line: u32,
846    column: u32,
847}
848
849struct CachedShaderSource {
850    callsite: ShaderSourceCallsite,
851    source_hash: u64,
852    source: Arc<str>,
853}
854
855struct CachedSharedShaderSourceHash {
856    byte_ptr: usize,
857    len: usize,
858    source_hash: u64,
859    source: Weak<str>,
860}
861
862fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
863    static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
864    let byte_ptr = source.as_ptr() as usize;
865    let len = source.len();
866    let mut cache = CACHE
867        .get_or_init(|| Mutex::new(Vec::new()))
868        .lock()
869        .unwrap_or_else(PoisonError::into_inner);
870
871    cache.retain(|entry| entry.source.strong_count() > 0);
872    if let Some(entry) = cache.iter().find(|entry| {
873        entry.byte_ptr == byte_ptr
874            && entry.len == len
875            && entry
876                .source
877                .upgrade()
878                .is_some_and(|cached| Arc::ptr_eq(&cached, source))
879    }) {
880        return entry.source_hash;
881    }
882
883    let source_hash = hash_shader_source(source);
884    cache.push(CachedSharedShaderSourceHash {
885        byte_ptr,
886        len,
887        source_hash,
888        source: Arc::downgrade(source),
889    });
890    source_hash
891}
892
893fn cached_shader_source(
894    location: &'static std::panic::Location<'static>,
895    source: &str,
896) -> (Arc<str>, u64) {
897    static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
898    let callsite = ShaderSourceCallsite {
899        file: location.file(),
900        line: location.line(),
901        column: location.column(),
902    };
903    let mut cache = CACHE
904        .get_or_init(|| Mutex::new(Vec::new()))
905        .lock()
906        .unwrap_or_else(PoisonError::into_inner);
907
908    if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
909        if entry.source.as_ref() == source {
910            return (entry.source.clone(), entry.source_hash);
911        }
912        let source_hash = hash_shader_source(source);
913        entry.source_hash = source_hash;
914        entry.source = Arc::<str>::from(source);
915        return (entry.source.clone(), entry.source_hash);
916    }
917
918    let source_hash = hash_shader_source(source);
919    let shared = Arc::<str>::from(source);
920    cache.push(CachedShaderSource {
921        callsite,
922        source_hash,
923        source: shared.clone(),
924    });
925    (shared, source_hash)
926}
927
928/// Where a runtime shader's pipeline draws, which decides how its output
929/// blends: `Page` composites the shader over what lies beneath (a backdrop
930/// effect, or a render effect the renderer draws straight onto the page),
931/// `Layer` renders into the layer's own texture, whose content the shader
932/// replaces (a render effect under a blend mode or clip the page draw cannot
933/// apply, such as a `DstOut` mask).
934#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
935pub enum ShaderTarget {
936    Page,
937    Layer,
938}
939
940/// A runtime shader to compile before its first draw, at the target it will
941/// draw to, so a renderer's background compiler builds the pipeline at
942/// start instead of inside the frame that first needs it.
943#[derive(Clone, Debug, PartialEq)]
944pub struct ShaderWarmUp {
945    pub shader: RuntimeShader,
946    pub target: ShaderTarget,
947}
948
949/// A render effect applied to a graphics layer's rendered content.
950///
951/// Matches Jetpack Compose's `RenderEffect` sealed class hierarchy,
952/// extended with `Shader` for custom WGSL effects.
953#[derive(Clone, Debug, PartialEq)]
954pub enum RenderEffect {
955    /// Gaussian blur applied to the layer's rendered content.
956    Blur {
957        radius_x: f32,
958        radius_y: f32,
959        edge_treatment: TileMode,
960    },
961    /// Offset the rendered content by a fixed amount.
962    Offset { offset_x: f32, offset_y: f32 },
963    /// Apply a custom WGSL shader effect.
964    Shader {
965        /// Shared shader configuration; use [`Arc::make_mut`] to edit a cloned effect independently.
966        shader: Arc<RuntimeShader>,
967    },
968    /// Chain two effects: apply `first`, then apply `second` to the result.
969    ///
970    /// Child effects are shared; use [`Arc::make_mut`] to edit a cloned chain independently.
971    Chain {
972        first: Arc<RenderEffect>,
973        second: Arc<RenderEffect>,
974    },
975}
976
977impl RenderEffect {
978    /// Create a blur effect with equal radius in both directions.
979    pub fn blur(radius: f32) -> Self {
980        Self::blur_with_edge_treatment(radius, TileMode::default())
981    }
982
983    /// Create a blur effect with equal radius in both directions and explicit
984    /// edge treatment semantics.
985    pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
986        Self::Blur {
987            radius_x: radius,
988            radius_y: radius,
989            edge_treatment,
990        }
991    }
992
993    /// Create a blur effect with separate horizontal and vertical radii.
994    pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
995        Self::Blur {
996            radius_x,
997            radius_y,
998            edge_treatment,
999        }
1000    }
1001
1002    /// Create an offset effect.
1003    pub fn offset(offset_x: f32, offset_y: f32) -> Self {
1004        Self::Offset { offset_x, offset_y }
1005    }
1006
1007    /// Create a custom shader effect from a RuntimeShader.
1008    pub fn runtime_shader(shader: RuntimeShader) -> Self {
1009        Self::Shader {
1010            shader: Arc::new(shader),
1011        }
1012    }
1013
1014    /// Chain this effect with another: `self` is applied first, then `other`.
1015    pub fn then(self, other: RenderEffect) -> Self {
1016        Self::Chain {
1017            first: Arc::new(self),
1018            second: Arc::new(other),
1019        }
1020    }
1021
1022    /// Returns `true` if this effect or any chained sub-effect is a
1023    /// `RuntimeShader`. Animated shaders produce different output every frame,
1024    /// so layer surface caching is counterproductive for them.
1025    pub fn contains_runtime_shader(&self) -> bool {
1026        match self {
1027            RenderEffect::Shader { .. } => true,
1028            RenderEffect::Chain { first, second } => {
1029                first.contains_runtime_shader() || second.contains_runtime_shader()
1030            }
1031            _ => false,
1032        }
1033    }
1034
1035    /// Whether the effect returns zero over a transparent input: a blur or
1036    /// an offset of nothing is nothing, a shader when it declares so, and a
1037    /// chain when every step does.
1038    pub fn preserves_transparency(&self) -> bool {
1039        match self {
1040            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => true,
1041            RenderEffect::Shader { shader } => shader.preserves_transparency(),
1042            RenderEffect::Chain { first, second } => {
1043                first.preserves_transparency() && second.preserves_transparency()
1044            }
1045        }
1046    }
1047
1048    /// Maximum logical-pixel input padding required by this effect.
1049    pub fn input_padding(&self) -> f32 {
1050        match self {
1051            RenderEffect::Blur {
1052                radius_x, radius_y, ..
1053            } => radius_x.abs().max(radius_y.abs()),
1054            RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
1055            RenderEffect::Shader { shader } => shader.input_padding(),
1056            RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
1057        }
1058    }
1059
1060    /// Maximum logical-pixel distance this effect WRITES outside its rect.
1061    /// Only runtime shaders may declare one (SDF coverage past node bounds);
1062    /// blur/offset stay confined to their tight rect.
1063    pub fn output_padding(&self) -> f32 {
1064        match self {
1065            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
1066            RenderEffect::Shader { shader } => shader.output_padding(),
1067            RenderEffect::Chain { first, second } => {
1068                first.output_padding() + second.output_padding()
1069            }
1070        }
1071    }
1072
1073    /// The rect outside which this effect writes nothing, in its logical
1074    /// space with the origin at its rect's top-left, when the stage that
1075    /// produces its output declared one; blur and offset write their whole
1076    /// rect and declare none.
1077    pub fn output_support(&self) -> Option<Rect> {
1078        match self {
1079            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
1080            RenderEffect::Shader { shader } => shader.output_support(),
1081            RenderEffect::Chain { second, .. } => second.output_support(),
1082        }
1083    }
1084
1085    /// The rect outside which the stage that produces this effect's output
1086    /// never samples what it is given, when it declared one; the whole
1087    /// input otherwise. A blur samples everything it writes and more.
1088    pub fn sample_domain(&self) -> Option<Rect> {
1089        match self {
1090            RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
1091            RenderEffect::Shader { shader } => shader.sample_domain(),
1092            RenderEffect::Chain { second, .. } => second.sample_domain(),
1093        }
1094    }
1095}
1096
1097#[cfg(test)]
1098#[path = "tests/render_effect_tests.rs"]
1099mod tests;