1use std::sync::{Arc, Mutex, OnceLock, Weak};
7
8use crate::{LayerShape, Rect};
9
10const RUNTIME_SHADER_INLINE_UNIFORMS: usize = 16;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
14pub enum TileMode {
15 #[default]
17 Clamp,
18 Repeated,
20 Mirror,
22 Decal,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct BlurredEdgeTreatment {
33 shape: Option<LayerShape>,
34}
35
36impl BlurredEdgeTreatment {
37 pub const RECTANGLE: Self = Self {
39 shape: Some(LayerShape::Rectangle),
40 };
41
42 pub const UNBOUNDED: Self = Self { shape: None };
44
45 pub const fn with_shape(shape: LayerShape) -> Self {
47 Self { shape: Some(shape) }
48 }
49
50 pub fn shape(self) -> Option<LayerShape> {
51 self.shape
52 }
53
54 pub fn clip(self) -> bool {
55 self.shape.is_some()
56 }
57
58 pub fn tile_mode(self) -> TileMode {
59 if self.clip() {
60 TileMode::Clamp
61 } else {
62 TileMode::Decal
63 }
64 }
65}
66
67impl Default for BlurredEdgeTreatment {
68 fn default() -> Self {
69 Self::RECTANGLE
70 }
71}
72
73pub const RUNTIME_SHADER_PRELUDE_WGSL: &str = concat!(
78 include_str!("../shaders/fullscreen_quad_vs.wgsl"),
79 include_str!("../shaders/runtime_shader_bindings.wgsl"),
80);
81
82#[derive(Clone, Debug)]
122pub struct RuntimeShader {
123 source: Arc<str>,
124 source_hash: u64,
125 uniforms: RuntimeShaderUniforms,
126 overrides: Vec<(&'static str, f64)>,
127 input_padding: f32,
128 output_padding: f32,
129 batched_source: bool,
130 substrates: Vec<SubstrateSpec>,
131 draw_split: Option<&'static str>,
132 domains: Option<Box<ShaderDomains>>,
133}
134
135#[derive(Clone, Copy, Debug, Default, PartialEq)]
136struct ShaderDomains {
137 output_support: Option<Rect>,
138 sample_domain: Option<Rect>,
139}
140
141fn finite_rect(rect: Option<Rect>) -> Option<Rect> {
142 rect.filter(|rect| {
143 rect.x.is_finite()
144 && rect.y.is_finite()
145 && rect.width.is_finite()
146 && rect.height.is_finite()
147 })
148}
149
150pub const MAX_SUBSTRATES: usize = 3;
152
153#[derive(Clone, Copy, Debug, PartialEq)]
156pub enum SubstrateSpec {
157 Average { block: u32 },
160 Blur { radius_px: f32 },
163}
164
165impl SubstrateSpec {
166 fn hash_bits<H: std::hash::Hasher>(&self, state: &mut H) {
167 use std::hash::Hash;
168 match self {
169 Self::Average { block } => {
170 0u8.hash(state);
171 block.hash(state);
172 }
173 Self::Blur { radius_px } => {
174 1u8.hash(state);
175 radius_px.to_bits().hash(state);
176 }
177 }
178 }
179}
180
181#[derive(Clone, Debug, PartialEq)]
182struct RuntimeShaderUniforms {
183 len: usize,
184 inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
185 heap: Option<Vec<f32>>,
186}
187
188impl RuntimeShaderUniforms {
189 fn new() -> Self {
190 Self {
191 len: 0,
192 inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
193 heap: None,
194 }
195 }
196
197 fn as_slice(&self) -> &[f32] {
198 if let Some(heap) = &self.heap {
199 heap.as_slice()
200 } else {
201 &self.inline[..self.len]
202 }
203 }
204
205 fn len(&self) -> usize {
206 self.as_slice().len()
207 }
208
209 fn ensure_len(&mut self, min_len: usize) {
210 if let Some(heap) = &mut self.heap {
211 if heap.len() < min_len {
212 heap.resize(min_len, 0.0);
213 }
214 return;
215 }
216
217 if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
218 self.len = self.len.max(min_len);
219 return;
220 }
221
222 let mut heap = Vec::with_capacity(min_len);
223 heap.extend_from_slice(&self.inline[..self.len]);
224 heap.resize(min_len, 0.0);
225 self.heap = Some(heap);
226 }
227
228 fn set(&mut self, index: usize, value: f32) {
229 if let Some(heap) = &mut self.heap {
230 heap[index] = value;
231 } else {
232 self.inline[index] = value;
233 }
234 }
235
236 #[cfg(test)]
237 fn is_inline(&self) -> bool {
238 self.heap.is_none()
239 }
240}
241
242#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
244pub enum RuntimeShaderUniformError {
245 #[error(
246 "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"
247 )]
248 OutOfUserRange {
249 index: usize,
250 width: usize,
251 max_user_uniforms: usize,
252 reserved_start: usize,
253 max_uniforms: usize,
254 },
255}
256
257impl RuntimeShader {
258 pub const MAX_UNIFORMS: usize = 256;
262 pub const RESERVED_UNIFORM_START: usize = 224;
264 pub const SUBSTRATE_REGION_UNIFORMS: [usize; MAX_SUBSTRATES] = [232, 228, 224];
267 pub const SOURCE_REGION_UNIFORM: usize = 236;
269 pub const MASK_RECT_UNIFORM: usize = 240;
271 pub const MASK_RADII_UNIFORM: usize = 244;
273 pub const EFFECT_RECT_UNIFORM: usize = 248;
275 pub const LOGICAL_SIZE_UNIFORM: usize = 252;
277 pub const ALPHA_UNIFORM: usize = 254;
279 pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
281
282 #[track_caller]
284 pub fn new(wgsl_source: &str) -> Self {
285 let (source, source_hash) =
286 cached_shader_source(std::panic::Location::caller(), wgsl_source);
287 Self::with_source(source, source_hash)
288 }
289
290 pub fn from_shared_source(source: Arc<str>) -> Self {
295 let source_hash = cached_shared_shader_source_hash(&source);
296 Self::with_source(source, source_hash)
297 }
298
299 fn with_source(source: Arc<str>, source_hash: u64) -> Self {
300 Self {
301 source,
302 source_hash,
303 uniforms: RuntimeShaderUniforms::new(),
304 overrides: Vec::new(),
305 input_padding: 0.0,
306 output_padding: 0.0,
307 batched_source: false,
308 substrates: Vec::new(),
309 draw_split: None,
310 domains: None,
311 }
312 }
313
314 pub fn set_override(&mut self, name: &'static str, value: f64) {
321 match self
322 .overrides
323 .binary_search_by(|(existing, _)| existing.cmp(&name))
324 {
325 Ok(index) => self.overrides[index].1 = value,
326 Err(index) => self.overrides.insert(index, (name, value)),
327 }
328 }
329
330 pub fn clear_override(&mut self, name: &str) -> bool {
332 let Ok(index) = self
333 .overrides
334 .binary_search_by(|(existing, _)| (*existing).cmp(name))
335 else {
336 return false;
337 };
338 self.overrides.remove(index);
339 true
340 }
341
342 pub fn overrides(&self) -> &[(&'static str, f64)] {
345 &self.overrides
346 }
347
348 pub fn overrides_hash(&self) -> u64 {
350 if self.overrides.is_empty() {
351 return 0;
352 }
353 let mut bytes = Vec::new();
354 for (name, value) in &self.overrides {
355 bytes.extend_from_slice(name.as_bytes());
356 bytes.push(0);
357 bytes.extend_from_slice(&value.to_bits().to_le_bytes());
358 }
359 hash_shader_bytes(&bytes)
360 }
361
362 pub fn set_input_padding(&mut self, padding: f32) {
366 self.input_padding = if padding.is_finite() {
367 padding.max(0.0)
368 } else {
369 0.0
370 };
371 }
372
373 pub fn input_padding(&self) -> f32 {
375 self.input_padding
376 }
377
378 pub fn set_output_padding(&mut self, padding: f32) {
383 self.output_padding = if padding.is_finite() {
384 padding.max(0.0)
385 } else {
386 0.0
387 };
388 }
389
390 pub fn output_padding(&self) -> f32 {
392 self.output_padding
393 }
394
395 pub fn set_output_support(&mut self, support: Option<Rect>) {
406 self.set_domains(ShaderDomains {
407 output_support: finite_rect(support),
408 sample_domain: self.sample_domain(),
409 });
410 }
411
412 pub fn output_support(&self) -> Option<Rect> {
414 self.domains
415 .as_ref()
416 .and_then(|domains| domains.output_support)
417 }
418
419 fn set_domains(&mut self, domains: ShaderDomains) {
420 self.domains = (domains != ShaderDomains::default()).then(|| Box::new(domains));
421 }
422
423 pub fn set_sample_domain(&mut self, domain: Option<Rect>) {
432 self.set_domains(ShaderDomains {
433 output_support: self.output_support(),
434 sample_domain: finite_rect(domain),
435 });
436 }
437
438 pub fn sample_domain(&self) -> Option<Rect> {
440 self.domains
441 .as_ref()
442 .and_then(|domains| domains.sample_domain)
443 }
444
445 pub fn set_float(&mut self, index: usize, value: f32) {
450 let _ = self.try_set_float(index, value);
451 }
452
453 pub fn try_set_float(
455 &mut self,
456 index: usize,
457 value: f32,
458 ) -> Result<(), RuntimeShaderUniformError> {
459 self.try_ensure_capacity(index, 1)?;
460 self.uniforms.set(index, value);
461 Ok(())
462 }
463
464 pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
469 let _ = self.try_set_float2(index, x, y);
470 }
471
472 pub fn try_set_float2(
474 &mut self,
475 index: usize,
476 x: f32,
477 y: f32,
478 ) -> Result<(), RuntimeShaderUniformError> {
479 self.try_ensure_capacity(index, 2)?;
480 self.uniforms.set(index, x);
481 self.uniforms.set(index + 1, y);
482 Ok(())
483 }
484
485 pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
490 let _ = self.try_set_float4(index, x, y, z, w);
491 }
492
493 pub fn try_set_float4(
495 &mut self,
496 index: usize,
497 x: f32,
498 y: f32,
499 z: f32,
500 w: f32,
501 ) -> Result<(), RuntimeShaderUniformError> {
502 self.try_ensure_capacity(index, 4)?;
503 self.uniforms.set(index, x);
504 self.uniforms.set(index + 1, y);
505 self.uniforms.set(index + 2, z);
506 self.uniforms.set(index + 3, w);
507 Ok(())
508 }
509
510 pub fn set_batched_source(&mut self, batched: bool) {
515 self.batched_source = batched;
516 }
517
518 pub fn batched_source(&self) -> bool {
521 self.batched_source
522 }
523
524 pub fn set_substrates(&mut self, substrates: Vec<SubstrateSpec>) {
533 assert!(
534 substrates.len() <= MAX_SUBSTRATES,
535 "a runtime shader declares at most {MAX_SUBSTRATES} substrates"
536 );
537 self.substrates = substrates;
538 }
539
540 pub fn substrates(&self) -> &[SubstrateSpec] {
542 &self.substrates
543 }
544
545 pub fn hash_substrates<H: std::hash::Hasher>(&self, state: &mut H) {
547 use std::hash::Hash;
548 self.substrates.len().hash(state);
549 for substrate in &self.substrates {
550 substrate.hash_bits(state);
551 }
552 self.draw_split.hash(state);
553 }
554
555 pub fn set_draw_split(&mut self, override_name: Option<&'static str>) {
562 self.draw_split = override_name;
563 }
564
565 pub fn draw_split(&self) -> Option<&'static str> {
567 self.draw_split
568 }
569
570 pub fn source(&self) -> &str {
572 &self.source
573 }
574
575 pub fn uniforms(&self) -> &[f32] {
577 self.uniforms.as_slice()
578 }
579
580 pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
582 let mut padded = [0.0f32; Self::MAX_UNIFORMS];
583 let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
584 padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
585 padded
586 }
587
588 pub fn source_hash(&self) -> u64 {
590 self.source_hash
591 }
592
593 fn try_ensure_capacity(
594 &mut self,
595 index: usize,
596 width: usize,
597 ) -> Result<(), RuntimeShaderUniformError> {
598 let min_len = index
599 .checked_add(width)
600 .ok_or_else(|| Self::uniform_range_error(index, width))?;
601 if min_len > Self::MAX_USER_UNIFORMS {
602 return Err(Self::uniform_range_error(index, width));
603 }
604 self.uniforms.ensure_len(min_len);
605 Ok(())
606 }
607
608 fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
609 RuntimeShaderUniformError::OutOfUserRange {
610 index,
611 width,
612 max_user_uniforms: Self::MAX_USER_UNIFORMS,
613 reserved_start: Self::RESERVED_UNIFORM_START,
614 max_uniforms: Self::MAX_UNIFORMS,
615 }
616 }
617}
618
619impl PartialEq for RuntimeShader {
620 fn eq(&self, other: &Self) -> bool {
621 self.source_hash == other.source_hash
622 && (Arc::ptr_eq(&self.source, &other.source)
623 || self.source.as_ref() == other.source.as_ref())
624 && self.uniforms == other.uniforms
625 && self.overrides.len() == other.overrides.len()
626 && self
627 .overrides
628 .iter()
629 .zip(&other.overrides)
630 .all(|(a, b)| a.0 == b.0 && a.1.to_bits() == b.1.to_bits())
631 && self.input_padding.to_bits() == other.input_padding.to_bits()
632 && self.output_padding.to_bits() == other.output_padding.to_bits()
633 && self.batched_source == other.batched_source
634 && self.substrates == other.substrates
635 && self.draw_split == other.draw_split
636 && self.domains == other.domains
637 }
638}
639
640fn hash_shader_source(source: &str) -> u64 {
641 hash_shader_bytes(source.as_bytes())
642}
643
644fn hash_shader_bytes(bytes: &[u8]) -> u64 {
645 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
646 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
647
648 bytes.iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
649 (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
650 })
651}
652
653#[derive(Clone, Copy, Debug, PartialEq, Eq)]
654struct ShaderSourceCallsite {
655 file: &'static str,
656 line: u32,
657 column: u32,
658}
659
660struct CachedShaderSource {
661 callsite: ShaderSourceCallsite,
662 source_hash: u64,
663 source: Arc<str>,
664}
665
666struct CachedSharedShaderSourceHash {
667 byte_ptr: usize,
668 len: usize,
669 source_hash: u64,
670 source: Weak<str>,
671}
672
673fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
674 static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
675 let byte_ptr = source.as_ptr() as usize;
676 let len = source.len();
677 let mut cache = CACHE
678 .get_or_init(|| Mutex::new(Vec::new()))
679 .lock()
680 .unwrap_or_else(|poisoned| poisoned.into_inner());
681
682 cache.retain(|entry| entry.source.strong_count() > 0);
683 if let Some(entry) = cache.iter().find(|entry| {
684 entry.byte_ptr == byte_ptr
685 && entry.len == len
686 && entry
687 .source
688 .upgrade()
689 .is_some_and(|cached| Arc::ptr_eq(&cached, source))
690 }) {
691 return entry.source_hash;
692 }
693
694 let source_hash = hash_shader_source(source);
695 cache.push(CachedSharedShaderSourceHash {
696 byte_ptr,
697 len,
698 source_hash,
699 source: Arc::downgrade(source),
700 });
701 source_hash
702}
703
704fn cached_shader_source(
705 location: &'static std::panic::Location<'static>,
706 source: &str,
707) -> (Arc<str>, u64) {
708 static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
709 let callsite = ShaderSourceCallsite {
710 file: location.file(),
711 line: location.line(),
712 column: location.column(),
713 };
714 let mut cache = CACHE
715 .get_or_init(|| Mutex::new(Vec::new()))
716 .lock()
717 .unwrap_or_else(|poisoned| poisoned.into_inner());
718
719 if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
720 if entry.source.as_ref() == source {
721 return (entry.source.clone(), entry.source_hash);
722 }
723 let source_hash = hash_shader_source(source);
724 entry.source_hash = source_hash;
725 entry.source = Arc::<str>::from(source);
726 return (entry.source.clone(), entry.source_hash);
727 }
728
729 let source_hash = hash_shader_source(source);
730 let shared = Arc::<str>::from(source);
731 cache.push(CachedShaderSource {
732 callsite,
733 source_hash,
734 source: shared.clone(),
735 });
736 (shared, source_hash)
737}
738
739#[derive(Clone, Debug, PartialEq)]
744pub enum RenderEffect {
745 Blur {
747 radius_x: f32,
748 radius_y: f32,
749 edge_treatment: TileMode,
750 },
751 Offset { offset_x: f32, offset_y: f32 },
753 Shader { shader: RuntimeShader },
755 Chain {
757 first: Box<RenderEffect>,
758 second: Box<RenderEffect>,
759 },
760}
761
762impl RenderEffect {
763 pub fn blur(radius: f32) -> Self {
765 Self::blur_with_edge_treatment(radius, TileMode::default())
766 }
767
768 pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
771 Self::Blur {
772 radius_x: radius,
773 radius_y: radius,
774 edge_treatment,
775 }
776 }
777
778 pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
780 Self::Blur {
781 radius_x,
782 radius_y,
783 edge_treatment,
784 }
785 }
786
787 pub fn offset(offset_x: f32, offset_y: f32) -> Self {
789 Self::Offset { offset_x, offset_y }
790 }
791
792 pub fn runtime_shader(shader: RuntimeShader) -> Self {
794 Self::Shader { shader }
795 }
796
797 pub fn then(self, other: RenderEffect) -> Self {
799 Self::Chain {
800 first: Box::new(self),
801 second: Box::new(other),
802 }
803 }
804
805 pub fn contains_runtime_shader(&self) -> bool {
809 match self {
810 RenderEffect::Shader { .. } => true,
811 RenderEffect::Chain { first, second } => {
812 first.contains_runtime_shader() || second.contains_runtime_shader()
813 }
814 _ => false,
815 }
816 }
817
818 pub fn input_padding(&self) -> f32 {
820 match self {
821 RenderEffect::Blur {
822 radius_x, radius_y, ..
823 } => radius_x.abs().max(radius_y.abs()),
824 RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
825 RenderEffect::Shader { shader } => shader.input_padding(),
826 RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
827 }
828 }
829
830 pub fn output_padding(&self) -> f32 {
834 match self {
835 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
836 RenderEffect::Shader { shader } => shader.output_padding(),
837 RenderEffect::Chain { first, second } => {
838 first.output_padding() + second.output_padding()
839 }
840 }
841 }
842
843 pub fn output_support(&self) -> Option<Rect> {
848 match self {
849 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
850 RenderEffect::Shader { shader } => shader.output_support(),
851 RenderEffect::Chain { second, .. } => second.output_support(),
852 }
853 }
854
855 pub fn sample_domain(&self) -> Option<Rect> {
859 match self {
860 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
861 RenderEffect::Shader { shader } => shader.sample_domain(),
862 RenderEffect::Chain { second, .. } => second.sample_domain(),
863 }
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 #[test]
870 fn overrides_stay_sorted_and_replace_by_name() {
871 let mut shader = super::RuntimeShader::new("// overrides");
872 shader.set_override("ZETA", 1.0);
873 shader.set_override("ALPHA", 0.0);
874 shader.set_override("ZETA", 2.0);
875 assert_eq!(shader.overrides(), &[("ALPHA", 0.0), ("ZETA", 2.0)]);
876 }
877
878 #[test]
879 fn clear_override_removes_present_name_and_preserves_remaining_set() {
880 let mut shader = super::RuntimeShader::new("");
881 shader.set_override("ZETA", 1.0);
882 shader.set_override("ALPHA", 2.0);
883 let mut expected = super::RuntimeShader::new("");
884 expected.set_override("ALPHA", 2.0);
885 assert!(shader.clear_override("ZETA"));
886 assert!(!shader.clear_override("MISSING"));
887 assert_eq!(shader.overrides(), &[("ALPHA", 2.0)]);
888 assert_eq!(shader.overrides_hash(), expected.overrides_hash());
889 assert!(shader.clear_override("ALPHA"));
890 assert!(shader.overrides().is_empty());
891 assert_eq!(shader.overrides_hash(), 0);
892 }
893
894 #[test]
895 fn overrides_distinguish_otherwise_equal_shaders() {
896 let plain = super::RuntimeShader::new("// overrides-eq");
897 let mut raised = plain.clone();
898 raised.set_override("FLAG", 1.0);
899 assert_eq!(plain.overrides_hash(), 0);
900 assert_ne!(plain.overrides_hash(), raised.overrides_hash());
901 assert_ne!(plain, raised);
902 let mut lowered = raised.clone();
903 lowered.set_override("FLAG", 0.0);
904 assert_ne!(raised.overrides_hash(), lowered.overrides_hash());
905 assert_ne!(raised, lowered);
906 }
907
908 use super::*;
909 use crate::RoundedCornerShape;
910
911 #[test]
912 fn runtime_shader_set_uniforms() {
913 let mut shader = RuntimeShader::new("// test");
914 shader.set_float(0, 1.0);
915 shader.set_float2(2, 3.0, 4.0);
916 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
917
918 assert_eq!(shader.uniforms()[0], 1.0);
919 assert_eq!(shader.uniforms()[1], 0.0);
920 assert_eq!(shader.uniforms()[2], 3.0);
921 assert_eq!(shader.uniforms()[3], 4.0);
922 assert_eq!(shader.uniforms()[4], 5.0);
923 assert_eq!(shader.uniforms()[5], 6.0);
924 assert_eq!(shader.uniforms()[6], 7.0);
925 assert_eq!(shader.uniforms()[7], 8.0);
926 }
927
928 #[test]
929 fn runtime_shader_padded() {
930 let mut shader = RuntimeShader::new("// test");
931 shader.set_float(0, 42.0);
932 let padded = shader.uniforms_padded();
933 assert_eq!(padded[0], 42.0);
934 assert_eq!(padded[1], 0.0);
935 assert_eq!(padded[255], 0.0);
936 }
937
938 #[test]
939 fn blur_and_offset_declare_input_padding() {
940 assert_eq!(
941 RenderEffect::blur_xy(6.0, 12.0, TileMode::Clamp).input_padding(),
942 12.0
943 );
944 assert_eq!(RenderEffect::offset(-8.0, 3.0).input_padding(), 8.0);
945 }
946
947 #[test]
948 fn chained_effect_padding_accumulates_sampling_ranges() {
949 let mut shader = RuntimeShader::new("// test");
950 shader.set_input_padding(9.0);
951 let effect = RenderEffect::blur_xy(4.0, 6.0, TileMode::Clamp)
952 .then(RenderEffect::runtime_shader(shader))
953 .then(RenderEffect::offset(2.0, -5.0));
954
955 assert_eq!(effect.input_padding(), 20.0);
956 }
957
958 #[test]
959 fn runtime_shader_keeps_common_uniform_payload_inline() {
960 let mut shader = RuntimeShader::new("// test");
961 shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
962 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
963 shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
964 shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
965
966 assert!(shader.uniforms.is_inline());
967 assert_eq!(shader.uniforms().len(), 16);
968
969 shader.set_float(16, 17.0);
970 assert!(!shader.uniforms.is_inline());
971 assert_eq!(shader.uniforms()[16], 17.0);
972 }
973
974 #[test]
975 fn runtime_shader_try_set_reports_reserved_uniform_slots() {
976 let mut shader = RuntimeShader::new("// test");
977
978 let err = shader
979 .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
980 .unwrap_err();
981 assert_eq!(
982 err,
983 RuntimeShaderUniformError::OutOfUserRange {
984 index: RuntimeShader::RESERVED_UNIFORM_START,
985 width: 1,
986 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
987 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
988 max_uniforms: RuntimeShader::MAX_UNIFORMS,
989 }
990 );
991 assert!(shader.uniforms().is_empty());
992
993 let err = shader
994 .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
995 .unwrap_err();
996 assert_eq!(
997 err,
998 RuntimeShaderUniformError::OutOfUserRange {
999 index: RuntimeShader::MAX_USER_UNIFORMS - 3,
1000 width: 4,
1001 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
1002 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
1003 max_uniforms: RuntimeShader::MAX_UNIFORMS,
1004 }
1005 );
1006 }
1007
1008 #[test]
1009 fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
1010 let mut shader = RuntimeShader::new("// test");
1011 shader.set_float(0, 7.0);
1012
1013 shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
1014 shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
1015
1016 assert_eq!(shader.uniforms(), &[7.0]);
1017 }
1018
1019 #[test]
1020 fn render_effect_chaining() {
1021 let blur = RenderEffect::blur(10.0);
1022 let offset = RenderEffect::offset(5.0, 5.0);
1023 let chained = blur.then(offset);
1024 match chained {
1025 RenderEffect::Chain { first, second } => {
1026 assert!(matches!(*first, RenderEffect::Blur { .. }));
1027 assert!(matches!(*second, RenderEffect::Offset { .. }));
1028 }
1029 _ => panic!("expected Chain"),
1030 }
1031 }
1032
1033 #[test]
1034 fn blur_convenience() {
1035 let effect = RenderEffect::blur(15.0);
1036 match effect {
1037 RenderEffect::Blur {
1038 radius_x,
1039 radius_y,
1040 edge_treatment,
1041 } => {
1042 assert_eq!(radius_x, 15.0);
1043 assert_eq!(radius_y, 15.0);
1044 assert_eq!(edge_treatment, TileMode::Clamp);
1045 }
1046 _ => panic!("expected Blur"),
1047 }
1048 }
1049
1050 #[test]
1051 fn blur_with_edge_treatment_uses_explicit_mode() {
1052 let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
1053 match effect {
1054 RenderEffect::Blur {
1055 radius_x,
1056 radius_y,
1057 edge_treatment,
1058 } => {
1059 assert_eq!(radius_x, 6.0);
1060 assert_eq!(radius_y, 6.0);
1061 assert_eq!(edge_treatment, TileMode::Decal);
1062 }
1063 _ => panic!("expected Blur"),
1064 }
1065 }
1066
1067 #[test]
1068 fn source_hash_consistent() {
1069 let s1 = RuntimeShader::new("fn main() {}");
1070 let s2 = RuntimeShader::new("fn main() {}");
1071 assert_eq!(s1.source_hash(), s2.source_hash());
1072 }
1073
1074 #[test]
1075 fn runtime_shader_from_shared_source_reuses_shared_source() {
1076 let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
1077 let s1 = RuntimeShader::from_shared_source(source.clone());
1078 let s2 = RuntimeShader::from_shared_source(source);
1079
1080 assert!(Arc::ptr_eq(&s1.source, &s2.source));
1081 assert_eq!(s1.source_hash(), s2.source_hash());
1082 }
1083
1084 fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
1085 RuntimeShader::new(source)
1086 }
1087
1088 fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
1089 RuntimeShader::new(source)
1090 }
1091
1092 #[test]
1093 fn runtime_shader_new_reuses_same_callsite_source() {
1094 let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
1095 let s1 = runtime_shader_from_reuse_callsite(source);
1096 let s2 = runtime_shader_from_reuse_callsite(source);
1097
1098 assert!(Arc::ptr_eq(&s1.source, &s2.source));
1099 assert_eq!(s1.source_hash(), s2.source_hash());
1100 }
1101
1102 #[test]
1103 fn runtime_shader_new_replaces_changed_callsite_source() {
1104 let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
1105 let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
1106
1107 assert!(!Arc::ptr_eq(&s1.source, &s2.source));
1108 assert_ne!(s1.source_hash(), s2.source_hash());
1109 assert_eq!(s2.source(), "fn b() {}");
1110 }
1111
1112 #[test]
1113 fn runtime_shader_source_storage_has_no_process_global_interner() {
1114 let source = include_str!("render_effect.rs");
1115 let blocked_static = ["static ", "INTERNER"].concat();
1116 let blocked_type = ["ShaderSource", "Interner"].concat();
1117
1118 assert!(
1119 !source.contains(&blocked_static) && !source.contains(&blocked_type),
1120 "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
1121 );
1122 }
1123
1124 #[test]
1125 fn blur_xy_preserves_tile_mode() {
1126 let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
1127 match effect {
1128 RenderEffect::Blur {
1129 radius_x,
1130 radius_y,
1131 edge_treatment,
1132 } => {
1133 assert_eq!(radius_x, 3.0);
1134 assert_eq!(radius_y, 7.0);
1135 assert_eq!(edge_treatment, TileMode::Clamp);
1136 }
1137 _ => panic!("expected Blur"),
1138 }
1139 }
1140
1141 #[test]
1142 fn offset_constructor_sets_components() {
1143 let effect = RenderEffect::offset(11.0, -5.0);
1144 match effect {
1145 RenderEffect::Offset { offset_x, offset_y } => {
1146 assert_eq!(offset_x, 11.0);
1147 assert_eq!(offset_y, -5.0);
1148 }
1149 _ => panic!("expected Offset"),
1150 }
1151 }
1152
1153 #[test]
1154 fn runtime_shader_equality_is_source_value_based() {
1155 let mut s1 = RuntimeShader::new("fn main() {}");
1156 let mut s2 = RuntimeShader::new("fn main() {}");
1157 s1.set_float(0, 1.0);
1158 s2.set_float(0, 1.0);
1159 assert_eq!(s1, s2);
1160 }
1161
1162 #[test]
1163 fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
1164 let treatment = BlurredEdgeTreatment::default();
1165 assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
1166 assert!(treatment.clip());
1167 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1168 }
1169
1170 #[test]
1171 fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
1172 let treatment = BlurredEdgeTreatment::UNBOUNDED;
1173 assert_eq!(treatment.shape(), None);
1174 assert!(!treatment.clip());
1175 assert_eq!(treatment.tile_mode(), TileMode::Decal);
1176 }
1177
1178 #[test]
1179 fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
1180 let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
1181 let treatment = BlurredEdgeTreatment::with_shape(rounded);
1182 assert_eq!(treatment.shape(), Some(rounded));
1183 assert!(treatment.clip());
1184 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1185 }
1186
1187 #[test]
1188 fn an_effect_chains_output_support_is_the_support_of_the_stage_that_writes_its_output() {
1189 let mut shader = RuntimeShader::new("fn glass_fs() {}");
1190 assert_eq!(shader.output_support(), None);
1191 let support = Rect {
1192 x: 4.0,
1193 y: 6.0,
1194 width: 30.0,
1195 height: 12.0,
1196 };
1197 shader.set_output_support(Some(support));
1198 assert_eq!(shader.output_support(), Some(support));
1199 let effect = RenderEffect::blur(3.0).then(RenderEffect::Shader {
1200 shader: shader.clone(),
1201 });
1202 assert_eq!(effect.output_support(), Some(support));
1203 let effect = RenderEffect::Shader {
1204 shader: shader.clone(),
1205 }
1206 .then(RenderEffect::blur(3.0));
1207 assert_eq!(effect.output_support(), None);
1208 assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1209 }
1210
1211 #[test]
1212 fn a_sample_domain_is_the_writers_and_a_blur_declares_none() {
1213 let mut shader = RuntimeShader::new("fn glass_fs() {}");
1214 let domain = Rect {
1215 x: -2.0,
1216 y: -2.0,
1217 width: 20.0,
1218 height: 12.0,
1219 };
1220 let plain = shader.clone();
1221 shader.set_sample_domain(Some(domain));
1222 assert_ne!(shader, plain);
1223 assert_eq!(shader.sample_domain(), Some(domain));
1224 let effect = RenderEffect::blur(3.0).then(RenderEffect::Shader {
1225 shader: shader.clone(),
1226 });
1227 assert_eq!(effect.sample_domain(), Some(domain));
1228 assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1229 assert_eq!(RenderEffect::blur(3.0).sample_domain(), None);
1230 shader.set_sample_domain(Some(Rect {
1231 x: f32::INFINITY,
1232 ..domain
1233 }));
1234 assert_eq!(shader.sample_domain(), None);
1235 }
1236
1237 #[test]
1238 fn a_non_finite_output_support_clears_the_declaration_and_a_support_tells_shaders_apart() {
1239 let mut shader = RuntimeShader::new("fn glass_fs() {}");
1240 let plain = shader.clone();
1241 shader.set_output_support(Some(Rect {
1242 x: 0.0,
1243 y: 0.0,
1244 width: 10.0,
1245 height: 10.0,
1246 }));
1247 assert_ne!(shader, plain);
1248 shader.set_output_support(Some(Rect {
1249 x: 0.0,
1250 y: 0.0,
1251 width: f32::NAN,
1252 height: 10.0,
1253 }));
1254 assert_eq!(shader.output_support(), None);
1255 assert_eq!(shader, plain);
1256 }
1257}