1use crate::LayerShape;
7use std::sync::{Arc, Mutex, OnceLock, Weak};
8
9const RUNTIME_SHADER_INLINE_UNIFORMS: usize = 16;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13pub enum TileMode {
14 #[default]
16 Clamp,
17 Repeated,
19 Mirror,
21 Decal,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct BlurredEdgeTreatment {
32 shape: Option<LayerShape>,
33}
34
35impl BlurredEdgeTreatment {
36 pub const RECTANGLE: Self = Self {
38 shape: Some(LayerShape::Rectangle),
39 };
40
41 pub const UNBOUNDED: Self = Self { shape: None };
43
44 pub const fn with_shape(shape: LayerShape) -> Self {
46 Self { shape: Some(shape) }
47 }
48
49 pub fn shape(self) -> Option<LayerShape> {
50 self.shape
51 }
52
53 pub fn clip(self) -> bool {
54 self.shape.is_some()
55 }
56
57 pub fn tile_mode(self) -> TileMode {
58 if self.clip() {
59 TileMode::Clamp
60 } else {
61 TileMode::Decal
62 }
63 }
64}
65
66impl Default for BlurredEdgeTreatment {
67 fn default() -> Self {
68 Self::RECTANGLE
69 }
70}
71
72#[derive(Clone, Debug)]
89pub struct RuntimeShader {
90 source: Arc<str>,
91 source_hash: u64,
92 uniforms: RuntimeShaderUniforms,
93 input_padding: f32,
94}
95
96#[derive(Clone, Debug, PartialEq)]
97struct RuntimeShaderUniforms {
98 len: usize,
99 inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
100 heap: Option<Vec<f32>>,
101}
102
103impl RuntimeShaderUniforms {
104 fn new() -> Self {
105 Self {
106 len: 0,
107 inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
108 heap: None,
109 }
110 }
111
112 fn as_slice(&self) -> &[f32] {
113 if let Some(heap) = &self.heap {
114 heap.as_slice()
115 } else {
116 &self.inline[..self.len]
117 }
118 }
119
120 fn len(&self) -> usize {
121 self.as_slice().len()
122 }
123
124 fn ensure_len(&mut self, min_len: usize) {
125 if let Some(heap) = &mut self.heap {
126 if heap.len() < min_len {
127 heap.resize(min_len, 0.0);
128 }
129 return;
130 }
131
132 if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
133 self.len = self.len.max(min_len);
134 return;
135 }
136
137 let mut heap = Vec::with_capacity(min_len);
138 heap.extend_from_slice(&self.inline[..self.len]);
139 heap.resize(min_len, 0.0);
140 self.heap = Some(heap);
141 }
142
143 fn set(&mut self, index: usize, value: f32) {
144 if let Some(heap) = &mut self.heap {
145 heap[index] = value;
146 } else {
147 self.inline[index] = value;
148 }
149 }
150
151 #[cfg(test)]
152 fn is_inline(&self) -> bool {
153 self.heap.is_none()
154 }
155}
156
157#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
159pub enum RuntimeShaderUniformError {
160 #[error(
161 "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"
162 )]
163 OutOfUserRange {
164 index: usize,
165 width: usize,
166 max_user_uniforms: usize,
167 reserved_start: usize,
168 max_uniforms: usize,
169 },
170}
171
172impl RuntimeShader {
173 pub const MAX_UNIFORMS: usize = 256;
177 pub const RESERVED_UNIFORM_START: usize = 248;
179 pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
181
182 #[track_caller]
184 pub fn new(wgsl_source: &str) -> Self {
185 let (source, source_hash) =
186 cached_shader_source(std::panic::Location::caller(), wgsl_source);
187 Self {
188 source,
189 source_hash,
190 uniforms: RuntimeShaderUniforms::new(),
191 input_padding: 0.0,
192 }
193 }
194
195 pub fn from_shared_source(source: Arc<str>) -> Self {
200 let source_hash = cached_shared_shader_source_hash(&source);
201 Self {
202 source,
203 source_hash,
204 uniforms: RuntimeShaderUniforms::new(),
205 input_padding: 0.0,
206 }
207 }
208
209 pub fn set_input_padding(&mut self, padding: f32) {
213 self.input_padding = if padding.is_finite() {
214 padding.max(0.0)
215 } else {
216 0.0
217 };
218 }
219
220 pub fn input_padding(&self) -> f32 {
222 self.input_padding
223 }
224
225 pub fn set_float(&mut self, index: usize, value: f32) {
230 let _ = self.try_set_float(index, value);
231 }
232
233 pub fn try_set_float(
235 &mut self,
236 index: usize,
237 value: f32,
238 ) -> Result<(), RuntimeShaderUniformError> {
239 self.try_ensure_capacity(index, 1)?;
240 self.uniforms.set(index, value);
241 Ok(())
242 }
243
244 pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
249 let _ = self.try_set_float2(index, x, y);
250 }
251
252 pub fn try_set_float2(
254 &mut self,
255 index: usize,
256 x: f32,
257 y: f32,
258 ) -> Result<(), RuntimeShaderUniformError> {
259 self.try_ensure_capacity(index, 2)?;
260 self.uniforms.set(index, x);
261 self.uniforms.set(index + 1, y);
262 Ok(())
263 }
264
265 pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
270 let _ = self.try_set_float4(index, x, y, z, w);
271 }
272
273 pub fn try_set_float4(
275 &mut self,
276 index: usize,
277 x: f32,
278 y: f32,
279 z: f32,
280 w: f32,
281 ) -> Result<(), RuntimeShaderUniformError> {
282 self.try_ensure_capacity(index, 4)?;
283 self.uniforms.set(index, x);
284 self.uniforms.set(index + 1, y);
285 self.uniforms.set(index + 2, z);
286 self.uniforms.set(index + 3, w);
287 Ok(())
288 }
289
290 pub fn source(&self) -> &str {
292 &self.source
293 }
294
295 pub fn uniforms(&self) -> &[f32] {
297 self.uniforms.as_slice()
298 }
299
300 pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
302 let mut padded = [0.0f32; Self::MAX_UNIFORMS];
303 let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
304 padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
305 padded
306 }
307
308 pub fn source_hash(&self) -> u64 {
310 self.source_hash
311 }
312
313 fn try_ensure_capacity(
314 &mut self,
315 index: usize,
316 width: usize,
317 ) -> Result<(), RuntimeShaderUniformError> {
318 let min_len = index
319 .checked_add(width)
320 .ok_or_else(|| Self::uniform_range_error(index, width))?;
321 if min_len > Self::MAX_USER_UNIFORMS {
322 return Err(Self::uniform_range_error(index, width));
323 }
324 self.uniforms.ensure_len(min_len);
325 Ok(())
326 }
327
328 fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
329 RuntimeShaderUniformError::OutOfUserRange {
330 index,
331 width,
332 max_user_uniforms: Self::MAX_USER_UNIFORMS,
333 reserved_start: Self::RESERVED_UNIFORM_START,
334 max_uniforms: Self::MAX_UNIFORMS,
335 }
336 }
337}
338
339impl PartialEq for RuntimeShader {
340 fn eq(&self, other: &Self) -> bool {
341 self.source_hash == other.source_hash
342 && (Arc::ptr_eq(&self.source, &other.source)
343 || self.source.as_ref() == other.source.as_ref())
344 && self.uniforms == other.uniforms
345 && self.input_padding.to_bits() == other.input_padding.to_bits()
346 }
347}
348
349fn hash_shader_source(source: &str) -> u64 {
350 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
351 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
352
353 source
354 .as_bytes()
355 .iter()
356 .fold(FNV_OFFSET_BASIS, |hash, byte| {
357 (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
358 })
359}
360
361#[derive(Clone, Copy, Debug, PartialEq, Eq)]
362struct ShaderSourceCallsite {
363 file: &'static str,
364 line: u32,
365 column: u32,
366}
367
368struct CachedShaderSource {
369 callsite: ShaderSourceCallsite,
370 source_hash: u64,
371 source: Arc<str>,
372}
373
374struct CachedSharedShaderSourceHash {
375 byte_ptr: usize,
376 len: usize,
377 source_hash: u64,
378 source: Weak<str>,
379}
380
381fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
382 static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
383 let byte_ptr = source.as_ptr() as usize;
384 let len = source.len();
385 let mut cache = CACHE
386 .get_or_init(|| Mutex::new(Vec::new()))
387 .lock()
388 .unwrap_or_else(|poisoned| poisoned.into_inner());
389
390 cache.retain(|entry| entry.source.strong_count() > 0);
391 if let Some(entry) = cache.iter().find(|entry| {
392 entry.byte_ptr == byte_ptr
393 && entry.len == len
394 && entry
395 .source
396 .upgrade()
397 .is_some_and(|cached| Arc::ptr_eq(&cached, source))
398 }) {
399 return entry.source_hash;
400 }
401
402 let source_hash = hash_shader_source(source);
403 cache.push(CachedSharedShaderSourceHash {
404 byte_ptr,
405 len,
406 source_hash,
407 source: Arc::downgrade(source),
408 });
409 source_hash
410}
411
412fn cached_shader_source(
413 location: &'static std::panic::Location<'static>,
414 source: &str,
415) -> (Arc<str>, u64) {
416 static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
417 let callsite = ShaderSourceCallsite {
418 file: location.file(),
419 line: location.line(),
420 column: location.column(),
421 };
422 let mut cache = CACHE
423 .get_or_init(|| Mutex::new(Vec::new()))
424 .lock()
425 .unwrap_or_else(|poisoned| poisoned.into_inner());
426
427 if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
428 if entry.source.as_ref() == source {
429 return (entry.source.clone(), entry.source_hash);
430 }
431 let source_hash = hash_shader_source(source);
432 entry.source_hash = source_hash;
433 entry.source = Arc::<str>::from(source);
434 return (entry.source.clone(), entry.source_hash);
435 }
436
437 let source_hash = hash_shader_source(source);
438 let shared = Arc::<str>::from(source);
439 cache.push(CachedShaderSource {
440 callsite,
441 source_hash,
442 source: shared.clone(),
443 });
444 (shared, source_hash)
445}
446
447#[derive(Clone, Debug, PartialEq)]
452pub enum RenderEffect {
453 Blur {
455 radius_x: f32,
456 radius_y: f32,
457 edge_treatment: TileMode,
458 },
459 Offset { offset_x: f32, offset_y: f32 },
461 Shader { shader: RuntimeShader },
463 Chain {
465 first: Box<RenderEffect>,
466 second: Box<RenderEffect>,
467 },
468}
469
470impl RenderEffect {
471 pub fn blur(radius: f32) -> Self {
473 Self::blur_with_edge_treatment(radius, TileMode::default())
474 }
475
476 pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
479 Self::Blur {
480 radius_x: radius,
481 radius_y: radius,
482 edge_treatment,
483 }
484 }
485
486 pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
488 Self::Blur {
489 radius_x,
490 radius_y,
491 edge_treatment,
492 }
493 }
494
495 pub fn offset(offset_x: f32, offset_y: f32) -> Self {
497 Self::Offset { offset_x, offset_y }
498 }
499
500 pub fn runtime_shader(shader: RuntimeShader) -> Self {
502 Self::Shader { shader }
503 }
504
505 pub fn then(self, other: RenderEffect) -> Self {
507 Self::Chain {
508 first: Box::new(self),
509 second: Box::new(other),
510 }
511 }
512
513 pub fn contains_runtime_shader(&self) -> bool {
517 match self {
518 RenderEffect::Shader { .. } => true,
519 RenderEffect::Chain { first, second } => {
520 first.contains_runtime_shader() || second.contains_runtime_shader()
521 }
522 _ => false,
523 }
524 }
525
526 pub fn input_padding(&self) -> f32 {
528 match self {
529 RenderEffect::Shader { shader } => shader.input_padding(),
530 RenderEffect::Chain { first, second } => {
531 first.input_padding().max(second.input_padding())
532 }
533 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
534 }
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use crate::RoundedCornerShape;
542
543 #[test]
544 fn runtime_shader_set_uniforms() {
545 let mut shader = RuntimeShader::new("// test");
546 shader.set_float(0, 1.0);
547 shader.set_float2(2, 3.0, 4.0);
548 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
549
550 assert_eq!(shader.uniforms()[0], 1.0);
551 assert_eq!(shader.uniforms()[1], 0.0); assert_eq!(shader.uniforms()[2], 3.0);
553 assert_eq!(shader.uniforms()[3], 4.0);
554 assert_eq!(shader.uniforms()[4], 5.0);
555 assert_eq!(shader.uniforms()[5], 6.0);
556 assert_eq!(shader.uniforms()[6], 7.0);
557 assert_eq!(shader.uniforms()[7], 8.0);
558 }
559
560 #[test]
561 fn runtime_shader_padded() {
562 let mut shader = RuntimeShader::new("// test");
563 shader.set_float(0, 42.0);
564 let padded = shader.uniforms_padded();
565 assert_eq!(padded[0], 42.0);
566 assert_eq!(padded[1], 0.0);
567 assert_eq!(padded[255], 0.0);
568 }
569
570 #[test]
571 fn runtime_shader_keeps_common_uniform_payload_inline() {
572 let mut shader = RuntimeShader::new("// test");
573 shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
574 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
575 shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
576 shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
577
578 assert!(shader.uniforms.is_inline());
579 assert_eq!(shader.uniforms().len(), 16);
580
581 shader.set_float(16, 17.0);
582 assert!(!shader.uniforms.is_inline());
583 assert_eq!(shader.uniforms()[16], 17.0);
584 }
585
586 #[test]
587 fn runtime_shader_try_set_reports_reserved_uniform_slots() {
588 let mut shader = RuntimeShader::new("// test");
589
590 let err = shader
591 .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
592 .unwrap_err();
593 assert_eq!(
594 err,
595 RuntimeShaderUniformError::OutOfUserRange {
596 index: RuntimeShader::RESERVED_UNIFORM_START,
597 width: 1,
598 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
599 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
600 max_uniforms: RuntimeShader::MAX_UNIFORMS,
601 }
602 );
603 assert!(shader.uniforms().is_empty());
604
605 let err = shader
606 .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
607 .unwrap_err();
608 assert_eq!(
609 err,
610 RuntimeShaderUniformError::OutOfUserRange {
611 index: RuntimeShader::MAX_USER_UNIFORMS - 3,
612 width: 4,
613 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
614 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
615 max_uniforms: RuntimeShader::MAX_UNIFORMS,
616 }
617 );
618 }
619
620 #[test]
621 fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
622 let mut shader = RuntimeShader::new("// test");
623 shader.set_float(0, 7.0);
624
625 shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
626 shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
627
628 assert_eq!(shader.uniforms(), &[7.0]);
629 }
630
631 #[test]
632 fn render_effect_chaining() {
633 let blur = RenderEffect::blur(10.0);
634 let offset = RenderEffect::offset(5.0, 5.0);
635 let chained = blur.then(offset);
636 match chained {
637 RenderEffect::Chain { first, second } => {
638 assert!(matches!(*first, RenderEffect::Blur { .. }));
639 assert!(matches!(*second, RenderEffect::Offset { .. }));
640 }
641 _ => panic!("expected Chain"),
642 }
643 }
644
645 #[test]
646 fn blur_convenience() {
647 let effect = RenderEffect::blur(15.0);
648 match effect {
649 RenderEffect::Blur {
650 radius_x,
651 radius_y,
652 edge_treatment,
653 } => {
654 assert_eq!(radius_x, 15.0);
655 assert_eq!(radius_y, 15.0);
656 assert_eq!(edge_treatment, TileMode::Clamp);
657 }
658 _ => panic!("expected Blur"),
659 }
660 }
661
662 #[test]
663 fn blur_with_edge_treatment_uses_explicit_mode() {
664 let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
665 match effect {
666 RenderEffect::Blur {
667 radius_x,
668 radius_y,
669 edge_treatment,
670 } => {
671 assert_eq!(radius_x, 6.0);
672 assert_eq!(radius_y, 6.0);
673 assert_eq!(edge_treatment, TileMode::Decal);
674 }
675 _ => panic!("expected Blur"),
676 }
677 }
678
679 #[test]
680 fn source_hash_consistent() {
681 let s1 = RuntimeShader::new("fn main() {}");
682 let s2 = RuntimeShader::new("fn main() {}");
683 assert_eq!(s1.source_hash(), s2.source_hash());
684 }
685
686 #[test]
687 fn runtime_shader_from_shared_source_reuses_shared_source() {
688 let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
689 let s1 = RuntimeShader::from_shared_source(source.clone());
690 let s2 = RuntimeShader::from_shared_source(source);
691
692 assert!(Arc::ptr_eq(&s1.source, &s2.source));
693 assert_eq!(s1.source_hash(), s2.source_hash());
694 }
695
696 fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
697 RuntimeShader::new(source)
698 }
699
700 fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
701 RuntimeShader::new(source)
702 }
703
704 #[test]
705 fn runtime_shader_new_reuses_same_callsite_source() {
706 let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
707 let s1 = runtime_shader_from_reuse_callsite(source);
708 let s2 = runtime_shader_from_reuse_callsite(source);
709
710 assert!(Arc::ptr_eq(&s1.source, &s2.source));
711 assert_eq!(s1.source_hash(), s2.source_hash());
712 }
713
714 #[test]
715 fn runtime_shader_new_replaces_changed_callsite_source() {
716 let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
717 let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
718
719 assert!(!Arc::ptr_eq(&s1.source, &s2.source));
720 assert_ne!(s1.source_hash(), s2.source_hash());
721 assert_eq!(s2.source(), "fn b() {}");
722 }
723
724 #[test]
725 fn runtime_shader_source_storage_has_no_process_global_interner() {
726 let source = include_str!("render_effect.rs");
727 let blocked_static = ["static ", "INTERNER"].concat();
728 let blocked_type = ["ShaderSource", "Interner"].concat();
729
730 assert!(
731 !source.contains(&blocked_static) && !source.contains(&blocked_type),
732 "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
733 );
734 }
735
736 #[test]
737 fn blur_xy_preserves_tile_mode() {
738 let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
739 match effect {
740 RenderEffect::Blur {
741 radius_x,
742 radius_y,
743 edge_treatment,
744 } => {
745 assert_eq!(radius_x, 3.0);
746 assert_eq!(radius_y, 7.0);
747 assert_eq!(edge_treatment, TileMode::Clamp);
748 }
749 _ => panic!("expected Blur"),
750 }
751 }
752
753 #[test]
754 fn offset_constructor_sets_components() {
755 let effect = RenderEffect::offset(11.0, -5.0);
756 match effect {
757 RenderEffect::Offset { offset_x, offset_y } => {
758 assert_eq!(offset_x, 11.0);
759 assert_eq!(offset_y, -5.0);
760 }
761 _ => panic!("expected Offset"),
762 }
763 }
764
765 #[test]
766 fn runtime_shader_equality_is_source_value_based() {
767 let mut s1 = RuntimeShader::new("fn main() {}");
768 let mut s2 = RuntimeShader::new("fn main() {}");
769 s1.set_float(0, 1.0);
770 s2.set_float(0, 1.0);
771 assert_eq!(s1, s2);
772 }
773
774 #[test]
775 fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
776 let treatment = BlurredEdgeTreatment::default();
777 assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
778 assert!(treatment.clip());
779 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
780 }
781
782 #[test]
783 fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
784 let treatment = BlurredEdgeTreatment::UNBOUNDED;
785 assert_eq!(treatment.shape(), None);
786 assert!(!treatment.clip());
787 assert_eq!(treatment.tile_mode(), TileMode::Decal);
788 }
789
790 #[test]
791 fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
792 let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
793 let treatment = BlurredEdgeTreatment::with_shape(rounded);
794 assert_eq!(treatment.shape(), Some(rounded));
795 assert!(treatment.clip());
796 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
797 }
798}