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