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