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