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;