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