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