1use std::sync::{Arc, Mutex, OnceLock, Weak};
7
8use crate::LayerShape;
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
73#[derive(Clone, Debug)]
90pub struct RuntimeShader {
91 source: Arc<str>,
92 source_hash: u64,
93 uniforms: RuntimeShaderUniforms,
94 overrides: Vec<(&'static str, f64)>,
95 input_padding: f32,
96 output_padding: f32,
97}
98
99#[derive(Clone, Debug, PartialEq)]
100struct RuntimeShaderUniforms {
101 len: usize,
102 inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
103 heap: Option<Vec<f32>>,
104}
105
106impl RuntimeShaderUniforms {
107 fn new() -> Self {
108 Self {
109 len: 0,
110 inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
111 heap: None,
112 }
113 }
114
115 fn as_slice(&self) -> &[f32] {
116 if let Some(heap) = &self.heap {
117 heap.as_slice()
118 } else {
119 &self.inline[..self.len]
120 }
121 }
122
123 fn len(&self) -> usize {
124 self.as_slice().len()
125 }
126
127 fn ensure_len(&mut self, min_len: usize) {
128 if let Some(heap) = &mut self.heap {
129 if heap.len() < min_len {
130 heap.resize(min_len, 0.0);
131 }
132 return;
133 }
134
135 if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
136 self.len = self.len.max(min_len);
137 return;
138 }
139
140 let mut heap = Vec::with_capacity(min_len);
141 heap.extend_from_slice(&self.inline[..self.len]);
142 heap.resize(min_len, 0.0);
143 self.heap = Some(heap);
144 }
145
146 fn set(&mut self, index: usize, value: f32) {
147 if let Some(heap) = &mut self.heap {
148 heap[index] = value;
149 } else {
150 self.inline[index] = value;
151 }
152 }
153
154 #[cfg(test)]
155 fn is_inline(&self) -> bool {
156 self.heap.is_none()
157 }
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
162pub enum RuntimeShaderUniformError {
163 #[error(
164 "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"
165 )]
166 OutOfUserRange {
167 index: usize,
168 width: usize,
169 max_user_uniforms: usize,
170 reserved_start: usize,
171 max_uniforms: usize,
172 },
173}
174
175impl RuntimeShader {
176 pub const MAX_UNIFORMS: usize = 256;
180 pub const RESERVED_UNIFORM_START: usize = 248;
182 pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
184
185 #[track_caller]
187 pub fn new(wgsl_source: &str) -> Self {
188 let (source, source_hash) =
189 cached_shader_source(std::panic::Location::caller(), wgsl_source);
190 Self {
191 source,
192 source_hash,
193 uniforms: RuntimeShaderUniforms::new(),
194 overrides: Vec::new(),
195 input_padding: 0.0,
196 output_padding: 0.0,
197 }
198 }
199
200 pub fn from_shared_source(source: Arc<str>) -> Self {
205 let source_hash = cached_shared_shader_source_hash(&source);
206 Self {
207 source,
208 source_hash,
209 uniforms: RuntimeShaderUniforms::new(),
210 overrides: Vec::new(),
211 input_padding: 0.0,
212 output_padding: 0.0,
213 }
214 }
215
216 pub fn set_override(&mut self, name: &'static str, value: f64) {
223 match self
224 .overrides
225 .binary_search_by(|(existing, _)| existing.cmp(&name))
226 {
227 Ok(index) => self.overrides[index].1 = value,
228 Err(index) => self.overrides.insert(index, (name, value)),
229 }
230 }
231
232 pub fn overrides(&self) -> &[(&'static str, f64)] {
235 &self.overrides
236 }
237
238 pub fn overrides_hash(&self) -> u64 {
240 if self.overrides.is_empty() {
241 return 0;
242 }
243 let mut bytes = Vec::new();
244 for (name, value) in &self.overrides {
245 bytes.extend_from_slice(name.as_bytes());
246 bytes.push(0);
247 bytes.extend_from_slice(&value.to_bits().to_le_bytes());
248 }
249 hash_shader_bytes(&bytes)
250 }
251
252 pub fn set_input_padding(&mut self, padding: f32) {
256 self.input_padding = if padding.is_finite() {
257 padding.max(0.0)
258 } else {
259 0.0
260 };
261 }
262
263 pub fn input_padding(&self) -> f32 {
265 self.input_padding
266 }
267
268 pub fn set_output_padding(&mut self, padding: f32) {
273 self.output_padding = if padding.is_finite() {
274 padding.max(0.0)
275 } else {
276 0.0
277 };
278 }
279
280 pub fn output_padding(&self) -> f32 {
282 self.output_padding
283 }
284
285 pub fn set_float(&mut self, index: usize, value: f32) {
290 let _ = self.try_set_float(index, value);
291 }
292
293 pub fn try_set_float(
295 &mut self,
296 index: usize,
297 value: f32,
298 ) -> Result<(), RuntimeShaderUniformError> {
299 self.try_ensure_capacity(index, 1)?;
300 self.uniforms.set(index, value);
301 Ok(())
302 }
303
304 pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
309 let _ = self.try_set_float2(index, x, y);
310 }
311
312 pub fn try_set_float2(
314 &mut self,
315 index: usize,
316 x: f32,
317 y: f32,
318 ) -> Result<(), RuntimeShaderUniformError> {
319 self.try_ensure_capacity(index, 2)?;
320 self.uniforms.set(index, x);
321 self.uniforms.set(index + 1, y);
322 Ok(())
323 }
324
325 pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
330 let _ = self.try_set_float4(index, x, y, z, w);
331 }
332
333 pub fn try_set_float4(
335 &mut self,
336 index: usize,
337 x: f32,
338 y: f32,
339 z: f32,
340 w: f32,
341 ) -> Result<(), RuntimeShaderUniformError> {
342 self.try_ensure_capacity(index, 4)?;
343 self.uniforms.set(index, x);
344 self.uniforms.set(index + 1, y);
345 self.uniforms.set(index + 2, z);
346 self.uniforms.set(index + 3, w);
347 Ok(())
348 }
349
350 pub fn source(&self) -> &str {
352 &self.source
353 }
354
355 pub fn uniforms(&self) -> &[f32] {
357 self.uniforms.as_slice()
358 }
359
360 pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
362 let mut padded = [0.0f32; Self::MAX_UNIFORMS];
363 let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
364 padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
365 padded
366 }
367
368 pub fn source_hash(&self) -> u64 {
370 self.source_hash
371 }
372
373 fn try_ensure_capacity(
374 &mut self,
375 index: usize,
376 width: usize,
377 ) -> Result<(), RuntimeShaderUniformError> {
378 let min_len = index
379 .checked_add(width)
380 .ok_or_else(|| Self::uniform_range_error(index, width))?;
381 if min_len > Self::MAX_USER_UNIFORMS {
382 return Err(Self::uniform_range_error(index, width));
383 }
384 self.uniforms.ensure_len(min_len);
385 Ok(())
386 }
387
388 fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
389 RuntimeShaderUniformError::OutOfUserRange {
390 index,
391 width,
392 max_user_uniforms: Self::MAX_USER_UNIFORMS,
393 reserved_start: Self::RESERVED_UNIFORM_START,
394 max_uniforms: Self::MAX_UNIFORMS,
395 }
396 }
397}
398
399impl PartialEq for RuntimeShader {
400 fn eq(&self, other: &Self) -> bool {
401 self.source_hash == other.source_hash
402 && (Arc::ptr_eq(&self.source, &other.source)
403 || self.source.as_ref() == other.source.as_ref())
404 && self.uniforms == other.uniforms
405 && self.overrides.len() == other.overrides.len()
406 && self
407 .overrides
408 .iter()
409 .zip(&other.overrides)
410 .all(|(a, b)| a.0 == b.0 && a.1.to_bits() == b.1.to_bits())
411 && self.input_padding.to_bits() == other.input_padding.to_bits()
412 && self.output_padding.to_bits() == other.output_padding.to_bits()
413 }
414}
415
416fn hash_shader_source(source: &str) -> u64 {
417 hash_shader_bytes(source.as_bytes())
418}
419
420fn hash_shader_bytes(bytes: &[u8]) -> u64 {
421 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
422 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
423
424 bytes.iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
425 (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
426 })
427}
428
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
430struct ShaderSourceCallsite {
431 file: &'static str,
432 line: u32,
433 column: u32,
434}
435
436struct CachedShaderSource {
437 callsite: ShaderSourceCallsite,
438 source_hash: u64,
439 source: Arc<str>,
440}
441
442struct CachedSharedShaderSourceHash {
443 byte_ptr: usize,
444 len: usize,
445 source_hash: u64,
446 source: Weak<str>,
447}
448
449fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
450 static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
451 let byte_ptr = source.as_ptr() as usize;
452 let len = source.len();
453 let mut cache = CACHE
454 .get_or_init(|| Mutex::new(Vec::new()))
455 .lock()
456 .unwrap_or_else(|poisoned| poisoned.into_inner());
457
458 cache.retain(|entry| entry.source.strong_count() > 0);
459 if let Some(entry) = cache.iter().find(|entry| {
460 entry.byte_ptr == byte_ptr
461 && entry.len == len
462 && entry
463 .source
464 .upgrade()
465 .is_some_and(|cached| Arc::ptr_eq(&cached, source))
466 }) {
467 return entry.source_hash;
468 }
469
470 let source_hash = hash_shader_source(source);
471 cache.push(CachedSharedShaderSourceHash {
472 byte_ptr,
473 len,
474 source_hash,
475 source: Arc::downgrade(source),
476 });
477 source_hash
478}
479
480fn cached_shader_source(
481 location: &'static std::panic::Location<'static>,
482 source: &str,
483) -> (Arc<str>, u64) {
484 static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
485 let callsite = ShaderSourceCallsite {
486 file: location.file(),
487 line: location.line(),
488 column: location.column(),
489 };
490 let mut cache = CACHE
491 .get_or_init(|| Mutex::new(Vec::new()))
492 .lock()
493 .unwrap_or_else(|poisoned| poisoned.into_inner());
494
495 if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
496 if entry.source.as_ref() == source {
497 return (entry.source.clone(), entry.source_hash);
498 }
499 let source_hash = hash_shader_source(source);
500 entry.source_hash = source_hash;
501 entry.source = Arc::<str>::from(source);
502 return (entry.source.clone(), entry.source_hash);
503 }
504
505 let source_hash = hash_shader_source(source);
506 let shared = Arc::<str>::from(source);
507 cache.push(CachedShaderSource {
508 callsite,
509 source_hash,
510 source: shared.clone(),
511 });
512 (shared, source_hash)
513}
514
515#[derive(Clone, Debug, PartialEq)]
520pub enum RenderEffect {
521 Blur {
523 radius_x: f32,
524 radius_y: f32,
525 edge_treatment: TileMode,
526 },
527 Offset { offset_x: f32, offset_y: f32 },
529 Shader { shader: RuntimeShader },
531 Chain {
533 first: Box<RenderEffect>,
534 second: Box<RenderEffect>,
535 },
536}
537
538impl RenderEffect {
539 pub fn blur(radius: f32) -> Self {
541 Self::blur_with_edge_treatment(radius, TileMode::default())
542 }
543
544 pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
547 Self::Blur {
548 radius_x: radius,
549 radius_y: radius,
550 edge_treatment,
551 }
552 }
553
554 pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
556 Self::Blur {
557 radius_x,
558 radius_y,
559 edge_treatment,
560 }
561 }
562
563 pub fn offset(offset_x: f32, offset_y: f32) -> Self {
565 Self::Offset { offset_x, offset_y }
566 }
567
568 pub fn runtime_shader(shader: RuntimeShader) -> Self {
570 Self::Shader { shader }
571 }
572
573 pub fn then(self, other: RenderEffect) -> Self {
575 Self::Chain {
576 first: Box::new(self),
577 second: Box::new(other),
578 }
579 }
580
581 pub fn contains_runtime_shader(&self) -> bool {
585 match self {
586 RenderEffect::Shader { .. } => true,
587 RenderEffect::Chain { first, second } => {
588 first.contains_runtime_shader() || second.contains_runtime_shader()
589 }
590 _ => false,
591 }
592 }
593
594 pub fn input_padding(&self) -> f32 {
596 match self {
597 RenderEffect::Blur {
598 radius_x, radius_y, ..
599 } => radius_x.abs().max(radius_y.abs()),
600 RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
601 RenderEffect::Shader { shader } => shader.input_padding(),
602 RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
603 }
604 }
605
606 pub fn output_padding(&self) -> f32 {
610 match self {
611 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
612 RenderEffect::Shader { shader } => shader.output_padding(),
613 RenderEffect::Chain { first, second } => {
614 first.output_padding() + second.output_padding()
615 }
616 }
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 #[test]
623 fn overrides_stay_sorted_and_replace_by_name() {
624 let mut shader = super::RuntimeShader::new("// overrides");
625 shader.set_override("ZETA", 1.0);
626 shader.set_override("ALPHA", 0.0);
627 shader.set_override("ZETA", 2.0);
628 assert_eq!(shader.overrides(), &[("ALPHA", 0.0), ("ZETA", 2.0)]);
629 }
630
631 #[test]
632 fn overrides_distinguish_otherwise_equal_shaders() {
633 let plain = super::RuntimeShader::new("// overrides-eq");
634 let mut raised = plain.clone();
635 raised.set_override("FLAG", 1.0);
636 assert_eq!(plain.overrides_hash(), 0);
637 assert_ne!(plain.overrides_hash(), raised.overrides_hash());
638 assert_ne!(plain, raised);
639 let mut lowered = raised.clone();
640 lowered.set_override("FLAG", 0.0);
641 assert_ne!(raised.overrides_hash(), lowered.overrides_hash());
642 assert_ne!(raised, lowered);
643 }
644
645 use super::*;
646 use crate::RoundedCornerShape;
647
648 #[test]
649 fn runtime_shader_set_uniforms() {
650 let mut shader = RuntimeShader::new("// test");
651 shader.set_float(0, 1.0);
652 shader.set_float2(2, 3.0, 4.0);
653 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
654
655 assert_eq!(shader.uniforms()[0], 1.0);
656 assert_eq!(shader.uniforms()[1], 0.0);
657 assert_eq!(shader.uniforms()[2], 3.0);
658 assert_eq!(shader.uniforms()[3], 4.0);
659 assert_eq!(shader.uniforms()[4], 5.0);
660 assert_eq!(shader.uniforms()[5], 6.0);
661 assert_eq!(shader.uniforms()[6], 7.0);
662 assert_eq!(shader.uniforms()[7], 8.0);
663 }
664
665 #[test]
666 fn runtime_shader_padded() {
667 let mut shader = RuntimeShader::new("// test");
668 shader.set_float(0, 42.0);
669 let padded = shader.uniforms_padded();
670 assert_eq!(padded[0], 42.0);
671 assert_eq!(padded[1], 0.0);
672 assert_eq!(padded[255], 0.0);
673 }
674
675 #[test]
676 fn blur_and_offset_declare_input_padding() {
677 assert_eq!(
678 RenderEffect::blur_xy(6.0, 12.0, TileMode::Clamp).input_padding(),
679 12.0
680 );
681 assert_eq!(RenderEffect::offset(-8.0, 3.0).input_padding(), 8.0);
682 }
683
684 #[test]
685 fn chained_effect_padding_accumulates_sampling_ranges() {
686 let mut shader = RuntimeShader::new("// test");
687 shader.set_input_padding(9.0);
688 let effect = RenderEffect::blur_xy(4.0, 6.0, TileMode::Clamp)
689 .then(RenderEffect::runtime_shader(shader))
690 .then(RenderEffect::offset(2.0, -5.0));
691
692 assert_eq!(effect.input_padding(), 20.0);
693 }
694
695 #[test]
696 fn runtime_shader_keeps_common_uniform_payload_inline() {
697 let mut shader = RuntimeShader::new("// test");
698 shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
699 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
700 shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
701 shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
702
703 assert!(shader.uniforms.is_inline());
704 assert_eq!(shader.uniforms().len(), 16);
705
706 shader.set_float(16, 17.0);
707 assert!(!shader.uniforms.is_inline());
708 assert_eq!(shader.uniforms()[16], 17.0);
709 }
710
711 #[test]
712 fn runtime_shader_try_set_reports_reserved_uniform_slots() {
713 let mut shader = RuntimeShader::new("// test");
714
715 let err = shader
716 .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
717 .unwrap_err();
718 assert_eq!(
719 err,
720 RuntimeShaderUniformError::OutOfUserRange {
721 index: RuntimeShader::RESERVED_UNIFORM_START,
722 width: 1,
723 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
724 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
725 max_uniforms: RuntimeShader::MAX_UNIFORMS,
726 }
727 );
728 assert!(shader.uniforms().is_empty());
729
730 let err = shader
731 .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
732 .unwrap_err();
733 assert_eq!(
734 err,
735 RuntimeShaderUniformError::OutOfUserRange {
736 index: RuntimeShader::MAX_USER_UNIFORMS - 3,
737 width: 4,
738 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
739 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
740 max_uniforms: RuntimeShader::MAX_UNIFORMS,
741 }
742 );
743 }
744
745 #[test]
746 fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
747 let mut shader = RuntimeShader::new("// test");
748 shader.set_float(0, 7.0);
749
750 shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
751 shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
752
753 assert_eq!(shader.uniforms(), &[7.0]);
754 }
755
756 #[test]
757 fn render_effect_chaining() {
758 let blur = RenderEffect::blur(10.0);
759 let offset = RenderEffect::offset(5.0, 5.0);
760 let chained = blur.then(offset);
761 match chained {
762 RenderEffect::Chain { first, second } => {
763 assert!(matches!(*first, RenderEffect::Blur { .. }));
764 assert!(matches!(*second, RenderEffect::Offset { .. }));
765 }
766 _ => panic!("expected Chain"),
767 }
768 }
769
770 #[test]
771 fn blur_convenience() {
772 let effect = RenderEffect::blur(15.0);
773 match effect {
774 RenderEffect::Blur {
775 radius_x,
776 radius_y,
777 edge_treatment,
778 } => {
779 assert_eq!(radius_x, 15.0);
780 assert_eq!(radius_y, 15.0);
781 assert_eq!(edge_treatment, TileMode::Clamp);
782 }
783 _ => panic!("expected Blur"),
784 }
785 }
786
787 #[test]
788 fn blur_with_edge_treatment_uses_explicit_mode() {
789 let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
790 match effect {
791 RenderEffect::Blur {
792 radius_x,
793 radius_y,
794 edge_treatment,
795 } => {
796 assert_eq!(radius_x, 6.0);
797 assert_eq!(radius_y, 6.0);
798 assert_eq!(edge_treatment, TileMode::Decal);
799 }
800 _ => panic!("expected Blur"),
801 }
802 }
803
804 #[test]
805 fn source_hash_consistent() {
806 let s1 = RuntimeShader::new("fn main() {}");
807 let s2 = RuntimeShader::new("fn main() {}");
808 assert_eq!(s1.source_hash(), s2.source_hash());
809 }
810
811 #[test]
812 fn runtime_shader_from_shared_source_reuses_shared_source() {
813 let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
814 let s1 = RuntimeShader::from_shared_source(source.clone());
815 let s2 = RuntimeShader::from_shared_source(source);
816
817 assert!(Arc::ptr_eq(&s1.source, &s2.source));
818 assert_eq!(s1.source_hash(), s2.source_hash());
819 }
820
821 fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
822 RuntimeShader::new(source)
823 }
824
825 fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
826 RuntimeShader::new(source)
827 }
828
829 #[test]
830 fn runtime_shader_new_reuses_same_callsite_source() {
831 let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
832 let s1 = runtime_shader_from_reuse_callsite(source);
833 let s2 = runtime_shader_from_reuse_callsite(source);
834
835 assert!(Arc::ptr_eq(&s1.source, &s2.source));
836 assert_eq!(s1.source_hash(), s2.source_hash());
837 }
838
839 #[test]
840 fn runtime_shader_new_replaces_changed_callsite_source() {
841 let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
842 let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
843
844 assert!(!Arc::ptr_eq(&s1.source, &s2.source));
845 assert_ne!(s1.source_hash(), s2.source_hash());
846 assert_eq!(s2.source(), "fn b() {}");
847 }
848
849 #[test]
850 fn runtime_shader_source_storage_has_no_process_global_interner() {
851 let source = include_str!("render_effect.rs");
852 let blocked_static = ["static ", "INTERNER"].concat();
853 let blocked_type = ["ShaderSource", "Interner"].concat();
854
855 assert!(
856 !source.contains(&blocked_static) && !source.contains(&blocked_type),
857 "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
858 );
859 }
860
861 #[test]
862 fn blur_xy_preserves_tile_mode() {
863 let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
864 match effect {
865 RenderEffect::Blur {
866 radius_x,
867 radius_y,
868 edge_treatment,
869 } => {
870 assert_eq!(radius_x, 3.0);
871 assert_eq!(radius_y, 7.0);
872 assert_eq!(edge_treatment, TileMode::Clamp);
873 }
874 _ => panic!("expected Blur"),
875 }
876 }
877
878 #[test]
879 fn offset_constructor_sets_components() {
880 let effect = RenderEffect::offset(11.0, -5.0);
881 match effect {
882 RenderEffect::Offset { offset_x, offset_y } => {
883 assert_eq!(offset_x, 11.0);
884 assert_eq!(offset_y, -5.0);
885 }
886 _ => panic!("expected Offset"),
887 }
888 }
889
890 #[test]
891 fn runtime_shader_equality_is_source_value_based() {
892 let mut s1 = RuntimeShader::new("fn main() {}");
893 let mut s2 = RuntimeShader::new("fn main() {}");
894 s1.set_float(0, 1.0);
895 s2.set_float(0, 1.0);
896 assert_eq!(s1, s2);
897 }
898
899 #[test]
900 fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
901 let treatment = BlurredEdgeTreatment::default();
902 assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
903 assert!(treatment.clip());
904 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
905 }
906
907 #[test]
908 fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
909 let treatment = BlurredEdgeTreatment::UNBOUNDED;
910 assert_eq!(treatment.shape(), None);
911 assert!(!treatment.clip());
912 assert_eq!(treatment.tile_mode(), TileMode::Decal);
913 }
914
915 #[test]
916 fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
917 let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
918 let treatment = BlurredEdgeTreatment::with_shape(rounded);
919 assert_eq!(treatment.shape(), Some(rounded));
920 assert!(treatment.clip());
921 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
922 }
923}