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